Merge branch 'develop' into main

feature/docker-ci
iglocska 2021-10-21 13:47:06 +02:00
commit 5a7f7dfc25
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
272 changed files with 126282 additions and 17837 deletions

View File

@ -5,6 +5,14 @@ An Ubuntu server (18.04/20.04 should both work fine) - though other linux instal
- php extensions for intl, mysql, sqlite3, mbstring, xml need to be installed and running
- composer
## Network requirements
Cerebrate communicates via HTTPS so in order to be able to connect to other cerebrate nodes, requiring the following ports to be open:
- port 443 needs to be open for outbound connections to be able to pull contactdb / sharing group information in
- Cerebrate also needs to be accessible (via port 443) from the outside if:
- you wish to pull interconnect local tools with remote cerebrate instances
- you wish to act as a hub node for a community where members are expected to pull data from your node
## Cerebrate installation instructions
@ -59,6 +67,7 @@ create your local configuration and set the db credentials
```bash
sudo -u www-data cp -a /var/www/cerebrate/config/app_local.example.php /var/www/cerebrate/config/app_local.php
sudo -u www-data cp -a /var/www/cerebrate/config/config.example.json /var/www/cerebrate/config/config.json
sudo -u www-data vim /var/www/cerebrate/config/app_local.php
```
@ -79,6 +88,20 @@ This would be, when following the steps above:
'password' => 'YOUR_PASSWORD',
'database' => 'cerebrate',
```
Run the database schema migrations
```bash
/var/www/cerebrate/bin/cake migrations migrate
/var/www/cerebrate/bin/cake migrations migrate -p tags
/var/www/cerebrate/bin/cake migrations migrate -p ADmad/SocialAuth
```
Clean cakephp caches
```bash
sudo rm /var/www/cerebrate/tmp/cache/models/*
sudo rm /var/www/cerebrate/tmp/cache/persistent/*
```
Create an apache config file for cerebrate / ssh key and point the document root to /var/www/cerebrate/webroot and you're good to go
For development installs the following can be done:

View File

@ -6,6 +6,7 @@
"license": "MIT",
"require": {
"php": ">=7.2",
"admad/cakephp-social-auth": "^1.1",
"cakephp/authentication": "^2.0",
"cakephp/authorization": "^2.0",
"cakephp/cakephp": "^4.0",
@ -29,12 +30,14 @@
},
"autoload": {
"psr-4": {
"App\\": "src/"
"App\\": "src/",
"Tags\\": "plugins/Tags/src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Test\\": "tests/",
"Tags\\Test\\": "plugins/Tags/tests/",
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
}
},

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
class TimestampBehavior extends AbstractMigration
{
public function change()
{
$alignments = $this->table('alignments')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$broods = $this->table('broods')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$encryption_keys = $this->table('encryption_keys')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$inbox = $this->table('inbox')
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$outbox = $this->table('outbox')
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$individuals = $this->table('individuals')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$local_tools = $this->table('local_tools')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$meta_templates = $this->table('meta_templates')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$organisations = $this->table('organisations')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$sharing_groups = $this->table('sharing_groups')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
$users = $this->table('users')
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->update();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
class RolesPermOrgAdmin extends AbstractMigration
{
public function change()
{
$table = $this->table('roles')
->addColumn('perm_org_admin', 'boolean', [
'default' => 0,
'null' => false,
])
->update();
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
class UserSettings extends AbstractMigration
{
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
public function change()
{
$table = $this->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();
}
}

View File

@ -99,13 +99,4 @@ return [
'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null),
],
],
'Cerebrate' => [
'open' => [],
'dark' => 0,
'baseurl' => ''
],
'App' => [
'base' => $base,
'fullBaseUrl' => $fullBaseUrl
]
];

View File

@ -87,6 +87,12 @@ try {
*/
if (file_exists(CONFIG . 'app_local.php')) {
Configure::load('app_local', 'default');
//Configure::load('cerebrate', 'default', true);
$settings = file_get_contents(CONFIG . 'config.json');
$settings = json_decode($settings, true);
foreach ($settings as $path => $setting) {
Configure::write($path, $setting);
}
}
/*

9
config/cerebrate.php Normal file
View File

@ -0,0 +1,9 @@
<?php
return [
'Cerebrate' => [
'open' => [],
'app.baseurl' => 'http://localhost:8000/',
'app.uuid' => 'cc9b9358-7c4b-4464-9a2c-f0cb089ff974',
'ui.bsTheme' => 'default',
]
];

View File

@ -0,0 +1 @@
{}

View File

@ -1,5 +1,6 @@
<?php
use Cake\ORM\TableRegistry;
use Authentication\PasswordHasher\DefaultPasswordHasher;
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'InboxProcessors' . DS . 'GenericInboxProcessor.php');
@ -42,13 +43,25 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
'message' => 'E-mail must be valid'
])
->notEmpty('first_name', 'A first name must be provided')
->notEmpty('last_name', 'A last name must be provided');
->notEmpty('last_name', 'A last name must be provided')
->add('password', 'password_complexity', [
'rule' => function($value, $context) {
if (!preg_match('/^((?=.*\d)|(?=.*\W+))(?![\n])(?=.*[A-Z])(?=.*[a-z]).*$|.{16,}/s', $value) || strlen($value) < 12) {
return false;
}
return true;
},
'message' => __('Invalid password. Passwords have to be either 16 character long or 12 character long with 3/4 special groups.')
]);
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['data']['password'] = (new DefaultPasswordHasher())->hash($requestData['data']['password']);
$requestData['title'] = __('User account creation requested for {0}', $requestData['data']['email']);
return parent::create($requestData);
$creationResponse = parent::create($requestData);
$creationResponse['message'] = __('User account creation requested. Please wait for an admin to approve your account.');
return $creationResponse;
}
public function getViewVariables($request)
@ -72,6 +85,11 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
'username' => !empty($request['data']['username']) ? $request['data']['username'] : '',
'role_id' => !empty($request['data']['role_id']) ? $request['data']['role_id'] : '',
'disabled' => !empty($request['data']['disabled']) ? $request['data']['disabled'] : '',
'email' => !empty($request['data']['email']) ? $request['data']['email'] : '',
'first_name' => !empty($request['data']['first_name']) ? $request['data']['first_name'] : '',
'last_name' => !empty($request['data']['last_name']) ? $request['data']['last_name'] : '',
'position' => !empty($request['data']['position']) ? $request['data']['position'] : '',
]);
return [
'dropdownData' => $dropdownData,
@ -82,6 +100,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
public function process($id, $requestData, $inboxRequest)
{
$hashedPassword = $inboxRequest['data']['password'];
if ($requestData['individual_id'] == -1) {
$individual = $this->Users->Individuals->newEntity([
'uuid' => $requestData['uuid'],
@ -101,6 +120,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
'role_id' => $requestData['role_id'],
'disabled' => $requestData['disabled'],
]);
$user->set('password', $hashedPassword, ['setter' => false]); // ignore default password hashing as it has already been hashed
$user = $this->Users->save($user);
if ($user !== false) {

View File

@ -1,99 +1,88 @@
<?php
$formUser = $this->element('genericElements/Form/genericForm', [
'entity' => $userEntity,
'ajax' => false,
'raw' => true,
'data' => [
'description' => __('Create user account'),
'model' => 'User',
'fields' => [
[
'field' => 'individual_id',
'type' => 'dropdown',
'label' => __('Associated individual'),
'options' => $dropdownData['individual'],
],
[
'field' => 'username',
'autocomplete' => 'off',
],
[
'field' => 'role_id',
'type' => 'dropdown',
'label' => __('Role'),
'options' => $dropdownData['role']
],
[
'field' => 'disabled',
'type' => 'checkbox',
'label' => 'Disable'
]
$combinedForm = $this->element('genericElements/Form/genericForm', [
'entity' => $userEntity,
'ajax' => false,
'raw' => true,
'data' => [
'description' => __('Create user account'),
'model' => 'User',
'fields' => [
[
'field' => 'individual_id',
'type' => 'dropdown',
'label' => __('Associated individual'),
'options' => $dropdownData['individual'],
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
$formIndividual = $this->element('genericElements/Form/genericForm', [
'entity' => $individualEntity,
'ajax' => false,
'raw' => true,
'data' => [
'description' => __('Create individual'),
'model' => 'Individual',
'fields' => [
[
'field' => 'email',
'autocomplete' => 'off'
],
[
'field' => 'uuid',
'label' => 'UUID',
'type' => 'uuid',
'autocomplete' => 'off'
],
[
'field' => 'first_name',
'autocomplete' => 'off'
],
[
'field' => 'last_name',
'autocomplete' => 'off'
],
[
'field' => 'position',
'autocomplete' => 'off'
],
[
'field' => 'username',
'autocomplete' => 'off',
],
[
'field' => 'role_id',
'type' => 'dropdown',
'label' => __('Role'),
'options' => $dropdownData['role']
],
[
'field' => 'disabled',
'type' => 'checkbox',
'label' => 'Disable'
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
echo $this->Bootstrap->modal([
'title' => __('Register user'),
'size' => 'lg',
'type' => 'confirm',
'bodyHtml' => sprintf('<div class="user-container">%s</div><div class="individual-container">%s</div>',
$formUser,
$formIndividual
),
'confirmText' => __('Create user'),
'confirmFunction' => 'submitRegistration'
]);
sprintf('<div class="pb-2 fs-4">%s</div>', __('Create individual')),
[
'field' => 'email',
'autocomplete' => 'off'
],
[
'field' => 'uuid',
'label' => 'UUID',
'type' => 'uuid',
'autocomplete' => 'off'
],
[
'field' => 'first_name',
'autocomplete' => 'off'
],
[
'field' => 'last_name',
'autocomplete' => 'off'
],
[
'field' => 'position',
'autocomplete' => 'off'
],
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
echo $this->Bootstrap->modal([
'title' => __('Register user'),
'size' => 'lg',
'type' => 'confirm',
'bodyHtml' => sprintf(
'<div class="form-container">%s</div>',
$combinedForm
),
'confirmText' => __('Create user'),
'confirmFunction' => 'submitRegistration'
]);
?>
</div>
<script>
function submitRegistration(modalObject, tmpApi) {
const $forms = modalObject.$modal.find('form')
const url = $forms[0].action
const data1 = getFormData($forms[0])
const data2 = getFormData($forms[1])
const data = {...data1, ...data2}
return tmpApi.postData(url, data)
const $form = modalObject.$modal.find('form')
return tmpApi.postForm($form[0]).then((result) => {
const url = '/inbox/index'
const $container = $('div[id^="table-container-"]')
const randomValue = $container.attr('id').split('-')[2]
return result
})
}
$(document).ready(function() {
@ -107,7 +96,7 @@
})
function getFormData(form) {
return Object.values(form).reduce((obj,field) => {
return Object.values(form).reduce((obj, field) => {
if (field.type === 'checkbox') {
obj[field.name] = field.checked;
} else {
@ -119,7 +108,8 @@
</script>
<style>
div.individual-container > div, div.user-container > div {
font-size: 1.5rem;
}
div.individual-container>div,
div.user-container>div {
font-size: 1.5rem;
}
</style>

View File

@ -18,18 +18,18 @@ $footerButtons = [
$tools = sprintf(
'<div class="mx-auto mb-3 mw-75 d-flex align-items-center">
<span class="flex-grow-1 text-right" style="font-size: large;">%s</span>
<span class="flex-grow-1 text-end" style="font-size: large;">%s</span>
<span class="mx-3">%s</span>
<span class="flex-grow-1 text-left" style="font-size: large;">%s</span>
<span class="flex-grow-1 text-start" style="font-size: large;">%s</span>
</div>',
sprintf('<span class="mr-2 d-inline-flex flex-column"><a href="%s" target="_blank" title="%s">%s</a><i style="font-size: medium;" class="text-center">%s</i></span>',
sprintf('<span class="me-2 d-inline-flex flex-column"><a href="%s" target="_blank" title="%s">%s</a><i style="font-size: medium;" class="text-center">%s</i></span>',
sprintf('/localTools/view/%s', h($request['localTool']->id)),
h($request['localTool']->description),
h($request['localTool']->name),
__('(local tool)')
),
sprintf('<i class="%s fa-lg"></i>', $this->FontAwesome->getClass('long-arrow-alt-right')),
sprintf('<span class="ml-2 d-inline-flex flex-column"><a href="%s" target="_blank" title="%s">%s</a><i style="font-size: medium;" class="text-center">%s</i></span>',
sprintf('<span class="ms-2 d-inline-flex flex-column"><a href="%s" target="_blank" title="%s">%s</a><i style="font-size: medium;" class="text-center">%s</i></span>',
sprintf('/localTools/broodTools/%s', h($request['data']['remote_tool']['id'])),
h($request['data']['remote_tool']['description'] ?? ''),
h($request['data']['remote_tool']['name']),
@ -77,9 +77,9 @@ $requestData = $this->Bootstrap->collapse([
sprintf('<pre class="p-2 rounded mb-0" style="background: #eeeeee55;"><code>%s</code></pre>', json_encode($request['data']['sent'], JSON_PRETTY_PRINT))
);
$rows = sprintf('<tr><td class="font-weight-bold">%s</td><td>%s</td></tr>', __('URL'), h($request['data']['url']));
$rows .= sprintf('<tr><td class="font-weight-bold">%s</td><td>%s</td></tr>', __('Reason'), h($request['data']['reason']['message']) ?? '');
$rows .= sprintf('<tr><td class="font-weight-bold">%s</td><td>%s</td></tr>', __('Errors'), h(json_encode($request['data']['reason']['errors'])) ?? '');
$rows = sprintf('<tr><td class="fw-bold">%s</td><td>%s</td></tr>', __('URL'), h($request['data']['url']));
$rows .= sprintf('<tr><td class="fw-bold">%s</td><td>%s</td></tr>', __('Reason'), h($request['data']['reason']['message']) ?? '');
$rows .= sprintf('<tr><td class="fw-bold">%s</td><td>%s</td></tr>', __('Errors'), h(json_encode($request['data']['reason']['errors'])) ?? '');
$table2 = sprintf('<table class="table table-sm table-borderless"><tbody>%s</tbody></table>', $rows);
$form = $this->element('genericElements/Form/genericForm', [

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
class TagSystem extends AbstractMigration
{
public function change() {
$tags = $this->table('tags_tags');
$tags->addColumn('namespace', 'string', [
'default' => null,
'limit' => 255,
'null' => true,
])
->addColumn('predicate', 'string', [
'default' => null,
'limit' => 255,
'null' => true,
])
->addColumn('value', 'string', [
'default' => null,
'limit' => 255,
'null' => true,
])
->addColumn('name', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
])
->addColumn('colour', 'string', [
'default' => null,
'limit' => 7,
'null' => false,
])
->addColumn('counter', 'integer', [
'default' => 0,
'length' => 11,
'null' => false,
'signed' => false,
'comment' => 'Field used by the CounterCache behaviour to count the occurence of tags'
])
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->create();
$tagged = $this->table('tags_tagged');
$tagged->addColumn('tag_id', 'integer', [
'default' => null,
'null' => false,
'signed' => false,
'length' => 10,
])
->addColumn('fk_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10,
'comment' => 'The ID of the entity being tagged'
])
->addColumn('fk_model', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
'comment' => 'The model name of the entity being tagged'
])
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
])
->create();
$tags->addIndex(['name'], ['unique' => true])
->update();
$tagged->addIndex(['tag_id', 'fk_id', 'fk_model'], ['unique' => true])
->update();
}
}

View File

@ -0,0 +1,36 @@
<?php
use Cake\Routing\Route\DashedRoute;
use Cake\Routing\RouteBuilder;
$routes->plugin(
'Tags',
['path' => '/tags'],
function ($routes) {
$routes->setRouteClass(DashedRoute::class);
$routes->connect(
'/{action}/*',
['controller' => 'Tags']
);
$routes->get('/', ['controller' => 'Tags', 'action' => 'index']);
// $routes->get('/{id}', ['controller' => 'Tags', 'action' => 'view']);
// $routes->put('/{id}', ['controller' => 'Tags', 'action' => 'edit']);
}
);
$routes->plugin(
'Tags',
['path' => '/Tags'],
function ($routes) {
$routes->setRouteClass(DashedRoute::class);
$routes->connect(
'/{action}/*',
['controller' => 'Tags']
);
$routes->get('/', ['controller' => 'Tags', 'action' => 'index']);
// $routes->get('/{id}', ['controller' => 'Tags', 'action' => 'view']);
// $routes->put('/{id}', ['controller' => 'Tags', 'action' => 'edit']);
}
);

View File

@ -0,0 +1,13 @@
<?php
namespace Tags\Controller;
use App\Controller\AppController as BaseController;
class AppController extends BaseController
{
public function initialize(): void
{
parent::initialize();
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace Tags\Controller;
use Tags\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
use Cake\Utility\Text;
use Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
use Cake\ORM\TableRegistry;
class TagsController extends AppController
{
public function index()
{
$this->CRUD->index([
'filters' => ['name', 'colour'],
'quickFilters' => [['name' => true], 'colour']
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function add()
{
$this->CRUD->add();
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function view($id)
{
$this->CRUD->view($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function edit($id)
{
$this->CRUD->edit($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->render('add');
}
public function delete($id)
{
$this->CRUD->delete($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
// public function tag($model, $id)
// {
// $controller = $this->getControllerBeingTagged($model);
// $controller->CRUD->tag($id);
// $responsePayload = $controller->CRUD->getResponsePayload();
// if (!empty($responsePayload)) {
// return $responsePayload;
// }
// return $controller->getResponse();
// }
// public function untag($model, $id)
// {
// $controller = $this->getControllerBeingTagged($model);
// $controller->CRUD->untag($id);
// $responsePayload = $controller->CRUD->getResponsePayload();
// if (!empty($responsePayload)) {
// return $responsePayload;
// }
// return $controller->getResponse();
// }
// public function viewTags($model, $id)
// {
// $controller = $this->getControllerBeingTagged($model);
// $controller->CRUD->viewTags($id);
// $responsePayload = $controller->CRUD->getResponsePayload();
// if (!empty($responsePayload)) {
// return $responsePayload;
// }
// return $controller->getResponse();
// }
// private function getControllerBeingTagged($model)
// {
// $modelName = Inflector::camelize($model);
// $controllerName = "\\App\\Controller\\{$modelName}Controller";
// if (!class_exists($controllerName)) {
// throw new MethodNotAllowedException(__('Model `{0}` does not exists', $model));
// }
// $controller = new $controllerName;
// // Make sure that the request is correctly assigned to this controller
// return $controller;
// }
}

View File

@ -0,0 +1,355 @@
<?php
namespace Tags\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\ORM\Table;
class TagBehavior extends Behavior
{
protected $_defaultConfig = [
'finderField' => 'name',
'tagsAssoc' => [
'className' => 'Tags.Tags',
'joinTable' => 'tags_tagged',
'foreignKey' => 'fk_id',
'targetForeignKey' => 'tag_id',
'propertyName' => 'tags',
],
'tagsCounter' => ['counter'],
'taggedAssoc' => [
'className' => 'Tags.Tagged',
'foreignKey' => 'fk_id'
],
'implementedEvents' => [
'Model.beforeMarshal' => 'beforeMarshal',
'Model.beforeFind' => 'beforeFind',
'Model.beforeSave' => 'beforeSave',
],
'implementedMethods' => [
'normalizeTags' => 'normalizeTags',
],
'implementedFinders' => [
'tagged' => 'findByTag',
'untagged' => 'findUntagged',
],
];
public function initialize(array $config): void {
$this->bindAssociations();
$this->attachCounters();
}
public function bindAssociations() {
$config = $this->getConfig();
$tagsAssoc = $config['tagsAssoc'];
$taggedAssoc = $config['taggedAssoc'];
$table = $this->_table;
$tableAlias = $this->_table->getAlias();
$assocConditions = ['Tagged.fk_model' => $tableAlias];
if (!$table->hasAssociation('Tagged')) {
$table->hasMany('Tagged', array_merge(
$taggedAssoc,
[
'conditions' => $assocConditions
]
));
}
if (!$table->hasAssociation('Tags')) {
$table->belongsToMany('Tags', array_merge(
$tagsAssoc,
[
'through' => $table->Tagged->getTarget(),
'conditions' => $assocConditions,
]
));
}
if (!$table->Tags->hasAssociation($tableAlias)) {
$table->Tags->belongsToMany($tableAlias, array_merge(
$tagsAssoc,
[
'className' => get_class($table),
]
));
}
if (!$table->Tagged->hasAssociation($tableAlias)) {
$table->Tagged->belongsTo($tableAlias, [
'className' => get_class($table),
'foreignKey' => $tagsAssoc['foreignKey'],
'conditions' => $assocConditions,
'joinType' => 'INNER',
]);
}
if (!$table->Tagged->hasAssociation($tableAlias . 'Tags')) {
$table->Tagged->belongsTo($tableAlias . 'Tags', [
'className' => $tagsAssoc['className'],
'foreignKey' => $tagsAssoc['targetForeignKey'],
'conditions' => $assocConditions,
'joinType' => 'INNER',
]);
}
}
public function attachCounters() {
$config = $this->getConfig();
$taggedTable = $this->_table->Tagged;
if (!$taggedTable->hasBehavior('CounterCache')) {
$taggedTable->addBehavior('CounterCache', [
'Tags' => $config['tagsCounter']
]);
}
}
public function beforeMarshal($event, $data, $options) {
$property = $this->getConfig('tagsAssoc.propertyName');
$options['accessibleFields'][$property] = true;
$options['associated']['Tags']['accessibleFields']['id'] = true;
if (isset($data['tags'])) {
if (!empty($data['tags'])) {
$data[$property] = $this->normalizeTags($data['tags']);
}
}
}
public function beforeSave($event, $entity, $options)
{
if (empty($entity->tags)) {
return;
}
foreach ($entity->tags as $k => $tag) {
if (!$tag->isNew()) {
continue;
}
$existingTag = $this->getExistingTag($tag->name);
if (!$existingTag) {
continue;
}
$joinData = $tag->_joinData;
$tag = $existingTag;
$tag->_joinData = $joinData;
$entity->tags[$k] = $tag;
}
}
public function normalizeTags($tags) {
$result = [];
$modelAlias = $this->_table->getAlias();
$common = [
'_joinData' => [
'fk_model' => $modelAlias
]
];
$tagsTable = $this->_table->Tags;
$displayField = $tagsTable->getDisplayField();
$tagIdentifiers = [];
foreach ($tags as $tag) {
if (empty($tag)) {
continue;
}
if (is_object($tag)) {
$result[] = $tag->toArray();
}
$tagIdentifier = $this->getTagIdentifier($tag);
if (isset($tagIdentifiers[$tagIdentifier])) {
continue;
}
$tagIdentifiers[$tagIdentifier] = true;
$existingTag = $this->getExistingTag($tagIdentifier);
if ($existingTag) {
$result[] = array_merge($common, ['id' => $existingTag->id]);
continue;
}
$result[] = array_merge(
$common,
[
'name' => $tagIdentifier,
]
);
}
return $result;
}
protected function getTagIdentifier($tag)
{
if (is_object($tag)) {
return $tag->name;
} else {
return trim($tag);
}
}
protected function getExistingTag($tagName)
{
$tagsTable = $this->_table->Tags->getTarget();
$query = $tagsTable->find()->where([
'Tags.name' => $tagName
])
->select('Tags.id');
return $query->first();
}
public function findByTag(Query $query, array $options) {
$finderField = $optionsKey = $this->getConfig('finderField');
if (!$finderField) {
$finderField = $optionsKey = 'name';
}
if (!isset($options[$optionsKey])) {
throw new RuntimeException(__('Expected key `{0}` not present in find(\'tagged\') options argument.', $optionsKey));
}
$isAndOperator = isset($options['OperatorAND']) ? $options['OperatorAND'] : true;
$filterValue = $options[$optionsKey];
if (!$filterValue) {
return $query;
}
$filterValue = $this->dissectArgs($filterValue);
if (!empty($filterValue['NOT']) || !empty($filterValue['LIKE'])) {
return $this->findByComplexQueryConditions($query, $filterValue, $finderField, $isAndOperator);
}
$subQuery = $this->buildQuerySnippet($filterValue, $finderField, $isAndOperator);
if (is_string($subQuery)) {
$query->matching('Tags', function ($q) use ($finderField, $subQuery) {
$key = 'Tags.' . $finderField;
return $q->where([
$key => $subQuery,
]);
});
return $query;
}
$modelAlias = $this->_table->getAlias();
return $query->where([$modelAlias . '.id IN' => $subQuery]);
}
public function findUntagged(Query $query, array $options) {
$modelAlias = $this->_table->getAlias();
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
$conditions = ['fk_model' => $modelAlias];
$this->_table->hasOne('NoTags', [
'className' => $this->getConfig('taggedAssoc.className'),
'foreignKey' => $foreignKey,
'conditions' => $conditions
]);
$query = $query->contain(['NoTags'])->where(['NoTags.id IS' => null]);
return $query;
}
protected function dissectArgs($filterValue): array
{
if (!is_array($filterValue)) {
return $filterValue;
}
$dissected = [
'AND' => [],
'NOT' => [],
'LIKE' => [],
];
foreach ($filterValue as $value) {
if (substr($value, 0, 1) == '!') {
$dissected['NOT'][] = substr($value, 1);
}
else if (strpos($value, '%') != false) {
$dissected['LIKE'][] = $value;
} else {
$dissected['AND'][] = $value;
}
}
if (empty($dissected['NOT']) && empty($dissected['LIKE'])) {
return $dissected['AND'];
}
return $dissected;
}
protected function buildQuerySnippet($filterValue, string $finderField, bool $OperatorAND=true)
{
if (!is_array($filterValue)) {
return $filterValue;
}
$key = 'Tags.' . $finderField;
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
$conditions = [
$key . ' IN' => $filterValue,
];
$query = $this->_table->Tagged->find();
if ($OperatorAND) {
$query->contain(['Tags'])
->group('Tagged.' . $foreignKey)
->having('COUNT(*) = ' . count($filterValue))
->select('Tagged.' . $foreignKey)
->where($conditions);
} else {
$query->contain(['Tags'])
->select('Tagged.' . $foreignKey)
->where($conditions);
}
return $query;
}
protected function findByComplexQueryConditions($query, $filterValue, string $finderField, bool $OperatorAND=true)
{
$key = 'Tags.' . $finderField;
$taggedAlias = 'Tagged';
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
if (!empty($filterValue['AND'])) {
$subQuery = $this->buildQuerySnippet($filterValue['AND'], $finderField, $OperatorAND);
$modelAlias = $this->_table->getAlias();
$query->where([$modelAlias . '.id IN' => $subQuery]);
}
if (!empty($filterValue['NOT'])) {
$subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, false);
$modelAlias = $this->_table->getAlias();
$query->where([$modelAlias . '.id NOT IN' => $subQuery]);
}
if (!empty($filterValue['LIKE'])) {
$conditions = ['OR' => []];
foreach($filterValue['LIKE'] as $likeValue) {
$conditions['OR'][] = [
$key . ' LIKE' => $likeValue,
];
}
$subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, $OperatorAND);
if ($OperatorAND) {
$subQuery = $this->_table->Tagged->find()
->contain(['Tags'])
->group('Tagged.' . $foreignKey)
->having('COUNT(*) >= ' . count($filterValue['LIKE']))
->select('Tagged.' . $foreignKey)
->where($conditions);
} else {
$subQuery = $this->_table->Tagged->find()
->contain(['Tags'])
->select('Tagged.' . $foreignKey)
->where($conditions);
}
$modelAlias = $this->_table->getAlias();
$query->where([$modelAlias . '.id IN' => $subQuery]);
}
return $query;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Tags\Model\Entity;
use App\Model\Entity\AppModel;
class Tag extends AppModel {
protected $_accessible = [
'id' => false,
'counter' => false,
'*' => true,
];
protected $_accessibleOnNew = [
'name' => true,
'colour' => true,
];
protected $_virtual = ['text_colour'];
protected function _getTextColour()
{
$textColour = null;
if (!empty($this->colour)) {
$textColour = $this->getTextColour($this->colour);
}
return $textColour;
}
protected function getTextColour($RGB) {
$r = hexdec(substr($RGB, 1, 2));
$g = hexdec(substr($RGB, 3, 2));
$b = hexdec(substr($RGB, 5, 2));
$average = ((2 * $r) + $b + (3 * $g))/6;
if ($average < 127) {
return 'white';
} else {
return 'black';
}
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Tags\Model\Entity;
use App\Model\Entity\AppModel;
class Tagged extends AppModel {
protected $_accessible = [
'id' => false,
'*' => true,
];
}

View File

@ -0,0 +1,33 @@
<?php
namespace Tags\Model\Table;
use App\Model\Table\AppTable;
use Cake\Validation\Validator;
class TaggedTable extends AppTable
{
protected $_accessible = [
'id' => false
];
public function initialize(array $config): void
{
$this->setTable('tags_tagged');
$this->belongsTo('Tags', [
'className' => 'Tags.Tags',
'foreignKey' => 'tag_id',
'propertyName' => 'tag',
]);
$this->addBehavior('Timestamp');
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notBlank('fk_model')
->notBlank('fk_id')
->notBlank('tag_id');
return $validator;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Tags\Model\Table;
use App\Model\Table\AppTable;
use Cake\Validation\Validator;
class TagsTable extends AppTable
{
protected $_accessible = [
'id' => false
];
public function initialize(array $config): void
{
$this->setTable('tags_tags');
$this->setDisplayField('name'); // Change to name?
$this->addBehavior('Timestamp');
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notBlank('name');
return $validator;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Tags;
use Cake\Core\BasePlugin;
use Cake\Core\PluginApplicationInterface;
use Cake\Console\CommandCollection;
use Cake\Http\MiddlewareQueue;
class Plugin extends BasePlugin
{
public function middleware(MiddlewareQueue $middleware): MiddlewareQueue
{
// Add middleware here.
$middleware = parent::middleware($middleware);
return $middleware;
}
public function console(CommandCollection $commands): CommandCollection
{
// Add console commands here.
$commands = parent::console($commands);
return $commands;
}
public function bootstrap(PluginApplicationInterface $app): void
{
// Add constants, load configuration defaults.
// By default will load `config/bootstrap.php` in the plugin.
parent::bootstrap($app);
}
public function routes($routes): void
{
// Add routes.
// By default will load `config/routes.php` in the plugin.
parent::routes($routes);
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace Tags\View\Helper;
use Cake\View\Helper;
use Cake\Utility\Hash;
class TagHelper extends Helper
{
public $helpers = [
'Bootstrap',
'TextColour',
'FontAwesome',
'Form',
'Url',
'Tags.Tag',
];
protected $defaultConfig = [
'default_colour' => '#924da6',
'picker' => false,
'editable' => false,
];
public function control(array $options = [])
{
$field = 'tag_list';
$values = !empty($options['allTags']) ? array_map(function($tag) {
return [
'text' => h($tag['name']),
'value' => h($tag['name']),
'data-colour' => h($tag['colour']),
'data-text-colour' => h($tag['text_colour']),
];
}, $options['allTags']) : [];
$classes = ['tag-input', 'flex-grow-1'];
$url = '';
if (!empty($this->getConfig('editable'))) {
$url = $this->Url->build([
'controller' => $this->getView()->getName(),
'action' => 'tag',
$this->getView()->get('entity')['id']
]);
$classes[] = 'd-none';
}
$selectConfig = [
'multiple' => true,
'class' => $classes,
'data-url' => $url,
];
return $this->Form->select($field, $values, $selectConfig);
}
protected function picker(array $options = [])
{
$html = $this->Tag->control($options);
if (!empty($this->getConfig('editable'))) {
$html .= $this->Bootstrap->button([
'size' => 'sm',
'icon' => 'plus',
'variant' => 'secondary',
'class' => ['badge'],
'params' => [
'onclick' => 'createTagPicker(this)',
]
]);
} else {
$html .= '<script>$(document).ready(function() { initSelect2Pickers() })</script>';
}
return $html;
}
public function tags($tags = [], array $options = [])
{
$tags = is_null($tags) ? [] : $tags;
$this->_config = array_merge($this->defaultConfig, $options);
$html = '<div class="tag-container-wrapper">';
$html .= '<div class="tag-container my-1 d-flex">';
$html .= '<div class="tag-list d-inline-block">';
foreach ($tags as $tag) {
if (is_object($tag)) {
$html .= $this->tag($tag);
} else {
$html .= $this->tag([
'name' => $tag
]);
}
}
$html .= '</div>';
if (!empty($this->getConfig('picker'))) {
$html .= $this->picker($options);
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
public function tag($tag, array $options = [])
{
if (empty($this->_config)) {
$this->_config = array_merge($this->defaultConfig, $options);
}
$tag['colour'] = !empty($tag['colour']) ? $tag['colour'] : $this->getConfig('default_colour');
$textColour = !empty($tag['text_colour']) ? $tag['text_colour'] : $this->TextColour->getTextColour(h($tag['colour']));;
if (!empty($this->getConfig('editable'))) {
$deleteButton = $this->Bootstrap->button([
'size' => 'sm',
'icon' => 'times',
'class' => ['ms-1', 'border-0', "text-${textColour}"],
'variant' => 'text',
'title' => __('Delete tag'),
'params' => [
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
$this->Url->build([
'controller' => $this->getView()->getName(),
'action' => 'untag',
$this->getView()->get('entity')['id']
]),
h($tag['name'])
),
],
]);
} else {
$deleteButton = '';
}
$html = $this->Bootstrap->genNode('span', [
'class' => [
'tag',
'badge',
'mx-1',
'align-middle',
],
'title' => h($tag['name']),
'style' => sprintf('color:%s; background-color:%s', $textColour, h($tag['colour'])),
], h($tag['name']) . $deleteButton);
return $html;
}
}

View File

@ -0,0 +1,22 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'data' => array(
'description' => __('Tags can be attached to entity to quickly classify them, allowing further filtering and searches.'),
'model' => 'Tags',
'fields' => array(
array(
'field' => 'name'
),
array(
'field' => 'colour',
'type' => 'color',
),
),
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
'submit' => array(
'action' => $this->request->getParam('action')
)
)
));
?>
</div>

View File

@ -0,0 +1,79 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'simple',
'children' => [
'data' => [
'type' => 'simple',
'text' => __('Add tag'),
'popover_url' => '/tags/add'
]
]
],
[
'type' => 'context_filters',
'context_filters' => $filteringContexts
],
[
'type' => 'search',
'button' => __('Filter'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
],
[
'name' => __('Name'),
'sort' => 'name',
'element' => 'tag'
],
[
'name' => __('Counter'),
'sort' => 'couter',
'data_path' => 'counter',
],
[
'name' => __('Colour'),
'sort' => 'colour',
'data_path' => 'colour',
],
[
'name' => __('Created'),
'sort' => 'created',
'data_path' => 'created',
],
],
'title' => __('Tag index'),
'description' => __('The list of all tags existing on this instance'),
'actions' => [
[
'url' => '/tags/view',
'url_params_data_paths' => ['id'],
'icon' => 'eye'
],
[
'open_modal' => '/tags/edit/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'edit'
],
[
'open_modal' => '/tags/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash'
],
]
]
]);
echo '</div>';
?>

View File

@ -0,0 +1,32 @@
<?php
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'data' => $entity,
'fields' => [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => __('Name'),
'path' => '',
'type' => 'tag',
],
[
'key' => __('Counter'),
'path' => 'counter',
'type' => 'string',
],
[
'key' => __('Colour'),
'path' => 'colour',
],
[
'key' => __('Created'),
'path' => 'created',
],
],
'children' => []
]
);

View File

@ -0,0 +1,3 @@
.tag {
filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.5));
}

View File

@ -0,0 +1,144 @@
function createTagPicker(clicked) {
function closePicker($select, $container) {
$select.appendTo($container)
$container.parent().find('.picker-container').remove()
}
function getEditableButtons($select, $container) {
const $saveButton = $('<button></button>').addClass(['btn btn-primary btn-sm', 'align-self-start']).attr('type', 'button')
.append($('<span></span>').text('Save').addClass('text-nowrap').prepend($('<i></i>').addClass('fa fa-save me-1')))
.click(function() {
const tags = $select.select2('data').map(tag => tag.text)
addTags($select.data('url'), tags, $(this))
})
const $cancelButton = $('<button></button>').addClass(['btn btn-secondary btn-sm', 'align-self-start']).attr('type', 'button')
.append($('<span></span>').text('Cancel').addClass('text-nowrap').prepend($('<i></i>').addClass('fa fa-times me-1')))
.click(function() {
closePicker($select, $container)
})
const $buttons = $('<span></span>').addClass(['picker-action', 'btn-group']).append($saveButton, $cancelButton)
return $buttons
}
const $clicked = $(clicked)
const $container = $clicked.closest('.tag-container')
const $select = $container.parent().find('select.tag-input').removeClass('d-none')
closePicker($select, $container)
const $pickerContainer = $('<div></div>').addClass(['picker-container', 'd-flex'])
$select.prependTo($pickerContainer)
$pickerContainer.append(getEditableButtons($select, $container))
$container.parent().append($pickerContainer)
initSelect2Picker($select)
}
function deleteTag(url, tags, clicked) {
if (!Array.isArray(tags)) {
tags = [tags];
}
const data = {
tag_list: JSON.stringify(tags)
}
const $statusNode = $(clicked).closest('.tag')
const APIOptions = {
statusNode: $statusNode,
skipFeedback: true,
}
return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((apiResult) => {
let $container = $statusNode.closest('.tag-container-wrapper')
refreshTagList(apiResult, $container).then(($tagContainer) => {
$container = $tagContainer // old container might not exist anymore since it was replaced after the refresh
})
const theToast = UI.toast({
variant: 'success',
title: apiResult.message,
bodyHtml: $('<div/>').append(
$('<span/>').text('Cancel untag operation.'),
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ms-3']).text('Restore tag').click(function() {
const split = url.split('/')
const controllerName = split[1]
const id = split[3]
const urlRetag = `/${controllerName}/tag/${id}`
addTags(urlRetag, tags, $container.find('.tag-container')).then(() => {
theToast.removeToast()
})
}),
),
})
}).catch((e) => {})
}
function addTags(url, tags, $statusNode) {
const data = {
tag_list: JSON.stringify(tags)
}
const APIOptions = {
statusNode: $statusNode
}
return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((apiResult) => {
const $container = $statusNode.closest('.tag-container-wrapper')
refreshTagList(apiResult, $container)
}).catch((e) => {})
}
function refreshTagList(apiResult, $container) {
const controllerName = apiResult.url.split('/')[1]
const entityId = apiResult.data.id
const url = `/${controllerName}/viewTags/${entityId}`
return UI.reload(url, $container)
}
function initSelect2Pickers() {
$('select.tag-input').each(function() {
if (!$(this).hasClass("select2-hidden-accessible")) {
initSelect2Picker($(this))
}
})
}
function initSelect2Picker($select) {
function templateTag(state, $select) {
if (!state.id) {
return state.name;
}
if (state.colour === undefined) {
state.colour = $(state.element).data('colour')
}
if ($select !== undefined && state.text[0] === '!') {
// fetch corresponding tag and set colors?
// const baseTag = state.text.slice(1)
// const existingBaseTag = $select.find('option').filter(function() {
// return $(this).val() === baseTag
// })
// if (existingBaseTag.length > 0) {
// state.colour = existingBaseTag.data('colour')
// state.text = baseTag
// }
}
return buildTag(state)
}
const $modal = $select.closest('.modal')
$select.select2({
dropdownParent: $modal.length != 0 ? $modal.find('.modal-body') : $(document.body),
placeholder: 'Pick a tag',
tags: true,
width: '100%',
templateResult: (state) => templateTag(state),
templateSelection: (state) => templateTag(state, $select),
})
}
function buildTag(options={}) {
if (!options.colour) {
options.colour = '#924da6'
}
const $tag = $('<span/>')
.addClass(['tag', 'badge', 'align-text-top'])
.css({color: getTextColour(options.colour), 'background-color': options.colour})
.text(options.text)
return $tag
}

View File

@ -29,6 +29,10 @@ use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use Tags\Plugin as TagsPlugin;
use App\Event\SocialAuthListener;
use Cake\Event\EventManager;
/**
* Application setup class.
*
@ -44,6 +48,8 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
*/
public function bootstrap(): void
{
$this->addPlugin('ADmad/SocialAuth');
// Call parent to load bootstrap from files.
parent::bootstrap();
@ -59,6 +65,8 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
$this->addPlugin('DebugKit');
}
$this->addPlugin('Authentication');
$this->addPlugin('Tags', ['routes' => true]);
EventManager::instance()->on(new SocialAuthListener());
// Load more plugins here
}
@ -86,8 +94,32 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
// creating the middleware instance specify the cache config name by
// using it's second constructor argument:
// `new RoutingMiddleware($this, '_cake_routes_')`
->add(new RoutingMiddleware($this))
->add(new AuthenticationMiddleware($this))
->add(new RoutingMiddleware($this));
if (!empty(Configure::read('keycloak'))) {
$middlewareQueue->add(new \ADmad\SocialAuth\Middleware\SocialAuthMiddleware([
'requestMethod' => 'POST',
'loginUrl' => '/users/login',
'loginRedirect' => '/',
'userEntity' => false,
'userModel' => 'Users',
'socialProfileModel' => 'ADmad/SocialAuth.SocialProfiles',
'finder' => 'all',
'fields' => [
'password' => 'password',
],
'sessionKey' => 'Auth',
'getUserCallback' => 'getUser',
'serviceConfig' => [
'provider' => [
'keycloak' => Configure::read('keycloak.provider')
],
],
'collectionFactory' => null,
'logErrors' => true,
]));
}
$middlewareQueue->add(new AuthenticationMiddleware($this))
->add(new BodyParserMiddleware());
return $middlewareQueue;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,32 @@
{
"format": "json",
"mapping": {
"name": "data.{n}.short-team-name",
"uuid": {
"path": "data.{n}.team-name",
"override": false,
"massage": "genUUID"
},
"url": "data.{n}.website",
"contacts": "data.{n}.email",
"ISO 3166-1 Code": "data.{n}.country-code",
"website": "data.{n}.website",
"enisa-geo-group": "data.{n}.enisa-geo-group",
"is-approved": "data.{n}.is_approved",
"first-member-type": "data.{n}.first-member-type",
"team-name": "data.{n}.team-name",
"oes-coverage": {
"path": "data.{n}.oes-coverage",
"massage": "nullToEmptyString"
},
"enisa-tistatus": "data.{n}.enisa-tistatus",
"csirt-network-status": "data.{n}.csirt-network-status",
"constituency": "data.{n}.constituency",
"establishment": "data.{n}.establishment",
"email": "data.{n}.email",
"country-name": "data.{n}.country-name",
"short-team-name": "data.{n}.short-team-name",
"key": "data.{n}.key"
},
"metaTemplateUUID": "089c68c7-d97e-4f21-a798-159cd10f7864"
}

View File

@ -1,25 +0,0 @@
{
"format": "json",
"mapping": {
"name": "data.{n}.short-team-name",
"uuid": {
"path": "data.{n}.team-name",
"override": false,
"massage": "genUUID"
},
"url": "data.{n}.website",
"nationality": "data.{n}.country-name",
"membership_type": "data.{n}.first-member-type",
"email": "data.{n}.email",
"country": "data.{n}.country-name",
"official_name": "data.{n}.team-name",
"established": "data.{n}.establishment",
"website": "data.{n}.website",
"constituency": "data.{n}.constituency",
"is_approved": "data.{n}.is_approved",
"enisa_geo_group": "data.{n}.enisa-geo-group",
"oes_coverage": "data.{n}.oes-coverage",
"enisa-tistatus": "data.{n}.enisa-tistatus",
"csirt_network_status": "data.{n}.csirt-network-status"
}
}

View File

@ -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~~"
}
}

View File

@ -40,6 +40,7 @@ class AppController extends Controller
public $isRest = null;
public $restResponsePayload = null;
public $user = null;
public $breadcrumb = [];
/**
* Initialization hook method.
@ -63,9 +64,10 @@ class AppController extends Controller
]);
$this->loadModel('MetaFields');
$this->loadModel('MetaTemplates');
$table = $this->getTableLocator()->get($this->modelClass);
$this->loadComponent('CRUD', [
'request' => $this->request,
'table' => $this->{$this->modelClass},
'table' => $table,
'MetaFields' => $this->MetaFields,
'MetaTemplates' => $this->MetaTemplates
]);
@ -74,6 +76,9 @@ class AppController extends Controller
'request' => $this->request,
'Authentication' => $this->Authentication
]);
$this->loadComponent('Navigation', [
'request' => $this->request,
]);
if (Configure::read('debug')) {
Configure::write('DebugKit.panels', ['DebugKit.Packages' => true]);
Configure::write('DebugKit.forceEnable', true);
@ -97,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();
@ -107,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.'));
}
@ -121,11 +128,19 @@ 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('darkMode', !empty(Configure::read('Cerebrate.dark')));
$this->set('baseurl', Configure::read('App.fullBaseUrl'));
if (!empty($user) && !empty($user->user_settings_by_name['ui.bsTheme']['value'])) {
$this->set('bsTheme', $user->user_settings_by_name['ui.bsTheme']['value']);
} else {
$this->set('bsTheme', Configure::check('ui.bsTheme') ? Configure::read('ui.bsTheme') : 'default');
}
if ($this->modelClass == 'Tags.Tags') {
$this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
}
}
private function authApiUser(): void

View File

@ -14,12 +14,16 @@ use Cake\Error\Debugger;
class AuthKeysController extends AppController
{
public $filterFields = ['Users.username', 'authkey', 'comment', 'Users.id'];
public $quickFilterFields = ['authkey', ['comment' => true]];
public $containFields = ['Users'];
public function index()
{
$this->CRUD->index([
'filters' => ['Users.username', 'authkey', 'comment', 'Users.id'],
'quickFilters' => ['authkey', 'comment'],
'contain' => ['Users'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contain' => $this->containFields,
'exclude_fields' => ['authkey']
]);
$responsePayload = $this->CRUD->getResponsePayload();

View File

@ -10,17 +10,21 @@ use Cake\ORM\TableRegistry;
class BroodsController extends AppController
{
public $filterFields = ['Broods.name', 'Broods.uuid', 'Broods.url', 'Broods.description', 'Organisations.id', 'Broods.trusted', 'pull', 'authkey'];
public $quickFilterFields = [['Broods.name' => true], 'Broods.uuid', ['Broods.description' => true]];
public $containFields = ['Organisations'];
public function index()
{
$this->CRUD->index([
'filters' => ['Broods.name', 'Broods.uuid', 'Broods.url', 'Broods.description', 'Organisations.id', 'Broods.trusted', 'pull', 'authkey'],
'quickFilters' => [['Broods.name' => true], 'Broods.uuid', ['Broods.description' => true]],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'pull',
]
],
'contain' => ['Organisations']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -11,11 +11,13 @@ use Cake\ORM\TableRegistry;
use Cake\Core\Configure;
use Cake\Core\Configure\Engine\PhpConfig;
use Cake\Utility\Inflector;
use Cake\Routing\Router;
class ACLComponent extends Component
{
private $user = null;
protected $components = ['Navigation'];
public function initialize(array $config): void
{
@ -252,7 +254,7 @@ class ACLComponent extends Component
*/
public function setPublicInterfaces(): void
{
$this->Authentication->allowUnauthenticated(['login']);
$this->Authentication->allowUnauthenticated(['login', 'register']);
}
private function checkAccessInternal($controller, $action, $soft): bool
@ -453,448 +455,11 @@ class ACLComponent extends Component
public function getMenu()
{
$open = Configure::read('Cerebrate.open');
$menu = [
'ContactDB' => [
'Individuals' => [
'label' => __('Individuals'),
'url' => '/individuals/index',
'children' => [
'index' => [
'url' => '/individuals/index',
'label' => __('List individuals')
],
'add' => [
'url' => '/individuals/add',
'label' => __('Add individual'),
'popup' => 1
],
'view' => [
'url' => '/individuals/view/{{id}}',
'label' => __('View individual'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'edit' => [
'url' => '/individuals/edit/{{id}}',
'label' => __('Edit individual'),
'actions' => ['edit', 'delete', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'delete' => [
'url' => '/individuals/delete/{{id}}',
'label' => __('Delete individual'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'Organisations' => [
'label' => __('Organisations'),
'url' => '/organisations/index',
'children' => [
'index' => [
'url' => '/organisations/index',
'label' => __('List organisations')
],
'add' => [
'url' => '/organisations/add',
'label' => __('Add organisation'),
'popup' => 1
],
'view' => [
'url' => '/organisations/view/{{id}}',
'label' => __('View organisation'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'edit' => [
'url' => '/organisations/edit/{{id}}',
'label' => __('Edit organisation'),
'actions' => ['edit', 'delete', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'delete' => [
'url' => '/organisations/delete/{{id}}',
'label' => __('Delete organisation'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'EncryptionKeys' => [
'label' => __('Encryption keys'),
'url' => '/encryptionKeys/index',
'children' => [
'index' => [
'url' => '/encryptionKeys/index',
'label' => __('List encryption keys')
],
'add' => [
'url' => '/encryptionKeys/add',
'label' => __('Add encryption key'),
'popup' => 1
],
'edit' => [
'url' => '/encryptionKeys/edit/{{id}}',
'label' => __('Edit organisation'),
'actions' => ['edit'],
'skipTopMenu' => 1,
'popup' => 1
]
]
]
],
'Trust Circles' => [
'SharingGroups' => [
'label' => __('Sharing Groups'),
'url' => '/sharingGroups/index',
'children' => [
'index' => [
'url' => '/sharingGroups/index',
'label' => __('List sharing groups')
],
'add' => [
'url' => '/SharingGroups/add',
'label' => __('Add sharing group'),
'popup' => 1
],
'edit' => [
'url' => '/SharingGroups/edit/{{id}}',
'label' => __('Edit sharing group'),
'actions' => ['edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'delete' => [
'url' => '/SharingGroups/delete/{{id}}',
'label' => __('Delete sharing group'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
]
],
'Sync' => [
'Broods' => [
'label' => __('Broods'),
'url' => '/broods/index',
'children' => [
'index' => [
'url' => '/broods/index',
'label' => __('List broods')
],
'add' => [
'url' => '/broods/add',
'label' => __('Add brood'),
'popup' => 1
],
'view' => [
'url' => '/broods/view/{{id}}',
'label' => __('View brood'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'edit' => [
'url' => '/broods/edit/{{id}}',
'label' => __('Edit brood'),
'actions' => ['edit', 'delete', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'delete' => [
'url' => '/broods/delete/{{id}}',
'label' => __('Delete brood'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
]
],
'Administration' => [
'Roles' => [
'label' => __('Roles'),
'url' => '/roles/index',
'children' => [
'index' => [
'url' => '/roles/index',
'label' => __('List roles')
],
'add' => [
'url' => '/roles/add',
'label' => __('Add role'),
'popup' => 1
],
'view' => [
'url' => '/roles/view/{{id}}',
'label' => __('View role'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'edit' => [
'url' => '/roles/edit/{{id}}',
'label' => __('Edit role'),
'actions' => ['edit', 'delete', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'delete' => [
'url' => '/roles/delete/{{id}}',
'label' => __('Delete role'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'Users' => [
'label' => __('Users'),
'url' => '/users/index',
'children' => [
'index' => [
'url' => '/users/index',
'label' => __('List users')
],
'add' => [
'url' => '/users/add',
'label' => __('Add user'),
'popup' => 1
],
'view' => [
'url' => '/users/view/{{id}}',
'label' => __('View user'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'edit' => [
'url' => '/users/edit/{{id}}',
'label' => __('Edit user'),
'actions' => ['edit', 'delete', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'delete' => [
'url' => '/users/delete/{{id}}',
'label' => __('Delete user'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'Inbox' => [
'label' => __('Inbox'),
'url' => '/inbox/index',
'children' => [
'index' => [
'url' => '/inbox/index',
'label' => __('Inbox')
],
'outbox' => [
'url' => '/outbox/index',
'label' => __('Outbox')
],
'view' => [
'url' => '/inbox/view/{{id}}',
'label' => __('View Message'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'delete' => [
'url' => '/inbox/delete/{{id}}',
'label' => __('Delete Message'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'listProcessors' => [
'url' => '/inbox/listProcessors',
'label' => __('List Inbox Processors'),
'skipTopMenu' => 1
]
]
],
'Outbox' => [
'label' => __('Outbox'),
'url' => '/outbox/index',
'children' => [
'index' => [
'url' => '/outbox/index',
'label' => __('Outbox'),
'skipTopMenu' => 1
],
'view' => [
'url' => '/outbox/view/{{id}}',
'label' => __('View Message'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'delete' => [
'url' => '/outbox/delete/{{id}}',
'label' => __('Delete Message'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'listProcessors' => [
'url' => '/outbox/listProcessors',
'label' => __('List Outbox Processors'),
'skipTopMenu' => 1
]
]
],
'MetaTemplates' => [
'label' => __('Meta Field Templates'),
'url' => '/metaTemplates/index',
'children' => [
'index' => [
'url' => '/metaTemplates/index',
'label' => __('List Meta Templates')
],
'view' => [
'url' => '/metaTemplates/view/{{id}}',
'label' => __('View Meta Template'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'delete' => [
'url' => '/metaTemplates/delete/{{id}}',
'label' => __('Delete Meta Template'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
],
'update' => [
'url' => '/metaTemplates/update',
'label' => __('Update Meta Templates'),
'actions' => ['index', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'LocalTools' => [
'label' => __('Local Tools'),
'url' => '/localTools/index',
'children' => [
'index' => [
'url' => '/localTools/index',
'label' => __('List Connectors')
],
'viewConnector' => [
'url' => '/localTools/viewConnector/{{connector}}',
'label' => __('View Connector'),
'actions' => ['view'],
'skipTopMenu' => 1
],
'add' => [
'url' => '/localTools/add/{{connector}}',
'label' => __('Add connection'),
'actions' => ['viewConnector'],
'skipTopMenu' => 1
],
'view' => [
'url' => '/localTools/view/{{id}}',
'label' => __('View Connection'),
'actions' => ['view'],
'skipTopMenu' => 1
]
]
],
'Instance' => [
__('Instance'),
'url' => '/instance/home',
'children' => [
'migration' => [
'url' => '/instance/migrationIndex',
'label' => __('Database migration')
]
]
],
],
'Cerebrate' => [
'Roles' => [
'label' => __('Roles'),
'url' => '/roles/index',
'children' => [
'index' => [
'url' => '/roles/index',
'label' => __('List roles')
],
'view' => [
'url' => '/roles/view/{{id}}',
'label' => __('View role'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'delete' => [
'url' => '/roles/delete/{{id}}',
'label' => __('Delete Role'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'Instance' => [
__('Instance'),
'url' => '/instance/home',
'children' => [
'home' => [
'url' => '/instance/home',
'label' => __('Home')
],
]
],
'Users' => [
__('My Profile'),
'children' => [
'View My Profile' => [
'url' => '/users/view',
'label' => __('View My Profile')
],
'Edit My Profile' => [
'url' => '/users/edit',
'label' => __('Edit My Profile'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
]
],
'Open' => [
'Organisations' => [
'label' => __('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'),
'url' => '/open/individuals/index',
'children' => [
'index' => [
'url' => '/open/individuals/index',
'label' => __('List individuals')
],
],
'open' => in_array('individuals', Configure::read('Cerebrate.open'))
]
]
];
$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]);

View File

@ -7,6 +7,7 @@ use Cake\Error\Debugger;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
use Cake\View\ViewBuilder;
use Cake\ORM\TableRegistry;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
@ -34,6 +35,9 @@ class CRUDComponent extends Component
$options['filters'][] = 'quickFilter';
}
$options['filters'][] = 'filteringLabel';
if ($this->taggingSupported()) {
$options['filters'][] = 'filteringTags';
}
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
foreach ($optionFilters as $i => $filter) {
@ -49,6 +53,9 @@ class CRUDComponent extends Component
if (!empty($options['contain'])) {
$query->contain($options['contain']);
}
if ($this->taggingSupported()) {
$query->contain('Tags');
}
if (!empty($options['fields'])) {
$query->select($options['fields']);
}
@ -72,16 +79,18 @@ class CRUDComponent extends Component
$data = $this->Table->{$options['afterFind']}($data);
}
}
if (!empty($options['contextFilters'])) {
$this->setFilteringContext($options['contextFilters'], $params);
}
$this->setFilteringContext($options['contextFilters'] ?? [], $params);
$this->Controller->set('data', $data);
}
}
public function filtering(): void
{
$filters = !empty($this->Controller->filters) ? $this->Controller->filters : [];
if ($this->taggingSupported()) {
$this->Controller->set('taggingEnabled', true);
$this->setAllTags();
}
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$this->Controller->set('filters', $filters);
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/filters');
@ -140,8 +149,14 @@ class CRUDComponent extends Component
$patchEntityParams['fields'] = $params['fields'];
}
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
if (isset($params['afterSave'])) {
$params['afterSave']($data);
}
$message = __('{0} added.', $this->ObjectAlias);
if (!empty($input['metaFields'])) {
$this->saveMetaFields($data->id, $input);
@ -181,10 +196,11 @@ class CRUDComponent extends Component
}
}
}
$this->Controller->entity = $data;
$this->Controller->set('entity', $data);
}
private function prepareValidationMessage($errors)
public function prepareValidationMessage($errors)
{
$validationMessage = '';
if (!empty($errors)) {
@ -243,7 +259,11 @@ class CRUDComponent extends Component
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
$this->getMetaTemplates();
$data = $this->Table->get($id, isset($params['get']) ? $params['get'] : []);
if ($this->taggingSupported()) {
$params['contain'][] = 'Tags';
$this->setAllTags();
}
$data = $this->Table->get($id, isset($params['get']) ? $params['get'] : $params);
$data = $this->getMetaFields($id, $data);
if (!empty($params['fields'])) {
$this->Controller->set('fields', $params['fields']);
@ -257,8 +277,14 @@ class CRUDComponent extends Component
$patchEntityParams['fields'] = $params['fields'];
}
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
if (isset($params['afterSave'])) {
$params['afterSave']($data);
}
$message = __('{0} `{1}` updated.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()});
if (!empty($input['metaFields'])) {
$this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $savedData->id]);
@ -280,8 +306,9 @@ class CRUDComponent extends Component
$validationErrors = $data->getErrors();
$validationMessage = $this->prepareValidationMessage($validationErrors);
$message = __(
__('{0} could not be modified.'),
$this->ObjectAlias
'{0} could not be modified.{1}',
$this->ObjectAlias,
empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage)
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
@ -291,6 +318,7 @@ class CRUDComponent extends Component
}
}
}
$this->Controller->entity = $data;
$this->Controller->set('entity', $data);
}
@ -349,6 +377,11 @@ class CRUDComponent extends Component
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
if ($this->taggingSupported()) {
$params['contain'][] = 'Tags';
$this->setAllTags();
}
$data = $this->Table->get($id, $params);
$data = $this->attachMetaData($id, $data);
if (isset($params['afterFind'])) {
@ -403,6 +436,149 @@ class CRUDComponent extends Component
$this->Controller->render('/genericTemplates/delete');
}
public function tag($id=false): void
{
if (!$this->taggingSupported()) {
throw new Exception("Table {$this->TableAlias} does not support tagging");
}
if ($this->request->is('get')) {
$this->setAllTags();
if(!empty($id)) {
$params = [
'contain' => 'Tags',
];
$entity = $this->Table->get($id, $params);
$this->Controller->set('id', $entity->id);
$this->Controller->set('data', $entity);
$this->Controller->set('bulkEnabled', false);
} else {
$this->Controller->set('bulkEnabled', true);
}
} else if ($this->request->is('post') || $this->request->is('delete')) {
$ids = $this->getIdsOrFail($id);
$isBulk = count($ids) > 1;
$bulkSuccesses = 0;
foreach ($ids as $id) {
$params = [
'contain' => 'Tags',
];
$entity = $this->Table->get($id, $params);
$input = $this->request->getData();
$tagsToAdd = json_decode($input['tag_list']);
// patching will mirror tag in the DB, however, we only want to add tags
$input['tags'] = array_merge($tagsToAdd, $entity->tags);
$patchEntityParams = [
'fields' => ['tags'],
];
$entity = $this->Table->patchEntity($entity, $input, $patchEntityParams);
$savedData = $this->Table->save($entity);
$success = true;
if ($success) {
$bulkSuccesses++;
}
}
$message = $this->getMessageBasedOnResult(
$bulkSuccesses == count($ids),
$isBulk,
__('{0} tagged with `{1}`.', $this->ObjectAlias, $input['tag_list']),
__('All {0} have been tagged.', Inflector::pluralize($this->ObjectAlias)),
__('Could not tag {0} with `{1}`.', $this->ObjectAlias, $input['tag_list']),
__('{0} / {1} {2} have been tagged.',
$bulkSuccesses,
count($ids),
Inflector::pluralize($this->ObjectAlias)
)
);
$this->setResponseForController('tag', $bulkSuccesses, $message, $savedData);
}
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/tagForm');
}
public function untag($id=false): void
{
if (!$this->taggingSupported()) {
throw new Exception("Table {$this->TableAlias} does not support tagging");
}
if ($this->request->is('get')) {
$this->setAllTags();
if(!empty($id)) {
$params = [
'contain' => 'Tags',
];
$entity = $this->Table->get($id, $params);
$this->Controller->set('id', $entity->id);
$this->Controller->set('data', $entity);
$this->Controller->set('bulkEnabled', false);
} else {
$this->Controller->set('bulkEnabled', true);
}
} else if ($this->request->is('post') || $this->request->is('delete')) {
$ids = $this->getIdsOrFail($id);
$isBulk = count($ids) > 1;
$bulkSuccesses = 0;
foreach ($ids as $id) {
$params = [
'contain' => 'Tags',
];
$entity = $this->Table->get($id, $params);
$input = $this->request->getData();
$tagsToRemove = json_decode($input['tag_list']);
// patching will mirror tag in the DB, however, we only want to remove tags
$input['tags'] = array_filter($entity->tags, function ($existingTag) use ($tagsToRemove) {
return !in_array($existingTag->name, $tagsToRemove);
});
$patchEntityParams = [
'fields' => ['tags'],
];
$entity = $this->Table->patchEntity($entity, $input, $patchEntityParams);
$savedData = $this->Table->save($entity);
$success = true;
if ($success) {
$bulkSuccesses++;
}
}
$message = $this->getMessageBasedOnResult(
$bulkSuccesses == count($ids),
$isBulk,
__('{0} untagged with `{1}`.', $this->ObjectAlias, implode(', ', $tagsToRemove)),
__('All {0} have been untagged.', Inflector::pluralize($this->ObjectAlias)),
__('Could not untag {0} with `{1}`.', $this->ObjectAlias, $input['tag_list']),
__('{0} / {1} {2} have been untagged.',
$bulkSuccesses,
count($ids),
Inflector::pluralize($this->ObjectAlias)
)
);
$this->setResponseForController('tag', $bulkSuccesses, $message, $entity);
}
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/tagForm');
}
public function viewTags(int $id, array $params = []): void
{
if (!$this->taggingSupported()) {
throw new Exception("Table {$this->TableAlias} does not support tagging");
}
if (empty($id)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
$params['contain'][] = 'Tags';
$data = $this->Table->get($id, $params);
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data);
}
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
}
$this->Controller->set('entity', $data);
$this->setAllTags();
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/tag');
}
public function setResponseForController($action, $success, $message, $data=[], $errors=null)
{
if ($success) {
@ -494,7 +670,7 @@ class CRUDComponent extends Component
return $massagedFilters;
}
protected function setQuickFilters(array $params, \Cake\ORM\Query $query, array $quickFilterFields): \Cake\ORM\Query
public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $quickFilterFields): \Cake\ORM\Query
{
$queryConditions = [];
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
@ -521,6 +697,8 @@ class CRUDComponent extends Component
{
$filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : '';
unset($params['filteringLabel']);
$filteringTags = !empty($params['filteringTags']) && $this->taggingSupported() ? $params['filteringTags'] : '';
unset($params['filteringTags']);
$customFilteringFunction = '';
$chosenFilter = '';
if (!empty($options['contextFilters']['custom'])) {
@ -564,10 +742,26 @@ class CRUDComponent extends Component
}
}
}
if ($this->taggingSupported() && !empty($filteringTags)) {
$activeFilters['filteringTags'] = $filteringTags;
$query = $this->setTagFilters($query, $filteringTags);
}
$this->Controller->set('activeFilters', $activeFilters);
return $query;
}
protected function setTagFilters($query, $tags)
{
$modelAlias = $this->Table->getAlias();
$subQuery = $this->Table->find('tagged', [
'name' => $tags,
'forceAnd' => true
])->select($modelAlias . '.id');
return $query->where([$modelAlias . '.id IN' => $subQuery]);
}
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
{
$modelName = $filterParts[0];
@ -682,6 +876,18 @@ class CRUDComponent extends Component
return $prefixedConditions;
}
public function taggingSupported()
{
return $this->Table->behaviors()->has('Tag');
}
public function setAllTags()
{
$this->Tags = TableRegistry::getTableLocator()->get('Tags.Tags');
$allTags = $this->Tags->find()->all()->toList();
$this->Controller->set('allTags', $allTags);
}
public function toggle(int $id, string $fieldName = 'enabled', array $params = []): void
{
if (empty($id)) {

View File

@ -0,0 +1,13 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class BroodsNavigation extends BaseNavigation
{
public function addLinks()
{
$this->bcf->addLink('Broods', 'view', 'LocalTools', 'broodTools');
$this->bcf->addLink('Broods', 'edit', 'LocalTools', 'broodTools');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class EncryptionKeysNavigation extends BaseNavigation
{
}

View File

@ -0,0 +1,44 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class InboxNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('Inbox', 'index', $this->bcf->defaultCRUD('Inbox', 'index'));
$this->bcf->addRoute('Inbox', 'view', $this->bcf->defaultCRUD('Inbox', 'view'));
$this->bcf->addRoute('Inbox', 'discard', [
'label' => __('Discard request'),
'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');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class IndividualsNavigation extends BaseNavigation
{
}

View File

@ -0,0 +1,26 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class InstanceNavigation extends BaseNavigation
{
function addRoutes()
{
$this->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'
]);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class LocalToolsNavigation extends BaseNavigation
{
function addRoutes()
{
$this->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');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class MetaTemplatesNavigation extends BaseNavigation
{
function addRoutes()
{
$this->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');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class OrganisationsNavigation extends BaseNavigation
{
}

View File

@ -0,0 +1,43 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class OutboxNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('Outbox', 'index', $this->bcf->defaultCRUD('Outbox', 'index'));
$this->bcf->addRoute('Outbox', 'view', $this->bcf->defaultCRUD('Outbox', 'view'));
$this->bcf->addRoute('Outbox', 'discard', [
'label' => __('Discard request'),
'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');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class RolesNavigation extends BaseNavigation
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class SharingGroupsNavigation extends BaseNavigation
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class TagsNavigation extends BaseNavigation
{
}

View File

@ -0,0 +1,50 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class UserSettingsNavigation extends BaseNavigation
{
public function addLinks()
{
$bcf = $this->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}"
]);
}
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class UsersNavigation extends BaseNavigation
{
public function addRoutes()
{
$this->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')
]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace BreadcrumbNavigation;
class BaseNavigation
{
protected $bcf;
protected $request;
public function __construct($bcf, $request)
{
$this->bcf = $bcf;
$this->request = $request;
}
public function addRoutes() {}
public function addParents() {}
public function addLinks() {}
public function addActions() {}
}

View File

@ -0,0 +1,147 @@
<?php
namespace SidemenuNavigation;
use Cake\Core\Configure;
class Sidemenu {
private $iconTable;
private $request;
public function __construct($iconTable, $request)
{
$this->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',
],
'LocalTools.index' => [
'label' => __('Local Tools'),
'icon' => $this->iconTable['LocalTools'],
'url' => '/localTools/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',
],
'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'))
]
]
];
}
}

View File

@ -0,0 +1,388 @@
<?php
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
{
private $user = null;
public $breadcrumb = null;
public $iconToTableMapping = [
'Individuals' => 'address-book',
'Organisations' => 'building',
'EncryptionKeys' => 'key',
'SharingGroups' => 'user-friends',
'Broods' => 'network-wired',
'Roles' => 'id-badge',
'Users' => 'users',
'UserSettings' => 'user-cog',
'Inbox' => 'inbox',
'Outbox' => 'inbox',
'MetaTemplates' => 'object-group',
'LocalTools' => 'tools',
'Instance' => 'server',
'Tags' => 'tags',
];
public function initialize(array $config): void
{
$this->request = $config['request'];
}
public function beforeFilter($event)
{
$this->fullBreadcrumb = $this->genBreadcrumb();
$this->breadcrumb = $this->getBreadcrumb();
}
public function getSideMenu(): array
{
$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][$action])) {
return [[
'label' => $controller,
'url' => Router::url(['controller' => $controller, 'action' => $action]),
]]; // no breadcrumb defined for this endpoint
}
$currentRoute = $this->fullBreadcrumb[$controller][$action];
$breadcrumbPath = $this->getBreadcrumbPath($currentRoute);
return $breadcrumbPath;
}
public function getBreadcrumbPath(array $currentRoute): array
{
$path = [];
$visitedURL = [];
while (empty($visitedURL[$currentRoute['url']])) {
$visitedURL[$currentRoute['url']] = true;
$path[] = $currentRoute;
if (!empty($currentRoute['after'])) {
if (is_callable($currentRoute['after'])) {
$route = $currentRoute['after']();
} else {
$route = $currentRoute['after'];
}
if (empty($route)) {
continue;
}
$currentRoute = $route;
}
}
$path = array_reverse($path);
return $path;
}
public function genBreadcrumb(): array
{
$request = $this->request;
$bcf = new BreadcrumbFactory($this->iconToTableMapping);
$fullConfig = $this->getFullConfig($bcf, $this->request);
return $fullConfig;
}
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;
}
}

View File

@ -14,17 +14,21 @@ use Cake\Error\Debugger;
class EncryptionKeysController extends AppController
{
public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'];
public $quickFilterFields = ['encryption_key'];
public $containFields = ['Individuals', 'Organisations'];
public function index()
{
$this->CRUD->index([
'quickFilters' => ['encryption_key'],
'filters' => ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'],
'quickFilters' => $this->quickFilterFields,
'filters' => $this->filterFields,
'contextFilters' => [
'fields' => [
'type'
]
],
'contain' => ['Individuals', 'Organisations']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -16,7 +16,9 @@ use Cake\Http\Exception\ForbiddenException;
class InboxController extends AppController
{
public $filters = ['scope', 'action', 'title', 'origin', 'comment'];
public $filterFields = ['scope', 'action', 'title', 'origin', 'comment'];
public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
public $containFields = ['Users'];
public function beforeFilter(EventInterface $event)
{
@ -28,14 +30,14 @@ class InboxController extends AppController
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'scope',
]
],
'contain' => ['Users']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -9,20 +9,25 @@ use Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
use Cake\ORM\TableRegistry;
class IndividualsController extends AppController
{
public $quickFilterFields = ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true], 'position'];
public $filterFields = ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'];
public $containFields = ['Alignments' => 'Organisations'];
public function index()
{
$this->CRUD->index([
'filters' => ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'],
'quickFilters' => ['uuid', 'email', 'first_name', 'last_name', 'position'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'Alignments.type'
]
],
'contain' => ['Alignments' => 'Organisations']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -32,6 +37,11 @@ class IndividualsController extends AppController
$this->set('metaGroup', 'ContactDB');
}
public function filtering()
{
$this->CRUD->filtering();
}
public function add()
{
$this->CRUD->add();
@ -72,4 +82,31 @@ class IndividualsController extends AppController
}
$this->set('metaGroup', 'ContactDB');
}
public function tag($id)
{
$this->CRUD->tag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function untag($id)
{
$this->CRUD->untag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function viewTags($id)
{
$this->CRUD->viewTags($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
}

View File

@ -3,10 +3,13 @@
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Inflector;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\ORM\TableRegistry;
use Cake\Event\EventInterface;
use Cake\Core\Configure;
class InstanceController extends AppController
{
@ -18,7 +21,9 @@ class InstanceController extends AppController
public function home()
{
$this->set('md', file_get_contents(ROOT . '/README.md'));
// $this->set('md', file_get_contents(ROOT . '/README.md'));
$statistics = $this->Instance->getStatistics();
$this->set('statistics', $statistics);
}
public function status()
@ -29,6 +34,24 @@ class InstanceController extends AppController
return $this->RestResponse->viewData($data, 'json');
}
public function searchAll()
{
$searchValue = $this->request->getQuery('search');
$model = $this->request->getQuery('model', null);
$limit = $this->request->getQuery('limit', 5);
if (!empty($this->request->getQuery('show_all', false))) {
$limit = null;
}
$data = [];
if (!empty($searchValue)) {
$data = $this->Instance->searchAll($searchValue, $limit, $model);
}
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($data, 'json');
}
$this->set('data', $data);
}
public function migrationIndex()
{
$migrationStatus = $this->Instance->getMigrationStatus();
@ -36,6 +59,17 @@ class InstanceController extends AppController
$this->loadModel('Phinxlog');
$status = $this->Phinxlog->mergeMigrationLogIntoStatus($migrationStatus['status']);
foreach ($status as $i => $entry) {
if (!empty($entry['plugin'])) {
$pluginTablename = sprintf('%s_phinxlog', Inflector::underscore($entry['plugin']));
$pluginTablename = str_replace(['\\', '/', '.'], '_', $pluginTablename);
$status[$i] = $this->Phinxlog->mergeMigrationLogIntoStatus([$entry], $pluginTablename)[0];
}
}
usort($status, function($a, $b) {
return strcmp($b['id'], $a['id']);
});
$this->set('status', $status);
$this->set('updateAvailables', $migrationStatus['updateAvailables']);
}
@ -101,4 +135,36 @@ class InstanceController extends AppController
$this->set('path', ['controller' => 'instance', 'action' => 'rollback']);
$this->render('/genericTemplates/confirm');
}
public function settings()
{
$this->Settings = $this->getTableLocator()->get('Settings');
$all = $this->Settings->getSettings(true);
$this->set('settingsProvider', $all['settingsProvider']);
$this->set('settings', $all['settings']);
$this->set('settingsFlattened', $all['settingsFlattened']);
$this->set('notices', $all['notices']);
}
public function saveSetting()
{
if ($this->request->is('post')) {
$data = $this->ParamHandler->harvestParams([
'name',
'value'
]);
$this->Settings = $this->getTableLocator()->get('Settings');
$errors = $this->Settings->saveSetting($data['name'], $data['value']);
$message = __('Could not save setting `{0}`', $data['name']);
if (empty($errors)) {
$message = __('Setting `{0}` saved', $data['name']);
$data = $this->Settings->getSetting($data['name']);
}
$this->CRUD->setResponseForController('saveSetting', empty($errors), $message, $data, $errors);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
}
}

View File

@ -23,12 +23,16 @@ class LocalToolsController extends AppController
$this->set('metaGroup', 'Administration');
}
public function connectorIndex()
public function connectorIndex($connectorName)
{
$this->set('metaGroup', 'Admin');
$this->CRUD->index([
'filters' => ['name', 'connector'],
'quickFilters' => ['name', 'connector'],
'filterFunction' => function($query) use ($connectorName) {
$query->where(['connector' => $connectorName]);
return $query;
},
'afterFind' => function($data) {
foreach ($data as $connector) {
$connector['health'] = [$this->LocalTools->healthCheckIndividual($connector)];
@ -40,7 +44,56 @@ class LocalToolsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
$connector = $this->LocalTools->getConnectors($connectorName)[$connectorName];
$this->set('metaGroup', 'Administration');
$this->set('connectorName', $connectorName);
$this->set('connector', $connector);
}
public function batchAction($actionName)
{
$params = $this->ParamHandler->harvestParams(['connection_ids']);
$params['connection_ids'] = explode(',', $params['connection_ids']);
$connections = $this->LocalTools->query()->where(['id IN' => $params['connection_ids']])->all();
if (empty($connections)) {
throw new NotFoundException(__('Invalid connector.'));
}
$connection = $connections->first();
if ($this->request->is(['post', 'put'])) {
$actionParams = $this->LocalTools->getActionFilterOptions($connection->connector, $actionName);
$params = array_merge($params, $this->ParamHandler->harvestParams($actionParams));
$results = [];
$successes = 0;
$this->LocalTools->loadConnector($connection->connector);
foreach ($connections as $connection) {
$actionDetails = $this->LocalTools->getActionDetails($actionName);
$params['connection'] = $connection;
$tmpResult = $this->LocalTools->action($this->ACL->getUser()['id'], $connection->connector, $actionName, $params, $this->request);
$tmpResult['connection'] = $connection;
$results[$connection->id] = $tmpResult;
$successes += $tmpResult['success'] ? 1 : 0;
}
$success = $successes > 0;
$message = __('{0} / {1} operations were successful', $successes, count($results));
$this->CRUD->setResponseForController('batchAction', $success, $message, $results, $results);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
if (!empty($success)) {
$this->Flash->success($message);
$this->redirect(['controller' => 'localTools', 'action' => 'connectorIndex', $actionName]);
} else {
$this->Flash->error($message);
$this->redirect(['controller' => 'localTools', 'action' => 'connectorIndex', $actionName]);
}
} else {
$params['connection'] = $connection;
$results = $this->LocalTools->action($this->ACL->getUser()['id'], $connection->connector, $actionName, $params, $this->request);
$this->set('data', $results);
$this->set('metaGroup', 'Administration');
$this->render('/Common/getForm');
}
}
public function action($connectionId, $actionName)
@ -100,18 +153,25 @@ class LocalToolsController extends AppController
}
}
public function add($connector = false)
public function add($connectorName = false)
{
$this->CRUD->add();
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors());
$localConnectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors());
$dropdownData = ['connectors' => []];
foreach ($connectors as $connector) {
$dropdownData['connectors'][$connector['connector']] = $connector['name'];
$connector = false;
$connectors = [];
foreach ($localConnectors as $c) {
if (empty($connectorName) || $c['connector'] == $connectorName) {
$dropdownData['connectors'][$c['connector']] = $c['name'];
$connectors[] = $c;
}
}
$this->set('connectorName', $connectorName);
$this->set('connectors', $connectors);
$this->set(compact('dropdownData'));
$this->set('metaGroup', 'Administration');
}
@ -121,7 +181,7 @@ class LocalToolsController extends AppController
$connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors());
$connector = false;
foreach ($connectors as $c) {
if ($connector === false || version_compare($c['version'], $connectors['version']) > 0) {
if ($connector_name == $c['connector'] && ($connector === false || version_compare($c['version'], $connectors['version']) > 0)) {
$connector = $c;
}
}
@ -134,20 +194,25 @@ class LocalToolsController extends AppController
public function edit($id)
{
$localTool = $this->LocalTools->get($id);
$this->CRUD->edit($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
if ($this->ParamHandler->isAjax() && !empty($this->ajaxResponsePayload)) {
return $this->ajaxResponsePayload;
}
$connectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors());
$localConnectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors());
$dropdownData = ['connectors' => []];
foreach ($connectors as $connector) {
$dropdownData['connectors'][$connector['connector']] = $connector['name'];
$connector = false;
$connectors = [];
foreach ($localConnectors as $c) {
if (empty($localTool->connector) || $c['connector'] == $localTool->connector) {
$dropdownData['connectors'][$c['connector']] = $c['name'];
$connectors[] = $c;
}
}
$this->set(compact('dropdownData'));
$this->set('connectorName', $localTool->connector);
$this->set('connectors', $connectors);
$this->set('metaGroup', 'Administration');
$this->render('add');
}
@ -215,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');
}

View File

@ -9,11 +9,15 @@ use \Cake\Database\Expression\QueryExpression;
class MetaTemplateFieldsController extends AppController
{
public $quickFilterFields = ['field', 'type'];
public $filterFields = ['field', 'type', 'meta_template_id'];
public $containFields = [];
public function index()
{
$this->CRUD->index([
'filters' => ['field', 'type', 'meta_template_id'],
'quickFilters' => ['field', 'type']
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -9,6 +9,9 @@ use \Cake\Database\Expression\QueryExpression;
class MetaTemplatesController extends AppController
{
public $quickFilterFields = ['name', 'uuid', 'scope'];
public $filterFields = ['name', 'uuid', 'scope', 'namespace'];
public $containFields = ['MetaTemplateFields'];
public function update()
{
@ -34,8 +37,8 @@ class MetaTemplatesController extends AppController
public function index()
{
$this->CRUD->index([
'filters' => ['name', 'uuid', 'scope', 'namespace'],
'quickFilters' => ['name', 'uuid', 'scope'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => ['scope'],
'custom' => [
@ -49,7 +52,7 @@ class MetaTemplatesController extends AppController
],
]
],
'contain' => ['MetaTemplateFields']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -13,13 +13,15 @@ use Cake\Http\Exception\ForbiddenException;
class OrganisationsController extends AppController
{
public $filters = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
public $quickFilterFields = [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'];
public $filterFields = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
public $containFields = ['Alignments' => 'Individuals'];
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'custom' => [
[
@ -57,7 +59,7 @@ class OrganisationsController extends AppController
]
],
],
'contain' => ['Alignments' => 'Individuals']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -112,4 +114,31 @@ class OrganisationsController extends AppController
}
$this->set('metaGroup', 'ContactDB');
}
public function tag($id)
{
$this->CRUD->tag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function untag($id)
{
$this->CRUD->untag($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function viewTags($id)
{
$this->CRUD->viewTags($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
}

View File

@ -16,7 +16,9 @@ use Cake\Http\Exception\ForbiddenException;
class OutboxController extends AppController
{
public $filters = ['scope', 'action', 'title', 'comment'];
public $filterFields = ['scope', 'action', 'title', 'comment'];
public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
public $containFields = ['Users'];
public function beforeFilter(EventInterface $event)
{
@ -28,14 +30,14 @@ class OutboxController extends AppController
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]],
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
'fields' => [
'scope',
]
],
'contain' => ['Users']
'contain' => $this->containFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -12,11 +12,15 @@ use Cake\Http\Exception\ForbiddenException;
class RolesController extends AppController
{
public $filterFields = ['name', 'uuid', 'perm_admin', 'Users.id', 'perm_org_admin'];
public $quickFilterFields = ['name'];
public $containFields = [];
public function index()
{
$this->CRUD->index([
'filters' => ['name', 'uuid', 'perm_admin', 'Users.id'],
'quickFilters' => ['name']
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -10,11 +10,16 @@ use Cake\Error\Debugger;
class SharingGroupsController extends AppController
{
public $filterFields = ['SharingGroups.uuid', 'SharingGroups.name', 'description', 'releasability', 'Organisations.name', 'Organisations.uuid'];
public $quickFilterFields = ['SharingGroups.uuid', ['SharingGroups.name' => true], ['description' => true], ['releasability' => true]];
public $containFields = ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]];
public function index()
{
$this->CRUD->index([
'contain' => ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]],
'filters' => ['uuid', 'description', 'releasability', 'Organisations.name', 'Organisations.uuid']
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -0,0 +1,203 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
class UserSettingsController extends AppController
{
public $quickFilterFields = [['name' => 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);
}
}

View File

@ -6,15 +6,21 @@ use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Core\Configure;
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', 'UserSettings'];
public function index()
{
$this->CRUD->index([
'contain' => ['Individuals', 'Roles'],
'filters' => ['Users.email', 'uuid'],
'quickFilters' => ['uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'],
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -25,7 +31,12 @@ class UsersController extends AppController
public function add()
{
$this->CRUD->add();
$this->CRUD->add([
'beforeSave' => function($data) {
$this->Users->enrollUserRouter($data);
return $data;
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@ -139,21 +150,39 @@ 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');
$processor = $this->InboxProcessors->getProcessor('User', 'Registration');
$data = [
'origin' => '127.0.0.1',
'comment' => 'Hi there!, please create an account',
'data' => [
'username' => 'foobar',
'email' => 'foobar@admin.test',
'first_name' => 'foo',
'last_name' => 'bar',
],
];
$processorResult = $processor->create($data);
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']);
if (empty(Configure::read('security.registration.self-registration'))) {
throw new UnauthorizedException(__('User self-registration is not open.'));
}
if ($this->request->is('post')) {
$data = $this->request->getData();
$this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
$processor = $this->InboxProcessors->getProcessor('User', 'Registration');
$data = [
'origin' => $this->request->clientIp(),
'comment' => '-no comment-',
'data' => [
'username' => $data['username'],
'email' => $data['email'],
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'password' => $data['password'],
],
];
$processorResult = $processor->create($data);
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']);
}
$this->viewBuilder()->setLayout('login');
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Event;
use ADmad\SocialAuth\Middleware\SocialAuthMiddleware;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\Event\EventListenerInterface;
use Cake\Http\ServerRequest;
use Cake\I18n\FrozenTime;
use Cake\ORM\Locator\LocatorAwareTrait;
class SocialAuthListener implements EventListenerInterface
{
use LocatorAwareTrait;
public function implementedEvents(): array
{
return [
SocialAuthMiddleware::EVENT_AFTER_IDENTIFY => 'afterIdentify',
SocialAuthMiddleware::EVENT_BEFORE_REDIRECT => 'beforeRedirect',
// Uncomment below if you want to use the event listener to return
// an entity for a new user instead of directly using `createUser()` table method.
// SocialAuthMiddleware::EVENT_CREATE_USER => 'createUser',
];
}
public function afterIdentify(EventInterface $event, EntityInterface $user): EntityInterface
{
// Update last login time
// $user->set('last_login', new FrozenTime());
// You can access the profile using $user->social_profile
$this->getTableLocator()->get('Users')->saveOrFail($user);
return $user;
}
/**
* @param \Cake\Event\EventInterface $event
* @param string|array $url
* @param string $status
* @param \Cake\Http\ServerRequest $request
* @return void
*/
public function beforeRedirect(EventInterface $event, $url, string $status, ServerRequest $request): void
{
$messages = (array)$request->getSession()->read('Flash.flash');
// Set flash message
switch ($status) {
case SocialAuthMiddleware::AUTH_STATUS_SUCCESS:
$loggedInUser = $request->getAttribute('session')->read('Auth');
$messages[] = [
'message' => __('You are now logged in as <strong>{0}</strong> via {1}', $loggedInUser['username'], $loggedInUser['social_profile']['provider']),
'key' => 'flash',
'element' => 'flash/success',
'params' => [
'escape' => false,
'toast' => true
],
];
break;
// Auth through provider failed. Details will be logged in
// `error.log` if `logErrors` option is set to `true`.
case SocialAuthMiddleware::AUTH_STATUS_PROVIDER_FAILURE:
// Table finder failed to return user record. An e.g. of this is a
// user has been authenticated through provider but your finder has
// a condition to not return an inactivated user.
case SocialAuthMiddleware::AUTH_STATUS_FINDER_FAILURE:
$messages[] = [
'message' => __('Authentication failed'),
'key' => 'flash',
'element' => 'flash/error',
'params' => [],
];
break;
}
$request->getSession()->write('Flash.flash', $messages);
// You can return a modified redirect URL if needed.
}
public function createUser(EventInterface $event, EntityInterface $profile, Session $session): EntityInterface
{
// Create and save entity for new user as shown in "createUser()" method above
return $user;
}
}

View File

@ -25,6 +25,13 @@ class CommonConnectorTools
$this->exposedFunctions[] = $functionName;
}
public function getBatchActionFunctions(): array
{
return array_filter($this->exposedFunctions, function($function) {
return $function['type'] == 'batchAction';
});
}
public function runAction($action, $params) {
if (!in_array($action, $exposedFunctions)) {
throw new MethodNotAllowedException(__('Invalid connector function called.'));

View File

@ -94,9 +94,45 @@ class MispConnector extends CommonConnectorTools
'sort',
'direction'
]
],
'batchAPIAction' => [
'type' => 'batchAction',
'scope' => 'childAction',
'params' => [
'method',
'url',
'body',
],
'ui' => [
'text' => 'Batch API',
'icon' => 'terminal',
'variant' => 'primary',
]
]
];
public $version = '0.1';
public $settings = [
'url' => [
'type' => 'text'
],
'authkey' => [
'type' => 'text'
],
'skip_ssl' => [
'type' => 'boolean'
],
];
public function addSettingValidatorRules($validator)
{
return $validator
->requirePresence('url')
->notEmpty('url', __('An URL must be provided'))
->requirePresence('authkey')
->notEmpty('authkey', __('An Authkey must be provided'))
->lengthBetween('authkey', [40, 40], __('The authkey must be 40 character long'))
->boolean('skip_ssl');
}
public function addExposedFunction(string $functionName): void
{
@ -208,7 +244,10 @@ class MispConnector extends CommonConnectorTools
throw new NotFoundException(__('No connection object received.'));
}
$url = $this->urlAppendParams($url, $params);
$response = $this->HTTPClientPOST($url, $params['connection'], json_encode($params['body']));
if (!is_string($params['body'])) {
$params['body'] = json_encode($params['body']);
}
$response = $this->HTTPClientPOST($url, $params['connection'], $params['body']);
if ($response->isOk()) {
return $response;
} else {
@ -769,6 +808,57 @@ class MispConnector extends CommonConnectorTools
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
}
public function batchAPIAction(array $params): array
{
if ($params['request']->is(['get'])) {
return [
'data' => [
'title' => __('Execute API Request'),
'description' => __('Perform an API Request on the list of selected connections'),
'fields' => [
[
'field' => 'connection_ids',
'type' => 'hidden',
'value' => $params['connection_ids']
],
[
'field' => 'method',
'label' => __('Method'),
'type' => 'dropdown',
'options' => ['GET' => 'GET', 'POST' => 'POST']
],
[
'field' => 'url',
'label' => __('Relative URL'),
'type' => 'text',
],
[
'field' => 'body',
'label' => __('POST Body'),
'type' => 'codemirror',
],
],
'submit' => [
'action' => $params['request']->getParam('action')
],
'url' => ['controller' => 'localTools', 'action' => 'batchAction', 'batchAPIAction']
]
];
} else if ($params['request']->is(['post'])) {
if ($params['method'] == 'GET') {
$response = $this->getData($params['url'], $params);
} else {
$response = $this->postData($params['url'], $params);
}
if ($response->getStatusCode() == 200) {
return ['success' => 1, 'message' => __('API query successful'), 'data' => $response->getJson()];
} else {
return ['success' => 0, 'message' => __('API query failed'), 'data' => $response->getJson()];
}
}
throw new MethodNotAllowedException(__('Invalid http request type for the given action.'));
}
public function initiateConnection(array $params): array
{
$params['connection_settings'] = json_decode($params['connection']['settings'], true);

View File

@ -0,0 +1,169 @@
<?php
namespace App\Model\Behavior;
use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\Utility\Text;
use Cake\Utility\Security;
use \Cake\Http\Session;
use Cake\Core\Configure;
use Cake\Http\Client;
use Cake\Http\Client\FormData;
class AuthKeycloakBehavior extends Behavior
{
public function getUser(EntityInterface $profile, Session $session)
{
$userId = $session->read('Auth.User.id');
if ($userId) {
return $this->_table->get($userId);
}
$raw_profile_payload = $profile->access_token->getJwt()->getPayload();
$user = $this->extractProfileData($raw_profile_payload);
if (!$user) {
throw new \RuntimeException('Unable to save new user');
}
return $user;
}
private function extractProfileData($profile_payload)
{
$mapping = Configure::read('keycloak.mapping');
$fields = [
'org_uuid' => 'org_uuid',
'role_name' => 'role_name',
'username' => 'preferred_username',
'email' => 'email',
'first_name' => 'given_name',
'last_name' => 'family_name'
];
foreach ($fields as $field => $default) {
if (!empty($mapping[$field])) {
$fields[$field] = $mapping[$field];
}
}
$user = [
'individual' => [
'email' => $profile_payload[$fields['email']],
'first_name' => $profile_payload[$fields['first_name']],
'last_name' => $profile_payload[$fields['last_name']]
],
'user' => [
'username' => $profile_payload[$fields['username']],
],
'organisation' => [
'uuid' => $profile_payload[$fields['org_uuid']],
],
'role' => [
'name' => $profile_payload[$fields['role_name']],
]
];
$user['user']['individual_id'] = $this->_table->captureIndividual($user);
$user['user']['role_id'] = $this->_table->captureRole($user);
$existingUser = $this->_table->find()->where(['username' => $user['user']['username']])->first();
if (empty($existingUser)) {
$user['user']['password'] = Security::randomString(16);
$existingUser = $this->_table->newEntity($user['user']);
if (!$this->_table->save($existingUser)) {
return false;
}
} else {
$dirty = false;
if ($user['user']['individual_id'] != $existingUser['individual_id']) {
$existingUser['individual_id'] = $user['user']['individual_id'];
$dirty = true;
}
if ($user['user']['role_id'] != $existingUser['role_id']) {
$existingUser['role_id'] = $user['user']['role_id'];
$dirty = true;
}
$existingUser;
if ($dirty) {
if (!$this->_table->save($existingUser)) {
return false;
}
}
}
return $existingUser;
}
public function enrollUser($data): bool
{
$individual = $this->_table->Individuals->find()->where(
['id' => $data['individual_id']]
)->contain(['Organisations'])->first();
$roleConditions = [
'id' => $data['role_id']
];
if (!empty(Configure::read('keycloak.user_management.actions'))) {
$roleConditions['name'] = Configure::read('keycloak.default_role_name');
}
$role = $this->_table->Roles->find()->where($roleConditions)->first();
$orgs = [];
foreach ($individual['organisations'] as $org) {
$orgs[] = $org['uuid'];
}
$token = $this->getAdminAccessToken();
$keyCloakUser = [
'firstName' => $individual['first_name'],
'lastName' => $individual['last_name'],
'username' => $data['username'],
'email' => $individual['email'],
'attributes' => [
'role_name' => empty($role['name']) ? Configure::read('keycloak.default_role_name') : $role['name'],
'org_uuid' => empty($orgs[0]) ? '' : $orgs[0]
]
];
$keycloakConfig = Configure::read('keycloak');
$http = new Client();
$url = sprintf(
'%s/admin/realms/%s/users',
$keycloakConfig['provider']['baseUrl'],
$keycloakConfig['provider']['realm']
);
$response = $http->post(
$url,
json_encode($keyCloakUser),
[
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $token
]
]
);
return true;
}
private function getAdminAccessToken()
{
$keycloakConfig = Configure::read('keycloak');
$http = new Client();
$tokenUrl = sprintf(
'%s/realms/%s/protocol/openid-connect/token',
$keycloakConfig['provider']['baseUrl'],
$keycloakConfig['provider']['realm']
);
$response = $http->post(
$tokenUrl,
sprintf(
'grant_type=client_credentials&client_id=%s&client_secret=%s',
urlencode(Configure::read('keycloak.provider.applicationId')),
urlencode(Configure::read('keycloak.provider.applicationSecret'))
),
[
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded'
]
]
);
$parsedResponse = json_decode($response->getStringBody(), true);
return $parsedResponse['access_token'];
}
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -0,0 +1,9 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
class UserSetting extends AppModel
{
}

View File

@ -13,6 +13,7 @@ class AlignmentsTable extends AppTable
parent::initialize($config);
$this->belongsTo('Individuals');
$this->belongsTo('Organisations');
$this->addBehavior('Timestamp');
}
public function validationDefault(Validator $validator): Validator

View File

@ -18,6 +18,7 @@ class BroodsTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->BelongsTo(
'Organisations'
);

View File

@ -14,6 +14,7 @@ class EncryptionKeysTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->belongsTo(
'Individuals',
[

View File

@ -18,13 +18,7 @@ class InboxTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp', [
'events' => [
'Model.beforeSave' => [
'created' => 'new'
]
]
]);
$this->addBehavior('Timestamp');
$this->belongsTo('Users');
$this->setDisplayField('title');

View File

@ -14,6 +14,8 @@ class IndividualsTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
$this->hasMany(
'Alignments',
[

View File

@ -4,11 +4,17 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;
use Migrations\Migrations;
use Cake\Filesystem\Folder;
use Cake\Http\Exception\MethodNotAllowedException;
class InstanceTable extends AppTable
{
protected $activePlugins = ['Tags', 'ADmad/SocialAuth'];
public $seachAllTables = ['Broods', 'Individuals', 'Organisations', 'SharingGroups', 'Users', 'EncryptionKeys', ];
public function initialize(array $config): void
{
parent::initialize($config);
@ -19,10 +25,115 @@ class InstanceTable extends AppTable
return $validator;
}
public function getStatistics($days=30): array
{
$models = ['Individuals', 'Organisations', 'Alignments', 'EncryptionKeys', 'SharingGroups', 'Users', 'Broods', 'Tags.Tags'];
foreach ($models as $model) {
$table = TableRegistry::getTableLocator()->get($model);
$statistics[$model]['amount'] = $table->find()->all()->count();
if ($table->behaviors()->has('Timestamp')) {
$query = $table->find();
$query->select([
'count' => $query->func()->count('id'),
'date' => 'DATE(modified)',
])
->where(['modified >' => new \DateTime("-{$days} days")])
->group(['date'])
->order(['date']);
$data = $query->toArray();
$interval = new \DateInterval('P1D');
$period = new \DatePeriod(new \DateTime("-{$days} days"), $interval, new \DateTime());
$timeline = [];
foreach ($period as $date) {
$timeline[$date->format("Y-m-d")] = [
'time' => $date->format("Y-m-d"),
'count' => 0
];
}
foreach ($data as $entry) {
$timeline[$entry->date]['count'] = $entry->count;
}
$statistics[$model]['timeline'] = array_values($timeline);
$startCount = $table->find()->where(['modified <' => new \DateTime("-{$days} days")])->all()->count();
$endCount = $statistics[$model]['amount'];
$statistics[$model]['variation'] = $endCount - $startCount;
} else {
$statistics[$model]['timeline'] = [];
$statistics[$model]['variation'] = 0;
}
}
return $statistics;
}
public function searchAll($value, $limit=5, $model=null)
{
$results = [];
$models = $this->seachAllTables;
if (!is_null($model)) {
if (in_array($model, $this->seachAllTables)) {
$models = [$model];
} else {
return $results; // Cannot search in this model
}
}
foreach ($models as $tableName) {
$controller = $this->getController($tableName);
$table = TableRegistry::get($tableName);
$query = $table->find();
$quickFilterOptions = $this->getQuickFiltersFieldsFromController($controller);
$containFields = $this->getContainFieldsFromController($controller);
if (empty($quickFilterOptions)) {
continue; // make sure we are filtering on something
}
$params = ['quickFilter' => $value];
$query = $controller->CRUD->setQuickFilters($params, $query, $quickFilterOptions);
if (!empty($containFields)) {
$query->contain($containFields);
}
$results[$tableName]['amount'] = $query->count();
$result = $query->limit($limit)->all()->toList();
if (!empty($result)) {
$results[$tableName]['entries'] = $result;
}
}
return $results;
}
public function getController($name)
{
$controllerName = "\\App\\Controller\\{$name}Controller";
if (!class_exists($controllerName)) {
throw new MethodNotAllowedException(__('Model `{0}` does not exists', $model));
}
$controller = new $controllerName;
return $controller;
}
public function getQuickFiltersFieldsFromController($controller)
{
return !empty($controller->quickFilterFields) ? $controller->quickFilterFields : [];
}
public function getContainFieldsFromController($controller)
{
return !empty($controller->containFields) ? $controller->containFields : [];
}
public function getMigrationStatus()
{
$migrations = new Migrations();
$status = $migrations->status();
foreach ($this->activePlugins as $pluginName) {
$pluginStatus = $migrations->status([
'plugin' => $pluginName
]);
$pluginStatus = array_map(function ($entry) use ($pluginName) {
$entry['plugin'] = $pluginName;
return $entry;
}, $pluginStatus);
$status = array_merge($status, $pluginStatus);
}
$status = array_reverse($status);
$updateAvailables = array_filter($status, function ($update) {
@ -41,6 +152,8 @@ class InstanceTable extends AppTable
} else {
$migrationResult = $migrations->migrate(['target' => $version]);
}
$command = ROOT . '/bin/cake schema_cache clear';
$output = shell_exec($command);
return [
'success' => true
];
@ -57,4 +170,22 @@ class InstanceTable extends AppTable
'success' => true
];
}
public function getAvailableThemes()
{
$themesPath = ROOT . '/webroot/css/themes';
$dir = new Folder($themesPath);
$filesRegex = 'bootstrap-(?P<themename>\w+)\.css';
$themeRegex = '/' . 'bootstrap-(?P<themename>\w+)\.css' . '/';
$files = $dir->find($filesRegex);
$themes = [];
foreach ($files as $filename) {
$matches = [];
$themeName = preg_match($themeRegex, $filename, $matches);
if (!empty($matches['themename'])) {
$themes[] = $matches['themename'];
}
}
return $themes;
}
}

View File

@ -4,6 +4,7 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
use Migrations\Migrations;
use Cake\Filesystem\Folder;
@ -29,11 +30,19 @@ class LocalToolsTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('Timestamp');
}
public function validationDefault(Validator $validator): Validator
{
return $validator;
return $validator->add('settings', 'validSettings', [
'rule' => 'isValidSettings',
'provider' => 'table',
'message' => __('Invalid settings'),
'on' => function ($context) {
return !empty($context['data']['settings']);
}
]);
}
public function loadConnector(string $connectorName): void
@ -132,7 +141,8 @@ class LocalToolsTable extends AppTable
'name' => $connector_class->name,
'connector' => $connector_type,
'connector_version' => $connector_class->version,
'connector_description' => $connector_class->description
'connector_description' => $connector_class->description,
'connector_settings' => $connector_class->settings ?? []
];
if ($includeConnections) {
$connector['connections'] = $this->healthCheck($connector_type, $connector_class);
@ -297,4 +307,32 @@ class LocalToolsTable extends AppTable
}
return $connection;
}
public function isValidSettings($settings, array $context)
{
$settings = json_decode($settings, true);
$validationErrors = $this->getLocalToolsSettingValidationErrors($context['data']['connector'], $settings);
return $this->getValidationMessage($validationErrors);
}
public function getValidationMessage($validationErrors)
{
$messages = [];
foreach ($validationErrors as $key => $errors) {
$messages[] = sprintf('%s: %s', $key, implode(', ', $errors));
}
return empty($messages) ? true : implode('; ', $messages);
}
public function getLocalToolsSettingValidationErrors($connectorName, array $settings): array
{
$connector = array_values($this->getConnectors($connectorName))[0];
$errors = [];
if (method_exists($connector, 'addSettingValidatorRules')) {
$validator = new Validator();
$validator = $connector->addSettingValidatorRules($validator);
$errors = $validator->validate($settings);
}
return $errors;
}
}

View File

@ -13,13 +13,14 @@ class MetaTemplatesTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('Timestamp');
$this->hasMany(
'MetaTemplateFields',
[
'foreignKey' => 'meta_template_id'
]
);
$this->setDisplayField('field');
$this->setDisplayField('name');
}
public function validationDefault(Validator $validator): Validator

View File

@ -18,6 +18,8 @@ class OrganisationsTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
$this->hasMany(
'Alignments',
[

View File

@ -18,13 +18,7 @@ class OutboxTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp', [
'events' => [
'Model.beforeSave' => [
'created' => 'new'
]
]
]);
$this->addBehavior('Timestamp');
$this->belongsTo('Users');
$this->setDisplayField('title');

View File

@ -12,8 +12,11 @@ class PhinxlogTable extends AppTable
parent::initialize($config);
}
public function mergeMigrationLogIntoStatus(array $status): array
public function mergeMigrationLogIntoStatus(array $status, $table=null): array
{
if (!is_null($table)) {
$this->setTable($table);
}
$logs = $this->find('list', [
'keyField' => 'version',
'valueField' => function ($entry) {

View File

@ -0,0 +1,239 @@
<?php
namespace App\Settings\SettingsProvider;
use App\Model\Table\AppTable;
use Cake\Validation\Validator;
use Cake\ORM\TableRegistry;
class BaseSettingsProvider
{
protected $settingsConfiguration = [];
protected $error_critical = '',
$error_warning = '',
$error_info = '';
protected $severities = ['info', 'warning', 'critical'];
public function __construct()
{
$this->settingsConfiguration = $this->generateSettingsConfiguration();
$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.');
if (!isset($this->settingValidator)) {
$this->settingValidator = new SettingValidator();
}
}
/**
* Supports up to 3 levels:
* Application -> Network -> Proxy -> Proxy.URL
* page -> [group] -> [panel] -> setting
* Keys of setting configuration are the actual setting name.
* Accepted setting configuration:
* name [required]: The human readable name of the setting.
* type [required]: The type of the setting.
* description [required]: A description of the setting.
* Default severity level is `info` if a `default` value is provided otherwise it becomes `critical`
* default [optional]: The default value of the setting if not specified in the configuration.
* options [optional]: Used to populate the select with options. Keys are values to be saved, values are human readable version of the value.
* Required paramter if `type` == `select`.
* severity [optional]: Severity level of the setting if the configuration is incorrect.
* dependsOn [optional]: If the validation of this setting depends on the validation of the provided setting name
* test [optional]: Could be either a string or an anonymous function to be called in order to warn user if setting is invalid.
* Could be either: `string`, `boolean`, `integer`, `select`
* beforeSave [optional]: Could be either a string or an anonymous function to be called in order to block a setting to be saved.
* afterSave [optional]: Could be either a string or an anonymous function to be called allowing to execute a function after the setting is saved.
* redacted [optional]: Should the setting value be redacted. FIXME: To implement
* cli_only [optional]: Should this setting be modified only via the CLI.
*/
protected function generateSettingsConfiguration()
{
return [];
}
/**
* getSettingsConfiguration Return the setting configuration and merge existing settings into it if provided
*
* @param null|array $settings - Settings to be merged in the provided setting configuration
* @return array
*/
public function getSettingsConfiguration($settings = null) {
$settingConf = $this->settingsConfiguration;
if (!is_null($settings)) {
$settingConf = $this->mergeSettingsIntoSettingConfiguration($settingConf, $settings);
}
return $settingConf;
}
/**
* mergeSettingsIntoSettingConfiguration Inject the provided settings into the configuration while performing depencency and validation checks
*
* @param array $settingConf the setting configuration to have the setting injected into
* @param array $settings the settings
* @return void
*/
protected function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
{
foreach ($settingConf as $key => $value) {
if ($this->isSettingMetaKey($key)) {
continue;
}
if ($this->isLeaf($value)) {
if (isset($settings[$key])) {
$settingConf[$key]['value'] = $settings[$key];
}
$settingConf[$key] = $this->evaluateLeaf($settingConf[$key], $settingConf);
$settingConf[$key]['setting-path'] = $path;
$settingConf[$key]['true-name'] = $key;
} else {
$currentPath = empty($path) ? $key : sprintf('%s.%s', $path, $key);
$settingConf[$key] = $this->mergeSettingsIntoSettingConfiguration($value, $settings, $currentPath);
}
}
return $settingConf;
}
public function flattenSettingsConfiguration(array $settingsProvider, $flattenedSettings=[]): array
{
foreach ($settingsProvider as $key => $value) {
if ($this->isSettingMetaKey($key)) {
continue;
}
if ($this->isLeaf($value)) {
$flattenedSettings[$key] = $value;
} else {
$flattenedSettings = $this->flattenSettingsConfiguration($value, $flattenedSettings);
}
}
return $flattenedSettings;
}
/**
* getNoticesFromSettingsConfiguration Summarize the validation errors
*
* @param array $settingsProvider the setting configuration having setting value assigned
* @return void
*/
public function getNoticesFromSettingsConfiguration(array $settingsProvider): array
{
$notices = [];
foreach ($settingsProvider as $key => $value) {
if ($this->isSettingMetaKey($key)) {
continue;
}
if ($this->isLeaf($value)) {
if (!empty($value['error'])) {
if (empty($notices[$value['severity']])) {
$notices[$value['severity']] = [];
}
$notices[$value['severity']][] = $value;
}
} else {
$notices = array_merge_recursive($notices, $this->getNoticesFromSettingsConfiguration($value));
}
}
return $notices;
}
protected function isLeaf($setting)
{
return !empty($setting['name']) && !empty($setting['type']);
}
protected function evaluateLeaf($setting, $settingSection)
{
$skipValidation = false;
if ($setting['type'] == 'select' || $setting['type'] == 'multi-select') {
if (!empty($setting['options']) && is_callable($setting['options'])) {
$setting['options'] = $setting['options']($this);
}
}
if (isset($setting['dependsOn'])) {
$parentSetting = null;
foreach ($settingSection as $settingSectionName => $settingSectionConfig) {
if ($settingSectionName == $setting['dependsOn']) {
$parentSetting = $settingSectionConfig;
}
}
if (!is_null($parentSetting)) {
$parentSetting = $this->evaluateLeaf($parentSetting, $settingSection);
$skipValidation = $parentSetting['error'] === true || empty($parentSetting['value']);
}
}
$setting['error'] = false;
if (!$skipValidation) {
$validationResult = true;
if (!isset($setting['value'])) {
$validationResult = $this->settingValidator->testEmptyBecomesDefault(null, $setting);
} else if (isset($setting['test'])) {
$setting['value'] = $setting['value'] ?? '';
$validationResult = $this->evaluateFunctionForSetting($setting['test'], $setting);
}
if ($validationResult !== true) {
$setting['severity'] = $setting['severity'] ?? 'warning';
if (!in_array($setting['severity'], $this->severities)) {
$setting['severity'] = 'warning';
}
$setting['errorMessage'] = $validationResult;
}
$setting['error'] = $validationResult !== true ? true : false;
}
return $setting;
}
/**
* evaluateFunctionForSetting - evaluate the provided function. If function could not be evaluated, its result is defaulted to true
*
* @param mixed $fun
* @param array $setting
* @return mixed
*/
public function evaluateFunctionForSetting($fun, $setting)
{
$functionResult = true;
if (is_callable($fun)) { // Validate with anonymous function
$functionResult = $fun($setting['value'], $setting, new Validator());
} else if (method_exists($this->settingValidator, $fun)) { // Validate with function defined in settingValidator class
$functionResult = $this->settingValidator->{$fun}($setting['value'], $setting);
} else {
$validator = new Validator();
if (method_exists($validator, $fun)) { // Validate with cake's validator function
$validator->{$fun};
$functionResult = $validator->validate($setting['value']);
}
}
return $functionResult;
}
function isSettingMetaKey($key)
{
return substr($key, 0, 1) == '_';
}
}
class SettingValidator
{
public function testEmptyBecomesDefault($value, &$setting)
{
if (!empty($value)) {
return true;
} else if (isset($setting['default'])) {
$setting['value'] = $setting['default'];
$setting['severity'] = $setting['severity'] ?? 'info';
if ($setting['type'] == 'boolean') {
return __('Setting is not set, fallback to default value: {0}', empty($setting['default']) ? 'false' : 'true');
} else {
return __('Setting is not set, fallback to default value: {0}', $setting['default']);
}
} else {
$setting['severity'] = $setting['severity'] ?? 'critical';
return __('Cannot be empty. Setting does not have a default value.');
}
}
public function testForEmpty($value, &$setting)
{
return !empty($value) ? true : __('Cannot be empty');
}
}

View File

@ -0,0 +1,342 @@
<?php
namespace App\Settings\SettingsProvider;
use Cake\ORM\TableRegistry;
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'BaseSettingsProvider.php');
use App\Settings\SettingsProvider\BaseSettingsProvider;
use App\Settings\SettingsProvider\SettingValidator;
class CerebrateSettingsProvider extends BaseSettingsProvider
{
public function __construct()
{
$this->settingValidator = new CerebrateSettingValidator();
parent::__construct();
}
public function retrieveSettingPathsBasedOnBlueprint(): array
{
$blueprint = $this->generateSettingsConfiguration();
$paths = [];
foreach ($blueprint as $l1) {
foreach ($l1 as $l2) {
foreach ($l2 as $l3) {
foreach ($l3 as $k => $v) {
if ($k[0] !== '_') {
$paths[] = $k;
}
}
}
}
}
return $paths;
}
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'
],
],
],
],
'Authentication' => [
'Providers' => [
'KeyCloak' => [
'keycloak.enabled' => [
'name' => 'Enabled',
'type' => 'boolean',
'severity' => 'warning',
'description' => __('Enable keycloak authentication'),
'default' => false,
],
'keycloak.provider.applicationId' => [
'name' => 'Client ID',
'type' => 'string',
'severity' => 'info',
'default' => '',
'description' => __('The Client ID configured for Cerebrate.'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.provider.applicationSecret' => [
'name' => 'Client Secret',
'type' => 'string',
'severity' => 'info',
'default' => '',
'description' => __('The client secret in Cerebrate used to request tokens.'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.provider.realm' => [
'name' => 'Realm',
'type' => 'string',
'severity' => 'info',
'default' => '',
'description' => __('The realm under which the Cerebrate client is enrolled in KeyCloak.'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.provider.baseUrl' => [
'name' => 'Baseurl',
'type' => 'string',
'severity' => 'info',
'default' => '',
'description' => __('The baseurl of the keycloak authentication endpoint, such as https://foo.bar/baz/auth.'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.authoritative' => [
'name' => 'Authoritative',
'type' => 'boolean',
'severity' => 'info',
'description' => __('Override local role and organisation settings based on the settings in KeyCloak'),
'default' => false,
'dependsOn' => 'keycloak.enabled'
],
'keycloak.default_role_name' => [
'name' => 'Default role',
'type' => 'select',
'severity' => 'info',
'description' => __('Select the default role name to be used when creating users'),
'options' => function ($settingsProviders) {
$roleTable = TableRegistry::getTableLocator()->get('Roles');
$allRoleNames = $roleTable->find()->toArray();
$allRoleNames = array_column($allRoleNames, 'name');
return array_combine($allRoleNames, $allRoleNames);
},
'dependsOn' => 'keycloak.enabled'
],
'keycloak.mapping.org_uuid' => [
'name' => 'org_uuid mapping',
'type' => 'string',
'severity' => 'info',
'default' => 'org_uuid',
'description' => __('org_uuid mapped name in keycloak'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.mapping.role_name' => [
'name' => 'role_name mapping',
'type' => 'string',
'severity' => 'info',
'default' => 'role_name',
'description' => __('role_name mapped name in keycloak'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.mapping.username' => [
'name' => 'username mapping',
'type' => 'string',
'severity' => 'info',
'default' => 'preferred_username',
'description' => __('username mapped name in keycloak'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.mapping.email' => [
'name' => 'email mapping',
'type' => 'string',
'severity' => 'info',
'default' => 'email',
'description' => __('email mapped name in keycloak'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.mapping.first_name' => [
'name' => 'first_name mapping',
'type' => 'string',
'severity' => 'info',
'default' => 'given_name',
'description' => __('first_name mapped name in keycloak'),
'dependsOn' => 'keycloak.enabled'
],
'keycloak.mapping.family_name' => [
'name' => 'family_name mapping',
'type' => 'string',
'severity' => 'info',
'default' => 'family_name',
'description' => __('family_name mapped name in keycloak'),
'dependsOn' => 'keycloak.enabled'
],
]
]
],
'Security' => [
'Registration' => [
'Registration' => [
'security.registration.self-registration' => [
'name' => __('Allow self-registration'),
'type' => 'boolean',
'description' => __('Enable the self-registration feature where user can request account creation. Admin can view the request and accept it in the application inbox.'),
'default' => false,
],
]
],
'Development' => [
'Debugging' => [
'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;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Settings\SettingsProvider;
use Cake\ORM\TableRegistry;
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'BaseSettingsProvider.php');
use App\Settings\SettingsProvider\BaseSettingsProvider;
class UserSettingsProvider extends BaseSettingsProvider
{
protected function generateSettingsConfiguration()
{
return [
__('Appearance') => [
__('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') => [
]
];
}
}

View File

@ -0,0 +1,135 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Core\Configure;
use Cake\Error\Debugger;
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'CerebrateSettingsProvider.php');
use App\Settings\SettingsProvider\CerebrateSettingsProvider;
class SettingsTable extends AppTable
{
private static $FILENAME = 'cerebrate';
private static $CONFIG_KEY = 'Cerebrate';
private static $DUMPABLE = [
'Cerebrate',
'proxy',
'ui',
'keycloak',
'app'
];
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable(false);
$this->SettingsProvider = new CerebrateSettingsProvider();
}
public function getSettings($full=false): array
{
$settings = $this->readSettings();
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 getSetting($name=false): array
{
$settings = $this->readSettings();
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
$settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
return $settingsFlattened[$name] ?? [];
}
public function saveSetting(string $name, string $value): array
{
$errors = [];
$setting = $this->getSetting($name);
$value = $this->normaliseValue($value, $setting);
if ($setting['type'] == 'select') {
if (!in_array($value, array_keys($setting['options']))) {
$errors[] = __('Invalid option provided');
}
}
if ($setting['type'] == 'multi-select') {
foreach ($value as $v) {
if (!in_array($v, array_keys($setting['options']))) {
$errors[] = __('Invalid option provided');
}
}
}
if (empty($errors) && !empty($setting['beforeSave'])) {
$setting['value'] = $value ?? '';
$beforeSaveResult = $this->SettingsProvider->evaluateFunctionForSetting($setting['beforeSave'], $setting);
if ($beforeSaveResult !== true) {
$errors[] = $beforeSaveResult;
}
}
if (empty($errors)) {
$saveResult = $this->saveSettingOnDisk($name, $value);
if ($saveResult) {
if (!empty($setting['afterSave'])) {
$this->SettingsProvider->evaluateFunctionForSetting($setting['afterSave'], $setting);
}
}
}
return $errors;
}
private function normaliseValue($value, $setting)
{
if ($setting['type'] == 'boolean') {
return (bool) $value;
}
if ($setting['type'] == 'multi-select') {
if (!is_array($value)) {
$value = json_decode($value);
}
}
return $value;
}
private function readSettings()
{
$settingPaths = $this->SettingsProvider->retrieveSettingPathsBasedOnBlueprint();
$settings = [];
foreach ($settingPaths as $path) {
if (Configure::check($path)) {
$settings[$path] = Configure::read($path);
}
}
return $settings;
}
private function loadSettings(): void
{
$settings = file_get_contents(CONFIG . 'config.json');
$settings = json_decode($settings, true);
foreach ($settings as $path => $setting) {
Configure::write($path, $setting);
}
}
private function saveSettingOnDisk($name, $value)
{
$settings = $this->readSettings();
$settings[$name] = $value;
$settings = json_encode($settings, JSON_PRETTY_PRINT);
file_put_contents(CONFIG . 'config.json', $settings);
$this->loadSettings();
return true;
}
}

View File

@ -14,6 +14,7 @@ class SharingGroupsTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->belongsTo(
'Users'
);

View File

@ -0,0 +1,138 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'UserSettingsProvider.php');
use App\Settings\SettingsProvider\UserSettingsProvider;
class UserSettingsTable extends AppTable
{
protected $BOOKMARK_SETTING_NAME = 'ui.bookmarks';
public function initialize(array $config): void
{
parent::initialize($config);
$this->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;
}
}

View File

@ -7,13 +7,20 @@ use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\RulesChecker;
use Cake\ORM\TableRegistry;
use \Cake\Datasource\EntityInterface;
use \Cake\Http\Session;
use Cake\Http\Client;
use Cake\Utility\Security;
use Cake\Core\Configure;
class UsersTable extends AppTable
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('Timestamp');
$this->addBehavior('UUID');
$this->initAuthBehaviors();
$this->belongsTo(
'Individuals',
[
@ -28,9 +35,23 @@ class UsersTable extends AppTable
'cascadeCallbacks' => false
]
);
$this->hasMany(
'UserSettings',
[
'dependent' => true,
'cascadeCallbacks' => true
]
);
$this->setDisplayField('username');
}
private function initAuthBehaviors()
{
if (!empty(Configure::read('keycloak'))) {
$this->addBehavior('AuthKeycloak');
}
}
public function validationDefault(Validator $validator): Validator
{
$validator
@ -93,4 +114,51 @@ class UsersTable extends AppTable
}
return true;
}
public function captureIndividual($user): int
{
$individual = $this->Individuals->find()->where(['email' => $user['individual']['email']])->first();
if (empty($individual)) {
$individual = $this->Individuals->newEntity($user['individual']);
if (!$this->Individuals->save($individual)) {
throw new BadRequestException(__('Could not save the associated individual'));
}
}
return $individual->id;
}
public function captureOrganisation($user): int
{
$organisation = $this->Organisations->find()->where(['uuid' => $user['organisation']['uuid']])->first();
if (empty($organisation)) {
$user['organisation']['name'] = $user['organisation']['uuid'];
$organisation = $this->Organisations->newEntity($user['organisation']);
if (!$this->Organisations->save($organisation)) {
throw new BadRequestException(__('Could not save the associated organisation'));
}
}
return $organisation->id;
}
public function captureRole($user): int
{
$role = $this->Roles->find()->where(['name' => $user['role']['name']])->first();
if (empty($role)) {
if (!empty(Configure::read('keycloak.default_role_name'))) {
$default_role_name = Configure::read('keycloak.default_role_name');
$role = $this->Roles->find()->where(['name' => $default_role_name])->first();
}
if (empty($role)) {
throw new NotFoundException(__('Invalid role'));
}
}
return $role->id;
}
public function enrollUserRouter($data): void
{
if (!empty(Configure::read('keycloak'))) {
$this->enrollUser($data);
}
}
}

View File

@ -43,5 +43,6 @@ class AppView extends View
$this->loadHelper('PrettyPrint');
$this->loadHelper('FormFieldMassage');
$this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']);
$this->loadHelper('Tags.Tag');
}
}

View File

@ -65,16 +65,28 @@ class BootstrapHelper extends Helper
public function table($options, $data)
{
$bsTable = new BoostrapTable($options, $data);
$bsTable = new BoostrapTable($options, $data, $this);
return $bsTable->table();
}
public function listTable($options, $data)
{
$bsListTable = new BoostrapListTable($options, $data, $this);
return $bsListTable->table();
}
public function button($options)
{
$bsButton = new BoostrapButton($options);
return $bsButton->button();
}
public function icon($icon, $options=[])
{
$bsIcon = new BoostrapIcon($icon, $options);
return $bsIcon->icon();
}
public function badge($options)
{
$bsBadge = new BoostrapBadge($options);
@ -110,22 +122,45 @@ class BootstrapHelper extends Helper
$bsProgressTimeline = new BoostrapProgressTimeline($options, $this);
return $bsProgressTimeline->progressTimeline();
}
public function listGroup($options, $data)
{
$bsListGroup = new BootstrapListGroup($options, $data, $this);
return $bsListGroup->listGroup();
}
public function genNode($node, $params=[], $content='')
{
return BootstrapGeneric::genNode($node, $params, $content);
}
public function switch($options)
{
$bsSwitch = new BoostrapSwitch($options, $this);
return $bsSwitch->switch();
}
public function dropdownMenu($options)
{
$bsDropdownMenu = new BoostrapDropdownMenu($options, $this);
return $bsDropdownMenu->dropdownMenu();
}
}
class BootstrapGeneric
{
public static $variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent'];
public static $textClassByVariants = [
'primary' => 'text-white',
'secondary' => 'text-white',
'success' => 'text-white',
'danger' => 'text-white',
'warning' => 'text-black',
'info' => 'text-white',
'light' => 'text-black',
'dark' => 'text-white',
'white' => 'text-black',
'transparent' => 'text-black'
'primary' => 'text-light',
'secondary' => 'text-light',
'success' => 'text-light',
'danger' => 'text-light',
'warning' => 'text-dark',
'info' => 'text-light',
'light' => 'text-dark',
'dark' => 'text-light',
'white' => 'text-dark',
'transparent' => 'text-dark'
];
protected $allowedOptionValues = [];
protected $options = [];
@ -142,7 +177,7 @@ class BootstrapGeneric
}
}
protected static function genNode($node, $params=[], $content="")
public static function genNode($node, $params=[], $content="")
{
return sprintf('<%s %s>%s</%s>', $node, BootstrapGeneric::genHTMLParams($params), $content, $node);
}
@ -161,7 +196,9 @@ class BootstrapGeneric
{
$html = '';
foreach ($params as $k => $v) {
$html .= BootstrapGeneric::genHTMLParam($k, $v) . ' ';
if (!empty($k) && !empty($v)) {
$html .= BootstrapGeneric::genHTMLParam($k, $v) . ' ';
}
}
return $html;
}
@ -178,12 +215,10 @@ class BootstrapGeneric
{
return BootstrapGeneric::genNode('button', [
'type' => 'button',
'class' => 'close',
'data-dismiss' => $dismissTarget,
'class' => 'btn-close',
'data-bs-dismiss' => $dismissTarget,
'arial-label' => __('Close')
], BootstrapGeneric::genNode('span', [
'arial-hidden' => 'true'
], '&times;'));
]);
}
protected static function getTextClassForVariant($variant)
@ -201,8 +236,9 @@ class BootstrapTabs extends BootstrapGeneric
'vertical' => false,
'vertical-size' => 3,
'card' => false,
'header-variant' => 'light',
'header-variant' => '',
'body-variant' => '',
'body-class' => [],
'nav-class' => [],
'nav-item-class' => [],
'content-class' => [],
@ -217,7 +253,7 @@ class BootstrapTabs extends BootstrapGeneric
$this->allowedOptionValues = [
'justify' => [false, 'center', 'end'],
'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
'header-variant' => BootstrapGeneric::$variants,
'header-variant' => array_merge(BootstrapGeneric::$variants, ['']),
];
$this->processOptions($options);
}
@ -238,7 +274,6 @@ class BootstrapTabs extends BootstrapGeneric
$this->bsClasses = [
'nav' => [],
'nav-item' => $this->options['nav-item-class'],
];
if (!empty($this->options['justify'])) {
@ -286,7 +321,9 @@ class BootstrapTabs extends BootstrapGeneric
}
$this->data['navs'][$activeTab]['active'] = true;
$this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size'];
if (!empty($this->options['vertical-size']) && $this->options['vertical-size'] != 'auto') {
$this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size'];
}
$this->options['header-text-variant'] = $this->options['header-variant'] == 'light' ? 'body' : 'white';
$this->options['header-border-variant'] = $this->options['header-variant'] == 'light' ? '' : $this->options['header-variant'];
@ -321,7 +358,13 @@ class BootstrapTabs extends BootstrapGeneric
$html .= $this->genNav();
if ($this->options['card']) {
$html .= $this->closeNode('div');
$html .= $this->openNode('div', ['class' => array_merge(['card-body'], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]);
$html .= $this->openNode('div', [
'class' => array_merge(
['card-body'],
$this->options['body-class'] ?? [],
["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"]
)
]);
}
$html .= $this->genContent();
if ($this->options['card']) {
@ -333,11 +376,37 @@ class BootstrapTabs extends BootstrapGeneric
private function genVerticalTabs()
{
$html = $this->openNode('div', ['class' => array_merge(['row', ($this->options['card'] ? 'card flex-row' : '')], ["border-{$this->options['header-border-variant']}"])]);
$html .= $this->openNode('div', ['class' => array_merge(['col-' . $this->options['vertical-size'], ($this->options['card'] ? 'card-header border-right' : '')], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}", "border-{$this->options['header-border-variant']}"])]);
$html = $this->openNode('div', ['class' => array_merge(
[
'row',
($this->options['card'] ? 'card flex-row' : ''),
($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '')
],
[
"border-{$this->options['header-border-variant']}"
]
)]);
$html .= $this->openNode('div', ['class' => array_merge(
[
($this->options['vertical-size'] != 'auto' ? 'col-' . $this->options['vertical-size'] : ''),
($this->options['card'] ? 'card-header border-end' : '')
],
[
"bg-{$this->options['header-variant']}",
"text-{$this->options['header-text-variant']}",
"border-{$this->options['header-border-variant']}"
])]);
$html .= $this->genNav();
$html .= $this->closeNode('div');
$html .= $this->openNode('div', ['class' => array_merge(['col-' . (12 - $this->options['vertical-size']), ($this->options['card'] ? 'card-body2' : '')], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]);
$html .= $this->openNode('div', ['class' => array_merge(
[
($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''),
($this->options['card'] ? 'card-body2' : '')
],
[
"bg-{$this->options['body-variant']}",
"text-{$this->options['body-text-variant']}"
])]);
$html .= $this->genContent();
$html .= $this->closeNode('div');
$html .= $this->closeNode('div');
@ -369,7 +438,7 @@ class BootstrapTabs extends BootstrapGeneric
[!empty($navItem['active']) ? 'active' : ''],
[!empty($navItem['disabled']) ? 'disabled' : '']
),
'data-toggle' => $this->options['pills'] ? 'pill' : 'tab',
'data-bs-toggle' => $this->options['pills'] ? 'pill' : 'tab',
'id' => $navItem['id'] . '-tab',
'href' => '#' . $navItem['id'],
'aria-controls' => $navItem['id'],
@ -487,7 +556,7 @@ class BoostrapTable extends BootstrapGeneric {
private $bsClasses = null;
function __construct($options, $data) {
function __construct($options, $data, $btHelper) {
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, [''])
];
@ -495,6 +564,7 @@ class BoostrapTable extends BootstrapGeneric {
$this->fields = $data['fields'];
$this->items = $data['items'];
$this->caption = !empty($data['caption']) ? $data['caption'] : '';
$this->btHelper = $btHelper;
}
private function processOptions($options)
@ -588,21 +658,26 @@ 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],
'field' => ['path' => '0']
]);
} else {
$cellContent = h($value);
}
@ -619,6 +694,145 @@ class BoostrapTable extends BootstrapGeneric {
}
}
class BoostrapListTable extends BootstrapGeneric {
private $defaultOptions = [
'striped' => true,
'bordered' => false,
'borderless' => false,
'hover' => true,
'small' => false,
'variant' => '',
'tableClass' => [],
'bodyClass' => [],
];
private $bsClasses = null;
function __construct($options, $data, $btHelper) {
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, [''])
];
$this->processOptions($options);
$this->fields = $data['fields'];
$this->item = $data['item'];
$this->caption = !empty($data['caption']) ? $data['caption'] : '';
$this->btHelper = $btHelper;
}
private function processOptions($options)
{
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
}
public function table()
{
return $this->genTable();
}
private function genTable()
{
$html = $this->openNode('table', [
'class' => [
'table',
"table-{$this->options['variant']}",
$this->options['striped'] ? 'table-striped' : '',
$this->options['bordered'] ? 'table-bordered' : '',
$this->options['borderless'] ? 'table-borderless' : '',
$this->options['hover'] ? 'table-hover' : '',
$this->options['small'] ? 'table-sm' : '',
!empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
!empty($this->options['tableClass']) ? (is_array($this->options['tableClass']) ? implode(' ', $this->options['tableClass']) : $this->options['tableClass']) : ''
],
'id' => $this->options['id'] ?? ''
]);
$html .= $this->genCaption();
$html .= $this->genBody();
$html .= $this->closeNode('table');
return $html;
}
private function genBody()
{
$body = $this->openNode('tbody', [
'class' => [
!empty($this->options['bodyClass']) ? (is_array($this->options['bodyClass']) ? implode(' ', $this->options['bodyClass']) : $this->options['bodyClass']) : ''
],
]);
foreach ($this->fields as $i => $field) {
$body .= $this->genRow($field);
}
$body .= $this->closeNode('tbody');
return $body;
}
private function genRow($field)
{
$rowValue = $this->genCell($field);
$rowKey = $this->genNode('th', [
'class' => [
'col-sm-2'
],
'scope' => 'row'
], h($field['key']));
$row = $this->genNode('tr',[
'class' => [
'd-flex',
!empty($field['_rowVariant']) ? "table-{$field['_rowVariant']}" : ''
]
], implode('', [$rowKey, $rowValue]));
return $row;
}
private function genCell($field=[])
{
if (isset($field['raw'])) {
$cellContent = h($field['raw']);
} else if (isset($field['formatter'])) {
$cellContent = $field['formatter']($this->getValueFromObject($field), $this->item);
} else if (isset($field['type'])) {
$cellContent = $this->btHelper->getView()->element($this->getElementPath($field['type']), [
'data' => $this->item,
'field' => $field
]);
} else {
$cellContent = h($this->getValueFromObject($field));
}
return $this->genNode('td', [
'class' => [
'col-sm-10',
!empty($row['_cellVariant']) ? "bg-{$row['_cellVariant']}" : ''
]
], $cellContent);
}
private function getValueFromObject($field)
{
if (is_array($field)) {
$key = $field['path'];
} else {
$key = $field;
}
$cellValue = Hash::get($this->item, $key);
return $cellValue;
}
private function getElementPath($type)
{
return sprintf('%s%sField',
$this->options['elementsRootPath'] ?? '',
$type
);
}
private function genCaption()
{
return !empty($this->caption) ? $this->genNode('caption', [], h($this->caption)) : '';
}
}
class BoostrapButton extends BootstrapGeneric {
private $defaultOptions = [
'id' => '',
@ -627,11 +841,12 @@ class BoostrapButton extends BootstrapGeneric {
'variant' => 'primary',
'outline' => false,
'size' => '',
'block' => false,
'icon' => null,
'image' => null,
'class' => [],
'type' => 'button',
'nodeType' => 'button',
'title' => '',
'params' => [],
'badge' => false
];
@ -640,7 +855,7 @@ class BoostrapButton extends BootstrapGeneric {
function __construct($options) {
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
'size' => ['', 'sm', 'lg'],
'type' => ['button', 'submit', 'reset']
];
@ -663,10 +878,11 @@ class BoostrapButton extends BootstrapGeneric {
$this->bsClasses[] = "btn-{$this->options['variant']}";
}
if (!empty($this->options['size'])) {
$this->bsClasses[] = "btn-$this->options['size']";
$this->bsClasses[] = "btn-{$this->options['size']}";
}
if ($this->options['block']) {
$this->bsClasses[] = 'btn-block';
if ($this->options['variant'] == 'text') {
$this->bsClasses[] = 'p-0';
$this->bsClasses[] = 'lh-1';
}
}
@ -680,10 +896,12 @@ class BoostrapButton extends BootstrapGeneric {
$html = $this->openNode($this->options['nodeType'], array_merge($this->options['params'], [
'class' => array_merge($this->options['class'], $this->bsClasses),
'role' => "alert",
'type' => $this->options['type']
'type' => $this->options['type'],
'title' => h($this->options['title']),
]));
$html .= $this->genIcon();
$html .= $this->genImage();
$html .= $this->genContent();
if (!empty($this->options['badge'])) {
$bsBadge = new BoostrapBadge($this->options['badge']);
@ -695,9 +913,27 @@ class BoostrapButton extends BootstrapGeneric {
private function genIcon()
{
return $this->genNode('span', [
'class' => ['mr-1', "fa fa-{$this->options['icon']}"],
]);
if (!empty($this->options['icon'])) {
$bsIcon = new BoostrapIcon($this->options['icon'], [
'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
]);
return $bsIcon->icon();
}
return '';
}
private function genImage()
{
if (!empty($this->options['image'])) {
return $this->genNode('img', [
'src' => $this->options['image']['path'] ?? '',
'class' => ['img-fluid', 'me-1'],
'width' => '26',
'height' => '26',
'alt' => $this->options['image']['alt'] ?? ''
]);
}
return '';
}
private function genContent()
@ -711,7 +947,8 @@ class BoostrapBadge extends BootstrapGeneric {
'text' => '',
'variant' => 'primary',
'pill' => false,
'title' => ''
'title' => '',
'class' => [],
];
function __construct($options) {
@ -735,17 +972,53 @@ class BoostrapBadge extends BootstrapGeneric {
private function genBadge()
{
$html = $this->genNode('span', [
'class' => [
'class' => array_merge($this->options['class'], [
'ms-1',
'badge',
"badge-{$this->options['variant']}",
$this->options['pill'] ? 'badge-pill' : '',
],
"bg-{$this->options['variant']}",
$this->getTextClassForVariant($this->options['variant']),
$this->options['pill'] ? 'rounded-pill' : '',
]),
'title' => $this->options['title']
], h($this->options['text']));
return $html;
}
}
class BoostrapIcon extends BootstrapGeneric {
private $icon = '';
private $defaultOptions = [
'class' => [],
];
function __construct($icon, $options=[]) {
$this->icon = $icon;
$this->processOptions($options);
}
private function processOptions($options)
{
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
}
public function icon()
{
return $this->genIcon();
}
private function genIcon()
{
$html = $this->genNode('span', [
'class' => array_merge(
is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']],
["fa fa-{$this->icon}"]
),
]);
return $html;
}
}
class BoostrapModal extends BootstrapGeneric {
private $defaultOptions = [
'size' => '',
@ -871,7 +1144,7 @@ class BoostrapModal extends BootstrapGeneric {
'variant' => 'primary',
'text' => __('Ok'),
'params' => [
'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
'data-bs-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
'onclick' => $this->options['confirmFunction']
]
]))->button();
@ -888,7 +1161,7 @@ class BoostrapModal extends BootstrapGeneric {
'variant' => 'secondary',
'text' => h($this->options['cancelText']),
'params' => [
'data-dismiss' => 'modal',
'data-bs-dismiss' => 'modal',
'onclick' => $this->options['cancelFunction']
]
]))->button();
@ -898,7 +1171,7 @@ class BoostrapModal extends BootstrapGeneric {
'text' => h($this->options['confirmText']),
'class' => 'modal-confirm-button',
'params' => [
// 'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
// 'data-bs-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
'data-confirmFunction' => sprintf('%s', $this->options['confirmFunction'])
]
]))->button();
@ -914,7 +1187,7 @@ class BoostrapModal extends BootstrapGeneric {
'text' => h($buttonConfig['text']),
'class' => 'modal-confirm-button',
'params' => [
'data-dismiss' => !empty($buttonConfig['clickFunction']) ? '' : 'modal',
'data-bs-dismiss' => !empty($buttonConfig['clickFunction']) ? '' : 'modal',
'data-clickFunction' => sprintf('%s', $buttonConfig['clickFunction'])
]
]))->button();
@ -933,6 +1206,7 @@ class BoostrapCard extends BootstrapGeneric
'headerHTML' => '',
'footerHTML' => '',
'bodyHTML' => '',
'class' => '',
'headerClass' => '',
'bodyClass' => '',
'footerClass' => '',
@ -964,6 +1238,7 @@ class BoostrapCard extends BootstrapGeneric
'card',
!empty($this->options['variant']) ? "bg-{$this->options['variant']}" : '',
!empty($this->options['variant']) ? $this->getTextClassForVariant($this->options['variant']) : '',
h($this->options['class']),
],
], implode('', [$this->genHeader(), $this->genBody(), $this->genFooter()]));
return $card;
@ -1015,6 +1290,60 @@ class BoostrapCard extends BootstrapGeneric
}
}
class BoostrapSwitch extends BootstrapGeneric {
private $defaultOptions = [
'label' => '',
'variant' => 'primary',
'disabled' => false,
'checked' => false,
'title' => '',
'class' => [],
'attrs' => [],
];
function __construct($options) {
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
$this->processOptions($options);
}
private function processOptions($options)
{
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
}
public function switch()
{
return $this->genSwitch();
}
private function genSwitch()
{
$tmpId = 'tmp-' . mt_rand();
$html = $this->genNode('div', [
'class' => [
'form-check form-switch',
],
'title' => $this->options['title']
], implode('', [
$this->genNode('input', array_merge([
'type' => "checkbox",
'class' => 'form-check-input',
'id' => $tmpId,
($this->options['disabled'] ? 'disabled' : '') => '',
($this->options['checked'] ? 'checked' : '') => $this->options['checked'] ? 'checked' : '',
], $this->options['attrs'])),
$this->genNode('label', [
'class' => 'form-check-label',
'for' => $tmpId,
], h($this->options['label']))
]));
return $html;
}
}
class BoostrapProgress extends BootstrapGeneric {
private $defaultOptions = [
'value' => 0,
@ -1103,7 +1432,7 @@ class BoostrapCollapse extends BootstrapGeneric {
{
$html = $this->genNode('a', [
'class' => ['text-decoration-none'],
'data-toggle' => 'collapse',
'data-bs-toggle' => 'collapse',
'href' => '#collapseExample',
'role' => 'button',
'aria-expanded' => 'false',
@ -1213,7 +1542,7 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
return $this->genNode('li', [
'class' => [
'text-center',
'font-weight-bold',
'fw-bold',
$isActive ? 'progress-active' : 'progress-inactive',
],
], h($step['text'] ?? ''));
@ -1244,4 +1573,234 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
], $ulIcons . $ulText);
return $html;
}
}
class BootstrapListGroup extends BootstrapGeneric
{
private $defaultOptions = [
'hover' => false,
];
private $bsClasses = null;
function __construct($options, $data, $btHelper) {
$this->data = $data;
$this->processOptions($options);
$this->btHelper = $btHelper;
}
private function processOptions($options)
{
$this->options = array_merge($this->defaultOptions, $options);
}
public function listGroup()
{
return $this->genListGroup();
}
private function genListGroup()
{
$html = $this->openNode('div', [
'class' => ['list-group',],
]);
foreach ($this->data as $item) {
$html .= $this->genItem($item);
}
$html .= $this->closeNode('div');
return $html;
}
private function genItem($item)
{
if (!empty($item['heading'])) { // complex layout with heading, badge and body
$html = $this->genNode('a', [
'class' => ['list-group-item', (!empty($this->options['hover']) ? 'list-group-item-action' : ''),],
], implode('', [
$this->genHeadingGroup($item),
$this->genBody($item),
]));
} else { // simple layout with just <li>-like elements
$html = $this->genNode('a', [
'class' => ['list-group-item', 'd-flex', 'align-items-center', 'justify-content-between'],
], implode('', [
h($item['text']),
$this->genBadge($item)
]));
}
return $html;
}
private function genHeadingGroup($item)
{
$html = $this->genNode('div', [
'class' => ['d-flex', 'w-100', 'justify-content-between',],
], implode('', [
$this->genHeading($item),
$this->genBadge($item)
]));
return $html;
}
private function genHeading($item)
{
if (empty($item['heading'])) {
return '';
}
return $this->genNode('h5', [
'class' => ['mb-1'],
], h($item['heading']));
}
private function genBadge($item)
{
if (empty($item['badge'])) {
return '';
}
return $this->genNode('span', [
'class' => ['badge rounded-pill', (!empty($item['badge-variant']) ? "bg-{$item['badge-variant']}" : 'bg-primary')],
], h($item['badge']));
}
private function genBody($item)
{
if (!empty($item['bodyHTML'])) {
return $item['bodyHTML'];
}
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);
}
}

View File

@ -49,10 +49,10 @@ class DataFromPathHelper extends Helper
if (empty($strArg['function'])) {
$varValue = $options['sanitize'] ? h($varValue) : $varValue;
}
$extractedVars[] = $varValue;
$extractedVars[$i] = $varValue;
}
foreach ($extractedVars as $i => $value) {
$value = $options['highlight'] ? "<span class=\"font-weight-light\">${value}</span>" : $value;
$value = $options['highlight'] ? "<span class=\"fw-light\">${value}</span>" : $value;
$str = str_replace(
"{{{$i}}}",
$value,

View File

@ -0,0 +1,38 @@
<?php
namespace App\View\Helper;
use Cake\View\Helper;
use Cake\Utility\Hash;
class SocialProviderHelper extends Helper
{
public $helpers = ['Bootstrap'];
private $providerImageMapping = [
'keycloak' => '/img/keycloak_logo.png',
];
public function getIcon($identity)
{
if (!empty($identity['social_profile'])) {
$provider = $identity['social_profile']['provider'];
if (!empty($this->providerImageMapping[$provider])) {
return $this->genImage($this->providerImageMapping[$provider], h($provider));
}
}
return '';
}
private function genImage($url, $alt)
{
return $this->Bootstrap->genNode('img', [
'src' => $url,
'class' => ['img-fluid'],
'width' => '16',
'height' => '16',
'alt' => $alt,
'title' => __('Authentication provided by {0}', $alt),
]);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\View\Helper;
use Cake\View\Helper;
// This helper helps determining the brightness of a colour (initially only used for the tagging) in order to decide
// what text colour to use against the background (black or white)
class TextColourHelper extends Helper {
public function getTextColour($RGB) {
$r = hexdec(substr($RGB, 1, 2));
$g = hexdec(substr($RGB, 3, 2));
$b = hexdec(substr($RGB, 5, 2));
$average = ((2 * $r) + $b + (3 * $g))/6;
if ($average < 127) {
return 'white';
} else {
return 'black';
}
}
}

View File

@ -5,9 +5,9 @@ echo $this->element('genericElements/genericModal', [
'<p>%s</p><p>%s</p><p>%s</p>',
__('Please make sure that you note down the authkey below, this is the only time the authkey is shown in plain text, so make sure you save it. If you lose the key, simply remove the entry and generate a new one.'),
__('Cerebrate will use the first and the last 4 digit for identification purposes.'),
sprintf('%s: <span class="font-weight-bold">%s</span>', __('Authkey'), h($entity->authkey_raw))
sprintf('%s: <span class="fw-bold">%s</span>', __('Authkey'), h($entity->authkey_raw))
),
'actionButton' => sprintf('<button" class="btn btn-primary" data-dismiss="modal">%s</button>', __('I have noted down my key, take me back now')),
'actionButton' => sprintf('<button" class="btn btn-primary" data-bs-dismiss="modal">%s</button>', __('I have noted down my key, take me back now')),
'noCancel' => true,
'staticBackdrop' => true,
]);

View File

@ -9,6 +9,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'top_bar' => [
'children' => [
[
'type' => 'multi_select_actions',
'children' => [
[
'text' => __('Discard requests'),
@ -16,7 +17,11 @@ echo $this->element('genericElements/IndexTable/index_table', [
'onclick' => 'discardRequests',
]
],
'type' => 'multi_select_actions',
'data' => [
'id' => [
'value_path' => 'id'
]
]
],
[
'type' => 'context_filters',
@ -33,15 +38,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
]
],
'fields' => [
[
'element' => 'selector',
'class' => 'short',
'data' => [
'id' => [
'value_path' => 'id'
]
]
],
[
'name' => '#',
'sort' => 'id',
@ -134,7 +130,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
)
const $footer = $(modalObject.ajaxApi.statusNode).parent()
modalObject.ajaxApi.statusNode.remove()
const $cancelButton = $footer.find('button[data-dismiss="modal"]')
const $cancelButton = $footer.find('button[data-bs-dismiss="modal"]')
$cancelButton.text('<?= __('OK') ?>').removeClass('btn-secondary').addClass('btn-primary')
}
UI.submissionModal('/inbox/delete', successCallback, failCallback).then(([modalObject, ajaxApi]) => {

Some files were not shown because too many files have changed in this diff Show More