diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 0000000..dd01dc8
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,52 @@
+name: Docker
+
+on:
+ push:
+ branches: [ main ]
+ tags: [ 'v*.*' ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Log into registry ${{ env.REGISTRY }}
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v1
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ # https://github.com/docker/metadata-action
+ - name: Extract Docker metadata
+ id: meta
+ uses: docker/metadata-action@v3
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+ # https://github.com/docker/build-push-action
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v2
+ with:
+ file: docker/Dockerfile
+ context: .
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ "COMPOSER_VERSION=2.1.5"
+ "PHP_VERSION=7.4"
+ "DEBIAN_RELEASE=buster"
diff --git a/.gitignore b/.gitignore
index f370d3e..4c169ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,13 @@
composer.lock
config/app_local.php
+config/Migrations/schema-dump-default.lock
logs
tmp
vendor
-
+webroot/theme/node_modules
+webroot/scss/*.css
+.vscode
+docker/run/
+.phpunit.result.cache
+config.json
+phpunit.xml
diff --git a/INSTALL/INSTALL.md b/INSTALL/INSTALL.md
index 9a216fc..f4cef5f 100644
--- a/INSTALL/INSTALL.md
+++ b/INSTALL/INSTALL.md
@@ -1,8 +1,9 @@
## Requirements
An Ubuntu server (18.04/20.04 should both work fine) - though other linux installations should work too.
-- apache2, mysql/mariadb, sqlite need to be installed and running
+- apache2 (or nginx), mysql/mariadb, sqlite need to be installed and running
- php extensions for intl, mysql, sqlite3, mbstring, xml need to be installed and running
+- php extention for curl (not required but makes composer run a little faster)
- composer
## Network requirements
@@ -17,8 +18,16 @@ Cerebrate communicates via HTTPS so in order to be able to connect to other cere
## Cerebrate installation instructions
It should be sufficient to issue the following command to install the dependencies:
+
+- for apache
+
```bash
-sudo apt install apache2 mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 sqlite libapache2-mod-php php-mysql
+sudo apt install apache2 mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 php-curl sqlite libapache2-mod-php php-mysql
+```
+
+- for nginx
+```bash
+sudo apt install nginx mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 sqlite php-fpm php-curl php-mysql
```
Clone this repository (for example into /var/www/cerebrate)
@@ -32,12 +41,19 @@ sudo -u www-data git clone https://github.com/cerebrate-project/cerebrate.git /v
Run composer
```bash
+sudo mkdir -p /var/www/.composer
+sudo chown www-data:www-data /var/www/.composer
cd /var/www/cerebrate
-sudo -u www-data composer install
+sudo -H -u www-data composer install
```
Create a database for cerebrate
+With a fresh install of Ubuntu sudo to the (system) root user before logging in as the mysql root
+```Bash
+sudo -i mysql -u root
+```
+
From SQL shell:
```mysql
mysql
@@ -46,6 +62,7 @@ CREATE USER 'cerebrate'@'localhost' IDENTIFIED BY 'YOUR_PASSWORD';
GRANT USAGE ON *.* to cerebrate@localhost;
GRANT ALL PRIVILEGES ON cerebrate.* to cerebrate@localhost;
FLUSH PRIVILEGES;
+QUIT;
```
Or from Bash:
@@ -57,20 +74,15 @@ sudo mysql -e "GRANT ALL PRIVILEGES ON cerebrate.* to cerebrate@localhost;"
sudo mysql -e "FLUSH PRIVILEGES;"
```
-Load the default table structure into the database
-
-```bash
-sudo mysql -u cerebrate -p cerebrate < /var/www/cerebrate/INSTALL/mysql.sql
-```
-
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
```
-mod_rewrite needs to be enabled:
+mod_rewrite needs to be enabled if __using apache__:
```bash
sudo a2enmod rewrite
@@ -87,18 +99,47 @@ This would be, when following the steps above:
'password' => 'YOUR_PASSWORD',
'database' => 'cerebrate',
```
+
+Run the database schema migrations
+```bash
+sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate
+sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate -p tags
+sudo -u www-data /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:
+For development installs the following can be done for either apache or nginx:
```bash
+# Apache
# This configuration is purely meant for local installations for development / testing
# Using HTTP on an unhardened apache is by no means meant to be used in any production environment
-sudo cp /var/www/cerebrate/INSTALL/cerebrate_dev.conf /etc/apache2/sites-available/
-sudo ln -s /etc/apache2/sites-available/cerebrate_dev.conf /etc/apache2/sites-enabled/
+sudo cp /var/www/cerebrate/INSTALL/cerebrate_apache_dev.conf /etc/apache2/sites-available/
+sudo ln -s /etc/apache2/sites-available/cerebrate_apache_dev.conf /etc/apache2/sites-enabled/
sudo service apache2 restart
```
+OR
+
+```bash
+# NGINX
+# This configuration is purely meant for local installations for development / testing
+# Using HTTP on an unhardened apache is by no means meant to be used in any production environment
+sudo cp /var/www/cerebrate/INSTALL/cerebrate_nginx.conf /etc/nginx/sites-available/
+sudo ln -s /etc/nginx/sites-available/cerebrate_nginx.conf /etc/nginx/sites-enabled/
+sudo systemctl disable apache2 # may be required if apache is using port
+sudo service nginx restart
+sudo systemctl enable nginx
+
+```
+
Now you can point your browser to: http://localhost:8000
To log in use the default credentials below:
diff --git a/INSTALL/cerebrate_dev.conf b/INSTALL/cerebrate_apache_dev.conf
old mode 100755
new mode 100644
similarity index 100%
rename from INSTALL/cerebrate_dev.conf
rename to INSTALL/cerebrate_apache_dev.conf
diff --git a/INSTALL/cerebrate_nginx.conf b/INSTALL/cerebrate_nginx.conf
new file mode 100644
index 0000000..59ad861
--- /dev/null
+++ b/INSTALL/cerebrate_nginx.conf
@@ -0,0 +1,37 @@
+## Cerebrate Nginx Web Server Configuration
+server {
+ listen 8000;
+ # listen 443 ssl;
+
+ root /var/www/cerebrate/webroot;
+ error_log /var/log/nginx/cerebrate_error.log;
+ access_log /var/log/nginx/cerebrate_access.log;
+
+ # Add index.php to the list if you are using PHP
+ index index.html index.htm index.nginx-debian.html index.php;
+
+ server_name _;
+
+ # Configure Crypto Keys/Certificates/DH
+ # If enabling this setting change port above, should also set the server name
+ # ssl_certificate /path/to/ssl/cert;
+ # ssl_certificate_key /path/to/ssl/cert;
+
+ # enable HSTS
+ # add_header Strict-Transport-Security "max-age=15768000; includeSubdomains";
+ # add_header X-Frame-Options SAMEORIGIN;
+
+ location / {
+ try_files $uri $uri/ /index.php?$args;
+ }
+
+ location ~ \.php$ {
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ fastcgi_param PATH_INFO $fastcgi_path_info;
+ }
+}
diff --git a/INSTALL/mysql.sql b/INSTALL/mysql.sql
deleted file mode 100644
index 88a3040..0000000
--- a/INSTALL/mysql.sql
+++ /dev/null
@@ -1,402 +0,0 @@
--- MySQL dump 10.16 Distrib 10.1.44-MariaDB, for debian-linux-gnu (x86_64)
---
--- Host: localhost Database: cerebrate
--- ------------------------------------------------------
--- Server version 10.1.44-MariaDB-0ubuntu0.18.04.1
-
-/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
-/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
-/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
-/*!40101 SET NAMES utf8mb4 */;
-/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
-/*!40103 SET TIME_ZONE='+00:00' */;
-/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
-/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
-/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
-/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-
---
--- Table structure for table `alignment_tags`
---
-
-DROP TABLE IF EXISTS `alignment_tags`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `alignment_tags` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `alignment_id` int(10) unsigned NOT NULL,
- `tag_id` int(10) unsigned NOT NULL,
- PRIMARY KEY (`id`),
- KEY `alignment_id` (`alignment_id`),
- KEY `tag_id` (`tag_id`),
- CONSTRAINT `alignment_tags_ibfk_1` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
- CONSTRAINT `alignment_tags_ibfk_10` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
- CONSTRAINT `alignment_tags_ibfk_11` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
- CONSTRAINT `alignment_tags_ibfk_12` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
- CONSTRAINT `alignment_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
- CONSTRAINT `alignment_tags_ibfk_3` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
- CONSTRAINT `alignment_tags_ibfk_4` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
- CONSTRAINT `alignment_tags_ibfk_5` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
- CONSTRAINT `alignment_tags_ibfk_6` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
- CONSTRAINT `alignment_tags_ibfk_7` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
- CONSTRAINT `alignment_tags_ibfk_8` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
- CONSTRAINT `alignment_tags_ibfk_9` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `alignments`
---
-
-DROP TABLE IF EXISTS `alignments`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `alignments` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `individual_id` int(10) unsigned NOT NULL,
- `organisation_id` int(10) unsigned NOT NULL,
- `type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT 'member',
- PRIMARY KEY (`id`),
- KEY `individual_id` (`individual_id`),
- KEY `organisation_id` (`organisation_id`),
- CONSTRAINT `alignments_ibfk_1` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
- CONSTRAINT `alignments_ibfk_2` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `auth_keys`
---
-
-DROP TABLE IF EXISTS `auth_keys`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `auth_keys` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
- `authkey` varchar(72) CHARACTER SET ascii DEFAULT NULL,
- `authkey_start` varchar(4) CHARACTER SET ascii DEFAULT NULL,
- `authkey_end` varchar(4) CHARACTER SET ascii DEFAULT NULL,
- `created` int(10) unsigned NOT NULL,
- `expiration` int(10) unsigned NOT NULL,
- `user_id` int(10) unsigned NOT NULL,
- `comment` text COLLATE utf8mb4_unicode_ci,
- PRIMARY KEY (`id`),
- KEY `authkey_start` (`authkey_start`),
- KEY `authkey_end` (`authkey_end`),
- KEY `created` (`created`),
- KEY `expiration` (`expiration`),
- KEY `user_id` (`user_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `broods`
---
-
-DROP TABLE IF EXISTS `broods`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `broods` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `url` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `description` text COLLATE utf8mb4_unicode_ci,
- `organisation_id` int(10) unsigned NOT NULL,
- `trusted` tinyint(1) DEFAULT NULL,
- `pull` tinyint(1) DEFAULT NULL,
- `skip_proxy` tinyint(1) DEFAULT NULL,
- `authkey` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- PRIMARY KEY (`id`),
- KEY `uuid` (`uuid`),
- KEY `name` (`name`),
- KEY `url` (`url`),
- KEY `authkey` (`authkey`),
- KEY `organisation_id` (`organisation_id`),
- FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `encryption_keys`
---
-
-DROP TABLE IF EXISTS `encryption_keys`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `encryption_keys` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `encryption_key` text COLLATE utf8mb4_unicode_ci,
- `revoked` tinyint(1) DEFAULT NULL,
- `expires` int(10) unsigned DEFAULT NULL,
- `owner_id` int(10) unsigned DEFAULT NULL,
- `owner_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
- PRIMARY KEY (`id`),
- KEY `uuid` (`uuid`),
- KEY `type` (`type`),
- KEY `expires` (`expires`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `individual_encryption_keys`
---
-
-DROP TABLE IF EXISTS `individual_encryption_keys`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `individual_encryption_keys` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `individual_id` int(10) unsigned NOT NULL,
- `encryption_key_id` int(10) unsigned NOT NULL,
- PRIMARY KEY (`id`),
- KEY `individual_id` (`individual_id`),
- KEY `encryption_key_id` (`encryption_key_id`),
- CONSTRAINT `individual_encryption_keys_ibfk_1` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
- CONSTRAINT `individual_encryption_keys_ibfk_2` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`),
- CONSTRAINT `individual_encryption_keys_ibfk_3` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
- CONSTRAINT `individual_encryption_keys_ibfk_4` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`),
- CONSTRAINT `individual_encryption_keys_ibfk_5` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
- CONSTRAINT `individual_encryption_keys_ibfk_6` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `individuals`
---
-
-DROP TABLE IF EXISTS `individuals`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `individuals` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `email` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `first_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `last_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `position` text COLLATE utf8mb4_unicode_ci,
- PRIMARY KEY (`id`),
- KEY `uuid` (`uuid`),
- KEY `email` (`email`),
- KEY `first_name` (`first_name`),
- KEY `last_name` (`last_name`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `local_tools`
---
-
-DROP TABLE IF EXISTS `local_tools`;
-CREATE TABLE `local_tools` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `connector` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `settings` text COLLATE utf8mb4_unicode_ci,
- `exposed` tinyint(1) NOT NULL,
- `description` text COLLATE utf8mb4_unicode_ci,
- PRIMARY KEY (`id`),
- KEY `name` (`name`),
- KEY `connector` (`connector`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
---
--- Table structure for table `organisation_encryption_keys`
---
-
-DROP TABLE IF EXISTS `organisation_encryption_keys`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `organisation_encryption_keys` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `organisation_id` int(10) unsigned NOT NULL,
- `encryption_key_id` int(10) unsigned NOT NULL,
- PRIMARY KEY (`id`),
- KEY `organisation_id` (`organisation_id`),
- KEY `encryption_key_id` (`encryption_key_id`),
- CONSTRAINT `organisation_encryption_keys_ibfk_1` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`),
- CONSTRAINT `organisation_encryption_keys_ibfk_2` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `organisations`
---
-
-DROP TABLE IF EXISTS `organisations`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `organisations` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `url` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- `nationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- `sector` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- `type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- `contacts` text COLLATE utf8mb4_unicode_ci,
- PRIMARY KEY (`id`),
- KEY `uuid` (`uuid`),
- KEY `name` (`name`),
- KEY `url` (`url`),
- KEY `nationality` (`nationality`),
- KEY `sector` (`sector`),
- KEY `type` (`type`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `roles`
---
-
-DROP TABLE IF EXISTS `roles`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `roles` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `is_default` tinyint(1) DEFAULT NULL,
- `perm_admin` tinyint(1) DEFAULT NULL,
- PRIMARY KEY (`id`),
- KEY `name` (`name`),
- KEY `uuid` (`uuid`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `tags`
---
-
-DROP TABLE IF EXISTS `tags`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `tags` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `description` text COLLATE utf8mb4_unicode_ci,
- `colour` varchar(6) CHARACTER SET ascii NOT NULL,
- PRIMARY KEY (`id`),
- KEY `name` (`name`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-
---
--- Table structure for table `users`
---
-
-DROP TABLE IF EXISTS `users`;
-/*!40101 SET @saved_cs_client = @@character_set_client */;
-/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `users` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `username` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- `password` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- `role_id` int(11) unsigned NOT NULL,
- `individual_id` int(11) unsigned NOT NULL,
- `disabled` tinyint(1) DEFAULT '0',
- PRIMARY KEY (`id`),
- KEY `uuid` (`uuid`),
- KEY `role_id` (`role_id`),
- KEY `individual_id` (`individual_id`),
- CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`),
- CONSTRAINT `users_ibfk_2` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-/*!40101 SET character_set_client = @saved_cs_client */;
-/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
-
-CREATE TABLE `sharing_groups` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
- `releasability` text DEFAULT NULL,
- `description` text DEFAULT NULL,
- `organisation_id` int(10) unsigned NOT NULL,
- `user_id` int(10) unsigned NOT NULL,
- `active` tinyint(1) DEFAULT '1',
- `local` tinyint(1) DEFAULT '1',
- PRIMARY KEY (`id`),
- KEY `uuid` (`uuid`),
- KEY `user_id` (`user_id`),
- KEY `organisation_id` (`organisation_id`),
- KEY `name` (`name`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE `sgo` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `sharing_group_id` int(10) unsigned NOT NULL,
- `organisation_id` int(10) unsigned NOT NULL,
- `deleted` tinyint(1) DEFAULT 0,
- PRIMARY KEY (`id`),
- KEY `sharing_group_id` (`sharing_group_id`),
- KEY `organisation_id` (`organisation_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE `meta_fields` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `scope` varchar(191) NOT NULL,
- `parent_id` int(10) unsigned NOT NULL,
- `field` varchar(191) NOT NULL,
- `value` varchar(191) NOT NULL,
- `uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
- `meta_template_id` int(10) unsigned NOT NULL,
- `meta_template_field_id` int(10) unsigned NOT NULL,
- `is_default` tinyint(1) NOT NULL DEFAULT 0,
- PRIMARY KEY (`id`),
- KEY `scope` (`scope`),
- KEY `uuid` (`uuid`),
- KEY `parent_id` (`parent_id`),
- KEY `field` (`field`),
- KEY `value` (`value`),
- KEY `meta_template_id` (`meta_template_id`),
- KEY `meta_template_field_id` (`meta_template_field_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE `meta_templates` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `scope` varchar(191) NOT NULL,
- `name` varchar(191) NOT NULL,
- `namespace` varchar(191) NOT NULL,
- `description` text,
- `version` varchar(191) NOT NULL,
- `uuid` varchar(40) CHARACTER SET ascii,
- `source` varchar(191),
- `enabled` tinyint(1) DEFAULT 0,
- `is_default` tinyint(1) NOT NULL DEFAULT 0,
- PRIMARY KEY (`id`),
- KEY `scope` (`scope`),
- KEY `source` (`source`),
- KEY `name` (`name`),
- KEY `namespace` (`namespace`),
- KEY `version` (`version`),
- KEY `uuid` (`uuid`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-CREATE TABLE `meta_template_fields` (
- `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `field` varchar(191) NOT NULL,
- `type` varchar(191) NOT NULL,
- `meta_template_id` int(10) unsigned NOT NULL,
- `regex` text,
- `multiple` tinyint(1) DEFAULT 0,
- `enabled` tinyint(1) DEFAULT 0,
- PRIMARY KEY (`id`),
- CONSTRAINT `meta_template_id` FOREIGN KEY (`meta_template_id`) REFERENCES `meta_templates` (`id`),
- KEY `field` (`field`),
- KEY `type` (`type`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-
-
-/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
-/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
-/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
-/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
-/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
-/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
-/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-
--- Dump completed on 2020-06-22 14:30:02
diff --git a/README.md b/README.md
index c2dfa33..0114d21 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,31 @@
# cerebrate
-The Cerebrate Sync Platform core software. Cerebrate is an open-source platform meant to act as a trusted contact information provider and interconnection orchestrator for other security tools.
+Cerebrate is an [open-source platform](https://github.com/cerebrate-project) meant to act as a trusted contact information provider and interconnection orchestrator for other security tools (such as [MISP](https://www.misp-project.org/)).
-It is currently being built under the MeliCERTes v2 project and is heavily work in progress.
+# Features
-# Current features
+- Advanced repository to manage individuals and organisations;
+- Key store for public encryption and signing cryptographic keys (e.g. PGP);
+- Distributed synchronisation model where multiple Cerebrate instances can be interconnected amongst organisations and/or departments;
+- Management of individuals and their affiliations to each organisations;
+- Advanced API and CLI to integrate with existing tools (e.g. importing existing directory information);
+- Dynamic model for creating new organisational structures;
+- Support existing organisational structures such as [FIRST.org](https://www.first.org/) directory, EU [CSIRTs network](https://csirtsnetwork.eu/);
+- Local tooling interconnection to easily connect existing tools with their native protocols;
-- Repository of organisations and individuals
-- Maintain signing and encryption keys
-- Maintain affiliations between organisations and individuals
+Cerebrate is developed in the scope of the MeliCERTes v2 project.
## Screenshots
+![Dashboard](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-31-56.png)
+
List of individuals along with their affiliations
-![List of individuals](/documentation/images/individuals.png)
+![List of individuals](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-32-35.png)
Adding organisations
-![Adding an organisation](/documentation/images/add_org.png)
+![Adding an organisation](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-33-04.png)
Everything is available via the API, here an example of a search query for all international organisations in the DB.
@@ -28,6 +35,10 @@ Managing public keys and assigning them to users both for communication and vali
![Encryption key management](/documentation/images/add_encryption_key.png)
+Dynamic model for creating new organisation structre
+
+![Meta Field Templates](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-38-21.png)
+
# Requirements and installation
The platform is built on CakePHP 4 along with Bootstrap 4 and shares parts of the code-base with [MISP](https://www.github.com/MISP).
@@ -45,6 +56,7 @@ For installation via docker, refer to the [cerebrate-docker](https://github.com/
~~~~
The software is released under the AGPLv3.
- Copyright (C) 2019, 2020 Andras Iklody
+ Copyright (C) 2019, 2021 Andras Iklody
+ Copyright (C) 2020-2021 Sami Mokaddem
Copyright (C) CIRCL - Computer Incident Response Center Luxembourg
~~~~
diff --git a/composer.json b/composer.json
index 42a49b2..f51bc6f 100644
--- a/composer.json
+++ b/composer.json
@@ -9,7 +9,7 @@
"admad/cakephp-social-auth": "^1.1",
"cakephp/authentication": "^2.0",
"cakephp/authorization": "^2.0",
- "cakephp/cakephp": "^4.0",
+ "cakephp/cakephp": "^4.3",
"cakephp/migrations": "^3.0",
"cakephp/plugin-installer": "^1.2",
"erusev/parsedown": "^1.7",
@@ -19,9 +19,12 @@
"cakephp/bake": "^2.0.3",
"cakephp/cakephp-codesniffer": "~4.0.0",
"cakephp/debug_kit": "^4.0",
+ "fzaninotto/faker": "^1.9",
"josegonzalez/dotenv": "^3.2",
+ "league/openapi-psr7-validator": "^0.16.4",
"phpunit/phpunit": "^8.5",
- "psy/psysh": "@stable"
+ "psy/psysh": "@stable",
+ "wiremock-php/wiremock-php": "^2.32"
},
"suggest": {
"markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.",
@@ -44,7 +47,6 @@
"scripts": {
"post-install-cmd": "App\\Console\\Installer::postInstall",
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
- "post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump",
"check": [
"@test",
"@cs-check"
@@ -52,11 +54,15 @@
"cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"stan": "phpstan analyse src/",
- "test": "phpunit --colors=always"
+ "test": [
+ "sh ./tests/Helper/wiremock/start.sh",
+ "phpunit",
+ "sh ./tests/Helper/wiremock/stop.sh"
+ ]
},
"prefer-stable": true,
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
-}
+}
\ No newline at end of file
diff --git a/config/Migrations/20210311000000_InitialSchema.php b/config/Migrations/20210311000000_InitialSchema.php
new file mode 100644
index 0000000..d1e2ed1
--- /dev/null
+++ b/config/Migrations/20210311000000_InitialSchema.php
@@ -0,0 +1,1464 @@
+execute('SET unique_checks=0; SET foreign_key_checks=0;');
+ $this->execute("ALTER DATABASE CHARACTER SET 'utf8mb4';");
+ $this->execute("ALTER DATABASE COLLATE='utf8mb4_general_ci';");
+ $this->table('broods', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'id',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('url', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'name',
+ ])
+ ->addColumn('description', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'url',
+ ])
+ ->addColumn('organisation_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'description',
+ ])
+ ->addColumn('trusted', 'boolean', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'organisation_id',
+ ])
+ ->addColumn('pull', 'boolean', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'trusted',
+ ])
+ ->addColumn('skip_proxy', 'boolean', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'pull',
+ ])
+ ->addColumn('authkey', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'skip_proxy',
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->addIndex(['name'], [
+ 'name' => 'name',
+ 'unique' => false,
+ ])
+ ->addIndex(['url'], [
+ 'name' => 'url',
+ 'unique' => false,
+ ])
+ ->addIndex(['authkey'], [
+ 'name' => 'authkey',
+ 'unique' => false,
+ ])
+ ->addIndex(['organisation_id'], [
+ 'name' => 'organisation_id',
+ 'unique' => false,
+ ])
+ ->addForeignKey('organisation_id', 'organisations', 'id', [
+ 'constraint' => 'broods_ibfk_1',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->create();
+ $this->table('sharing_groups', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'id',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('releasability', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'name',
+ ])
+ ->addColumn('description', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'releasability',
+ ])
+ ->addColumn('organisation_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'description',
+ ])
+ ->addColumn('user_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'organisation_id',
+ ])
+ ->addColumn('active', 'boolean', [
+ 'null' => true,
+ 'default' => '1',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'user_id',
+ ])
+ ->addColumn('local', 'boolean', [
+ 'null' => true,
+ 'default' => '1',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'active',
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->addIndex(['user_id'], [
+ 'name' => 'user_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['organisation_id'], [
+ 'name' => 'organisation_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['name'], [
+ 'name' => 'name',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('alignment_tags', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('alignment_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'id',
+ ])
+ ->addColumn('tag_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'alignment_id',
+ ])
+ ->addIndex(['alignment_id'], [
+ 'name' => 'alignment_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['tag_id'], [
+ 'name' => 'tag_id',
+ 'unique' => false,
+ ])
+ ->addForeignKey('alignment_id', 'alignments', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_1',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('tag_id', 'tags', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_10',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('alignment_id', 'alignments', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_11',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('tag_id', 'tags', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_12',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('tag_id', 'tags', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_2',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('alignment_id', 'alignments', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_3',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('tag_id', 'tags', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_4',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('alignment_id', 'alignments', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_5',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('tag_id', 'tags', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_6',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('alignment_id', 'alignments', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_7',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('tag_id', 'tags', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_8',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('alignment_id', 'alignments', 'id', [
+ 'constraint' => 'alignment_tags_ibfk_9',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->create();
+ $this->table('meta_templates', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('scope', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'id',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'scope',
+ ])
+ ->addColumn('namespace', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'name',
+ ])
+ ->addColumn('description', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'namespace',
+ ])
+ ->addColumn('version', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'description',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'version',
+ ])
+ ->addColumn('source', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('enabled', 'boolean', [
+ 'null' => true,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'source',
+ ])
+ ->addColumn('is_default', 'boolean', [
+ 'null' => false,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'enabled',
+ ])
+ ->addIndex(['scope'], [
+ 'name' => 'scope',
+ 'unique' => false,
+ ])
+ ->addIndex(['source'], [
+ 'name' => 'source',
+ 'unique' => false,
+ ])
+ ->addIndex(['name'], [
+ 'name' => 'name',
+ 'unique' => false,
+ ])
+ ->addIndex(['namespace'], [
+ 'name' => 'namespace',
+ 'unique' => false,
+ ])
+ ->addIndex(['version'], [
+ 'name' => 'version',
+ 'unique' => false,
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('individuals', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'id',
+ ])
+ ->addColumn('email', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('first_name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'email',
+ ])
+ ->addColumn('last_name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'first_name',
+ ])
+ ->addColumn('position', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'last_name',
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->addIndex(['email'], [
+ 'name' => 'email',
+ 'unique' => false,
+ ])
+ ->addIndex(['first_name'], [
+ 'name' => 'first_name',
+ 'unique' => false,
+ ])
+ ->addIndex(['last_name'], [
+ 'name' => 'last_name',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('organisations', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'id',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('url', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'name',
+ ])
+ ->addColumn('nationality', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'url',
+ ])
+ ->addColumn('sector', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'nationality',
+ ])
+ ->addColumn('type', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'sector',
+ ])
+ ->addColumn('contacts', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'type',
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->addIndex(['name'], [
+ 'name' => 'name',
+ 'unique' => false,
+ ])
+ ->addIndex(['url'], [
+ 'name' => 'url',
+ 'unique' => false,
+ ])
+ ->addIndex(['nationality'], [
+ 'name' => 'nationality',
+ 'unique' => false,
+ ])
+ ->addIndex(['sector'], [
+ 'name' => 'sector',
+ 'unique' => false,
+ ])
+ ->addIndex(['type'], [
+ 'name' => 'type',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('encryption_keys', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'id',
+ ])
+ ->addColumn('type', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('encryption_key', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'type',
+ ])
+ ->addColumn('revoked', 'boolean', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'encryption_key',
+ ])
+ ->addColumn('expires', 'integer', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'revoked',
+ ])
+ ->addColumn('owner_id', 'integer', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'expires',
+ ])
+ ->addColumn('owner_type', 'string', [
+ 'null' => false,
+ 'limit' => 20,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'owner_id',
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->addIndex(['type'], [
+ 'name' => 'type',
+ 'unique' => false,
+ ])
+ ->addIndex(['expires'], [
+ 'name' => 'expires',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('meta_fields', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('scope', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'id',
+ ])
+ ->addColumn('parent_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'scope',
+ ])
+ ->addColumn('field', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'parent_id',
+ ])
+ ->addColumn('value', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'field',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'value',
+ ])
+ ->addColumn('meta_template_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'uuid',
+ ])
+ ->addColumn('meta_template_field_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'meta_template_id',
+ ])
+ ->addColumn('is_default', 'boolean', [
+ 'null' => false,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'meta_template_field_id',
+ ])
+ ->addIndex(['scope'], [
+ 'name' => 'scope',
+ 'unique' => false,
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->addIndex(['parent_id'], [
+ 'name' => 'parent_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['field'], [
+ 'name' => 'field',
+ 'unique' => false,
+ ])
+ ->addIndex(['value'], [
+ 'name' => 'value',
+ 'unique' => false,
+ ])
+ ->addIndex(['meta_template_id'], [
+ 'name' => 'meta_template_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['meta_template_field_id'], [
+ 'name' => 'meta_template_field_id',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('audit_logs', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('created', 'datetime', [
+ 'null' => false,
+ 'after' => 'id',
+ ])
+ ->addColumn('user_id', 'integer', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'created',
+ ])
+ ->addColumn('authkey_id', 'integer', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'user_id',
+ ])
+ ->addColumn('request_ip', 'varbinary', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 16,
+ 'after' => 'authkey_id',
+ ])
+ ->addColumn('request_type', 'integer', [
+ 'null' => false,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'request_ip',
+ ])
+ ->addColumn('request_id', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'request_type',
+ ])
+ ->addColumn('request_action', 'string', [
+ 'null' => false,
+ 'limit' => 20,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'request_id',
+ ])
+ ->addColumn('model', 'string', [
+ 'null' => false,
+ 'limit' => 80,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'request_action',
+ ])
+ ->addColumn('model_id', 'integer', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'model',
+ ])
+ ->addColumn('model_title', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'model_id',
+ ])
+ ->addColumn('change', 'blob', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => MysqlAdapter::BLOB_REGULAR,
+ 'after' => 'model_title',
+ ])
+ ->addIndex(['user_id'], [
+ 'name' => 'user_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['request_ip'], [
+ 'name' => 'request_ip',
+ 'unique' => false,
+ ])
+ ->addIndex(['model'], [
+ 'name' => 'model',
+ 'unique' => false,
+ ])
+ ->addIndex(['request_action'], [
+ 'name' => 'request_action',
+ 'unique' => false,
+ ])
+ ->addIndex(['model_id'], [
+ 'name' => 'model_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['created'], [
+ 'name' => 'created',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('organisation_encryption_keys', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('organisation_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'id',
+ ])
+ ->addColumn('encryption_key_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'organisation_id',
+ ])
+ ->addIndex(['organisation_id'], [
+ 'name' => 'organisation_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['encryption_key_id'], [
+ 'name' => 'encryption_key_id',
+ 'unique' => false,
+ ])
+ ->addForeignKey('organisation_id', 'organisations', 'id', [
+ 'constraint' => 'organisation_encryption_keys_ibfk_1',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [
+ 'constraint' => 'organisation_encryption_keys_ibfk_2',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->create();
+ $this->table('users', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'id',
+ ])
+ ->addColumn('username', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('password', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'username',
+ ])
+ ->addColumn('role_id', 'integer', [
+ 'null' => false,
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'signed' => false,
+ 'after' => 'password',
+ ])
+ ->addColumn('individual_id', 'integer', [
+ 'null' => false,
+ 'limit' => MysqlAdapter::INT_REGULAR,
+ 'signed' => false,
+ 'after' => 'role_id',
+ ])
+ ->addColumn('disabled', 'boolean', [
+ 'null' => true,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'individual_id',
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->addIndex(['role_id'], [
+ 'name' => 'role_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['individual_id'], [
+ 'name' => 'individual_id',
+ 'unique' => false,
+ ])
+ ->addForeignKey('role_id', 'roles', 'id', [
+ 'constraint' => 'users_ibfk_1',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('individual_id', 'individuals', 'id', [
+ 'constraint' => 'users_ibfk_2',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->create();
+ $this->table('roles', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'id',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('is_default', 'boolean', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'name',
+ ])
+ ->addColumn('perm_admin', 'boolean', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'is_default',
+ ])
+ ->addIndex(['name'], [
+ 'name' => 'name',
+ 'unique' => false,
+ ])
+ ->addIndex(['uuid'], [
+ 'name' => 'uuid',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('tags', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'id',
+ ])
+ ->addColumn('description', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'name',
+ ])
+ ->addColumn('colour', 'string', [
+ 'null' => false,
+ 'limit' => 6,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'description',
+ ])
+ ->addIndex(['name'], [
+ 'name' => 'name',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('individual_encryption_keys', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('individual_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'id',
+ ])
+ ->addColumn('encryption_key_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'individual_id',
+ ])
+ ->addIndex(['individual_id'], [
+ 'name' => 'individual_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['encryption_key_id'], [
+ 'name' => 'encryption_key_id',
+ 'unique' => false,
+ ])
+ ->addForeignKey('individual_id', 'individuals', 'id', [
+ 'constraint' => 'individual_encryption_keys_ibfk_1',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [
+ 'constraint' => 'individual_encryption_keys_ibfk_2',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('individual_id', 'individuals', 'id', [
+ 'constraint' => 'individual_encryption_keys_ibfk_3',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [
+ 'constraint' => 'individual_encryption_keys_ibfk_4',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('individual_id', 'individuals', 'id', [
+ 'constraint' => 'individual_encryption_keys_ibfk_5',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('encryption_key_id', 'encryption_keys', 'id', [
+ 'constraint' => 'individual_encryption_keys_ibfk_6',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->create();
+ $this->table('meta_template_fields', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('field', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'id',
+ ])
+ ->addColumn('type', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'field',
+ ])
+ ->addColumn('meta_template_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'type',
+ ])
+ ->addColumn('regex', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'meta_template_id',
+ ])
+ ->addColumn('multiple', 'boolean', [
+ 'null' => true,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'regex',
+ ])
+ ->addColumn('enabled', 'boolean', [
+ 'null' => true,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'multiple',
+ ])
+ ->addIndex(['meta_template_id'], [
+ 'name' => 'meta_template_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['field'], [
+ 'name' => 'field',
+ 'unique' => false,
+ ])
+ ->addIndex(['type'], [
+ 'name' => 'type',
+ 'unique' => false,
+ ])
+ ->addForeignKey('meta_template_id', 'meta_templates', 'id', [
+ 'constraint' => 'meta_template_id',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->create();
+ $this->table('auth_keys', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('uuid', 'string', [
+ 'null' => false,
+ 'limit' => 40,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'id',
+ ])
+ ->addColumn('authkey', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 72,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'uuid',
+ ])
+ ->addColumn('authkey_start', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 4,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'authkey',
+ ])
+ ->addColumn('authkey_end', 'string', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 4,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ 'after' => 'authkey_start',
+ ])
+ ->addColumn('created', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'authkey_end',
+ ])
+ ->addColumn('expiration', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'created',
+ ])
+ ->addColumn('user_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'expiration',
+ ])
+ ->addColumn('comment', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'user_id',
+ ])
+ ->addIndex(['authkey_start'], [
+ 'name' => 'authkey_start',
+ 'unique' => false,
+ ])
+ ->addIndex(['authkey_end'], [
+ 'name' => 'authkey_end',
+ 'unique' => false,
+ ])
+ ->addIndex(['created'], [
+ 'name' => 'created',
+ 'unique' => false,
+ ])
+ ->addIndex(['expiration'], [
+ 'name' => 'expiration',
+ 'unique' => false,
+ ])
+ ->addIndex(['user_id'], [
+ 'name' => 'user_id',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('local_tools', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'id',
+ ])
+ ->addColumn('connector', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'name',
+ ])
+ ->addColumn('settings', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'connector',
+ ])
+ ->addColumn('exposed', 'boolean', [
+ 'null' => false,
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'settings',
+ ])
+ ->addColumn('description', 'text', [
+ 'null' => true,
+ 'default' => null,
+ 'limit' => 65535,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'exposed',
+ ])
+ ->addIndex(['name'], [
+ 'name' => 'name',
+ 'unique' => false,
+ ])
+ ->addIndex(['connector'], [
+ 'name' => 'connector',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->table('alignments', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('individual_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'id',
+ ])
+ ->addColumn('organisation_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'individual_id',
+ ])
+ ->addColumn('type', 'string', [
+ 'null' => true,
+ 'default' => 'member',
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ 'after' => 'organisation_id',
+ ])
+ ->addIndex(['individual_id'], [
+ 'name' => 'individual_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['organisation_id'], [
+ 'name' => 'organisation_id',
+ 'unique' => false,
+ ])
+ ->addForeignKey('individual_id', 'individuals', 'id', [
+ 'constraint' => 'alignments_ibfk_1',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->addForeignKey('organisation_id', 'organisations', 'id', [
+ 'constraint' => 'alignments_ibfk_2',
+ 'update' => 'RESTRICT',
+ 'delete' => 'RESTRICT',
+ ])
+ ->create();
+ $this->table('sgo', [
+ 'id' => false,
+ 'primary_key' => ['id'],
+ 'engine' => 'InnoDB',
+ 'encoding' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'comment' => '',
+ 'row_format' => 'DYNAMIC',
+ ])
+ ->addColumn('id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'identity' => 'enable',
+ ])
+ ->addColumn('sharing_group_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'id',
+ ])
+ ->addColumn('organisation_id', 'integer', [
+ 'null' => false,
+ 'limit' => '10',
+ 'signed' => false,
+ 'after' => 'sharing_group_id',
+ ])
+ ->addColumn('deleted', 'boolean', [
+ 'null' => true,
+ 'default' => '0',
+ 'limit' => MysqlAdapter::INT_TINY,
+ 'after' => 'organisation_id',
+ ])
+ ->addIndex(['sharing_group_id'], [
+ 'name' => 'sharing_group_id',
+ 'unique' => false,
+ ])
+ ->addIndex(['organisation_id'], [
+ 'name' => 'organisation_id',
+ 'unique' => false,
+ ])
+ ->create();
+ $this->execute('SET unique_checks=1; SET foreign_key_checks=1;');
+ }
+}
diff --git a/config/Migrations/20211005163854_UserSettings.php b/config/Migrations/20211005163854_UserSettings.php
new file mode 100644
index 0000000..2049680
--- /dev/null
+++ b/config/Migrations/20211005163854_UserSettings.php
@@ -0,0 +1,63 @@
+table('user_settings', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+ $table
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('name', 'string', [
+ 'default' => null,
+ 'null' => false,
+ 'limit' => 255,
+ 'comment' => 'The name of the user setting',
+ ])
+ ->addColumn('value', 'text', [
+ 'default' => null,
+ 'null' => true,
+ 'limit' => MysqlAdapter::TEXT_LONG,
+ 'comment' => 'The value of the user setting',
+ ])
+ ->addColumn('user_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->addColumn('created', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ])
+ ->addColumn('modified', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ]);
+
+ $table->addForeignKey('user_id', 'users', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']);
+
+ $table->addIndex('name')
+ ->addIndex('user_id')
+ ->addIndex('created')
+ ->addIndex('modified');
+
+ $table->create();
+ }
+}
diff --git a/config/Migrations/20211025100313_MailingLists.php b/config/Migrations/20211025100313_MailingLists.php
new file mode 100644
index 0000000..cabc846
--- /dev/null
+++ b/config/Migrations/20211025100313_MailingLists.php
@@ -0,0 +1,154 @@
+table('mailing_lists', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+ $mailinglists
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('uuid', 'uuid', [
+ 'default' => null,
+ 'null' => false,
+ ])
+ ->addColumn('name', 'string', [
+ 'default' => null,
+ 'null' => false,
+ 'limit' => 191,
+ 'comment' => 'The name of the mailing list',
+ ])
+ ->addColumn('recipients', 'string', [
+ 'default' => null,
+ 'null' => true,
+ 'limit' => 191,
+ 'comment' => 'Human-readable description of who the intended recipients.',
+ ])
+ ->addColumn('description', 'text', [
+ 'default' => null,
+ 'null' => true,
+ 'comment' => 'Additional description of the mailing list'
+ ])
+ ->addColumn('user_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->addColumn('active', 'boolean', [
+ 'default' => 0,
+ 'null' => false,
+ ])
+ ->addColumn('deleted', 'boolean', [
+ 'default' => 0,
+ 'null' => false,
+ ])
+ ->addColumn('created', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ])
+ ->addColumn('modified', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ]);
+
+
+ $mailinglists->addForeignKey('user_id', 'users', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']);
+
+ $mailinglists->addIndex(['uuid'], ['unique' => true])
+ ->addIndex('name')
+ ->addIndex('recipients')
+ ->addIndex('user_id')
+ ->addIndex('active')
+ ->addIndex('deleted')
+ ->addIndex('created')
+ ->addIndex('modified');
+
+ $mailinglists->create();
+
+
+ $mailinglists_individuals = $this->table('mailing_lists_individuals', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+
+ $mailinglists_individuals
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('mailing_list_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->addColumn('individual_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->addColumn('include_primary_email', 'boolean', [
+ 'default' => 1,
+ 'null' => false,
+ 'comment' => 'Should the primary email address by included in the mailing list'
+ ])
+ ->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
+ ->addForeignKey('individual_id', 'individuals', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']);
+
+ $mailinglists_individuals->addIndex(['mailing_list_id', 'individual_id'], ['unique' => true]);
+
+ $mailinglists_individuals->create();
+
+
+ $mailinglists_metafields = $this->table('mailing_lists_meta_fields', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci',
+ ]);
+
+ $mailinglists_metafields
+ ->addColumn('mailing_list_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->addColumn('meta_field_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->addPrimaryKey(['mailing_list_id', 'meta_field_id'])
+ ->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])
+ ->addForeignKey('meta_field_id', 'meta_fields', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']);
+
+ $mailinglists_metafields->create();
+ }
+}
+
diff --git a/config/Migrations/20211104072514_MoreMetaFieldColumns.php b/config/Migrations/20211104072514_MoreMetaFieldColumns.php
new file mode 100644
index 0000000..3cfbd9a
--- /dev/null
+++ b/config/Migrations/20211104072514_MoreMetaFieldColumns.php
@@ -0,0 +1,46 @@
+table('meta_fields');
+
+ $metaFieldsTable
+ ->addColumn('created', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ])
+ ->addColumn('modified', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ])
+ ->update();
+
+ $metaFieldsTable
+ ->addIndex('created')
+ ->addIndex('modified');
+
+ $metaTemplateFieldsTable = $this->table('meta_template_fields')
+ ->addColumn('counter', 'integer', [
+ 'default' => 0,
+ 'length' => 11,
+ 'null' => false,
+ 'signed' => false,
+ 'comment' => 'Field used by the CounterCache behaviour to count the occurence of meta_template_fields'
+ ])
+ ->update();
+
+ $metaTemplate = $this->table('meta_templates')
+ ->removeIndex(['uuid'])
+ ->addIndex(['uuid', 'version'])
+ ->update();
+
+ // TODO: Make sure FK constraints are set between meta_field, meta_template and meta_template_fields
+ // TODO: Make sure to add constraints on meta_template `uuid` and `version`
+ }
+}
\ No newline at end of file
diff --git a/config/Migrations/20211117135403_audit_logs.php b/config/Migrations/20211117135403_audit_logs.php
new file mode 100644
index 0000000..c44847b
--- /dev/null
+++ b/config/Migrations/20211117135403_audit_logs.php
@@ -0,0 +1,96 @@
+hasTable('audit_logs');
+ if (!$exists) {
+ $table = $this->table('audit_logs', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci'
+ ]);
+ $table
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('user_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10
+ ])
+ ->addColumn('authkey_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10
+ ])
+ ->addColumn('request_ip', 'varbinary', [
+ 'default' => null,
+ 'null' => true,
+ 'length' => 16
+ ])
+ ->addColumn('request_type', 'boolean', [
+ 'null' => false
+ ])
+ ->addColumn('request_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10
+ ])
+ ->addColumn('request_action', 'string', [
+ 'null' => false,
+ 'length' => 20
+ ])
+ ->addColumn('model', 'string', [
+ 'null' => false,
+ 'length' => 80
+ ])
+ ->addColumn('model_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10
+ ])
+ ->addColumn('model_title', 'text', [
+ 'default' => null,
+ 'null' => true
+ ])
+ ->addColumn('change', 'blob', [
+ ])
+ ->addColumn('created', 'datetime', [
+ 'default' => null,
+ 'null' => false,
+ ])
+ ->addIndex('user_id')
+ ->addIndex('request_ip')
+ ->addIndex('model')
+ ->addIndex('model_id')
+ ->addIndex('request_action')
+ ->addIndex('created');
+ $table->create();
+ }
+ }
+}
diff --git a/config/Migrations/20211123152707_user_org.php b/config/Migrations/20211123152707_user_org.php
new file mode 100644
index 0000000..47d8a5b
--- /dev/null
+++ b/config/Migrations/20211123152707_user_org.php
@@ -0,0 +1,43 @@
+table('users')->hasColumn('organisation_id');
+ if (!$exists) {
+ $this->table('users')
+ ->addColumn('organisation_id', 'integer', [
+ 'default' => null,
+ 'null' => true,
+ 'signed' => false,
+ 'length' => 10
+ ])
+ ->addIndex('organisation_id')
+ ->update();
+ }
+ $q1 = $this->getQueryBuilder();
+ $org_id = $q1->select(['min(id)'])->from('organisations')->execute()->fetchAll()[0][0];
+ if (!empty($org_id)) {
+ $q2 = $this->getQueryBuilder();
+ $q2->update('users')
+ ->set('organisation_id', $org_id)
+ ->where(['organisation_id IS NULL'])
+ ->execute();
+ }
+ }
+}
diff --git a/config/Migrations/20211124234433_audit_changed.php b/config/Migrations/20211124234433_audit_changed.php
new file mode 100644
index 0000000..607c630
--- /dev/null
+++ b/config/Migrations/20211124234433_audit_changed.php
@@ -0,0 +1,28 @@
+table('audit_logs')->hasColumn('change');
+ if ($exists) {
+ $this->table('audit_logs')
+ ->renameColumn('change', 'changed')
+ ->update();
+ }
+ }
+}
diff --git a/config/Migrations/schema-dump-default.lock b/config/Migrations/schema-dump-default.lock
index 291060c..8c9385c 100644
Binary files a/config/Migrations/schema-dump-default.lock and b/config/Migrations/schema-dump-default.lock differ
diff --git a/config/app_local.example.php b/config/app_local.example.php
index 637128c..1ec0f4a 100644
--- a/config/app_local.example.php
+++ b/config/app_local.example.php
@@ -5,6 +5,16 @@
* Note: It is not recommended to commit files with credentials such as app_local.php
* into source code version control.
*/
+
+// set the baseurl here if you want to set it manually
+$baseurl = env('CEREBRATE_BASEURL', false);
+
+
+// Do not modify the this block
+$temp = parse_url($baseurl);
+$base = empty($temp['path']) ? false : $temp['path'];
+// end of block
+
return [
/*
* Debug Level:
@@ -89,4 +99,8 @@ return [
'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null),
],
],
+ 'Cerebrate' => [
+ 'open' => [],
+ 'dark' => 0
+ ]
];
diff --git a/config/bootstrap.php b/config/bootstrap.php
index ed41346..fcf5507 100644
--- a/config/bootstrap.php
+++ b/config/bootstrap.php
@@ -87,7 +87,12 @@ try {
*/
if (file_exists(CONFIG . 'app_local.php')) {
Configure::load('app_local', 'default');
- Configure::load('cerebrate', 'default', true);
+ //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);
+ }
}
/*
diff --git a/config/config.example.json b/config/config.example.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/config/config.example.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/debian/install b/debian/install
index 2ee55e9..12d7cc8 100755
--- a/debian/install
+++ b/debian/install
@@ -7,4 +7,3 @@ webroot /usr/share/php-cerebrate
config /usr/share/php-cerebrate
debian/cerebrate.local.conf /etc/apache2/sites-available/
debian/config.php /etc/cerebrate/
-INSTALL/mysql.sql => /usr/share/dbconfig-common/data/php-cerebrate/install/mysql
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..6e0cb03
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,79 @@
+ARG COMPOSER_VERSION
+ARG PHP_VERSION
+ARG DEBIAN_RELEASE
+
+FROM php:${PHP_VERSION}-apache-${DEBIAN_RELEASE}
+
+# we need some extra libs to be installed in the runtime
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends curl git zip unzip && \
+ apt-get install -y --no-install-recommends libicu-dev libxml2-dev && \
+ docker-php-ext-install intl pdo pdo_mysql mysqli simplexml && \
+ apt-get remove -y --purge libicu-dev libxml2-dev && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+COPY composer.json composer.json
+
+# install composer as root
+ARG COMPOSER_VERSION
+RUN curl -sL https://getcomposer.org/installer | \
+ php -- --install-dir=/usr/bin/ --filename=composer --version=${COMPOSER_VERSION}
+
+# switch back to unprivileged user for composer install
+USER www-data
+
+RUN composer install \
+ --no-interaction \
+ --no-plugins \
+ --no-scripts \
+ --prefer-dist
+
+# web server configuration
+USER root
+
+# allow .htaccess overrides and push them
+RUN a2enmod rewrite
+RUN sed -i -r '/DocumentRoot/a \\t \n\t\tAllowOverride all\n\t' /etc/apache2/sites-available/000-default.conf
+COPY --chown=www-data docker/etc/DocumentRoot.htaccess /var/www/html/.htaccess
+COPY --chown=www-data docker/etc/webroot.htaccess /var/www/html/webroot/.htaccess
+
+# passing environment variables through apache
+RUN a2enmod env
+RUN echo 'PassEnv CEREBRATE_DB_HOST' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_DB_NAME' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_DB_PASSWORD' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_DB_PORT' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_DB_SCHEMA' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_DB_USERNAME' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_EMAIL_HOST' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_EMAIL_PASSWORD' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_EMAIL_PORT' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_EMAIL_TLS' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_EMAIL_USERNAME' >> /etc/apache2/conf-enabled/environment.conf
+RUN echo 'PassEnv CEREBRATE_SECURITY_SALT' >> /etc/apache2/conf-enabled/environment.conf
+
+# entrypoint
+COPY docker/entrypoint.sh /entrypoint.sh
+RUN chmod 755 /entrypoint.sh
+
+# copy actual codebase
+COPY --chown=www-data . /var/www/html
+
+# last checks with unprivileged user
+USER www-data
+
+# CakePHP seems to not handle very well externally installed components
+# this will chown/chmod/symlink all in place for its own good
+RUN composer install --no-interaction
+
+# app config override making use of environment variables
+COPY --chown=www-data docker/etc/app_local.php /var/www/html/config/app_local.php
+# version 1.0 addition requires a config/config.json file
+# can still be overriden by a docker volume
+RUN cp -a /var/www/html/config/config.example.json /var/www/html/config/config.json
+
+# also can be overridin by a docker volume
+RUN mkdir -p /var/www/html/logs
+
+ENTRYPOINT [ "/entrypoint.sh" ]
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000..9bf0154
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,30 @@
+# Actual data and volumes
+
+The actual database will be located in `./run/database` exposed with the
+following volume `- ./run/database:/var/lib/mysql`
+
+Application logs (CakePHP / Cerebrate) will be stored in `./run/logs`,
+volume `- ./run/logs:/var/www/html/logs`
+
+You're free to change those parameters if you're using Swarm, Kubernetes or
+your favorite config management tool to deploy this stack
+
+# Building yourself
+
+You can create the following Makefile in basedir of this repository
+and issue `make image`
+
+```
+COMPOSER_VERSION?=2.1.5
+PHP_VERSION?=7.4
+DEBIAN_RELEASE?=buster
+IMAGE_NAME?=cerebrate:latest
+
+image:
+ docker build -t $(IMAGE_NAME) \
+ -f docker/Dockerfile \
+ --build-arg COMPOSER_VERSION=$(COMPOSER_VERSION) \
+ --build-arg PHP_VERSION=$(PHP_VERSION) \
+ --build-arg DEBIAN_RELEASE=$(DEBIAN_RELEASE) \
+ .
+```
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000..821ab5f
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,29 @@
+version: "3"
+services:
+ database:
+ image: mariadb:10.6
+ restart: always
+ volumes:
+ - ./run/database:/var/lib/mysql
+ environment:
+ MARIADB_RANDOM_ROOT_PASSWORD: "yes"
+ MYSQL_DATABASE: "cerebrate"
+ MYSQL_USER: "cerebrate"
+ MYSQL_PASSWORD: "etarberec"
+ www:
+ image: ghcr.io/cerebrate-project/cerebrate:main
+ ports:
+ - "8080:80"
+ volumes:
+ - ./run/logs:/var/www/html/logs
+ - ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh:ro
+ entrypoint: wait-for-it.sh -t 0 -h database -p 3306 -- /entrypoint.sh
+ environment:
+ DEBUG: "true"
+ CEREBRATE_DB_USERNAME: "cerebrate"
+ CEREBRATE_DB_PASSWORD: "etarberec"
+ CEREBRATE_DB_NAME: "cerebrate"
+ CEREBRATE_DB_HOST: database
+ CEREBRATE_SECURITY_SALT: supersecret
+ depends_on:
+ - database
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100755
index 0000000..e4c1bfd
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+set -e
+
+
+run_all_migrations() {
+ ./bin/cake migrations migrate
+ ./bin/cake migrations migrate -p tags
+ ./bin/cake migrations migrate -p ADmad/SocialAuth
+}
+
+delete_model_cache() {
+ echo >&2 "Deleting cakephp cache..."
+ rm -rf ./tmp/cache/models/*
+ rm -rf ./tmp/cache/persistent/*
+}
+
+delete_model_cache
+
+# waiting for DB to come up
+for try in 1 2 3 4 5 6; do
+ echo >&2 "migration - attempt $try"
+ run_all_migrations && break || true
+ sleep 5
+ [ "$try" = "6" ] && exit 1
+done
+
+exec /usr/local/bin/apache2-foreground "$@"
diff --git a/docker/etc/DocumentRoot.htaccess b/docker/etc/DocumentRoot.htaccess
new file mode 100644
index 0000000..ef9940b
--- /dev/null
+++ b/docker/etc/DocumentRoot.htaccess
@@ -0,0 +1,3 @@
+RewriteEngine on
+RewriteRule ^$ webroot/ [L]
+RewriteRule (.*) webroot/$1 [L]
diff --git a/docker/etc/app_local.php b/docker/etc/app_local.php
new file mode 100644
index 0000000..ac1d212
--- /dev/null
+++ b/docker/etc/app_local.php
@@ -0,0 +1,44 @@
+ env('CEREBRATE_DB_USERNAME', 'cerebrate'),
+ 'password' => env('CEREBRATE_DB_PASSWORD', ''),
+ 'host' => env('CEREBRATE_DB_HOST', 'localhost'),
+ 'database' => env('CEREBRATE_DB_NAME', 'cerebrate'),
+];
+
+// non-default port can be set on demand - otherwise the DB driver will choose the default
+if (!empty(env('CEREBRATE_DB_PORT'))) {
+ $db['port'] = env('CEREBRATE_DB_PORT');
+}
+
+// If not using the default 'public' schema with the PostgreSQL driver set it here.
+if (!empty(env('CEREBRATE_DB_SCHEMA'))) {
+ $db['schema'] = env('CEREBRATE_DB_SCHEMA');
+}
+
+return [
+ 'debug' => filter_var(env('DEBUG', false), FILTER_VALIDATE_BOOLEAN),
+
+ 'Security' => [
+ 'salt' => env('CEREBRATE_SECURITY_SALT'),
+ ],
+
+ 'Datasources' => [
+ 'default' => $db,
+ ],
+
+ 'EmailTransport' => [
+ 'default' => [
+ // host could be ssl://smtp.gmail.com then set port to 465
+ 'host' => env('CEREBRATE_EMAIL_HOST', 'localhost'),
+ 'port' => env('CEREBRATE_EMAIL_PORT', 25),
+ 'username' => env('CEREBRATE_EMAIL_USERNAME', null),
+ 'password' => env('CEREBRATE_EMAIL_PASSWORD', null),
+ 'tls' => env('CEREBRATE_EMAIL_TLS', null)
+ ],
+ ],
+ 'Cerebrate' => [
+ 'open' => [],
+ 'dark' => 0
+ ]
+];
diff --git a/docker/etc/webroot.htaccess b/docker/etc/webroot.htaccess
new file mode 100644
index 0000000..879f805
--- /dev/null
+++ b/docker/etc/webroot.htaccess
@@ -0,0 +1,3 @@
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^ index.php [L]
diff --git a/docker/wait-for-it.sh b/docker/wait-for-it.sh
new file mode 100755
index 0000000..65b7f1f
--- /dev/null
+++ b/docker/wait-for-it.sh
@@ -0,0 +1,186 @@
+#!/usr/bin/env bash
+# Use this script to test if a given TCP host/port are available
+
+# The MIT License (MIT)
+# Copyright (c) 2016 Giles Hall
+# Source: https://github.com/vishnubob/wait-for-it
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ # Check if busybox timeout uses -t flag
+ # (recent Alpine versions don't support -t anymore)
+ if timeout &>/dev/stdout | grep -q -e '-t '; then
+ WAITFORIT_BUSYTIMEFLAG="-t"
+ fi
+else
+ WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
diff --git a/documentation/cerebrate-synchornization.pdf b/documentation/cerebrate-synchornization.pdf
new file mode 100644
index 0000000..946306d
Binary files /dev/null and b/documentation/cerebrate-synchornization.pdf differ
diff --git a/documentation/local-tools-inter-connection.pdf b/documentation/local-tools-inter-connection.pdf
new file mode 100644
index 0000000..5014c98
Binary files /dev/null and b/documentation/local-tools-inter-connection.pdf differ
diff --git a/documentation/prerequisites.md b/documentation/prerequisites.md
new file mode 100644
index 0000000..7b020ad
--- /dev/null
+++ b/documentation/prerequisites.md
@@ -0,0 +1,17 @@
+# Prerequisites based on usecases
+
+This document list the requirements that have to be met in order to perform the desired usecase.
+
+## Connect a local tool to cerebrate
+- **Networking**: The *cerebrate* application must be able to contact the local tool service. That means the address and the port of the local tool must be reachable by *cerebrate*.
+- **User permissions**: Depends on the actions performed by Cerebrate on the local tool.
+ - Example: For a standard MISP configuration, a simple user with the `user` role is enough for Cerebrate to pass the health check.
+
+## Conect two cerebrate instances together
+- **Networking**: The two *cerebrate* applications must be able to contact each others. That means the address and the port of both tools must be reachable by the other one.
+- **User permissions**: No specific role or set of permission is required. Any user role can be used.
+
+## Connect two local tools through cerebrate
+- **Networking**: The two *cerebrate* applications must be able to contact each others. That means the address and the port of both tools must be reachable by the other one. This also applies to both the local tools.
+- **User permissions**: Depends on the actions performed by Cerebrate on the local tool.
+ - Example: For a standard MISP configuration, in order to have two instance connected, the API key used by *cebrate* to orchestrate the inter-connection must belong to a user having the `site-admin` permission flag. This is essential as only the `site-admin` permission allows to create synchronisation links between MISP instances.
\ No newline at end of file
diff --git a/libraries/default/InboxProcessors/UserInboxProcessor.php b/libraries/default/InboxProcessors/UserInboxProcessor.php
index 074ce13..53312d5 100644
--- a/libraries/default/InboxProcessors/UserInboxProcessor.php
+++ b/libraries/default/InboxProcessors/UserInboxProcessor.php
@@ -1,5 +1,6 @@
'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) {
diff --git a/libraries/default/InboxProcessors/templates/User/Registration.php b/libraries/default/InboxProcessors/templates/User/Registration.php
index ebcc819..5480862 100644
--- a/libraries/default/InboxProcessors/templates/User/Registration.php
+++ b/libraries/default/InboxProcessors/templates/User/Registration.php
@@ -1,99 +1,88 @@
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('
%s
%s
',
- $formUser,
- $formIndividual
- ),
- 'confirmText' => __('Create user'),
- 'confirmFunction' => 'submitRegistration'
- ]);
+ sprintf('%s
', __('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(
+ '%s
',
+ $combinedForm
+ ),
+ 'confirmText' => __('Create user'),
+ 'confirmFunction' => 'submitRegistration'
+]);
?>
\ No newline at end of file
diff --git a/libraries/default/meta_fields/cerebrate_individual_extended.json b/libraries/default/meta_fields/cerebrate_individual_extended.json
new file mode 100644
index 0000000..e3999c1
--- /dev/null
+++ b/libraries/default/meta_fields/cerebrate_individual_extended.json
@@ -0,0 +1,21 @@
+{
+ "name": "Cerebrate Individuals extended",
+ "namespace": "cerebrate",
+ "description": "Template to extend fields of individuals",
+ "version": 2,
+ "scope": "individual",
+ "uuid": "3bc374c8-3cdd-4900-823e-cc9100ad5179",
+ "source": "Cerebrate",
+ "metaFields": [
+ {
+ "field": "alternate_email",
+ "type": "text",
+ "multiple": true
+ },
+ {
+ "field": "mobile_phone",
+ "type": "text",
+ "multiple": true
+ }
+ ]
+}
diff --git a/libraries/default/meta_fields/enisa-csirt-inventory.json b/libraries/default/meta_fields/enisa-csirt-inventory.json
index 9171a5c..24a1fe7 100644
--- a/libraries/default/meta_fields/enisa-csirt-inventory.json
+++ b/libraries/default/meta_fields/enisa-csirt-inventory.json
@@ -9,7 +9,7 @@
{
"field": "website",
"type": "text",
- "regex": "(http(s)?:\\\/\\\/.)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&\/\/=]*)"
+ "regex": "https?:\\\/\\\/.+"
},
{
"field": "enisa-geo-group",
@@ -50,7 +50,7 @@
{
"field": "email",
"type": "text",
- "regex": "(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"
+ "regex": "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
},
{
"field": "country-name",
@@ -70,5 +70,5 @@
"scope": "organisation",
"source": "enisa.europa.eu/topics/csirts-in-europe/csirt-inventory/certs-by-country-interactive-map",
"uuid": "089c68c7-d97e-4f21-a798-159cd10f7864",
- "version": 1
+ "version": 3
}
\ No newline at end of file
diff --git a/libraries/default/meta_fields/it_infrastructure_and_services.json b/libraries/default/meta_fields/it_infrastructure_and_services.json
new file mode 100644
index 0000000..56e5867
--- /dev/null
+++ b/libraries/default/meta_fields/it_infrastructure_and_services.json
@@ -0,0 +1,36 @@
+{
+ "name": "IT infrastructure and services",
+ "namespace": "cerebrate",
+ "description": "Offers the possiblity to register the IP of part of the infrastructure or services.",
+ "version": 2,
+ "scope": "organisation",
+ "uuid": "a7674718-57c8-40e7-969e-d26ca911cb4a",
+ "source": "Cerebrate",
+ "metaFields": [
+ {
+ "field": "Microsoft Exchange Server IP",
+ "type": "text",
+ "multiple": true
+ },
+ {
+ "field": "Microsfot Office 365 IP",
+ "type": "text",
+ "multiple": true
+ },
+ {
+ "field": "Microsoft SharePoint IP",
+ "type": "text",
+ "multiple": true
+ },
+ {
+ "field": "Microsoft Active Directory IP",
+ "type": "text",
+ "multiple": true
+ },
+ {
+ "field": "Proxy IP",
+ "type": "text",
+ "multiple": true
+ }
+ ]
+}
diff --git a/logs/.gitkeep b/logs/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/logs/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 7107122..403e1e5 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,13 +1,12 @@
-
+
-
-
+
+
+
+
+
+
@@ -15,17 +14,17 @@
tests/TestCase/
-
+
+ ./tests/TestCase/Controller
+
+
+ ./tests/TestCase/Api
+
-
-
-
-
-
-
-
-
+
+
+
@@ -37,4 +36,4 @@
-
+
\ No newline at end of file
diff --git a/plugins/Tags/config/Migrations/20210831121348_TagSystem.php b/plugins/Tags/config/Migrations/20210831121348_TagSystem.php
index 7b15623..98492a3 100644
--- a/plugins/Tags/config/Migrations/20210831121348_TagSystem.php
+++ b/plugins/Tags/config/Migrations/20210831121348_TagSystem.php
@@ -7,25 +7,25 @@ use Migrations\AbstractMigration;
class TagSystem extends AbstractMigration
{
public function change() {
- $tags = $this->table('tags_tags')
- ->addColumn('namespace', 'string', [
+ $tags = $this->table('tags_tags');
+ $tags->addColumn('namespace', 'string', [
'default' => null,
- 'limit' => 255,
+ 'limit' => 191,
'null' => true,
])
->addColumn('predicate', 'string', [
'default' => null,
- 'limit' => 255,
+ 'limit' => 191,
'null' => true,
])
->addColumn('value', 'string', [
'default' => null,
- 'limit' => 255,
+ 'limit' => 191,
'null' => true,
])
->addColumn('name', 'string', [
'default' => null,
- 'limit' => 255,
+ 'limit' => 191,
'null' => false,
])
->addColumn('colour', 'string', [
@@ -50,8 +50,8 @@ class TagSystem extends AbstractMigration
])
->create();
- $tagged = $this->table('tags_tagged')
- ->addColumn('tag_id', 'integer', [
+ $tagged = $this->table('tags_tagged');
+ $tagged->addColumn('tag_id', 'integer', [
'default' => null,
'null' => false,
'signed' => false,
@@ -66,7 +66,7 @@ class TagSystem extends AbstractMigration
])
->addColumn('fk_model', 'string', [
'default' => null,
- 'limit' => 255,
+ 'limit' => 191,
'null' => false,
'comment' => 'The model name of the entity being tagged'
])
@@ -83,7 +83,7 @@ class TagSystem extends AbstractMigration
$tags->addIndex(['name'], ['unique' => true])
->update();
- $tagged->addIndex(['tag_id', 'fk_id', 'fk_table'], ['unique' => true])
+ $tagged->addIndex(['tag_id', 'fk_id', 'fk_model'], ['unique' => true])
->update();
}
-}
\ No newline at end of file
+}
diff --git a/plugins/Tags/src/Model/Behavior/TagBehavior.php b/plugins/Tags/src/Model/Behavior/TagBehavior.php
index b27ded9..5772d82 100644
--- a/plugins/Tags/src/Model/Behavior/TagBehavior.php
+++ b/plugins/Tags/src/Model/Behavior/TagBehavior.php
@@ -46,10 +46,10 @@ class TagBehavior extends Behavior
$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')) {
@@ -114,7 +114,6 @@ class TagBehavior extends Behavior
$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']);
@@ -131,7 +130,6 @@ class TagBehavior extends Behavior
if (!$tag->isNew()) {
continue;
}
-
$existingTag = $this->getExistingTag($tag->name);
if (!$existingTag) {
continue;
@@ -176,15 +174,14 @@ class TagBehavior extends Behavior
$result[] = array_merge($common, ['id' => $existingTag->id]);
continue;
}
-
$result[] = array_merge(
$common,
[
'name' => $tagIdentifier,
+ 'colour' => '#924da6'
]
);
}
-
return $result;
}
@@ -312,7 +309,7 @@ class TagBehavior extends Behavior
$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();
@@ -352,4 +349,4 @@ class TagBehavior extends Behavior
return $query;
}
-}
\ No newline at end of file
+}
diff --git a/plugins/Tags/src/View/Helper/TagHelper.php b/plugins/Tags/src/View/Helper/TagHelper.php
index ab9122e..71b48d7 100644
--- a/plugins/Tags/src/View/Helper/TagHelper.php
+++ b/plugins/Tags/src/View/Helper/TagHelper.php
@@ -33,7 +33,7 @@ class TagHelper extends Helper
'data-text-colour' => h($tag['text_colour']),
];
}, $options['allTags']) : [];
- $classes = ['tag-input', 'flex-grow-1'];
+ $classes = ['select2-input', 'flex-grow-1'];
$url = '';
if (!empty($this->getConfig('editable'))) {
$url = $this->Url->build([
diff --git a/plugins/Tags/templates/Tags/add.php b/plugins/Tags/templates/Tags/add.php
index 89eb5e9..c8ecfe8 100644
--- a/plugins/Tags/templates/Tags/add.php
+++ b/plugins/Tags/templates/Tags/add.php
@@ -12,7 +12,6 @@
'type' => 'color',
),
),
- 'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
'submit' => array(
'action' => $this->request->getParam('action')
)
diff --git a/plugins/Tags/templates/Tags/index.php b/plugins/Tags/templates/Tags/index.php
index 4f1a041..5f2fded 100644
--- a/plugins/Tags/templates/Tags/index.php
+++ b/plugins/Tags/templates/Tags/index.php
@@ -20,7 +20,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
diff --git a/plugins/Tags/webroot/js/tagging.js b/plugins/Tags/webroot/js/tagging.js
index 606ad21..4f32b46 100644
--- a/plugins/Tags/webroot/js/tagging.js
+++ b/plugins/Tags/webroot/js/tagging.js
@@ -23,7 +23,7 @@ function createTagPicker(clicked) {
const $clicked = $(clicked)
const $container = $clicked.closest('.tag-container')
- const $select = $container.parent().find('select.tag-input').removeClass('d-none')
+ const $select = $container.parent().find('select.select2-input').removeClass('d-none')
closePicker($select, $container)
const $pickerContainer = $('
').addClass(['picker-container', 'd-flex'])
@@ -90,7 +90,7 @@ function refreshTagList(apiResult, $container) {
}
function initSelect2Pickers() {
- $('select.tag-input').each(function() {
+ $('select.select2-input').each(function() {
if (!$(this).hasClass("select2-hidden-accessible")) {
initSelect2Picker($(this))
}
@@ -119,8 +119,10 @@ function initSelect2Picker($select) {
}
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%',
diff --git a/src/Application.php b/src/Application.php
index a63a6ce..0bf89c9 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -118,6 +118,7 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
'collectionFactory' => null,
'logErrors' => true,
]));
+ \SocialConnect\JWX\JWT::$screw = Configure::check('keycloak.screw') ? Configure::read('keycloak.screw') : 0;
}
$middlewareQueue->add(new AuthenticationMiddleware($this))
->add(new BodyParserMiddleware());
diff --git a/src/Command/ImporterCommand.php b/src/Command/ImporterCommand.php
index c79ba76..2e0c3f8 100644
--- a/src/Command/ImporterCommand.php
+++ b/src/Command/ImporterCommand.php
@@ -2,7 +2,7 @@
/**
* Generic importer to feed data to cerebrate from JSON or CSV.
- *
+ *
* - JSON configuration file must have the `format` key which can either take the value `json` or `csv`
* - If `csv` is provided, the file must contains the header.
* - If `json` is provided, a `mapping` key on how to reach each fields using the cakephp4's Hash syntax must be provided.
@@ -10,7 +10,7 @@
* - The key is the field name
* - The value
* - Can either be the string representing the path from which to get the value
- * - Or a JSON containg the `path`, the optional `override` parameter specifying if the existing data should be overriden
+ * - Or a JSON containg the `path`, the optional `override` parameter specifying if the existing data should be overriden
* and an optional `massage` function able to alter the data.
* - Example
* {
@@ -22,7 +22,7 @@
* },
*
* - The optional primary key argument provides a way to make import replayable. It can typically be used when an ID or UUID is not provided in the source file but can be replaced by something else (e.g. team-name or other type of unique data).
- *
+ *
*/
namespace App\Command;
@@ -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;
}
}
@@ -161,7 +165,7 @@ class ImporterCommand extends Command
'valueField' => 'id'
])->where(['meta_template_id' => $metaTemplate->id])->toArray();
} else {
- $this->io->error("Unkown template for UUID {$config['metaTemplateUUID']}");
+ $this->io->error("Unknown template for UUID {$config['metaTemplateUUID']}");
die(1);
}
}
@@ -192,7 +196,7 @@ class ImporterCommand extends Command
$metaEntity->meta_template_field_id = $metaTemplateFieldsMapping[$fieldName];
} else {
$hasErrors = true;
- $this->io->error("Field $fieldName is unkown for template {$metaTemplate->name}");
+ $this->io->error("Field $fieldName is unknown for template {$metaTemplate->name}");
break;
}
}
@@ -525,4 +529,4 @@ class ImporterCommand extends Command
{
return is_null($value) ? '' : $value;
}
-}
\ No newline at end of file
+}
diff --git a/src/Command/config/config-misp-format-organisation.json b/src/Command/config/config-misp-format-organisation.json
new file mode 100644
index 0000000..7e591c0
--- /dev/null
+++ b/src/Command/config/config-misp-format-organisation.json
@@ -0,0 +1,11 @@
+{
+ "format": "json",
+ "mapping": {
+ "name": "{n}.Organisation.name",
+ "uuid": "{n}.Organisation.uuid",
+ "nationality": "{n}.Organisation.nationality"
+ },
+ "sourceHeaders": {
+ "Authorization": "~~YOUR_API_KEY_HERE~~"
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php
new file mode 100644
index 0000000..65cd11c
--- /dev/null
+++ b/src/Controller/ApiController.php
@@ -0,0 +1,19 @@
+set('url', $url);
+ }
+}
diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php
index 34ab727..439c7dd 100644
--- a/src/Controller/AppController.php
+++ b/src/Controller/AppController.php
@@ -54,7 +54,6 @@ class AppController extends Controller
public function initialize(): void
{
parent::initialize();
-
$this->loadComponent('RequestHandler');
$this->loadComponent('Flash');
$this->loadComponent('RestResponse');
@@ -102,7 +101,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', 'Organisations']
]);
if (!empty($user['disabled'])) {
$this->Authentication->logout();
@@ -111,7 +110,13 @@ class AppController extends Controller
}
unset($user['password']);
$this->ACL->setUser($user);
+ $this->request->getSession()->write('authUser', $user);
$this->isAdmin = $user['role']['perm_admin'];
+ if (!$this->ParamHandler->isRest()) {
+ $this->set('menu', $this->ACL->getMenu());
+ $this->set('loggedUser', $this->ACL->getUser());
+ $this->set('roleAccess', $this->ACL->getRoleAccess(false, false));
+ }
} else if ($this->ParamHandler->isRest()) {
throw new MethodNotAllowedException(__('Invalid user credentials.'));
}
@@ -126,15 +131,27 @@ class AppController extends Controller
}
$this->ACL->checkAccess();
- $this->set('menu', $this->ACL->getMenu());
- $this->set('breadcrumb', $this->Navigation->getBreadcrumb());
- $this->set('ajax', $this->request->is('ajax'));
- $this->request->getParam('prefix');
- $this->set('baseurl', Configure::read('App.fullBaseUrl'));
- $this->set('bsTheme', Configure::read('Cerebrate')['ui.bsTheme']);
+ if (!$this->ParamHandler->isRest()) {
+ $this->set('breadcrumb', $this->Navigation->getBreadcrumb());
+ $this->set('ajax', $this->request->is('ajax'));
+ $this->request->getParam('prefix');
+ $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');
+ if ($this->modelClass == 'Tags.Tags') {
+ $this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
+ }
+ }
+ }
+
+ public function beforeRender(EventInterface $event)
+ {
+ if (!$this->ParamHandler->isRest()) {
+ $this->set('breadcrumb', $this->Navigation->getBreadcrumb());
}
}
@@ -142,13 +159,30 @@ class AppController extends Controller
{
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && strlen($_SERVER['HTTP_AUTHORIZATION'])) {
$this->loadModel('AuthKeys');
+ $logModel = $this->Users->auditLogs();
$authKey = $this->AuthKeys->checkKey($_SERVER['HTTP_AUTHORIZATION']);
if (!empty($authKey)) {
$this->loadModel('Users');
$user = $this->Users->get($authKey['user_id']);
+ $logModel->insert([
+ 'request_action' => 'login',
+ 'model' => 'Users',
+ 'model_id' => $user['id'],
+ 'model_title' => $user['username'],
+ 'changed' => []
+ ]);
if (!empty($user)) {
$this->Authentication->setIdentity($user);
}
+ } else {
+ $user = $logModel->userInfo();
+ $logModel->insert([
+ 'request_action' => 'login',
+ 'model' => 'Users',
+ 'model_id' => $user['id'],
+ 'model_title' => $user['name'],
+ 'changed' => []
+ ]);
}
}
}
@@ -163,4 +197,9 @@ class AppController extends Controller
{
return $this->RestResponse->viewData($this->ACL->findMissingFunctionNames());
}
+
+ public function getRoleAccess()
+ {
+ return $this->RestResponse->viewData($this->ACL->getRoleAccess(false, false));
+ }
}
diff --git a/src/Controller/AuditLogsController.php b/src/Controller/AuditLogsController.php
new file mode 100644
index 0000000..e72327d
--- /dev/null
+++ b/src/Controller/AuditLogsController.php
@@ -0,0 +1,41 @@
+CRUD->index([
+ 'contain' => $this->containFields,
+ 'filters' => $this->filterFields,
+ 'quickFilters' => $this->quickFilterFields,
+ 'afterFind' => function($data) {
+ $data['request_ip'] = inet_ntop(stream_get_contents($data['request_ip']));
+ $data['changed'] = stream_get_contents($data['changed']);
+ return $data;
+ }
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function filtering()
+ {
+ $this->CRUD->filtering();
+ }
+
+}
diff --git a/src/Controller/AuthKeysController.php b/src/Controller/AuthKeysController.php
index 5f75b4a..9ed43c3 100644
--- a/src/Controller/AuthKeysController.php
+++ b/src/Controller/AuthKeysController.php
@@ -16,15 +16,25 @@ class AuthKeysController extends AppController
{
public $filterFields = ['Users.username', 'authkey', 'comment', 'Users.id'];
public $quickFilterFields = ['authkey', ['comment' => true]];
- public $containFields = ['Users'];
+ public $containFields = ['Users' => ['fields' => ['id', 'username']]];
public function index()
{
+ $currentUser = $this->ACL->getUser();
+ $conditions = [];
+ if (empty($currentUser['role']['perm_admin'])) {
+ $conditions['Users.organisation_id'] = $currentUser['organisation_id'];
+ if (empty($currentUser['role']['perm_org_admin'])) {
+ $conditions['Users.id'] = $currentUser['id'];
+ }
+ }
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contain' => $this->containFields,
- 'exclude_fields' => ['authkey']
+ 'exclude_fields' => ['authkey'],
+ 'conditions' => $conditions,
+ 'hidden' => []
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@@ -35,7 +45,15 @@ class AuthKeysController extends AppController
public function delete($id)
{
- $this->CRUD->delete($id);
+ $currentUser = $this->ACL->getUser();
+ $conditions = [];
+ if (empty($currentUser['role']['perm_admin'])) {
+ $conditions['Users.organisation_id'] = $currentUser['organisation_id'];
+ if (empty($currentUser['role']['perm_org_admin'])) {
+ $conditions['Users.id'] = $currentUser['id'];
+ }
+ }
+ $this->CRUD->delete($id, ['conditions' => $conditions, 'contain' => 'Users']);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@@ -46,8 +64,30 @@ class AuthKeysController extends AppController
public function add()
{
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
+ $validUsers = [];
+ $userConditions = [];
+ $currentUser = $this->ACL->getUser();
+ if (empty($currentUser['role']['perm_admin'])) {
+ if (empty($currentUser['role']['perm_org_admin'])) {
+ $userConditions['id'] = $currentUser['id'];
+ } else {
+ $role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0])->all()->extract('id')->toList();
+ $userConditions['role_id IN'] = $role_ids;
+ }
+ }
+ $users = $this->Users->find('list');
+ if (!empty($userConditions)) {
+ $users->where($userConditions);
+ }
+ $users = $users->order(['username' => 'asc'])->all()->toArray();
$this->CRUD->add([
- 'displayOnSuccess' => 'authkey_display'
+ 'displayOnSuccess' => 'authkey_display',
+ 'beforeSave' => function($data) use ($users) {
+ if (!in_array($data['user_id'], array_keys($users))) {
+ return false;
+ }
+ return $data;
+ }
]);
$responsePayload = $this->CRUD->getResponsePayload([
'displayOnSuccess' => 'authkey_display'
@@ -57,9 +97,7 @@ class AuthKeysController extends AppController
}
$this->loadModel('Users');
$dropdownData = [
- 'user' => $this->Users->find('list', [
- 'sort' => ['username' => 'asc']
- ])
+ 'user' => $users
];
$this->set(compact('dropdownData'));
}
diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php
index 58c19fc..fb51f49 100644
--- a/src/Controller/Component/ACLComponent.php
+++ b/src/Controller/Component/ACLComponent.php
@@ -37,6 +37,7 @@ class ACLComponent extends Component
'*' => [
'checkPermission' => ['*'],
'generateUUID' => ['*'],
+ 'getRoleAccess' => ['*'],
'queryACL' => ['perm_admin']
],
'Alignments' => [
@@ -45,6 +46,9 @@ class ACLComponent extends Component
'index' => ['*'],
'view' => ['*']
],
+ 'AuditLogs' => [
+ 'index' => ['perm_admin']
+ ],
'AuthKeys' => [
'add' => ['*'],
'delete' => ['*'],
@@ -64,6 +68,7 @@ class ACLComponent extends Component
'view' => ['perm_admin']
],
'EncryptionKeys' => [
+ 'view' => ['*'],
'add' => ['*'],
'edit' => ['*'],
'delete' => ['*'],
@@ -82,22 +87,30 @@ class ACLComponent extends Component
'add' => ['perm_admin'],
'delete' => ['perm_admin'],
'edit' => ['perm_admin'],
+ 'filtering' => ['*'],
'index' => ['*'],
- 'view' => ['*']
+ 'tag' => ['perm_tagger'],
+ 'untag' => ['perm_tagger'],
+ 'view' => ['*'],
+ 'viewTags' => ['*']
],
'Instance' => [
'home' => ['*'],
'migrate' => ['perm_admin'],
'migrationIndex' => ['perm_admin'],
'rollback' => ['perm_admin'],
+ 'saveSetting' => ['perm_admin'],
+ 'searchAll' => ['*'],
+ 'settings' => ['perm_admin'],
'status' => ['*']
],
'LocalTools' => [
'action' => ['perm_admin'],
'add' => ['perm_admin'],
+ 'batchAction' => ['perm_admin'],
'broodTools' => ['perm_admin'],
'connectionRequest' => ['perm_admin'],
- 'connectLocal' => ['perm_admin'],
+ // 'connectLocal' => ['perm_admin'],
'delete' => ['perm_admin'],
'edit' => ['perm_admin'],
'exposedTools' => ['OR' => ['perm_admin', 'perm_sync']],
@@ -123,7 +136,10 @@ class ACLComponent extends Component
'edit' => ['perm_admin'],
'filtering' => ['*'],
'index' => ['*'],
- 'view' => ['*']
+ 'tag' => ['perm_tagger'],
+ 'untag' => ['perm_tagger'],
+ 'view' => ['*'],
+ 'viewTags' => ['*']
],
'Outbox' => [
'createEntry' => ['perm_admin'],
@@ -145,25 +161,42 @@ class ACLComponent extends Component
'view' => ['*']
],
'SharingGroups' => [
- 'add' => ['perm_admin'],
- 'addOrg' => ['perm_admin'],
- 'delete' => ['perm_admin'],
- 'edit' => ['perm_admin'],
+ 'add' => ['perm_org_admin'],
+ 'addOrg' => ['perm_org_admin'],
+ 'delete' => ['perm_org_admin'],
+ 'edit' => ['perm_org_admin'],
'index' => ['*'],
'listOrgs' => ['*'],
- 'removeOrg' => ['perm_admin'],
+ 'removeOrg' => ['perm_org_admin'],
'view' => ['*']
],
'Users' => [
- 'add' => ['perm_admin'],
- 'delete' => ['perm_admin'],
+ 'add' => ['perm_org_admin'],
+ 'delete' => ['perm_org_admin'],
'edit' => ['*'],
- 'index' => ['perm_admin'],
+ 'index' => ['perm_org_admin'],
'login' => ['*'],
'logout' => ['*'],
'register' => ['*'],
- 'toggle' => ['perm_admin'],
+ 'settings' => ['*'],
+ 'toggle' => ['perm_org_admin'],
'view' => ['*']
+ ],
+ 'UserSettings' => [
+ 'index' => ['*'],
+ 'view' => ['*'],
+ 'add' => ['*'],
+ 'edit' => ['*'],
+ 'delete' => ['*'],
+ 'getSettingByName' => ['*'],
+ 'setSetting' => ['*'],
+ 'saveSetting' => ['*'],
+ 'getBookmarks' => ['*'],
+ 'saveBookmark' => ['*'],
+ 'deleteBookmark' => ['*']
+ ],
+ 'Api' => [
+ 'index' => ['*']
]
);
@@ -254,7 +287,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
@@ -267,9 +300,19 @@ class ACLComponent extends Component
return true;
}
//$this->__checkLoggedActions($user, $controller, $action);
+ if (isset($this->aclList['*'][$action])) {
+ if ($this->evaluateAccessLeaf('*', $action)) {
+ return true;
+ }
+ }
if (!isset($this->aclList[$controller])) {
return $this->__error(404, __('Invalid controller.'), $soft);
}
+ return $this->evaluateAccessLeaf($controller, $action);
+ }
+
+ private function evaluateAccessLeaf(string $controller, string $action): bool
+ {
if (isset($this->aclList[$controller][$action]) && !empty($this->aclList[$controller][$action])) {
if (in_array('*', $this->aclList[$controller][$action])) {
return true;
@@ -290,6 +333,12 @@ class ACLComponent extends Component
if ($allConditionsMet) {
return true;
}
+ } else {
+ foreach ($this->aclList[$controller][$action] as $permission) {
+ if ($this->user['role'][$permission]) {
+ return true;
+ }
+ }
}
}
return false;
@@ -391,13 +440,18 @@ class ACLComponent extends Component
return $missing;
}
+ public function getRoleAccess($role = false, $url_mode = true)
+ {
+ return $this->__checkRoleAccess($role, $url_mode);
+ }
+
public function printRoleAccess($content = false)
{
$results = [];
- $this->Role = TableRegistry::get('Role');
+ $this->Role = TableRegistry::get('Roles');
$conditions = [];
if (is_numeric($content)) {
- $conditions = array('Role.id' => $content);
+ $conditions = array('id' => $content);
}
$roles = $this->Role->find('all', array(
'recursive' => -1,
@@ -413,50 +467,63 @@ class ACLComponent extends Component
return $results;
}
- private function __checkRoleAccess($role)
+ private function __formatControllerAction(array $results, string $controller, string $action, $url_mode = true): array
{
- $result = [];
- foreach ($this->__aclList as $controller => $actions) {
- $controllerNames = Inflector::variable($controller) == Inflector::underscore($controller) ? array(Inflector::variable($controller)) : array(Inflector::variable($controller), Inflector::underscore($controller));
- foreach ($controllerNames as $controllerName) {
- foreach ($actions as $action => $permissions) {
- if ($role['perm_site_admin']) {
- $result[] = DS . $controllerName . DS . $action;
- } elseif (in_array('*', $permissions)) {
- $result[] = DS . $controllerName . DS . $action . DS . '*';
- } elseif (isset($permissions['OR'])) {
- $access = false;
- foreach ($permissions['OR'] as $permission) {
- if ($role[$permission]) {
- $access = true;
- }
+ if ($url_mode) {
+ $results[] = DS . $controller . DS . $action . DS . '*';
+ } else {
+ $results[$controller][] = $action;
+ }
+ return $results;
+ }
+
+ private function __checkRoleAccess($role = false, $url_mode = true)
+ {
+ $results = [];
+ if ($role === false) {
+ $role = $this->getUser()['role'];
+ }
+ foreach ($this->aclList as $controller => $actions) {
+ foreach ($actions as $action => $permissions) {
+ if ($role['perm_admin']) {
+ $results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
+ } elseif (in_array('*', $permissions)) {
+ $results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
+ } elseif (isset($permissions['OR'])) {
+ $access = false;
+ foreach ($permissions['OR'] as $permission) {
+ if ($role[$permission]) {
+ $access = true;
}
- if ($access) {
- $result[] = DS . $controllerName . DS . $action . DS . '*';
- }
- } elseif (isset($permissions['AND'])) {
- $access = true;
- foreach ($permissions['AND'] as $permission) {
- if ($role[$permission]) {
- $access = false;
- }
- }
- if ($access) {
- $result[] = DS . $controllerName . DS . $action . DS . '*';
- }
- } elseif (isset($permissions[0]) && $role[$permissions[0]]) {
- $result[] = DS . $controllerName . DS . $action . DS . '*';
}
+ if ($access) {
+ $results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
+ }
+ } elseif (isset($permissions['AND'])) {
+ $access = true;
+ foreach ($permissions['AND'] as $permission) {
+ if ($role[$permission]) {
+ $access = false;
+ }
+ }
+ if ($access) {
+ $results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
+ }
+ } elseif (isset($permissions[0]) && $role[$permissions[0]]) {
+ $results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
}
}
}
- return $result;
+ return $results;
}
public function getMenu()
{
$menu = $this->Navigation->getSideMenu();
foreach ($menu as $group => $subMenu) {
+ if ($group == '__bookmarks') {
+ continue;
+ }
foreach ($subMenu as $subMenuElementName => $subMenuElement) {
if (!empty($subMenuElement['url']) && !$this->checkAccessUrl($subMenuElement['url'], true) === true) {
unset($menu[$group][$subMenuElementName]);
diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php
index 827ac1c..d479a27 100644
--- a/src/Controller/Component/CRUDComponent.php
+++ b/src/Controller/Component/CRUDComponent.php
@@ -6,10 +6,14 @@ use Cake\Controller\Component;
use Cake\Error\Debugger;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
+use Cake\Utility\Text;
use Cake\View\ViewBuilder;
use Cake\ORM\TableRegistry;
+use Cake\Routing\Router;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
+use Cake\Collection\Collection;
+use App\Utility\UI\IndexSetting;
class CRUDComponent extends Component
{
@@ -33,6 +37,8 @@ class CRUDComponent extends Component
$options['filters'] = [];
}
$options['filters'][] = 'quickFilter';
+ } else {
+ $options['quickFilters'] = [];
}
$options['filters'][] = 'filteringLabel';
if ($this->taggingSupported()) {
@@ -49,7 +55,10 @@ class CRUDComponent extends Component
$query = $options['filterFunction']($query);
}
$query = $this->setFilters($params, $query, $options);
- $query = $this->setQuickFilters($params, $query, empty($options['quickFilters']) ? [] : $options['quickFilters']);
+ $query = $this->setQuickFilters($params, $query, $options);
+ if (!empty($options['conditions'])) {
+ $query->where($options['conditions']);
+ }
if (!empty($options['contain'])) {
$query->contain($options['contain']);
}
@@ -61,25 +70,86 @@ class CRUDComponent extends Component
}
if ($this->Controller->ParamHandler->isRest()) {
$data = $query->all();
+ if (isset($options['hidden'])) {
+ $data->each(function($value, $key) use ($options) {
+ $hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']];
+ $value->setHidden($hidden);
+ return $value;
+ });
+ }
if (isset($options['afterFind'])) {
- if (is_callable($options['afterFind'])) {
- $data = $options['afterFind']($data);
+ $function = $options['afterFind'];
+ if (is_callable($function)) {
+ $data->each(function($value, $key) use ($function) {
+ return $function($value);
+ });
} else {
- $data = $this->Table->{$options['afterFind']}($data);
+ $t = $this->Table;
+ $data->each(function($value, $key) use ($t, $function) {
+ return $t->$function($value);
+ });
}
}
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
} else {
+ if ($this->metaFieldsSupported()) {
+ $query = $this->includeRequestedMetaFields($query);
+ }
$this->Controller->loadComponent('Paginator');
$data = $this->Controller->Paginator->paginate($query);
if (isset($options['afterFind'])) {
- if (is_callable($options['afterFind'])) {
- $data = $options['afterFind']($data);
+ $function = $options['afterFind'];
+ if (is_callable($function)) {
+ $data->each(function($value, $key) use ($function) {
+ return $function($value);
+ });
} else {
- $data = $this->Table->{$options['afterFind']}($data);
+ $t = $this->Table;
+ $data->each(function($value, $key) use ($t, $function) {
+ return $t->$function($value);
+ });
}
}
$this->setFilteringContext($options['contextFilters'] ?? [], $params);
+ if ($this->metaFieldsSupported()) {
+ $data = $data->toArray();
+ $metaTemplates = $this->getMetaTemplates()->toArray();
+ foreach ($data as $i => $row) {
+ $data[$i] = $this->attachMetaTemplatesIfNeeded($row, $metaTemplates);
+ }
+ $this->Controller->set('meta_templates', $metaTemplates);
+ }
+ if (true) { // check if stats are requested
+ $modelStatistics = [];
+ if ($this->Table->hasBehavior('Timestamp')) {
+ $modelStatistics = $this->Table->getActivityStatisticsForModel(
+ $this->Table,
+ !is_numeric($this->request->getQuery('statistics_days')) ? 7 : $this->request->getQuery('statistics_days')
+ );
+ }
+ if (!empty($options['statisticsFields'])) {
+ $statIncludeRemaining = $this->request->getQuery('statistics_include_remainging', true);
+ if (is_string($statIncludeRemaining)) {
+ $statIncludeRemaining = $statIncludeRemaining == 'true' ? true : false;
+ }
+ $statIgnoreNull = $this->request->getQuery('statistics_ignore_null', true);
+ if (is_string($statIgnoreNull)) {
+ $statIgnoreNull = $statIgnoreNull == 'true' ? true : false;
+ }
+ $statsOptions = [
+ 'limit' => !is_numeric($this->request->getQuery('statistics_entry_amount')) ? 5 : $this->request->getQuery('statistics_entry_amount'),
+ 'includeOthers' => $statIncludeRemaining,
+ 'ignoreNull' => $statIgnoreNull,
+ ];
+ $modelStatistics['usage'] = $this->Table->getStatisticsUsageForModel(
+ $this->Table,
+ $options['statisticsFields'],
+ $statsOptions
+ );
+ }
+ $this->Controller->set('modelStatistics', $modelStatistics);
+ }
+ $this->Controller->set('model', $this->Table);
$this->Controller->set('data', $data);
}
}
@@ -89,8 +159,17 @@ class CRUDComponent extends Component
if ($this->taggingSupported()) {
$this->Controller->set('taggingEnabled', true);
$this->setAllTags();
+ } else {
+ $this->Controller->set('taggingEnabled', false);
}
- $filters = !empty($this->Controller->filters) ? $this->Controller->filters : [];
+ if ($this->metaFieldsSupported()) {
+ $metaTemplates = $this->getMetaTemplates()->toArray();
+ $this->Controller->set('metaFieldsEnabled', true);
+ $this->Controller->set('metaTemplates', $metaTemplates);
+ } else {
+ $this->Controller->set('metaFieldsEnabled', false);
+ }
+ $filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$this->Controller->set('filters', $filters);
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/filters');
@@ -114,25 +193,36 @@ class CRUDComponent extends Component
private function getMetaTemplates()
{
$metaTemplates = [];
- if (!empty($this->Table->metaFields)) {
- $metaQuery = $this->MetaTemplates->find();
- $metaQuery
- ->order(['is_default' => 'DESC'])
- ->where([
- 'scope' => $this->Table->metaFields,
- 'enabled' => 1
- ]);
- $metaQuery->contain(['MetaTemplateFields']);
- $metaTemplates = $metaQuery->all();
+ if (!$this->metaFieldsSupported()) {
+ throw new \Exception(__("Table {$this->TableAlias} does not support meta_fields"));
}
- $this->Controller->set('metaTemplates', $metaTemplates);
- return true;
+ $metaFieldsBehavior = $this->Table->getBehavior('MetaFields');
+ $metaQuery = $this->MetaTemplates->find();
+ $metaQuery
+ ->order(['is_default' => 'DESC'])
+ ->where([
+ 'scope' => $metaFieldsBehavior->getScope(),
+ ])
+ ->contain('MetaTemplateFields')
+ ->formatResults(function (\Cake\Collection\CollectionInterface $metaTemplates) { // Set meta-template && meta-template-fields indexed by their ID
+ return $metaTemplates
+ ->map(function ($metaTemplate) {
+ $metaTemplate->meta_template_fields = Hash::combine($metaTemplate->meta_template_fields, '{n}.id', '{n}');
+ return $metaTemplate;
+ })
+ ->indexBy('id');
+ });
+ $metaTemplates = $metaQuery->all();
+ return $metaTemplates;
}
public function add(array $params = []): void
{
- $this->getMetaTemplates();
$data = $this->Table->newEmptyEntity();
+ if ($this->metaFieldsSupported()) {
+ $metaTemplates = $this->getMetaTemplates();
+ $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
+ }
if (!empty($params['fields'])) {
$this->Controller->set('fields', $params['fields']);
}
@@ -148,9 +238,17 @@ class CRUDComponent extends Component
if (!empty($params['fields'])) {
$patchEntityParams['fields'] = $params['fields'];
}
+ if ($this->metaFieldsSupported()) {
+ $massagedData = $this->massageMetaFields($data, $input, $metaTemplates);
+ unset($input['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity
+ $data = $massagedData['entity'];
+ }
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
+ if ($data === false) {
+ throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
+ }
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
@@ -158,9 +256,6 @@ class CRUDComponent extends Component
$params['afterSave']($data);
}
$message = __('{0} added.', $this->ObjectAlias);
- if (!empty($input['metaFields'])) {
- $this->saveMetaFields($data->id, $input);
- }
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
@@ -196,6 +291,7 @@ class CRUDComponent extends Component
}
}
}
+ $this->Controller->entity = $data;
$this->Controller->set('entity', $data);
}
@@ -221,7 +317,11 @@ class CRUDComponent extends Component
foreach ($data->getErrors() as $field => $errorData) {
$errorMessages = [];
foreach ($errorData as $key => $value) {
- $errorMessages[] = $value;
+ if (is_array($value)) {
+ $errorMessages[] = implode('& ', Hash::extract($value, "{s}.{s}"));
+ } else {
+ $errorMessages[] = $value;
+ }
}
$validationMessage .= __('{0}: {1}', $field, implode(',', $errorMessages));
}
@@ -234,6 +334,91 @@ class CRUDComponent extends Component
$this->Table->saveMetaFields($id, $input, $this->Table);
}
+ // prune empty values and marshall fields
+ public function massageMetaFields($entity, $input, $allMetaTemplates=[])
+ {
+ if (empty($input['MetaTemplates'] || !$this->metaFieldsSupported())) {
+ return ['entity' => $entity, 'metafields_to_delete' => []];
+ }
+
+ $metaFieldsTable = TableRegistry::getTableLocator()->get('MetaFields');
+ $metaFieldsIndex = [];
+ if (empty($metaTemplates)) {
+ $allMetaTemplates = $this->getMetaTemplates()->toArray();
+ }
+ if (!empty($entity->meta_fields)) {
+ foreach ($entity->meta_fields as $i => $metaField) {
+ $metaFieldsIndex[$metaField->id] = $i;
+ }
+ } else {
+ $entity->meta_fields = [];
+ }
+
+ $metaFieldsToDelete = [];
+ foreach ($input['MetaTemplates'] as $template_id => $template) {
+ foreach ($template['meta_template_fields'] as $meta_template_field_id => $meta_template_field) {
+ $rawMetaTemplateField = $allMetaTemplates[$template_id]['meta_template_fields'][$meta_template_field_id];
+ foreach ($meta_template_field['metaFields'] as $meta_field_id => $meta_field) {
+ if ($meta_field_id == 'new') { // create new meta_field
+ $new_meta_fields = $meta_field;
+ foreach ($new_meta_fields as $new_value) {
+ if (!empty($new_value)) {
+ $metaField = $metaFieldsTable->newEmptyEntity();
+ $metaFieldsTable->patchEntity($metaField, [
+ 'value' => $new_value,
+ 'scope' => $this->Table->getBehavior('MetaFields')->getScope(),
+ 'field' => $rawMetaTemplateField->field,
+ 'meta_template_id' => $rawMetaTemplateField->meta_template_id,
+ 'meta_template_field_id' => $rawMetaTemplateField->id,
+ 'parent_id' => $entity->id,
+ 'uuid' => Text::uuid(),
+ ]);
+ $entity->meta_fields[] = $metaField;
+ $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField;
+ }
+ }
+ } else {
+ $new_value = $meta_field['value'];
+ if (!empty($new_value)) { // update meta_field and attach validation errors
+ if (!empty($metaFieldsIndex[$meta_field_id])) {
+ $index = $metaFieldsIndex[$meta_field_id];
+ $metaFieldsTable->patchEntity($entity->meta_fields[$index], [
+ 'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id
+ ], ['value']);
+ $metaFieldsTable->patchEntity(
+ $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id],
+ ['value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id],
+ ['value']
+ );
+ } else { // metafield comes from a second post where the temporary entity has already been created
+ $metaField = $metaFieldsTable->newEmptyEntity();
+ $metaFieldsTable->patchEntity($metaField, [
+ 'value' => $new_value,
+ 'scope' => $this->Table->getBehavior('MetaFields')->getScope(), // get scope from behavior
+ 'field' => $rawMetaTemplateField->field,
+ 'meta_template_id' => $rawMetaTemplateField->meta_template_id,
+ 'meta_template_field_id' => $rawMetaTemplateField->id,
+ 'parent_id' => $entity->id,
+ 'uuid' => Text::uuid(),
+ ]);
+ $entity->meta_fields[] = $metaField;
+ $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField;
+ }
+ } else { // Metafield value is empty, indicating the field should be removed
+ $index = $metaFieldsIndex[$meta_field_id];
+ $metaFieldsToDelete[] = $entity->meta_fields[$index];
+ unset($entity->meta_fields[$index]);
+ unset($entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id]);
+ }
+ }
+ }
+ }
+ }
+
+ $entity->setDirty('meta_fields', true);
+ return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
+ }
+
private function __massageInput($params)
{
$input = $this->request->getData();
@@ -257,13 +442,33 @@ class CRUDComponent extends Component
if (empty($id)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
- $this->getMetaTemplates();
if ($this->taggingSupported()) {
$params['contain'][] = 'Tags';
$this->setAllTags();
}
- $data = $this->Table->get($id, isset($params['get']) ? $params['get'] : $params);
- $data = $this->getMetaFields($id, $data);
+ $queryParam = isset($params['get']) ? $params['get'] : $params;
+ if ($this->metaFieldsSupported()) {
+ if (empty( $queryParam['contain'])) {
+ $queryParam['contain'] = [];
+ }
+ if (is_array( $queryParam['contain'])) {
+ $queryParam['contain'][] = 'MetaFields';
+ } else {
+ $queryParam['contain'] = [ $queryParam['contain'], 'MetaFields'];
+ }
+ }
+ $query = $this->Table->find()->where(['id' => $id]);
+ if (!empty($params['conditions'])) {
+ $query->where($params['conditions']);
+ }
+ $data = $query->first();
+ if (empty($data)) {
+ throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
+ }
+ if ($this->metaFieldsSupported()) {
+ $metaTemplates = $this->getMetaTemplates();
+ $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
+ }
if (!empty($params['fields'])) {
$this->Controller->set('fields', $params['fields']);
}
@@ -275,20 +480,25 @@ class CRUDComponent extends Component
if (!empty($params['fields'])) {
$patchEntityParams['fields'] = $params['fields'];
}
+ if ($this->metaFieldsSupported()) {
+ $massagedData = $this->massageMetaFields($data, $input, $metaTemplates);
+ unset($input['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity
+ $data = $massagedData['entity'];
+ $metaFieldsToDelete = $massagedData['metafields_to_delete'];
+ }
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
+ if ($this->metaFieldsSupported() && !empty($metaFieldsToDelete)) {
+ $this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
+ }
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]);
- $this->saveMetaFields($savedData->id, $input);
- }
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
@@ -303,7 +513,7 @@ class CRUDComponent extends Component
}
} else {
$validationErrors = $data->getErrors();
- $validationMessage = $this->prepareValidationMessage($validationErrors);
+ $validationMessage = $this->prepareValidationError($data);
$message = __(
'{0} could not be modified.{1}',
$this->ObjectAlias,
@@ -317,15 +527,16 @@ class CRUDComponent extends Component
}
}
}
+ $this->Controller->entity = $data;
$this->Controller->set('entity', $data);
}
public function attachMetaData($id, $data)
{
- if (empty($this->Table->metaFields)) {
- return $data;
+ if (!$this->metaFieldsSupported()) {
+ throw new \Exception(__("Table {$this->TableAlias} does not support meta_fields"));
}
- $metaFieldScope = $this->Table->metaFields;
+ $metaFieldScope = $this->Table->getBehavior('MetaFields')->getScope();
$query = $this->MetaTemplates->find()->where(['MetaTemplates.scope' => $metaFieldScope]);
$query->contain(['MetaTemplateFields.MetaFields' => function ($q) use ($id, $metaFieldScope) {
return $q->where(['MetaFields.scope' => $metaFieldScope, 'MetaFields.parent_id' => $id]);
@@ -354,21 +565,80 @@ class CRUDComponent extends Component
return $metaTemplates;
}
- public function getMetaFields($id, $data)
+ public function getMetaFields($id)
{
- if (empty($this->Table->metaFields)) {
- return $data;
+ if (!$this->metaFieldsSupported()) {
+ throw new \Exception(__("Table {$this->TableAlias} does not support meta_fields"));
}
$query = $this->MetaFields->find();
- $query->where(['MetaFields.scope' => $this->Table->metaFields, 'MetaFields.parent_id' => $id]);
+ $query->where(['MetaFields.scope' => $this->Table->getBehavior('MetaFields')->getScope(), 'MetaFields.parent_id' => $id]);
$metaFields = $query->all();
- $data['metaFields'] = [];
- foreach($metaFields as $metaField) {
- $data['metaFields'][$metaField->meta_template_id][$metaField->field] = $metaField->value;
+ $data = [];
+ foreach ($metaFields as $metaField) {
+ if (empty($data[$metaField->meta_template_id][$metaField->meta_template_field_id])) {
+ $data[$metaField->meta_template_id][$metaField->meta_template_field_id] = [];
+ }
+ $data[$metaField->meta_template_id][$metaField->meta_template_field_id][$metaField->id] = $metaField;
}
return $data;
}
+ public function attachMetaTemplates($data, $metaTemplates, $pruneEmptyDisabled=true)
+ {
+ $this->MetaTemplates = TableRegistry::getTableLocator()->get('MetaTemplates');
+ $metaFields = [];
+ if (!empty($data->id)) {
+ $metaFields = $this->getMetaFields($data->id);
+ }
+ foreach ($metaTemplates as $i => $metaTemplate) {
+ if (isset($metaFields[$metaTemplate->id])) {
+ foreach ($metaTemplate->meta_template_fields as $j => $meta_template_field) {
+ if (isset($metaFields[$metaTemplate->id][$meta_template_field->id])) {
+ $metaTemplates[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = $metaFields[$metaTemplate->id][$meta_template_field->id];
+ } else {
+ $metaTemplates[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = [];
+ }
+ }
+ } else {
+ if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) {
+ unset($metaTemplates[$i]);
+ }
+ }
+ $newestTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate);
+ if (!empty($newestTemplate) && !empty($metaTemplates[$i])) {
+ $metaTemplates[$i]['hasNewerVersion'] = $newestTemplate;
+ }
+ }
+ $data['MetaTemplates'] = $metaTemplates;
+ return $data;
+ }
+
+ protected function includeRequestedMetaFields($query)
+ {
+ $user = $this->Controller->ACL->getUser();
+ $tableSettings = IndexSetting::getTableSetting($user, $this->Table);
+ if (empty($tableSettings['visible_meta_column'])) {
+ return $query;
+ }
+ $containConditions = ['OR' => []];
+ $requestedMetaFields = [];
+ foreach ($tableSettings['visible_meta_column'] as $template_id => $fields) {
+ $containConditions['OR'][] = [
+ 'meta_template_id' => $template_id,
+ 'meta_template_field_id IN' => array_map('intval', $fields),
+ ];
+ foreach ($fields as $field) {
+ $requestedMetaFields[] = ['template_id' => $template_id, 'meta_template_field_id' => intval($field)];
+ }
+ }
+ $this->Controller->set('requestedMetaFields', $requestedMetaFields);
+ return $query->contain([
+ 'MetaFields' => [
+ 'conditions' => $containConditions
+ ]
+ ]);
+ }
+
public function view(int $id, array $params = []): void
{
if (empty($id)) {
@@ -379,9 +649,17 @@ class CRUDComponent extends Component
$params['contain'][] = 'Tags';
$this->setAllTags();
}
+ if ($this->metaFieldsSupported()) {
+ if (!empty($this->request->getQuery('full'))) {
+ $params['contain']['MetaFields'] = ['MetaTemplateFields' => 'MetaTemplates'];
+ } else {
+ $params['contain'][] = 'MetaFields';
+ }
+ }
$data = $this->Table->get($id, $params);
- $data = $this->attachMetaData($id, $data);
+ $data = $this->attachMetaTemplatesIfNeeded($data);
+
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data);
}
@@ -391,11 +669,47 @@ class CRUDComponent extends Component
$this->Controller->set('entity', $data);
}
- public function delete($id=false): void
+ public function attachMetaTemplatesIfNeeded($data, array $metaTemplates = null)
+ {
+ if (!$this->metaFieldsSupported()) {
+ return $data;
+ }
+ if (!is_null($metaTemplates)) {
+ // We might be in the case where $metaTemplates gets re-used in a while loop
+ // We deep copy the meta-template so that the data attached is not preserved for the next iteration
+ $metaTemplates = array_map(function ($metaTemplate) {
+ $tmpEntity = $this->MetaTemplates->newEntity($metaTemplate->toArray());
+ $tmpEntity['meta_template_fields'] = Hash::combine($tmpEntity['meta_template_fields'], '{n}.id', '{n}'); // newEntity resets array indexing, see https://github.com/cakephp/cakephp/blob/32e3c532fea8abe2db8b697f07dfddf4dfc134ca/src/ORM/Marshaller.php#L369
+ return $tmpEntity;
+ }, $metaTemplates);
+ } else {
+ $metaTemplates = $this->getMetaTemplates()->toArray();
+ }
+ $data = $this->attachMetaTemplates($data, $metaTemplates);
+ return $data;
+ }
+
+ public function delete($id=false, $params=[]): void
{
if ($this->request->is('get')) {
if(!empty($id)) {
- $data = $this->Table->get($id);
+ $query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
+ if (!empty($params['conditions'])) {
+ $query->where($params['conditions']);
+ }
+ if (!empty($params['contain'])) {
+ $query->contain($params['contain']);
+ }
+ $data = $query->first();
+ if (empty($data)) {
+ throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
+ }
+ if (isset($params['beforeSave'])) {
+ $data = $params['beforeSave']($data);
+ if ($data === false) {
+ throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
+ }
+ }
$this->Controller->set('id', $data['id']);
$this->Controller->set('data', $data);
$this->Controller->set('bulkEnabled', false);
@@ -407,9 +721,26 @@ class CRUDComponent extends Component
$isBulk = count($ids) > 1;
$bulkSuccesses = 0;
foreach ($ids as $id) {
- $data = $this->Table->get($id);
- $success = $this->Table->delete($data);
- $success = true;
+ $query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
+ if (!empty($params['conditions'])) {
+ $query->where($params['conditions']);
+ }
+ if (!empty($params['contain'])) {
+ $query->contain($params['contain']);
+ }
+ $data = $query->first();
+ if (isset($params['beforeSave'])) {
+ $data = $params['beforeSave']($data);
+ if ($data === false) {
+ throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
+ }
+ }
+ if (!empty($data)) {
+ $success = $this->Table->delete($data);
+ $success = true;
+ } else {
+ $success = false;
+ }
if ($success) {
$bulkSuccesses++;
}
@@ -420,28 +751,32 @@ class CRUDComponent extends Component
__('{0} deleted.', $this->ObjectAlias),
__('All {0} have been deleted.', Inflector::pluralize($this->ObjectAlias)),
__('Could not delete {0}.', $this->ObjectAlias),
- __('{0} / {1} {2} have been deleted.',
+ __(
+ '{0} / {1} {2} have been deleted.',
$bulkSuccesses,
count($ids),
Inflector::pluralize($this->ObjectAlias)
)
);
- $this->setResponseForController('delete', $bulkSuccesses, $message, $data);
+ $additionalData = [];
+ if ($bulkSuccesses > 0) {
+ $additionalData['redirect'] = Router::url(['controller' => $this->Controller->getName(), 'action' => 'index']);
+ }
+ $this->setResponseForController('delete', $bulkSuccesses, $message, $data, null, $additionalData);
}
- $this->Controller->set('metaGroup', 'ContactDB');
$this->Controller->set('scope', 'users');
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/delete');
}
- public function tag($id=false): void
+ 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)) {
+ if (!empty($id)) {
$params = [
'contain' => 'Tags',
];
@@ -481,7 +816,8 @@ class CRUDComponent extends Component
__('{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.',
+ __(
+ '{0} / {1} {2} have been tagged.',
$bulkSuccesses,
count($ids),
Inflector::pluralize($this->ObjectAlias)
@@ -493,14 +829,14 @@ class CRUDComponent extends Component
$this->Controller->render('/genericTemplates/tagForm');
}
- public function untag($id=false): void
+ 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)) {
+ if (!empty($id)) {
$params = [
'contain' => 'Tags',
];
@@ -542,7 +878,8 @@ class CRUDComponent extends Component
__('{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.',
+ __(
+ '{0} / {1} {2} have been untagged.',
$bulkSuccesses,
count($ids),
Inflector::pluralize($this->ObjectAlias)
@@ -577,13 +914,16 @@ class CRUDComponent extends Component
$this->Controller->render('/genericTemplates/tag');
}
- public function setResponseForController($action, $success, $message, $data=[], $errors=null)
+ public function setResponseForController($action, $success, $message, $data = [], $errors = null, $additionalData = [])
{
if ($success) {
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
} elseif ($this->Controller->ParamHandler->isAjax()) {
- $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, $action, $data, $message);
+ if (!empty($additionalData['redirect'])) { // If a redirection occurs, we need to make sure the flash message gets displayed
+ $this->Controller->Flash->success($message);
+ }
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, $action, $data, $message, $additionalData);
} else {
$this->Controller->Flash->success($message);
$this->Controller->redirect($this->Controller->referer());
@@ -592,6 +932,9 @@ class CRUDComponent extends Component
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
} elseif ($this->Controller->ParamHandler->isAjax()) {
+ if (!empty($additionalData['redirect'])) { // If a redirection occors, we need to make sure the flash message gets displayed
+ $this->Controller->Flash->error($message);
+ }
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, $action, $data, $message, !is_null($errors) ? $errors : $data->getErrors());
} else {
$this->Controller->Flash->error($message);
@@ -617,7 +960,7 @@ class CRUDComponent extends Component
* @return Array The ID converted to a list or the list of provided IDs from the request
* @throws NotFoundException when no ID could be found
*/
- public function getIdsOrFail($id=false): Array
+ public function getIdsOrFail($id = false): array
{
$params = $this->Controller->ParamHandler->harvestParams(['ids']);
if (!empty($params['ids'])) {
@@ -668,22 +1011,27 @@ class CRUDComponent extends Component
return $massagedFilters;
}
- public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $quickFilterFields): \Cake\ORM\Query
+ public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
{
+ $quickFilterFields = $options['quickFilters'];
$queryConditions = [];
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
+ if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
+ $this->Controller->set('quickFilterForMetaField', [
+ 'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false,
+ 'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false,
+ ]);
+ }
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
$this->Controller->set('quickFilterValue', $params['quickFilter']);
- foreach ($quickFilterFields as $filterField) {
- $likeCondition = false;
- if (is_array($filterField)) {
- $likeCondition = reset($filterField);
- $filterFieldName = array_key_first($filterField);
- $queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] .'%';
- } else {
- $queryConditions[$filterField] = $params['quickFilter'];
- }
+ $queryConditions = $this->genQuickFilterConditions($params, $quickFilterFields);
+
+ if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
+ $searchValue = !empty($options['quickFilterForMetaField']['wildcard_search']) ? "%{$params['quickFilter']}%" : $params['quickFilter'];
+ $metaFieldConditions = $this->Table->buildMetaFieldQuerySnippetForMatchingParent(['value' => $searchValue]);
+ $queryConditions[] = $metaFieldConditions;
}
+
$query->where(['OR' => $queryConditions]);
} else {
$this->Controller->set('quickFilterValue', '');
@@ -691,6 +1039,25 @@ class CRUDComponent extends Component
return $query;
}
+ public function genQuickFilterConditions(array $params, array $quickFilterFields): array
+ {
+ $queryConditions = [];
+ foreach ($quickFilterFields as $filterField) {
+ if (is_array($filterField)) {
+ reset($filterField);
+ $filterFieldName = array_key_first($filterField);
+ if (!empty($filterField[$filterFieldName])) {
+ $queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] . '%';
+ } else {
+ $queryConditions[$filterField] = $params['quickFilter'];
+ }
+ } else {
+ $queryConditions[$filterField] = $params['quickFilter'];
+ }
+ }
+ return $queryConditions;
+ }
+
protected function setFilters($params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
{
$filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : '';
@@ -721,10 +1088,10 @@ class CRUDComponent extends Component
}
if (!empty($params['simpleFilters'])) {
foreach ($params['simpleFilters'] as $filter => $filterValue) {
- $activeFilters[$filter] = $filterValue;
if ($filter === 'quickFilter') {
continue;
}
+ $activeFilters[$filter] = $filterValue;
if (is_array($filterValue)) {
$query->where([($filter . ' IN') => $filterValue]);
} else {
@@ -733,6 +1100,7 @@ class CRUDComponent extends Component
}
}
if (!empty($params['relatedFilters'])) {
+ // $query->group("{$this->TableAlias}.id");
foreach ($params['relatedFilters'] as $filter => $filterValue) {
$activeFilters[$filter] = $filterValue;
$filterParts = explode('.', $filter);
@@ -746,20 +1114,39 @@ class CRUDComponent extends Component
$query = $this->setTagFilters($query, $filteringTags);
}
+ if ($this->metaFieldsSupported()) {
+ $filteringMetaFields = $this->getMetaFieldFiltersFromQuery();
+ if (!empty($filteringMetaFields)) {
+ $activeFilters['filteringMetaFields'] = $filteringMetaFields;
+ }
+ $query = $this->setMetaFieldFilters($query, $filteringMetaFields);
+ }
+
$this->Controller->set('activeFilters', $activeFilters);
return $query;
}
+ protected function setMetaFieldFilters($query, $metaFieldFilters)
+ {
+ $metaFieldConditions = $this->Table->buildMetaFieldQuerySnippetForMatchingParent($metaFieldFilters);
+ $query->where($metaFieldConditions);
+
+ return $query;
+ }
+
protected function setTagFilters($query, $tags)
{
$modelAlias = $this->Table->getAlias();
$subQuery = $this->Table->find('tagged', [
'name' => $tags,
- 'forceAnd' => true
+ 'OperatorAND' => true
])->select($modelAlias . '.id');
return $query->where([$modelAlias . '.id IN' => $subQuery]);
}
+ // FIXME: Adding related condition with association having `through` setup might include duplicate in the result set
+ // We should probably rely on `innerJoinWith` and perform deduplication via `distinct`
+ // Or grouping by primary key for the main model (however this is not optimal/efficient/clean)
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
{
$modelName = $filterParts[0];
@@ -768,7 +1155,7 @@ class CRUDComponent extends Component
$query = $this->setRelatedCondition($query, $modelName, $fieldName, $filterValue);
} else {
$filterParts = array_slice($filterParts, 1);
- $query = $query->matching($modelName, function(\Cake\ORM\Query $q) use ($filterParts, $filterValue) {
+ $query = $query->matching($modelName, function (\Cake\ORM\Query $q) use ($filterParts, $filterValue) {
return $this->setNestedRelatedCondition($q, $filterParts, $filterValue);
});
}
@@ -777,7 +1164,7 @@ class CRUDComponent extends Component
protected function setRelatedCondition($query, $modelName, $fieldName, $filterValue)
{
- return $query->matching($modelName, function(\Cake\ORM\Query $q) use ($fieldName, $filterValue) {
+ return $query->matching($modelName, function (\Cake\ORM\Query $q) use ($fieldName, $filterValue) {
return $this->setValueCondition($q, $fieldName, $filterValue);
});
}
@@ -796,8 +1183,14 @@ class CRUDComponent extends Component
protected function setFilteringContext($contextFilters, $params)
{
$filteringContexts = [];
- if (!isset($contextFilters['allow_all']) || $contextFilters['allow_all']) {
- $filteringContexts[] = ['label' => __('All')];
+ if (
+ !isset($contextFilters['_all']) ||
+ !isset($contextFilters['_all']['enabled']) ||
+ !empty($contextFilters['_all']['enabled'])
+ ) {
+ $filteringContexts[] = [
+ 'label' => !empty($contextFilters['_all']['text']) ? h($contextFilters['_all']['text']) : __('All')
+ ];
}
if (!empty($contextFilters['fields'])) {
foreach ($contextFilters['fields'] as $field) {
@@ -847,7 +1240,7 @@ class CRUDComponent extends Component
throw new Exception('Invalid passed conditions');
}
foreach ($metaANDConditions as $i => $conditions) {
- $metaANDConditions[$i]['scope'] = $this->Table->metaFields;
+ $metaANDConditions[$i]['scope'] = $this->Table->getBehavior('MetaFields')->getScope();
}
$firstCondition = $this->prefixConditions('MetaFields', $metaANDConditions[0]);
$conditionsToJoin = array_slice($metaANDConditions, 1);
@@ -879,6 +1272,11 @@ class CRUDComponent extends Component
return $this->Table->behaviors()->has('Tag');
}
+ public function metaFieldsSupported()
+ {
+ return $this->Table->hasBehavior('MetaFields');
+ }
+
public function setAllTags()
{
$this->Tags = TableRegistry::getTableLocator()->get('Tags.Tags');
@@ -901,7 +1299,8 @@ class CRUDComponent extends Component
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
- $message = __('{0} field {1}. (ID: {2} {3})',
+ $message = __(
+ '{0} field {1}. (ID: {2} {3})',
$fieldName,
$data->{$fieldName} ? __('enabled') : __('disabled'),
Inflector::humanize($this->ObjectAlias),
@@ -983,9 +1382,9 @@ class CRUDComponent extends Component
[$this->Table->getAlias() => $this->Table->getTable()],
[sprintf('%s.id = %s.%s', $this->Table->getAlias(), $associatedTable->getAlias(), $association->getForeignKey())]
)
- ->where([
- ["${field} IS NOT" => NULL]
- ]);
+ ->where([
+ ["${field} IS NOT" => NULL]
+ ]);
} else if ($associationType == 'manyToOne') {
$fieldToExtract = sprintf('%s.%s', Inflector::singularize(strtolower($model)), $subField);
$query = $this->Table->find()->contain($model);
@@ -1002,6 +1401,25 @@ class CRUDComponent extends Component
->toList();
}
+ private function getMetaFieldFiltersFromQuery(): array
+ {
+ $filters = [];
+ foreach ($this->request->getQueryParams() as $filterName => $value) {
+ $prefix = '_metafield';
+ if (substr($filterName, 0, strlen($prefix)) === $prefix) {
+ $dissected = explode('_', substr($filterName, strlen($prefix)));
+ if (count($dissected) == 3) { // Skip if template_id or template_field_id not provided
+ $filters[] = [
+ 'meta_template_id' => intval($dissected[1]),
+ 'meta_template_field_id' => intval($dissected[2]),
+ 'value' => $value,
+ ];
+ }
+ }
+ }
+ return $filters;
+ }
+
private function renderViewInVariable($templateRelativeName, $data)
{
$builder = new ViewBuilder();
diff --git a/src/Controller/Component/Navigation/Api.php b/src/Controller/Component/Navigation/Api.php
new file mode 100644
index 0000000..022f9fc
--- /dev/null
+++ b/src/Controller/Component/Navigation/Api.php
@@ -0,0 +1,16 @@
+bcf->addRoute('Api', 'index', [
+ 'label' => __('API'),
+ 'url' => '/api/index',
+ 'icon' => 'code'
+ ]);
+ }
+}
diff --git a/src/Controller/Component/Navigation/Broods.php b/src/Controller/Component/Navigation/Broods.php
new file mode 100644
index 0000000..105c34a
--- /dev/null
+++ b/src/Controller/Component/Navigation/Broods.php
@@ -0,0 +1,13 @@
+bcf->addLink('Broods', 'view', 'LocalTools', 'broodTools');
+ $this->bcf->addLink('Broods', 'edit', 'LocalTools', 'broodTools');
+ }
+}
diff --git a/src/Controller/Component/Navigation/EncryptionKeys.php b/src/Controller/Component/Navigation/EncryptionKeys.php
new file mode 100644
index 0000000..e11c137
--- /dev/null
+++ b/src/Controller/Component/Navigation/EncryptionKeys.php
@@ -0,0 +1,8 @@
+bcf->addRoute('Inbox', 'index', $this->bcf->defaultCRUD('Inbox', 'index'));
+ $this->bcf->addRoute('Inbox', 'view', $this->bcf->defaultCRUD('Inbox', 'view'));
+ $this->bcf->addRoute('Inbox', 'discard', [
+ 'label' => __('Discard request'),
+ 'icon' => 'trash',
+ 'url' => '/inbox/discard/{{id}}',
+ 'url_vars' => ['id' => 'id'],
+ ]);
+ $this->bcf->addRoute('Inbox', 'process', [
+ 'label' => __('Process request'),
+ 'icon' => 'cogs',
+ 'url' => '/inbox/process/{{id}}',
+ 'url_vars' => ['id' => 'id'],
+ ]);
+ }
+
+ public function addParents()
+ {
+ $this->bcf->addParent('Inbox', 'view', 'Inbox', 'index');
+ $this->bcf->addParent('Inbox', 'discard', 'Inbox', 'index');
+ $this->bcf->addParent('Inbox', 'process', 'Inbox', 'index');
+ }
+
+ public function addLinks()
+ {
+ $this->bcf->addSelfLink('Inbox', 'view');
+ }
+
+ public function addActions()
+ {
+ $this->bcf->addAction('Inbox', 'view', 'Inbox', 'process');
+ $this->bcf->addAction('Inbox', 'view', 'Inbox', 'discard');
+
+ }
+}
diff --git a/src/Controller/Component/Navigation/Individuals.php b/src/Controller/Component/Navigation/Individuals.php
new file mode 100644
index 0000000..e592e13
--- /dev/null
+++ b/src/Controller/Component/Navigation/Individuals.php
@@ -0,0 +1,8 @@
+bcf->addRoute('Instance', 'home', [
+ 'label' => __('Home'),
+ 'url' => '/',
+ 'icon' => 'home'
+ ]);
+ $this->bcf->addRoute('Instance', 'settings', [
+ 'label' => __('Settings'),
+ 'url' => '/instance/settings',
+ 'icon' => 'cogs'
+ ]);
+ $this->bcf->addRoute('Instance', 'migrationIndex', [
+ 'label' => __('Database Migration'),
+ 'url' => '/instance/migrationIndex',
+ 'icon' => 'database'
+ ]);
+ }
+}
diff --git a/src/Controller/Component/Navigation/LocalTools.php b/src/Controller/Component/Navigation/LocalTools.php
new file mode 100644
index 0000000..7ab2e72
--- /dev/null
+++ b/src/Controller/Component/Navigation/LocalTools.php
@@ -0,0 +1,49 @@
+bcf->addRoute('LocalTools', 'viewConnector', [
+ 'label' => __('View'),
+ 'textGetter' => 'connector',
+ 'url' => '/localTools/viewConnector/{{connector}}',
+ 'url_vars' => ['connector' => 'connector'],
+ ]);
+ $this->bcf->addRoute('LocalTools', 'broodTools', [
+ 'label' => __('Brood Tools'),
+ 'url' => '/localTools/broodTools/{{id}}',
+ 'url_vars' => ['id' => 'id'],
+ ]);
+ }
+
+ public function addParents()
+ {
+ $this->bcf->addParent('LocalTools', 'viewConnector', 'LocalTools', 'index');
+ }
+
+ public function addLinks()
+ {
+ $passedData = $this->request->getParam('pass');
+ if (!empty($passedData[0])) {
+ $brood_id = $passedData[0];
+ $this->bcf->addParent('LocalTools', 'broodTools', 'Broods', 'view', [
+ 'textGetter' => [
+ 'path' => 'name',
+ 'varname' => 'broodEntity',
+ ],
+ 'url' => "/broods/view/{$brood_id}",
+ ]);
+ $this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'view', [
+ 'url' => "/broods/view/{$brood_id}",
+ ]);
+ $this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'edit', [
+ 'url' => "/broods/view/{$brood_id}",
+ ]);
+ }
+ $this->bcf->addSelfLink('LocalTools', 'broodTools');
+ }
+}
diff --git a/src/Controller/Component/Navigation/MetaTemplates.php b/src/Controller/Component/Navigation/MetaTemplates.php
new file mode 100644
index 0000000..ca83ef9
--- /dev/null
+++ b/src/Controller/Component/Navigation/MetaTemplates.php
@@ -0,0 +1,109 @@
+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-square',
+ 'url' => '/metaTemplates/enable/{{id}}/enabled',
+ 'url_vars' => ['id' => 'id'],
+ ]);
+ $this->bcf->addRoute('MetaTemplates', 'set_default', [
+ 'label' => __('Set as default'),
+ 'icon' => 'check-square',
+ 'url' => '/metaTemplates/toggle/{{id}}/default',
+ 'url_vars' => ['id' => 'id'],
+ ]);
+
+ $totalUpdateCount = 0;
+ if (!empty($this->viewVars['updateableTemplates']['automatically-updateable']) && !empty($this->viewVars['updateableTemplates']['new'])) {
+ $udpateCount = count($this->viewVars['updateableTemplates']['automatically-updateable']) ?? 0;
+ $newCount = count($this->viewVars['updateableTemplates']['new']) ?? 0;
+ $totalUpdateCount = $udpateCount + $newCount;
+ }
+ $updateRouteConfig = [
+ 'label' => __('Update all templates'),
+ 'icon' => 'download',
+ 'url' => '/metaTemplates/updateAllTemplates',
+ ];
+ if ($totalUpdateCount > 0) {
+ $updateRouteConfig['badge'] = [
+ 'text' => h($totalUpdateCount),
+ 'variant' => 'warning',
+ 'title' => __('There are {0} new meta-template(s) and {1} update(s) available', h($newCount), h($udpateCount)),
+ ];
+ }
+ $this->bcf->addRoute('MetaTemplates', 'update_all_templates', $updateRouteConfig);
+ $this->bcf->addRoute('MetaTemplates', 'update', [
+ 'label' => __('Update template'),
+ 'icon' => 'download',
+ 'url' => '/metaTemplates/update',
+ ]);
+ $this->bcf->addRoute('MetaTemplates', 'prune_outdated_template', [
+ 'label' => __('Prune outdated template'),
+ 'icon' => 'trash',
+ 'url' => '/metaTemplates/prune_outdated_template',
+ ]);
+ }
+
+ public function addParents()
+ {
+ $this->bcf->addParent('MetaTemplates', 'view', 'MetaTemplates', 'index');
+ $this->bcf->addParent('MetaTemplates', 'update', 'MetaTemplates', 'index');
+ }
+
+ public function addLinks()
+ {
+ $this->bcf->addSelfLink('MetaTemplates', 'view');
+ }
+
+ public function addActions()
+ {
+ $totalUpdateCount = 0;
+ if (!empty($this->viewVars['updateableTemplates']['not-up-to-date']) && !empty($this->viewVars['updateableTemplates']['new'])) {
+ $udpateCount = count($this->viewVars['updateableTemplates']['not-up-to-date']) ?? 0;
+ $newCount = count($this->viewVars['updateableTemplates']['new']) ?? 0;
+ $totalUpdateCount = $udpateCount + $newCount;
+ }
+ $updateAllActionConfig = [
+ 'label' => __('Update template'),
+ 'url' => '/metaTemplates/updateAllTemplates',
+ 'url_vars' => ['id' => 'id'],
+ ];
+ if ($totalUpdateCount > 0) {
+ $updateAllActionConfig['badge'] = [
+ 'text' => h($totalUpdateCount),
+ 'variant' => 'warning',
+ 'title' => __('There are {0} new meta-template(s) and {1} update(s) available', h($newCount), h($udpateCount)),
+ ];
+ }
+ $this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'update_all_templates', $updateAllActionConfig);
+ $this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'prune_outdated_template', [
+ 'label' => __('Prune outdated template'),
+ 'url' => '/metaTemplates/prune_outdated_template',
+ ]);
+
+ if (empty($this->viewVars['updateableTemplates']['up-to-date'])) {
+ $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [
+ 'label' => __('Update template'),
+ 'url' => '/metaTemplates/update/{{id}}',
+ 'url_vars' => ['id' => 'id'],
+ 'variant' => 'warning',
+ 'badge' => [
+ 'variant' => 'warning',
+ 'title' => __('Update available')
+ ]
+ ]);
+ }
+ $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'enable');
+ $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'set_default');
+ }
+}
diff --git a/src/Controller/Component/Navigation/Organisations.php b/src/Controller/Component/Navigation/Organisations.php
new file mode 100644
index 0000000..3df07c1
--- /dev/null
+++ b/src/Controller/Component/Navigation/Organisations.php
@@ -0,0 +1,8 @@
+bcf->addRoute('Outbox', 'index', $this->bcf->defaultCRUD('Outbox', 'index'));
+ $this->bcf->addRoute('Outbox', 'view', $this->bcf->defaultCRUD('Outbox', 'view'));
+ $this->bcf->addRoute('Outbox', 'discard', [
+ 'label' => __('Discard request'),
+ 'icon' => 'trash',
+ 'url' => '/outbox/discard/{{id}}',
+ 'url_vars' => ['id' => 'id'],
+ ]);
+ $this->bcf->addRoute('Outbox', 'process', [
+ 'label' => __('Process request'),
+ 'icon' => 'cogs',
+ 'url' => '/outbox/process/{{id}}',
+ 'url_vars' => ['id' => 'id'],
+ ]);
+ }
+
+ public function addParents()
+ {
+ $this->bcf->addParent('Outbox', 'view', 'Outbox', 'index');
+ $this->bcf->addParent('Outbox', 'discard', 'Outbox', 'index');
+ $this->bcf->addParent('Outbox', 'process', 'Outbox', 'index');
+ }
+
+ public function addLinks()
+ {
+ $this->bcf->addSelfLink('Outbox', 'view');
+ }
+
+ public function addActions()
+ {
+ $this->bcf->addAction('Outbox', 'view', 'Outbox', 'process');
+ $this->bcf->addAction('Outbox', 'view', 'Outbox', 'discard');
+ }
+}
diff --git a/src/Controller/Component/Navigation/Roles.php b/src/Controller/Component/Navigation/Roles.php
new file mode 100644
index 0000000..b0501cb
--- /dev/null
+++ b/src/Controller/Component/Navigation/Roles.php
@@ -0,0 +1,8 @@
+bcf;
+ $request = $this->request;
+ $this->bcf->addLink('UserSettings', 'index', 'Users', 'view', function ($config) use ($bcf, $request) {
+ if (!empty($request->getQuery('Users_id'))) {
+ $user_id = h($request->getQuery('Users_id'));
+ $linkData = [
+ 'label' => __('View user [{0}]', h($user_id)),
+ 'url' => sprintf('/users/view/%s', h($user_id))
+ ];
+ return $linkData;
+ }
+ return null;
+ });
+ $this->bcf->addLink('UserSettings', 'index', 'Users', 'edit', function ($config) use ($bcf, $request) {
+ if (!empty($request->getQuery('Users_id'))) {
+ $user_id = h($request->getQuery('Users_id'));
+ $linkData = [
+ 'label' => __('Edit user [{0}]', h($user_id)),
+ 'url' => sprintf('/users/edit/%s', h($user_id))
+ ];
+ return $linkData;
+ }
+ return null;
+ });
+ if (!empty($request->getQuery('Users_id'))) {
+ $this->bcf->addSelfLink('UserSettings', 'index');
+ }
+ if ($this->request->getParam('controller') == 'UserSettings' && $this->request->getParam('action') == 'index') {
+ if (!empty($this->request->getQuery('Users_id'))) {
+ $user_id = $this->request->getQuery('Users_id');
+ $this->bcf->addParent('UserSettings', 'index', 'Users', 'view', [
+ 'textGetter' => [
+ 'path' => 'username',
+ 'varname' => 'settingsForUser',
+ ],
+ 'url' => "/users/view/{$user_id}"
+ ]);
+ }
+ }
+ }
+}
diff --git a/src/Controller/Component/Navigation/Users.php b/src/Controller/Component/Navigation/Users.php
new file mode 100644
index 0000000..7e228db
--- /dev/null
+++ b/src/Controller/Component/Navigation/Users.php
@@ -0,0 +1,87 @@
+bcf->addRoute('Users', 'settings', [
+ 'label' => __('User settings'),
+ 'url' => '/users/settings/',
+ 'icon' => 'user-cog'
+ ]);
+ }
+
+ public function addParents()
+ {
+ // $this->bcf->addParent('Users', 'settings', 'Users', 'view');
+ }
+
+ public function addLinks()
+ {
+ $bcf = $this->bcf;
+ $request = $this->request;
+ $passedData = $this->request->getParam('pass');
+ $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
+ if (!empty($passedData[0])) {
+ $user_id = $passedData[0];
+ $linkData = [
+ 'label' => __('Account settings', h($user_id)),
+ 'url' => sprintf('/users/settings/%s', h($user_id))
+ ];
+ return $linkData;
+ }
+ return [];
+ });
+ $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
+ if (!empty($passedData[0])) {
+ $user_id = $passedData[0];
+ $linkData = [
+ 'label' => __('User Setting [{0}]', h($user_id)),
+ 'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id))
+ ];
+ return $linkData;
+ }
+ return [];
+ });
+ $this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
+ if (!empty($passedData[0])) {
+ $user_id = $passedData[0];
+ $linkData = [
+ 'label' => __('Account settings', h($user_id)),
+ 'url' => sprintf('/users/settings/%s', h($user_id))
+ ];
+ return $linkData;
+ }
+ return [];
+ });
+ $this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
+ if (!empty($passedData[0])) {
+ $user_id = $passedData[0];
+ $linkData = [
+ 'label' => __('User Setting [{0}]', h($user_id)),
+ 'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id))
+ ];
+ return $linkData;
+ }
+ return [];
+ });
+
+ $this->bcf->addLink('Users', 'settings', 'Users', 'view', function ($config) use ($bcf, $request, $passedData) {
+ if (!empty($passedData[0])) {
+ $user_id = $passedData[0];
+ $linkData = [
+ 'label' => __('View user', h($user_id)),
+ 'url' => sprintf('/users/view/%s', h($user_id))
+ ];
+ return $linkData;
+ }
+ return [];
+ });
+ $this->bcf->addSelfLink('Users', 'settings', [
+ 'label' => __('Account settings')
+ ]);
+ }
+}
diff --git a/src/Controller/Component/Navigation/base.php b/src/Controller/Component/Navigation/base.php
new file mode 100644
index 0000000..3900e69
--- /dev/null
+++ b/src/Controller/Component/Navigation/base.php
@@ -0,0 +1,21 @@
+bcf = $bcf;
+ $this->request = $request;
+ $this->viewVars = $viewVars;
+ }
+
+ public function addRoutes() {}
+ public function addParents() {}
+ public function addLinks() {}
+ public function addActions() {}
+}
\ No newline at end of file
diff --git a/src/Controller/Component/Navigation/sidemenu.php b/src/Controller/Component/Navigation/sidemenu.php
new file mode 100644
index 0000000..d4da3a6
--- /dev/null
+++ b/src/Controller/Component/Navigation/sidemenu.php
@@ -0,0 +1,162 @@
+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',
+ ],
+ 'MailingLists' => [
+ 'label' => __('Mailing Lists'),
+ 'icon' => $this->iconTable['MailingLists'],
+ 'url' => '/mailingLists/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',
+ ],
+ 'AuditLogs' => [
+ 'label' => __('Audit Logs'),
+ 'url' => '/auditLogs/index',
+ 'icon' => 'history',
+ ],
+ ]
+ ],
+ 'API' => [
+ 'label' => __('API'),
+ 'icon' => $this->iconTable['API'],
+ 'url' => '/api/index',
+ ],
+ ],
+ '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'))
+ ]
+ ]
+ ];
+ }
+}
diff --git a/src/Controller/Component/NavigationComponent.php b/src/Controller/Component/NavigationComponent.php
index a183b7e..5d3d1e5 100644
--- a/src/Controller/Component/NavigationComponent.php
+++ b/src/Controller/Component/NavigationComponent.php
@@ -4,10 +4,16 @@ namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Core\Configure;
+use Cake\Core\App;
use Cake\Utility\Inflector;
use Cake\Utility\Hash;
+use Cake\Filesystem\Folder;
use Cake\Routing\Router;
use Cake\ORM\TableRegistry;
+use Exception;
+
+use SidemenuNavigation\Sidemenu;
+require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'sidemenu.php');
class NavigationComponent extends Component
{
@@ -18,15 +24,18 @@ class NavigationComponent extends Component
'Organisations' => 'building',
'EncryptionKeys' => 'key',
'SharingGroups' => 'user-friends',
+ 'MailingLists' => 'mail-bulk',
'Broods' => 'network-wired',
'Roles' => 'id-badge',
'Users' => 'users',
+ 'UserSettings' => 'user-cog',
'Inbox' => 'inbox',
'Outbox' => 'inbox',
'MetaTemplates' => 'object-group',
'LocalTools' => 'tools',
'Instance' => 'server',
'Tags' => 'tags',
+ 'API' => 'code',
];
public function initialize(array $config): void
@@ -34,7 +43,7 @@ class NavigationComponent extends Component
$this->request = $config['request'];
}
- public function beforeFilter($event)
+ public function beforeRender($event)
{
$this->fullBreadcrumb = $this->genBreadcrumb();
$this->breadcrumb = $this->getBreadcrumb();
@@ -42,389 +51,343 @@ class NavigationComponent extends Component
public function getSideMenu(): array
{
- return [
- 'ContactDB' => [
- 'Individuals' => [
- 'label' => __('Individuals'),
- 'icon' => $this->iconToTableMapping['Individuals'],
- 'url' => '/individuals/index',
- ],
- 'Organisations' => [
- 'label' => __('Organisations'),
- 'icon' => $this->iconToTableMapping['Organisations'],
- 'url' => '/organisations/index',
- ],
- 'EncryptionKeys' => [
- 'label' => __('Encryption keys'),
- 'icon' => $this->iconToTableMapping['EncryptionKeys'],
- 'url' => '/encryptionKeys/index',
- ]
- ],
- 'Trust Circles' => [
- 'SharingGroups' => [
- 'label' => __('Sharing Groups'),
- 'icon' => $this->iconToTableMapping['SharingGroups'],
- 'url' => '/sharingGroups/index',
- ]
- ],
- 'Sync' => [
- 'Broods' => [
- 'label' => __('Broods'),
- 'icon' => $this->iconToTableMapping['Broods'],
- 'url' => '/broods/index',
- ]
- ],
- 'Administration' => [
- 'Roles' => [
- 'label' => __('Roles'),
- 'icon' => $this->iconToTableMapping['Roles'],
- 'url' => '/roles/index',
- ],
- 'Users' => [
- 'label' => __('Users'),
- 'icon' => $this->iconToTableMapping['Users'],
- 'url' => '/users/index',
- ],
- 'Messages' => [
- 'label' => __('Messages'),
- 'icon' => $this->iconToTableMapping['Inbox'],
- 'url' => '/inbox/index',
- 'children' => [
- 'index' => [
- 'url' => '/inbox/index',
- 'label' => __('Inbox')
- ],
- 'outbox' => [
- 'url' => '/outbox/index',
- 'label' => __('Outbox')
- ],
- ]
- ],
- 'Add-ons' => [
- 'label' => __('Add-ons'),
- 'icon' => 'puzzle-piece',
- 'children' => [
- 'MetaTemplates.index' => [
- 'label' => __('Meta Field Templates'),
- 'icon' => $this->iconToTableMapping['MetaTemplates'],
- 'url' => '/metaTemplates/index',
- ],
- 'LocalTools.index' => [
- 'label' => __('Local Tools'),
- 'icon' => $this->iconToTableMapping['LocalTools'],
- 'url' => '/localTools/index',
- ],
- 'Tags.index' => [
- 'label' => __('Tags'),
- 'icon' => $this->iconToTableMapping['Tags'],
- 'url' => '/tags/index',
- ],
- ]
- ],
- 'Instance' => [
- 'label' => __('Instance'),
- 'icon' => $this->iconToTableMapping['Instance'],
- 'children' => [
- 'Settings' => [
- 'label' => __('Settings'),
- 'url' => '/instance/settings',
- 'icon' => 'cogs',
- ],
- 'Database' => [
- 'label' => __('Database'),
- 'url' => '/instance/migrationIndex',
- 'icon' => 'database',
- ],
- ]
- ],
- ],
- 'Open' => [
- 'Organisations' => [
- 'label' => __('Organisations'),
- 'icon' => $this->iconToTableMapping['Organisations'],
- 'url' => '/open/organisations/index',
- 'children' => [
- 'index' => [
- 'url' => '/open/organisations/index',
- 'label' => __('List organisations')
- ],
- ],
- 'open' => in_array('organisations', Configure::read('Cerebrate.open'))
- ],
- 'Individuals' => [
- 'label' => __('Individuals'),
- 'icon' => $this->iconToTableMapping['Individuals'],
- 'url' => '/open/individuals/index',
- 'children' => [
- 'index' => [
- 'url' => '/open/individuals/index',
- 'label' => __('List individuals')
- ],
- ],
- 'open' => in_array('individuals', Configure::read('Cerebrate.open'))
- ]
- ]
- ];
+ $sidemenu = new Sidemenu($this->iconToTableMapping, $this->request);
+ $sidemenu = $sidemenu->get();
+ $sidemenu = $this->addUserBookmarks($sidemenu);
+ return $sidemenu;
}
+
+ public function addUserBookmarks($sidemenu): array
+ {
+ $bookmarks = $this->getUserBookmarks();
+ $sidemenu = array_merge([
+ '__bookmarks' => $bookmarks
+ ], $sidemenu);
+ return $sidemenu;
+ }
+
+ public function getUserBookmarks(): array
+ {
+ $userSettingTable = TableRegistry::getTableLocator()->get('UserSettings');
+ $setting = $userSettingTable->getSettingByName($this->request->getAttribute('identity'), 'ui.bookmarks');
+ $bookmarks = is_null($setting) ? [] : json_decode($setting->value, true);
+
+ $links = array_map(function($bookmark) {
+ return [
+ 'name' => $bookmark['name'],
+ 'label' => $bookmark['label'],
+ 'url' => $bookmark['url'],
+ ];
+ }, $bookmarks);
+ return $links;
+ }
+
public function getBreadcrumb(): array
{
$controller = $this->request->getParam('controller');
$action = $this->request->getParam('action');
- if (empty($this->fullBreadcrumb[$controller]['routes']["{$controller}:{$action}"])) {
+ if (empty($this->fullBreadcrumb[$controller][$action])) {
return [[
'label' => $controller,
'url' => Router::url(['controller' => $controller, 'action' => $action]),
]]; // no breadcrumb defined for this endpoint
}
- $currentRoute = $this->fullBreadcrumb[$controller]['routes']["{$controller}:{$action}"];
- $breadcrumbPath = $this->getBreadcrumbPath("{$controller}:{$action}", $currentRoute);
- return $breadcrumbPath['objects'];
+ $currentRoute = $this->fullBreadcrumb[$controller][$action];
+ $breadcrumbPath = $this->getBreadcrumbPath($currentRoute);
+ return $breadcrumbPath;
}
- public function getBreadcrumbPath(string $startRoute, array $currentRoute): array
+ public function getBreadcrumbPath(array $currentRoute): array
{
- $route = $startRoute;
- $path = [
- 'routes' => [],
- 'objects' => [],
- ];
- $visited = [];
- while (empty($visited[$route])) {
- $visited[$route] = true;
- $path['routes'][] = $route;
- $path['objects'][] = $currentRoute;
+ $path = [];
+ $visitedURL = [];
+ while (empty($visitedURL[$currentRoute['url']])) {
+ $visitedURL[$currentRoute['url']] = true;
+ $path[] = $currentRoute;
if (!empty($currentRoute['after'])) {
- $route = $currentRoute['after'];
- $split = explode(':', $currentRoute['after']);
- $currentRoute = $this->fullBreadcrumb[$split[0]]['routes'][$currentRoute['after']];
+ if (is_callable($currentRoute['after'])) {
+ $route = $currentRoute['after']();
+ } else {
+ $route = $currentRoute['after'];
+ }
+ if (empty($route)) {
+ continue;
+ }
+ $currentRoute = $route;
}
}
- $path['routes'] = array_reverse($path['routes']);
- $path['objects'] = array_reverse($path['objects']);
+ $path = array_reverse($path);
return $path;
}
- private function insertInheritance(array $config, array $fullConfig): array
- {
- if (!empty($config['routes'])) {
- foreach ($config['routes'] as $routeName => $value) {
- $config['routes'][$routeName]['route_path'] = $routeName;
- if (!empty($value['inherit'])) {
- $default = $config['defaults'][$value['inherit']] ?? [];
- $config['routes'][$routeName] = array_merge($config['routes'][$routeName], $default);
- unset($config['routes'][$routeName]['inherit']);
- }
- }
- }
- return $config;
- }
-
- private function insertRelated(array $config, array $fullConfig): array
- {
- if (!empty($config['routes'])) {
- foreach ($config['routes'] as $routeName => $value) {
- if (!empty($value['links'])) {
- foreach ($value['links'] as $i => $linkedRoute) {
- $split = explode(':', $linkedRoute);
- if (!empty($fullConfig[$split[0]]['routes'][$linkedRoute])) {
- $linkedRouteObject = $fullConfig[$split[0]]['routes'][$linkedRoute];
- if (!empty($linkedRouteObject)) {
- $config['routes'][$routeName]['links'][$i] = $linkedRouteObject;
- continue;
- }
- }
- unset($config['routes'][$routeName]['links'][$i]);
- }
- }
- if (!empty($value['actions'])) {
- foreach ($value['actions'] as $i => $linkedRoute) {
- $split = explode(':', $linkedRoute);
- if (!empty($fullConfig[$split[0]]['routes'][$linkedRoute])) {
- $linkedRouteObject = $fullConfig[$split[0]]['routes'][$linkedRoute];
- if (!empty($linkedRouteObject)) {
- $config['routes'][$routeName]['actions'][$i] = $linkedRouteObject;
- continue;
- }
- }
- unset($config['routes'][$routeName]['actions'][$i]);
- }
- }
- }
- }
- return $config;
- }
-
- public function getDefaultCRUDConfig(string $controller, array $overrides=[], array $merges=[]): array
- {
- $table = TableRegistry::getTableLocator()->get($controller);
- $default = [
- 'defaults' => [
- 'depth-1' => [
- 'after' => "{$controller}:index",
- 'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id',
- 'links' => [
- "{$controller}:view",
- "{$controller}:edit",
- ],
- 'actions' => [
- "{$controller}:delete",
- ],
- ]
- ],
- 'routes' => [
- "{$controller}:index" => [
- 'label' => Inflector::humanize($controller),
- 'url' => "/{$controller}/index",
- 'icon' => $this->iconToTableMapping[$controller]
- ],
- "{$controller}:view" => [
- 'label' => __('View'),
- 'icon' => 'eye',
- 'inherit' => 'depth-1',
- 'url' => "/{$controller}/view/{{id}}",
- 'url_vars' => ['id' => 'id'],
- ],
- "{$controller}:edit" => [
- 'label' => __('Edit'),
- 'icon' => 'edit',
- 'inherit' => 'depth-1',
- 'url' => "/{$controller}/edit/{{id}}",
- 'url_vars' => ['id' => 'id'],
- ],
- "{$controller}:delete" => [
- 'label' => __('Delete'),
- 'icon' => 'trash',
- 'inherit' => 'depth-1',
- 'url' => "/{$controller}/delete/{{id}}",
- 'url_vars' => ['id' => 'id'],
- ],
- ]
- ];
- $merged = array_merge_recursive($default, $merges);
- $overridden = array_replace_recursive($merged, $overrides);
- return $overridden;
- }
-
public function genBreadcrumb(): array
{
- $fullConfig = [
- 'Individuals' => $this->getDefaultCRUDConfig('Individuals'),
- 'Organisations' => $this->getDefaultCRUDConfig('Organisations'),
- 'EncryptionKeys' => $this->getDefaultCRUDConfig('EncryptionKeys'),
- 'SharingGroups' => $this->getDefaultCRUDConfig('SharingGroups'),
- 'Broods' => $this->getDefaultCRUDConfig('Broods', [], [
- 'defaults' => ['depth-1' => ['links' => 'LocalTools:brood_tools']]
- ]),
- 'Roles' => $this->getDefaultCRUDConfig('Roles'),
- 'Users' => $this->getDefaultCRUDConfig('Users'),
- 'Inbox' => $this->getDefaultCRUDConfig('Inbox', [
- 'defaults' => ['depth-1' => [
- 'links' => ['Inbox:view', 'Inbox:process'],
- 'actions' => ['Inbox:process', 'Inbox:delete'],
- ]]
- ], [
- 'routes' => [
- 'Inbox:discard' => [
- 'label' => __('Discard request'),
- 'inherit' => 'depth-1',
- 'url' => '/inbox/discard/{{id}}',
- 'url_vars' => ['id' => 'id'],
- ],
- 'Inbox:process' => [
- 'label' => __('Process request'),
- 'inherit' => 'depth-1',
- 'url' => '/inbox/process/{{id}}',
- 'url_vars' => ['id' => 'id'],
- ],
- ]
- ]),
- 'Outbox' => $this->getDefaultCRUDConfig('Outbox', [
- 'defaults' => ['depth-1' => [
- 'links' => ['Outbox:view', 'Outbox:process'],
- 'actions' => ['Outbox:process', 'Outbox:delete'],
- ]]
- ], [
- 'routes' => [
- 'Outbox:discard' => [
- 'label' => __('Discard request'),
- 'inherit' => 'depth-1',
- 'url' => '/outbox/discard/{{id}}',
- 'url_vars' => ['id' => 'id'],
- ],
- 'Outbox:process' => [
- 'label' => __('Process request'),
- 'inherit' => 'depth-1',
- 'url' => '/outbox/process/{{id}}',
- 'url_vars' => ['id' => 'id'],
- ],
- ]
- ]),
- 'MetaTemplates' => $this->getDefaultCRUDConfig('MetaTemplates', [
- 'defaults' => ['depth-1' => [
- 'links' => ['MetaTemplates:view', ''], // '' to remove leftovers. Related to https://www.php.net/manual/en/function.array-replace-recursive.php#124705
- 'actions' => ['MetaTemplates:toggle'],
- ]]
- ], [
- 'routes' => [
- 'MetaTemplates:toggle' => [
- 'label' => __('Toggle Meta-template'),
- 'inherit' => 'depth-1',
- 'url' => '/MetaTemplates/toggle/{{id}}',
- 'url_vars' => ['id' => 'id'],
- ],
- ]
- ]),
- 'Tags' => $this->getDefaultCRUDConfig('Tags', [
- 'defaults' => ['depth-1' => ['textGetter' => 'name']]
- ]),
- 'LocalTools' => [
- 'routes' => [
- 'LocalTools:index' => [
- 'label' => __('Local Tools'),
- 'url' => '/localTools/index',
- 'icon' => $this->iconToTableMapping['LocalTools'],
- ],
- 'LocalTools:viewConnector' => [
- 'label' => __('View'),
- 'textGetter' => 'name',
- 'url' => '/localTools/viewConnector/{{connector}}',
- 'url_vars' => ['connector' => 'connector'],
- 'after' => 'LocalTools:index',
- ],
- 'LocalTools:broodTools' => [
- 'label' => __('Brood Tools'),
- 'url' => '/localTools/broodTools/{{id}}',
- 'url_vars' => ['id' => 'id'],
- ],
- ]
- ],
- 'Instance' => [
- 'routes' => [
- 'Instance:home' => [
- 'label' => __('Home'),
- 'url' => '/',
- 'icon' => 'home'
- ],
- 'Instance:settings' => [
- 'label' => __('Settings'),
- 'url' => '/instance/settings',
- 'icon' => 'cogs'
- ],
- 'Instance:migrationIndex' => [
- 'label' => __('Database Migration'),
- 'url' => '/instance/migrationIndex',
- 'icon' => 'database'
- ],
- ]
- ]
- ];
- foreach ($fullConfig as $controller => $config) {
- $fullConfig[$controller] = $this->insertInheritance($config, $fullConfig);
- }
- foreach ($fullConfig as $controller => $config) {
- $fullConfig[$controller] = $this->insertRelated($config, $fullConfig);
- }
+ $request = $this->request;
+ $bcf = new BreadcrumbFactory($this->iconToTableMapping);
+ $fullConfig = $this->getFullConfig($bcf, $this->request);
return $fullConfig;
}
-}
\ No newline at end of file
+
+ private function loadNavigationClasses($bcf, $request)
+ {
+ $navigationClasses = [];
+ $navigationDir = new Folder(APP . DS . 'Controller' . DS . 'Component' . DS . 'Navigation');
+ $navigationFiles = $navigationDir->find('.*\.php', true);
+ foreach ($navigationFiles as $navigationFile) {
+ if ($navigationFile == 'base.php' || $navigationFile == 'sidemenu.php') {
+ continue;
+ }
+ $navigationClassname = str_replace('.php', '', $navigationFile);
+ require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . $navigationFile);
+ $reflection = new \ReflectionClass("BreadcrumbNavigation\\{$navigationClassname}Navigation");
+ $viewVars = $this->_registry->getController()->viewBuilder()->getVars();
+ $navigationClasses[$navigationClassname] = $reflection->newInstance($bcf, $request, $viewVars);
+ }
+ return $navigationClasses;
+ }
+
+ public function getFullConfig($bcf, $request)
+ {
+ $navigationClasses = $this->loadNavigationClasses($bcf, $request);
+ $CRUDControllers = [
+ 'Individuals',
+ 'Organisations',
+ 'EncryptionKeys',
+ 'SharingGroups',
+ 'Broods',
+ 'Roles',
+ 'Users',
+ 'Tags',
+ 'LocalTools',
+ 'UserSettings',
+ 'MailingLists',
+ ];
+ 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');
+ $routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'badge');
+ return $routeConfig;
+ }
+
+ private function addIfNotEmpty($arr, $data, $key, $default = null)
+ {
+ if (!empty($data[$key])) {
+ $arr[$key] = $data[$key];
+ } else {
+ if (!is_null($default)) {
+ $arr[$key] = $default;
+ }
+ }
+ return $arr;
+ }
+
+ public function addRoute($controller, $action, $config = []) {
+ $this->endpoints[$controller][$action] = $this->genRouteConfig($controller, $action, $config);
+ }
+
+ public function setDefaultCRUDForModel($controller)
+ {
+ $this->addRoute($controller, 'index', $this->defaultCRUD($controller, 'index'));
+ $this->addRoute($controller, 'view', $this->defaultCRUD($controller, 'view'));
+ $this->addRoute($controller, 'add', $this->defaultCRUD($controller, 'add'));
+ $this->addRoute($controller, 'edit', $this->defaultCRUD($controller, 'edit'));
+ $this->addRoute($controller, 'delete', $this->defaultCRUD($controller, 'delete'));
+
+ $this->addParent($controller, 'view', $controller, 'index');
+ $this->addParent($controller, 'add', $controller, 'index');
+ $this->addParent($controller, 'edit', $controller, 'index');
+ $this->addParent($controller, 'delete', $controller, 'index');
+
+ $this->addSelfLink($controller, 'view');
+ $this->addLink($controller, 'view', $controller, 'edit');
+ $this->addLink($controller, 'edit', $controller, 'view');
+ $this->addSelfLink($controller, 'edit');
+
+ $this->addAction($controller, 'view', $controller, 'add');
+ $this->addAction($controller, 'view', $controller, 'delete');
+ $this->addAction($controller, 'edit', $controller, 'add');
+ $this->addAction($controller, 'edit', $controller, 'delete');
+ }
+
+ public function get($controller, $action)
+ {
+ if (empty($this->endpoints[$controller]) || empty($this->endpoints[$controller][$action])) {
+ throw new \Exception(sprintf("Tried to add a reference to %s:%s which does not exists", $controller, $action), 1);
+ }
+ return $this->endpoints[$controller][$action];
+ }
+
+ public function getEndpoints()
+ {
+ return $this->endpoints;
+ }
+
+ public function addParent(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = [])
+ {
+ $routeSourceConfig = $this->get($sourceController, $sourceAction);
+ $routeTargetConfig = $this->get($targetController, $targetAction);
+ $overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
+ if (!is_array($overrides)) {
+ throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
+ }
+ $routeTargetConfig = array_merge($routeTargetConfig, $overrides);
+ $parents = array_merge($routeSourceConfig['after'] ?? [], $routeTargetConfig);
+ $this->endpoints[$sourceController][$sourceAction]['after'] = $parents;
+ }
+
+ public function addSelfLink(string $controller, string $action, array $options=[])
+ {
+ $this->addLink($controller, $action, $controller, $action, array_merge($options, [
+ 'selfLink' => true,
+ ]));
+ }
+
+ public function addLink(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = [])
+ {
+ $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
+ $routeTargetConfig = $this->getRouteConfig($targetController, $targetAction);
+ $overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
+ if (is_null($overrides)) {
+ // Overrides is null, the link should not be added
+ return;
+ }
+ if (!is_array($overrides)) {
+ throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
+ }
+ $routeTargetConfig = array_merge($routeTargetConfig, $overrides);
+ $links = array_merge($routeSourceConfig['links'] ?? [], [$routeTargetConfig]);
+ $this->endpoints[$sourceController][$sourceAction]['links'] = $links;
+ }
+
+ public function addAction(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = [])
+ {
+ $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
+ $routeTargetConfig = $this->getRouteConfig($targetController, $targetAction);
+ $overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig);
+ if (!is_array($overrides)) {
+ throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1);
+ }
+ $routeTargetConfig = array_merge($routeTargetConfig, $overrides);
+ $links = array_merge($routeSourceConfig['actions'] ?? [], [$routeTargetConfig]);
+ $this->endpoints[$sourceController][$sourceAction]['actions'] = $links;
+ }
+
+ public function removeLink(string $sourceController, string $sourceAction, string $targetController, string $targetAction)
+ {
+ $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true);
+ if (!empty($routeSourceConfig['links'])) {
+ foreach ($routeSourceConfig['links'] as $i => $routeConfig) {
+ if ($routeConfig['controller'] == $targetController && $routeConfig['action'] == $targetAction) {
+ unset($routeSourceConfig['links'][$i]);
+ $this->endpoints[$sourceController][$sourceAction]['links'] = $routeSourceConfig['links'];
+ break;
+ }
+ }
+ }
+ }
+
+ public function getRouteConfig($controller, $action, $fullRoute = false)
+ {
+ $routeConfig = $this->get($controller, $action);
+ if (empty($fullRoute)) {
+ unset($routeConfig['after']);
+ unset($routeConfig['links']);
+ unset($routeConfig['actions']);
+ }
+ return $routeConfig;
+ }
+
+ private function execClosureIfNeeded($closure, $routeConfig=[])
+ {
+ if (is_callable($closure)) {
+ return $closure($routeConfig);
+ }
+ return $closure;
+ }
+}
diff --git a/src/Controller/Component/ParamHandlerComponent.php b/src/Controller/Component/ParamHandlerComponent.php
index f76a578..ae59f73 100644
--- a/src/Controller/Component/ParamHandlerComponent.php
+++ b/src/Controller/Component/ParamHandlerComponent.php
@@ -4,6 +4,7 @@ namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Core\Configure;
+use Cake\Http\Exception\MethodNotAllowedException;
class ParamHandlerComponent extends Component
{
@@ -26,15 +27,15 @@ class ParamHandlerComponent extends Component
$queryString = str_replace('.', '_', $filter);
$queryString = str_replace(' ', '_', $queryString);
if ($this->request->getQuery($queryString) !== null) {
- $parsedParams[$filter] = $this->request->getQuery($queryString);
- continue;
- }
- if (($this->request->getQuery($filter)) !== null) {
- $parsedParams[$filter] = $this->request->getQuery($filter);
+ if (is_array($this->request->getQuery($queryString))) {
+ $parsedParams[$filter] = array_map('trim', $this->request->getQuery($queryString));
+ } else {
+ $parsedParams[$filter] = trim($this->request->getQuery($queryString));
+ }
continue;
}
if (($this->request->is('post') || $this->request->is('put')) && $this->request->getData($filter) !== null) {
- $parsedParams[$filter] = $this->request->getData($filter);
+ $parsedParams[$filter] = trim($this->request->getData($filter));
}
}
return $parsedParams;
@@ -47,7 +48,7 @@ class ParamHandlerComponent extends Component
return $this->isRest;
}
if ($this->request->is('json')) {
- if (!empty($this->request->input()) && empty($this->request->input('json_decode'))) {
+ if (!empty((string)$this->request->getBody()) && empty($this->request->getParsedBody())) {
throw new MethodNotAllowedException('Invalid JSON input. Make sure that the JSON input is a correctly formatted JSON string. This request has been blocked to avoid an unfiltered request.');
}
$this->isRest = true;
diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php
index 4a7346f..50fb40c 100644
--- a/src/Controller/Component/RestResponseComponent.php
+++ b/src/Controller/Component/RestResponseComponent.php
@@ -718,11 +718,11 @@ class RestResponseComponent extends Component
'operators' => array('equal'),
'help' => __('A valid x509 certificate ')
),
- 'change' => array(
+ 'changed' => array(
'input' => 'text',
'type' => 'string',
'operators' => array('equal'),
- 'help' => __('The text contained in the change field')
+ 'help' => __('The text contained in the changed field')
),
'change_pw' => array(
'input' => 'radio',
diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php
index 65183cb..d0376db 100644
--- a/src/Controller/EncryptionKeysController.php
+++ b/src/Controller/EncryptionKeysController.php
@@ -14,9 +14,10 @@ use Cake\Error\Debugger;
class EncryptionKeysController extends AppController
{
- public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'];
+ public $filterFields = ['owner_model', 'owner_id', 'encryption_key'];
public $quickFilterFields = ['encryption_key'];
public $containFields = ['Individuals', 'Organisations'];
+ public $statisticsFields = ['type'];
public function index()
{
@@ -28,7 +29,8 @@ class EncryptionKeysController extends AppController
'type'
]
],
- 'contain' => $this->containFields
+ 'contain' => $this->containFields,
+ 'statisticsFields' => $this->statisticsFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@@ -39,7 +41,15 @@ class EncryptionKeysController extends AppController
public function delete($id)
{
- $this->CRUD->delete($id);
+ $orgConditions = [];
+ $individualConditions = [];
+ $dropdownData = [];
+ $currentUser = $this->ACL->getUser();
+ $params = [];
+ if (empty($currentUser['role']['perm_admin'])) {
+ $params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData);
+ }
+ $this->CRUD->delete($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@@ -47,35 +57,92 @@ class EncryptionKeysController extends AppController
$this->set('metaGroup', 'ContactDB');
}
- public function add()
+ private function buildBeforeSave(array $params, $currentUser, array &$orgConditions, array &$individualConditions, array &$dropdownData): array
{
- $this->CRUD->add(['redirect' => $this->referer()]);
- $responsePayload = $this->CRUD->getResponsePayload();
- if (!empty($responsePayload)) {
- return $responsePayload;
+ if (empty($currentUser['role']['perm_admin'])) {
+ $orgConditions = [
+ 'id' => $currentUser['organisation_id']
+ ];
+ if (empty($currentUser['role']['perm_org_admin'])) {
+ $individualConditions = [
+ 'id' => $currentUser['individual_id']
+ ];
+ } else {
+ $this->loadModel('Alignments');
+ $individualConditions = ['id IN' => $this->Alignments->find('list', [
+ 'keyField' => 'id',
+ 'valueField' => 'individual_id',
+ 'conditions' => ['organisation_id' => $currentUser['organisation_id']]
+ ])->toArray()];
+ }
+ $params['beforeSave'] = function($entity) use($currentUser) {
+ if ($entity['owner_model'] === 'organisation') {
+ if ($entity['owner_id'] !== $currentUser['organisation_id']) {
+ throw new MethodNotAllowedException(__('Selected organisation cannot be linked by the current user.'));
+ }
+ } else {
+ if ($currentUser['role']['perm_org_admin']) {
+ $this->loadModel('Alignments');
+ $validIndividuals = $this->Alignments->find('list', [
+ 'keyField' => 'individual_id',
+ 'valueField' => 'id',
+ 'conditions' => ['organisation_id' => $currentUser['organisation_id']]
+ ])->toArray();
+ if (!isset($validIndividuals[$entity['owner_id']])) {
+ throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.'));
+ }
+ } else {
+ if ($entity['owner_id'] !== $currentUser['id']) {
+ throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.'));
+ }
+ }
+ }
+ return $entity;
+ };
}
$this->loadModel('Organisations');
$this->loadModel('Individuals');
$dropdownData = [
- 'organisation' => $this->Organisations->find('list', [
- 'sort' => ['name' => 'asc']
- ]),
- 'individual' => $this->Individuals->find('list', [
- 'sort' => ['email' => 'asc']
- ])
+ 'organisation' => $this->Organisations->find('list')->order(['name' => 'asc'])->where($orgConditions)->all()->toArray(),
+ 'individual' => $this->Individuals->find('list')->order(['email' => 'asc'])->where($individualConditions)->all()->toArray()
];
+ return $params;
+ }
+
+ public function add()
+ {
+ $orgConditions = [];
+ $individualConditions = [];
+ $dropdownData = [];
+ $currentUser = $this->ACL->getUser();
+ $params = [
+ 'redirect' => $this->referer()
+ ];
+ $params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData);
+ $this->CRUD->add($params);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
$this->set(compact('dropdownData'));
$this->set('metaGroup', 'ContactDB');
}
public function edit($id = false)
{
+ $orgConditions = [];
+ $individualConditions = [];
+ $dropdownData = [];
+ $currentUser = $this->ACL->getUser();
$params = [
'fields' => [
'type', 'encryption_key', 'revoked'
],
'redirect' => $this->referer()
];
+ if (empty($currentUser['role']['perm_admin'])) {
+ $params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData);
+ }
$this->CRUD->edit($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@@ -85,4 +152,16 @@ class EncryptionKeysController extends AppController
$this->set('metaGroup', 'ContactDB');
$this->render('add');
}
+
+ public function view($id = false)
+ {
+ $this->CRUD->view($id, [
+ 'contain' => ['Individuals', 'Organisations']
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ $this->set('metaGroup', 'ContactDB');
+ }
}
diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php
index 996a886..cc5a8fa 100644
--- a/src/Controller/IndividualsController.php
+++ b/src/Controller/IndividualsController.php
@@ -16,25 +16,22 @@ 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 $statisticsFields = ['position'];
public function index()
{
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
- 'contextFilters' => [
- 'fields' => [
- 'Alignments.type'
- ]
- ],
- 'contain' => $this->containFields
+ 'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
+ 'contain' => $this->containFields,
+ 'statisticsFields' => $this->statisticsFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('alignmentScope', 'individuals');
- $this->set('metaGroup', 'ContactDB');
}
public function filtering()
@@ -49,7 +46,6 @@ class IndividualsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'ContactDB');
}
public function view($id)
@@ -59,7 +55,6 @@ class IndividualsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'ContactDB');
}
public function edit($id)
@@ -69,7 +64,6 @@ class IndividualsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'ContactDB');
$this->render('add');
}
@@ -80,7 +74,6 @@ class IndividualsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'ContactDB');
}
public function tag($id)
diff --git a/src/Controller/InstanceController.php b/src/Controller/InstanceController.php
index 8479e5c..15fbfd4 100644
--- a/src/Controller/InstanceController.php
+++ b/src/Controller/InstanceController.php
@@ -21,7 +21,6 @@ class InstanceController extends AppController
public function home()
{
- // $this->set('md', file_get_contents(ROOT . '/README.md'));
$statistics = $this->Instance->getStatistics();
$this->set('statistics', $statistics);
}
@@ -70,6 +69,12 @@ class InstanceController extends AppController
usort($status, function($a, $b) {
return strcmp($b['id'], $a['id']);
});
+ if ($this->ParamHandler->isRest()) {
+ return $this->RestResponse->viewData([
+ 'status' => $status,
+ 'updateAvailables' => $migrationStatus['updateAvailables'],
+ ], 'json');
+ }
$this->set('status', $status);
$this->set('updateAvailables', $migrationStatus['updateAvailables']);
}
@@ -140,6 +145,14 @@ class InstanceController extends AppController
{
$this->Settings = $this->getTableLocator()->get('Settings');
$all = $this->Settings->getSettings(true);
+ if ($this->ParamHandler->isRest()) {
+ return $this->RestResponse->viewData([
+ 'settingsProvider' => $all['settingsProvider'],
+ 'settings' => $all['settings'],
+ 'settingsFlattened' => $all['settingsFlattened'],
+ 'notices' => $all['notices'],
+ ], 'json');
+ }
$this->set('settingsProvider', $all['settingsProvider']);
$this->set('settings', $all['settings']);
$this->set('settingsFlattened', $all['settingsFlattened']);
diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php
index 901407a..12d9d62 100644
--- a/src/Controller/LocalToolsController.php
+++ b/src/Controller/LocalToolsController.php
@@ -68,7 +68,11 @@ class LocalToolsController extends AppController
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);
+ try {
+ $tmpResult = $this->LocalTools->action($this->ACL->getUser()['id'], $connection->connector, $actionName, $params, $this->request);
+ } catch (\Exception $e) {
+ $tmpResult = ['success' => false, 'message' => $e->getMessage(), 'data' => []];
+ }
$tmpResult['connection'] = $connection;
$results[$connection->id] = $tmpResult;
$successes += $tmpResult['success'] ? 1 : 0;
@@ -280,6 +284,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');
}
@@ -334,6 +340,7 @@ class LocalToolsController extends AppController
}
}
+/*
public function connectLocal($local_tool_id)
{
$params = [
@@ -349,10 +356,8 @@ class LocalToolsController extends AppController
$params['target_tool_id'] = $postParams['target_tool_id'];
$result = $this->LocalTools->encodeLocalConnection($params);
// Send message to remote inbox
- debug($result);
} else {
$target_tools = $this->LocalTools->findConnectable($local_tool);
- debug($target_tools);
if (empty($target_tools)) {
throw new NotFoundException(__('No tools found to connect.'));
}
@@ -363,4 +368,5 @@ class LocalToolsController extends AppController
]);
}
}
+*/
}
diff --git a/src/Controller/MailingListsController.php b/src/Controller/MailingListsController.php
new file mode 100644
index 0000000..a3ea108
--- /dev/null
+++ b/src/Controller/MailingListsController.php
@@ -0,0 +1,301 @@
+ true], ['description' => true], ['releasability' => true]];
+ public $containFields = ['Users', 'Individuals', 'MetaFields'];
+ public $statisticsFields = ['active'];
+
+ public function index()
+ {
+ $this->CRUD->index([
+ 'contain' => $this->containFields,
+ 'filters' => $this->filterFields,
+ 'quickFilters' => $this->quickFilterFields,
+ 'statisticsFields' => $this->statisticsFields,
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function add()
+ {
+ $this->CRUD->add([
+ 'override' => [
+ 'user_id' => $this->ACL->getUser()['id']
+ ]
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function view($id)
+ {
+ $this->CRUD->view($id, [
+ 'contain' => $this->containFields
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function edit($id = false)
+ {
+ $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 listIndividuals($mailinglist_id)
+ {
+ $quickFilter = [
+ 'uuid',
+ ['first_name' => true],
+ ['last_name' => true],
+ ];
+ $quickFilterUI = array_merge($quickFilter, [
+ ['Registered emails' => true],
+ ]);
+ $filters = ['uuid', 'first_name', 'last_name', 'quickFilter'];
+ $queryParams = $this->ParamHandler->harvestParams($filters);
+ $activeFilters = $queryParams['quickFilter'] ?? [];
+
+ $mailingList = $this->MailingLists->find()
+ ->where(['MailingLists.id' => $mailinglist_id])
+ ->contain('MetaFields')
+ ->first();
+
+ $filteringActive = !empty($queryParams['quickFilter']);
+ $matchingMetaFieldParentIDs = [];
+ if ($filteringActive) {
+ // Collect individuals having a matching meta_field for the requested search value
+ foreach ($mailingList->meta_fields as $metaField) {
+ if (
+ empty($queryParams['quickFilter']) ||
+ (
+ str_contains($metaField->field, 'email') &&
+ str_contains($metaField->value, $queryParams['quickFilter'])
+ )
+ ) {
+ $matchingMetaFieldParentIDs[$metaField->parent_id] = true;
+ }
+ }
+ }
+ $matchingMetaFieldParentIDs = array_keys($matchingMetaFieldParentIDs);
+ $mailingList = $this->MailingLists->loadInto($mailingList, [
+ 'Individuals' => function (Query $q) use ($queryParams, $quickFilter, $filteringActive, $matchingMetaFieldParentIDs) {
+ $conditions = [];
+ if (!empty($queryParams)) {
+ $conditions = $this->CRUD->genQuickFilterConditions($queryParams, $quickFilter);
+ }
+ if ($filteringActive && !empty($matchingMetaFieldParentIDs)) {
+ $conditions[] = function (QueryExpression $exp) use ($matchingMetaFieldParentIDs) {
+ return $exp->in('Individuals.id', $matchingMetaFieldParentIDs);
+ };
+ }
+ if ($filteringActive && !empty($queryParams['quickFilter'])) {
+ $conditions[] = [
+ 'MailingListsIndividuals.include_primary_email' => true,
+ 'Individuals.email LIKE' => "%{$queryParams['quickFilter']}%"
+ ];
+ }
+ $q->where([
+ 'OR' => $conditions
+ ]);
+ return $q;
+ }
+ ]);
+ $mailingList->injectRegisteredEmailsIntoIndividuals();
+ if ($this->ParamHandler->isRest()) {
+ return $this->RestResponse->viewData($mailingList->individuals, 'json');
+ }
+ $individuals = $this->CustomPagination->paginate($mailingList->individuals);
+ $this->set('mailing_list_id', $mailinglist_id);
+ $this->set('quickFilter', $quickFilterUI);
+ $this->set('activeFilters', $activeFilters);
+ $this->set('quickFilterValue', $queryParams['quickFilter'] ?? '');
+ $this->set('individuals', $individuals);
+ }
+
+ public function addIndividual($mailinglist_id)
+ {
+ $mailingList = $this->MailingLists->get($mailinglist_id, [
+ 'contain' => ['Individuals', 'MetaFields']
+ ]);
+ $linkedIndividualsIDs = Hash::extract($mailingList, 'individuals.{n}.id');
+ $conditions = [
+ 'id NOT IN' => $linkedIndividualsIDs
+ ];
+ $dropdownData = [
+ 'individuals' => $this->MailingLists->Individuals->getTarget()->find()
+ ->order(['first_name' => 'asc'])
+ ->where($conditions)
+ ->all()
+ ->combine('id', 'full_name')
+ ->toArray()
+ ];
+ if ($this->request->is('post') || $this->request->is('put')) {
+ $memberIDs = $this->request->getData()['individuals'];
+ $chosen_emails = $this->request->getData()['chosen_emails'];
+ if (!empty($chosen_emails)) {
+ $chosen_emails = json_decode($chosen_emails, true);
+ $chosen_emails = !is_null($chosen_emails) ? $chosen_emails : [];
+ } else {
+ $chosen_emails = [];
+ }
+ $members = $this->MailingLists->Individuals->getTarget()->find()->where([
+ 'id IN' => $memberIDs
+ ])->all()->toArray();
+ $memberToLink = [];
+ foreach ($members as $i => $member) {
+ $includePrimary = in_array('primary', $chosen_emails[$member->id]);
+ $chosen_emails[$member->id] = array_filter($chosen_emails[$member->id], function($entry) {
+ return $entry != 'primary';
+ });
+ $members[$i]->_joinData = new Entity(['include_primary_email' => $includePrimary]);
+ if (!in_array($member->id, $linkedIndividualsIDs)) { // individual are not already in the list
+ $memberToLink[] = $members[$i];
+ }
+ }
+
+ // save new individuals
+ if (!empty($memberToLink)) {
+ $success = (bool)$this->MailingLists->Individuals->link($mailingList, $memberToLink);
+ if ($success && !empty($chosen_emails[$member->id])) { // Include any remaining emails from the metaFields
+ $emailsFromMetaFields = $this->MailingLists->MetaFields->find()->where([
+ 'id IN' => $chosen_emails[$member->id]
+ ])->all()->toArray();
+ $success = (bool)$this->MailingLists->MetaFields->link($mailingList, $emailsFromMetaFields);
+ }
+ }
+
+ if ($success) {
+ $message = __n('{0} individual added to the mailing list.', '{0} Individuals added to the mailing list.', count($members), count($members));
+ $mailingList = $this->MailingLists->get($mailingList->id);
+ } else {
+ $message = __n('The individual could not be added to the mailing list.', 'The Individuals could not be added to the mailing list.', count($members));
+ }
+ $this->CRUD->setResponseForController('add_individuals', $success, $message, $mailingList, $mailingList->getErrors());
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ $this->set(compact('dropdownData'));
+ $this->set('mailinglist_id', $mailinglist_id);
+ $this->set('mailingList', $mailingList);
+ }
+
+ public function removeIndividual($mailinglist_id, $individual_id=null)
+ {
+ $mailingList = $this->MailingLists->get($mailinglist_id, [
+ 'contain' => ['Individuals', 'MetaFields']
+ ]);
+ $individual = [];
+ if (!is_null($individual_id)) {
+ $individual = $this->MailingLists->Individuals->get($individual_id);
+ }
+ if ($this->request->is('post') || $this->request->is('delete')) {
+ $success = false;
+ if (!is_null($individual_id)) {
+ $individualToRemove = $this->MailingLists->Individuals->get($individual_id);
+ $metaFieldsIDsToRemove = Hash::extract($mailingList, 'meta_fields.{n}.id');
+ if (!empty($metaFieldsIDsToRemove)) {
+ $metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([
+ 'id IN' => $metaFieldsIDsToRemove,
+ 'parent_id' => $individual_id,
+ ])->all()->toArray();
+ }
+ $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]);
+ if ($success && !empty($metaFieldsToRemove)) {
+ $success = (bool)$this->MailingLists->MetaFields->unlink($mailingList, $metaFieldsToRemove);
+ }
+ if ($success) {
+ $message = __('{0} removed from the mailing list.', $individualToRemove->full_name);
+ $mailingList = $this->MailingLists->get($mailingList->id);
+ } else {
+ $message = __n('{0} could not be removed from the mailing list.', $individual->full_name);
+ }
+ $this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, $mailingList->getErrors());
+ } else {
+ $params = $this->ParamHandler->harvestParams(['ids']);
+ if (!empty($params['ids'])) {
+ $params['ids'] = json_decode($params['ids']);
+ }
+ if (empty($params['ids'])) {
+ throw new NotFoundException(__('Invalid {0}.', Inflector::singularize($this->MailingLists->Individuals->getAlias())));
+ }
+ $individualsToRemove = $this->MailingLists->Individuals->find()->where([
+ 'id IN' => array_map('intval', $params['ids'])
+ ])->all()->toArray();
+ $metaFieldsIDsToRemove = Hash::extract($mailingList, 'meta_fields.{n}.id');
+ if (!empty($metaFieldsIDsToRemove)) {
+ $metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([
+ 'id IN' => $metaFieldsIDsToRemove,
+ ])->all()->toArray();
+ }
+ $unlinkSuccesses = 0;
+ foreach ($individualsToRemove as $individualToRemove) {
+ $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]);
+ $results[] = $success;
+ if ($success) {
+ $unlinkSuccesses++;
+ }
+ }
+ $mailingList = $this->MailingLists->get($mailingList->id);
+ $success = $unlinkSuccesses == count($individualsToRemove);
+ $message = __(
+ '{0} {1} have been removed.',
+ $unlinkSuccesses == count($individualsToRemove) ? __('All') : sprintf('%s / %s', $unlinkSuccesses, count($individualsToRemove)),
+ Inflector::singularize($this->MailingLists->Individuals->getAlias())
+ );
+ $this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, []);
+ }
+
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ $this->set('mailinglist_id', $mailinglist_id);
+ $this->set('mailingList', $mailingList);
+ if (!empty($individual)) {
+ $this->set('deletionText', __('Are you sure you want to remove `{0} ({1})` from the mailing list?', $individual->full_name, $individual->email));
+ } else {
+ $this->set('deletionText', __('Are you sure you want to remove multiples individuals from the mailing list?'));
+ }
+ $this->set('postLinkParameters', ['action' => 'removeIndividual', $mailinglist_id, $individual_id]);
+ $this->viewBuilder()->setLayout('ajax');
+ $this->render('/genericTemplates/delete');
+ }
+
+}
\ No newline at end of file
diff --git a/src/Controller/MetaTemplatesController.php b/src/Controller/MetaTemplatesController.php
index 904ac8d..dc5607b 100644
--- a/src/Controller/MetaTemplatesController.php
+++ b/src/Controller/MetaTemplatesController.php
@@ -5,62 +5,282 @@ namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
+use Cake\Utility\Inflector;
+use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
+use Cake\Http\Exception\NotFoundException;
+use Cake\Http\Exception\MethodNotAllowedException;
+use Cake\Routing\Router;
+
class MetaTemplatesController extends AppController
{
- public $quickFilterFields = ['name', 'uuid', 'scope'];
+ public $quickFilterFields = [['name' => true], 'uuid', ['scope' => true]];
public $filterFields = ['name', 'uuid', 'scope', 'namespace'];
public $containFields = ['MetaTemplateFields'];
- public function update()
+ public function updateAllTemplates()
{
if ($this->request->is('post')) {
- $result = $this->MetaTemplates->update();
+ $result = $this->MetaTemplates->updateAllTemplates();
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($result, 'json');
} else {
- $this->Flash->success(__('{0} templates updated.', count($result)));
- $this->redirect($this->referer());
+ if ($result['success']) {
+ $message = __n('{0} templates updated.', 'The template has been updated.', empty($template_id), $result['files_processed']);
+ } else {
+ $message = __n('{0} templates could not be updated.', 'The template could not be updated.', empty($template_id), $result['files_processed']);
+ }
+ $this->CRUD->setResponseForController('updateAllTemplate', $result['success'], $message, $result['files_processed'], $result['update_errors'], ['redirect' => $this->referer()]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
}
} else {
if (!$this->ParamHandler->isRest()) {
- $this->set('title', __('Update Meta Templates'));
- $this->set('question', __('Are you sure you wish to update the Meta Template definitions?'));
- $this->set('actionName', __('Update'));
- $this->set('path', ['controller' => 'metaTemplates', 'action' => 'update']);
+ $this->set('title', __('Update All Meta Templates'));
+ $this->set('question', __('Are you sure you wish to update all the Meta Template definitions'));
+ $templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();
+ $this->set('templatesUpdateStatus', $templatesUpdateStatus);
+ $this->render('updateAll');
+ }
+ }
+ }
+
+ /**
+ * Update the provided template or all templates
+ *
+ * @param int|null $template_id
+ */
+ public function update($template_id=null)
+ {
+ $metaTemplate = false;
+ if (!is_null($template_id)) {
+ if (!is_numeric($template_id)) {
+ throw new NotFoundException(__('Invalid {0} for provided ID.', $this->MetaTemplates->getAlias(), $template_id));
+ }
+ $metaTemplate = $this->MetaTemplates->get($template_id);
+ if (empty($metaTemplate)) {
+ throw new NotFoundException(__('Invalid {0} {1}.', $this->MetaTemplates->getAlias(), $template_id));
+ }
+ }
+ if ($this->request->is('post')) {
+ $params = $this->ParamHandler->harvestParams(['update_strategy']);
+ $updateStrategy = $params['update_strategy'] ?? null;
+ $result = $this->MetaTemplates->update($metaTemplate, $updateStrategy);
+ if ($this->ParamHandler->isRest()) {
+ return $this->RestResponse->viewData($result, 'json');
+ } else {
+ if ($result['success']) {
+ $message = __n('{0} templates updated.', 'The template has been updated.', empty($template_id), $result['files_processed']);
+ } else {
+ $message = __n('{0} templates could not be updated.', 'The template could not be updated.', empty($template_id), $result['files_processed']);
+ }
+ $this->CRUD->setResponseForController('update', $result['success'], $message, $result['files_processed'], $result['update_errors'], ['redirect' => $this->referer()]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ } else {
+ if (!$this->ParamHandler->isRest()) {
+ if (!empty($metaTemplate)) {
+ $this->set('metaTemplate', $metaTemplate);
+ $statuses = $this->setUpdateStatus($metaTemplate->id);
+ $this->set('updateStatus', $this->MetaTemplates->computeFullUpdateStatusForMetaTemplate($statuses['templateStatus'], $metaTemplate));
+ } else {
+ $this->set('title', __('Update All Meta Templates'));
+ $this->set('question', __('Are you sure you wish to update all the Meta Template definitions'));
+ $templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();
+ $this->set('templatesUpdateStatus', $templatesUpdateStatus);
+ $this->render('updateAll');
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a new template by loading the template on the disk having the provided UUID.
+ *
+ * @param string $uuid
+ */
+ public function createNewTemplate(string $uuid)
+ {
+ if ($this->request->is('post')) {
+ $result = $this->MetaTemplates->createNewTemplate($uuid);
+ if ($this->ParamHandler->isRest()) {
+ return $this->RestResponse->viewData($result, 'json');
+ } else {
+ if ($result['success']) {
+ $message = __('The template {0} has been created.', $uuid);
+ } else {
+ $message = __('The template {0} could not be created.', $uuid);
+ }
+ $this->CRUD->setResponseForController('createNewTemplate', $result['success'], $message, $result['files_processed'], $result['update_errors']);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ } else {
+ if (!$this->ParamHandler->isRest()) {
+ $this->set('title', __('Create Meta Template'));
+ $this->set('question', __('Are you sure you wish to load the meta template with UUID: {0} in the database', h($uuid)));
+ $this->set('actionName', __('Create template'));
+ $this->set('path', ['controller' => 'meta-templates', 'action' => 'create_new_template', $uuid]);
$this->render('/genericTemplates/confirm');
}
}
}
+ public function getMetaFieldsToUpdate($template_id)
+ {
+ $metaTemplate = $this->MetaTemplates->get($template_id);
+ $newestMetaTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate);
+ $amountOfEntitiesToUpdate = 0;
+ $entities = $this->MetaTemplates->getEntitiesHavingMetaFieldsFromTemplate($template_id, 10, $amountOfEntitiesToUpdate);
+ $this->set('metaTemplate', $metaTemplate);
+ $this->set('newestMetaTemplate', $newestMetaTemplate);
+ $this->set('entities', $entities);
+ $this->set('amountOfEntitiesToUpdate', $amountOfEntitiesToUpdate);
+ }
+
+ public function migrateOldMetaTemplateToNewestVersionForEntity($template_id, $entity_id)
+ {
+ $metaTemplate = $this->MetaTemplates->get($template_id, [
+ 'contain' => ['MetaTemplateFields']
+ ]);
+ $newestMetaTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate, true);
+ $entity = $this->MetaTemplates->getEntity($metaTemplate, $entity_id);
+ $conditions = [
+ 'MetaFields.meta_template_id IN' => [$metaTemplate->id, $newestMetaTemplate->id],
+ 'MetaFields.scope' => $metaTemplate->scope,
+ ];
+ $keyedMetaFields = $this->MetaTemplates->getKeyedMetaFieldsForEntity($entity_id, $conditions);
+ if (empty($keyedMetaFields[$metaTemplate->id])) {
+ throw new NotFoundException(__('Invalid {0}. This entities does not have meta-fields to be moved to a newer template.', $this->MetaTemplates->getAlias()));
+ }
+ $mergedMetaFields = $this->MetaTemplates->insertMetaFieldsInMetaTemplates($keyedMetaFields, [$metaTemplate, $newestMetaTemplate]);
+ $entity['MetaTemplates'] = $mergedMetaFields;
+ if ($this->request->is('post') || $this->request->is('put')) {
+ $className = Inflector::camelize(Inflector::pluralize($newestMetaTemplate->scope));
+ $entityTable = TableRegistry::getTableLocator()->get($className);
+ $inputData = $this->request->getData();
+ $massagedData = $this->MetaTemplates->massageMetaFieldsBeforeSave($entity, $inputData, $newestMetaTemplate);
+ unset($inputData['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity
+ $data = $massagedData['entity'];
+ $metaFieldsToDelete = $massagedData['metafields_to_delete'];
+ foreach ($entity->meta_fields as $i => $metaField) {
+ if ($metaField->meta_template_id == $template_id) {
+ $metaFieldsToDelete[] = $entity->meta_fields[$i];
+ }
+ }
+ $data = $entityTable->patchEntity($data, $inputData);
+ $savedData = $entityTable->save($data);
+ if ($savedData !== false) {
+ if (!empty($metaFieldsToDelete)) {
+ $entityTable->MetaFields->unlink($savedData, $metaFieldsToDelete);
+ }
+ $message = __('Data on old meta-template has been migrated to newest meta-template');
+ } else {
+ $message = __('Could not migrate data to newest meta-template');
+ }
+ $this->CRUD->setResponseForController(
+ 'migrateOldMetaTemplateToNewestVersionForEntity',
+ $savedData !== false,
+ $message,
+ $savedData,
+ [],
+ [
+ 'redirect' => [
+ 'controller' => $className,
+ 'action' => 'view', $entity_id,
+ 'url' => Router::url(['controller' => $className, 'action' => 'view', $entity_id])
+ ]
+ ]
+ );
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ $conflicts = $this->MetaTemplates->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate);
+ foreach ($conflicts as $conflict) {
+ if (!empty($conflict['existing_meta_template_field'])) {
+ $existingMetaTemplateField = $conflict['existing_meta_template_field'];
+ foreach ($existingMetaTemplateField->metaFields as $metaField) {
+ $metaField->setError('value', implode(', ', $existingMetaTemplateField->conflicts));
+ }
+ }
+ }
+ // automatically convert non-conflicting fields to new meta-template
+ $movedMetaTemplateFields = [];
+ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
+ if (!empty($conflicts[$metaTemplateField->field]['conflicts'])) {
+ continue;
+ }
+ foreach ($newestMetaTemplate->meta_template_fields as $i => $newMetaTemplateField) {
+ if ($metaTemplateField->field == $newMetaTemplateField->field && empty($newMetaTemplateField->metaFields)) {
+ $movedMetaTemplateFields[] = $metaTemplateField->id;
+ $copiedMetaFields = array_map(function ($e) use ($newMetaTemplateField) {
+ $e = $e->toArray();
+ $e['meta_template_id'] = $newMetaTemplateField->meta_template_id;
+ $e['meta_template_field_id'] = $newMetaTemplateField->id;
+ unset($e['id']);
+ return $e;
+ }, $metaTemplateField->metaFields);
+ $newMetaTemplateField->metaFields = $this->MetaTemplates->MetaTemplateFields->MetaFields->newEntities($copiedMetaFields);
+ }
+ }
+ }
+ $this->set('oldMetaTemplate', $metaTemplate);
+ $this->set('newMetaTemplate', $newestMetaTemplate);
+ $this->set('entity', $entity);
+ $this->set('conflicts', $conflicts);
+ $this->set('movedMetaTemplateFields', $movedMetaTemplateFields);
+ }
+
public function index()
{
+ $templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
- 'fields' => ['scope'],
'custom' => [
[
- 'label' => __('Contact DB'),
- 'filterCondition' => ['scope' => ['individual', 'organisation']]
- ],
- [
- 'label' => __('Namespace CNW'),
- 'filterCondition' => ['namespace' => 'cnw']
+ 'default' => true,
+ 'label' => __('Newest Templates'),
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'id IN' => $this->MetaTemplates->genQueryForAllNewestVersionIDs()
+ ]);
+ }
],
]
],
- 'contain' => $this->containFields
+ 'contain' => $this->containFields,
+ 'afterFind' => function($metaTemplate) use ($templatesUpdateStatus) {
+ if (!empty($templatesUpdateStatus[$metaTemplate->uuid])) {
+ $templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templatesUpdateStatus[$metaTemplate->uuid]['template'], $metaTemplate);
+ $metaTemplate->set('updateStatus', $this->MetaTemplates->computeFullUpdateStatusForMetaTemplate($templateStatus, $metaTemplate));
+ }
+ return $metaTemplate;
+ }
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
+ $updateableTemplates = [
+ 'not-up-to-date' => $this->MetaTemplates->getNotUpToDateTemplates(),
+ 'can-be-removed' => $this->MetaTemplates->getCanBeRemovedTemplates(),
+ 'new' => $this->MetaTemplates->getNewTemplates(),
+ ];
$this->set('defaultTemplatePerScope', $this->MetaTemplates->getDefaultTemplatePerScope());
$this->set('alignmentScope', 'individuals');
- $this->set('metaGroup', 'Administration');
+ $this->set('updateableTemplates', $updateableTemplates);
}
public function view($id)
@@ -72,7 +292,25 @@ class MetaTemplatesController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'Administration');
+ $this->setUpdateStatus($id);
+ }
+
+ public function delete($id)
+ {
+ $metaTemplate = $this->MetaTemplates->get($id, [
+ 'contain' => ['MetaTemplateFields']
+ ]);
+ $templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid);
+ $templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
+ if (empty($templateStatus['can-be-removed'])) {
+ throw new MethodNotAllowedException(__('This meta-template cannot be removed'));
+ }
+ $this->set('deletionText', __('The meta-template "{0}" has no meta-field and can be safely removed.', h($templateStatus['existing_template']->name)));
+ $this->CRUD->delete($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
}
public function toggle($id, $fieldName = 'enabled')
@@ -89,4 +327,35 @@ class MetaTemplatesController extends AppController
return $responsePayload;
}
}
+
+ private function getUpdateStatus($id): array
+ {
+ $metaTemplate = $this->MetaTemplates->get($id, [
+ 'contain' => ['MetaTemplateFields']
+ ]);
+ $templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid);
+ $templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
+ return $templateStatus;
+ }
+
+ /**
+ * Retreive the template stored on disk and compute the status for the provided template id.
+ *
+ * @param [type] $id
+ * @return array
+ */
+ private function setUpdateStatus($template_id): array
+ {
+ $metaTemplate = $this->MetaTemplates->get($template_id, [
+ 'contain' => ['MetaTemplateFields']
+ ]);
+ $templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid);
+ $templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
+ $this->set('templateOnDisk', $templateOnDisk);
+ $this->set('templateStatus', $templateStatus);
+ return [
+ 'templateOnDisk' => $templateOnDisk,
+ 'templateStatus' => $templateStatus,
+ ];
+ }
}
diff --git a/src/Controller/Open/IndividualsController.php b/src/Controller/Open/IndividualsController.php
index fa75aaa..28cd51d 100644
--- a/src/Controller/Open/IndividualsController.php
+++ b/src/Controller/Open/IndividualsController.php
@@ -6,6 +6,7 @@ use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
+use Cake\Http\Exception\BadRequestException;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php
index a45f5f3..7f2ce6d 100644
--- a/src/Controller/OrganisationsController.php
+++ b/src/Controller/OrganisationsController.php
@@ -16,6 +16,7 @@ class OrganisationsController extends AppController
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 $statisticsFields = ['nationality', 'sector'];
public function index()
{
@@ -59,7 +60,8 @@ class OrganisationsController extends AppController
]
],
],
- 'contain' => $this->containFields
+ 'contain' => $this->containFields,
+ 'statisticsFields' => $this->statisticsFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
diff --git a/src/Controller/SharingGroupsController.php b/src/Controller/SharingGroupsController.php
index c8f8f79..cf4c9b0 100644
--- a/src/Controller/SharingGroupsController.php
+++ b/src/Controller/SharingGroupsController.php
@@ -16,16 +16,21 @@ class SharingGroupsController extends AppController
public function index()
{
+ $currentUser = $this->ACL->getUser();
+ $conditions = [];
+ if (empty($currentUser['role']['perm_admin'])) {
+ $conditions['SharingGroups.organisation_id'] = $currentUser['organisation_id'];
+ }
$this->CRUD->index([
'contain' => $this->containFields,
'filters' => $this->filterFields,
- 'quickFilters' => $this->quickFilterFields
+ 'quickFilters' => $this->quickFilterFields,
+ 'conditions' => $conditions
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'Trust Circles');
}
public function add()
@@ -43,7 +48,6 @@ class SharingGroupsController extends AppController
return $responsePayload;
}
$this->set(compact('dropdownData'));
- $this->set('metaGroup', 'Trust Circles');
}
public function view($id)
@@ -55,12 +59,16 @@ class SharingGroupsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'Trust Circles');
}
public function edit($id = false)
{
- $this->CRUD->edit($id);
+ $params = [];
+ $currentUser = $this->ACL->getUser();
+ if (empty($currentUser['role']['perm_admin'])) {
+ $params['conditions'] = ['organisation_id' => $currentUser['organisation_id']];
+ }
+ $this->CRUD->edit($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@@ -69,7 +77,6 @@ class SharingGroupsController extends AppController
'organisation' => $this->getAvailableOrgForSg($this->ACL->getUser())
];
$this->set(compact('dropdownData'));
- $this->set('metaGroup', 'Trust Circles');
$this->render('add');
}
@@ -80,7 +87,6 @@ class SharingGroupsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('metaGroup', 'Trust Circles');
}
public function addOrg($id)
@@ -206,11 +212,11 @@ class SharingGroupsController extends AppController
$organisations = [];
if (!empty($user['role']['perm_admin'])) {
$organisations = $this->SharingGroups->Organisations->find('list')->order(['name' => 'ASC'])->toArray();
- } else if (!empty($user['individual']['organisations'])) {
+ } else {
$organisations = $this->SharingGroups->Organisations->find('list', [
'sort' => ['name' => 'asc'],
'conditions' => [
- 'id IN' => array_values(\Cake\Utility\Hash::extract($user, 'individual.organisations.{n}.id'))
+ 'id' => $user['organisation_id']
]
]);
}
diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php
new file mode 100644
index 0000000..d28f6ca
--- /dev/null
+++ b/src/Controller/UserSettingsController.php
@@ -0,0 +1,251 @@
+ true], ['value' => true]];
+ public $filterFields = ['name', 'value', 'Users.id'];
+ public $containFields = ['Users'];
+
+ public function index()
+ {
+ $conditions = [];
+ $currentUser = $this->ACL->getUser();
+ if (empty($currentUser['role']['perm_admin'])) {
+ $conditions['user_id'] = $currentUser->id;
+ }
+ $this->CRUD->index([
+ 'conditions' => $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)
+ {
+ if (!$this->isLoggedUserAllowedToEdit($id)) {
+ throw new NotFoundException(__('Invalid {0}.', 'user setting'));
+ }
+ $this->CRUD->view($id, [
+ 'contain' => ['Users']
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function add($user_id = false)
+ {
+ $currentUser = $this->ACL->getUser();
+ $this->CRUD->add([
+ 'redirect' => ['action' => 'index', $user_id],
+ 'beforeSave' => function ($data) use ($currentUser) {
+ if (empty($currentUser['role']['perm_admin'])) {
+ $data['user_id'] = $currentUser->id;
+ }
+ return $data;
+ }
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ $allUsers = $this->UserSettings->Users->find('list', ['keyField' => 'id', 'valueField' => 'username'])->order(['username' => 'ASC']);
+ if (empty($currentUser['role']['perm_admin'])) {
+ $allUsers->where(['id' => $currentUser->id]);
+ $user_id = $currentUser->id;
+ }
+ $dropdownData = [
+ 'user' => $allUsers->all()->toArray(),
+ ];
+ $this->set(compact('dropdownData'));
+ $this->set('user_id', $user_id);
+ }
+
+ public function edit($id)
+ {
+ $entity = $this->UserSettings->find()->where([
+ 'id' => $id
+ ])->first();
+
+ if (!$this->isLoggedUserAllowedToEdit($entity)) {
+ throw new NotFoundException(__('Invalid {0}.', 'user setting'));
+ }
+
+ $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)
+ {
+ if (!$this->isLoggedUserAllowedToEdit($id)) {
+ throw new NotFoundException(__('Invalid {0}.', 'user setting'));
+ }
+ $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);
+ }
+
+ /**
+ * isLoggedUserAllowedToEdit
+ *
+ * @param int|\App\Model\Entity\UserSetting $setting
+ * @return boolean
+ */
+ private function isLoggedUserAllowedToEdit($setting): bool
+ {
+ $currentUser = $this->ACL->getUser();
+ $isAllowed = false;
+ if (!empty($currentUser['role']['perm_admin'])) {
+ $isAllowed = true;
+ } else {
+ if (is_numeric($setting)) {
+ $setting = $this->UserSettings->find()->where([
+ 'id' => $setting
+ ])->first();
+ if (empty($setting)) {
+ return false;
+ }
+ }
+ $isAllowed = $setting->user_id == $currentUser->id;
+ }
+ return $isAllowed;
+ }
+}
diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php
index f3059b2..9ffb2fe 100644
--- a/src/Controller/UsersController.php
+++ b/src/Controller/UsersController.php
@@ -6,19 +6,28 @@ use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
+use Cake\Http\Exception\UnauthorizedException;
+use Cake\Http\Exception\MethodNotAllowedException;
+use Cake\Core\Configure;
class UsersController extends AppController
{
- public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name'];
+ public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name'];
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
- public $containFields = ['Individuals', 'Roles'];
+ public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations'];
public function index()
{
+ $currentUser = $this->ACL->getUser();
+ $conditions = [];
+ if (empty($currentUser['role']['perm_admin'])) {
+ $conditions['organisation_id'] = $currentUser['organisation_id'];
+ }
$this->CRUD->index([
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
+ 'conditions' => $conditions
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@@ -29,8 +38,12 @@ class UsersController extends AppController
public function add()
{
+ $currentUser = $this->ACL->getUser();
$this->CRUD->add([
- 'beforeSave' => function($data) {
+ 'beforeSave' => function($data) use ($currentUser) {
+ if (!$currentUser['role']['perm_admin']) {
+ $data['organisation_id'] = $currentUser['organisation_id'];
+ }
$this->Users->enrollUserRouter($data);
return $data;
}
@@ -39,12 +52,28 @@ class UsersController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
+ /*
+ $alignments = $this->Users->Individuals->Alignments->find('list', [
+ //'keyField' => 'id',
+ 'valueField' => 'organisation_id',
+ 'groupField' => 'individual_id'
+ ])->toArray();
+ $alignments = array_map(function($value) { return array_values($value); }, $alignments);
+ */
+ $org_conditions = [];
+ if (empty($currentUser['role']['perm_admin'])) {
+ $org_conditions = ['id' => $currentUser['organisation_id']];
+ }
$dropdownData = [
'role' => $this->Users->Roles->find('list', [
'sort' => ['name' => 'asc']
]),
'individual' => $this->Users->Individuals->find('list', [
'sort' => ['email' => 'asc']
+ ]),
+ 'organisation' => $this->Users->Organisations->find('list', [
+ 'sort' => ['name' => 'asc'],
+ 'conditions' => $org_conditions
])
];
$this->set(compact('dropdownData'));
@@ -57,7 +86,7 @@ class UsersController extends AppController
$id = $this->ACL->getUser()['id'];
}
$this->CRUD->view($id, [
- 'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles']
+ 'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations']
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@@ -68,9 +97,18 @@ class UsersController extends AppController
public function edit($id = false)
{
- if (empty($id) || empty($this->ACL->getUser()['role']['perm_admin'])) {
- $id = $this->ACL->getUser()['id'];
+ $currentUser = $this->ACL->getUser();
+ if (empty($id)) {
+ $id = $currentUser['id'];
+ } else {
+ $id = intval($id);
+ if ((empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
+ if ($id !== $currentUser['id']) {
+ throw new MethodNotAllowedException(__('You are not authorised to edit that user.'));
+ }
+ }
}
+
$params = [
'get' => [
'fields' => [
@@ -81,11 +119,15 @@ class UsersController extends AppController
'password'
],
'fields' => [
- 'id', 'individual_id', 'username', 'disabled', 'password', 'confirm_password'
+ 'password', 'confirm_password'
]
];
if (!empty($this->ACL->getUser()['role']['perm_admin'])) {
+ $params['fields'][] = 'individual_id';
+ $params['fields'][] = 'username';
$params['fields'][] = 'role_id';
+ $params['fields'][] = 'organisation_id';
+ $params['fields'][] = 'disabled';
}
$this->CRUD->edit($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
@@ -98,6 +140,9 @@ class UsersController extends AppController
]),
'individual' => $this->Users->Individuals->find('list', [
'sort' => ['email' => 'asc']
+ ]),
+ 'organisation' => $this->Users->Organisations->find('list', [
+ 'sort' => ['name' => 'asc']
])
];
$this->set(compact('dropdownData'));
@@ -128,11 +173,27 @@ class UsersController extends AppController
{
$result = $this->Authentication->getResult();
// If the user is logged in send them away.
+ $logModel = $this->Users->auditLogs();
if ($result->isValid()) {
+ $user = $logModel->userInfo();
+ $logModel->insert([
+ 'request_action' => 'login',
+ 'model' => 'Users',
+ 'model_id' => $user['id'],
+ 'model_title' => $user['name'],
+ 'changed' => []
+ ]);
$target = $this->Authentication->getLoginRedirect() ?? '/instance/home';
return $this->redirect($target);
}
if ($this->request->is('post') && !$result->isValid()) {
+ $logModel->insert([
+ 'request_action' => 'login_fail',
+ 'model' => 'Users',
+ 'model_id' => 0,
+ 'model_title' => 'unknown_user',
+ 'changed' => []
+ ]);
$this->Flash->error(__('Invalid username or password'));
}
$this->viewBuilder()->setLayout('login');
@@ -142,27 +203,54 @@ class UsersController extends AppController
{
$result = $this->Authentication->getResult();
if ($result->isValid()) {
+ $logModel = $this->Users->auditLogs();
+ $user = $logModel->userInfo();
+ $logModel->insert([
+ 'request_action' => 'logout',
+ 'model' => 'Users',
+ 'model_id' => $user['id'],
+ 'model_title' => $user['name'],
+ 'changed' => []
+ ]);
$this->Authentication->logout();
$this->Flash->success(__('Goodbye.'));
return $this->redirect(\Cake\Routing\Router::url('/users/login'));
}
}
+ 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');
}
}
diff --git a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php
index 179a238..d01e9d9 100644
--- a/src/Lib/default/local_tool_connectors/CommonConnectorTools.php
+++ b/src/Lib/default/local_tool_connectors/CommonConnectorTools.php
@@ -2,6 +2,8 @@
namespace CommonConnectorTools;
use Cake\ORM\Locator\LocatorAwareTrait;
+use Cake\Log\Log;
+use Cake\Log\Engine\FileLog;
class CommonConnectorTools
{
@@ -20,6 +22,38 @@ class CommonConnectorTools
const STATE_CANCELLED = 'Request cancelled';
const STATE_DECLINED = 'Request declined by remote';
+ public function __construct()
+ {
+ if (empty(Log::getConfig("LocalToolDebug{$this->connectorName}"))) {
+ Log::setConfig("LocalToolDebug{$this->connectorName}", [
+ 'className' => FileLog::class,
+ 'path' => LOGS,
+ 'file' => "{$this->connectorName}-debug",
+ 'scopes' => [$this->connectorName],
+ 'levels' => ['notice', 'info', 'debug'],
+ ]);
+ }
+ if (empty(Log::getConfig("LocalToolError{$this->connectorName}"))) {
+ Log::setConfig("LocalToolError{$this->connectorName}", [
+ 'className' => FileLog::class,
+ 'path' => LOGS,
+ 'file' => "{$this->connectorName}-error",
+ 'scopes' => [$this->connectorName],
+ 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
+ ]);
+ }
+ }
+
+ protected function logDebug($message)
+ {
+ Log::debug($message, [$this->connectorName]);
+ }
+
+ protected function logError($message, $scope=[])
+ {
+ Log::error($message, [$this->connectorName]);
+ }
+
public function addExposedFunction(string $functionName): void
{
$this->exposedFunctions[] = $functionName;
diff --git a/src/Lib/default/local_tool_connectors/MispConnector.php b/src/Lib/default/local_tool_connectors/MispConnector.php
index e84f082..1ffc98f 100644
--- a/src/Lib/default/local_tool_connectors/MispConnector.php
+++ b/src/Lib/default/local_tool_connectors/MispConnector.php
@@ -122,6 +122,11 @@ class MispConnector extends CommonConnectorTools
'type' => 'boolean'
],
];
+ public $settingsPlaceholder = [
+ 'url' => 'https://your.misp.intance',
+ 'authkey' => '',
+ 'skip_ssl' => '0',
+ ];
public function addSettingValidatorRules($validator)
{
@@ -183,6 +188,7 @@ class MispConnector extends CommonConnectorTools
$settings = json_decode($connection->settings, true);
$http = $this->genHTTPClient($connection, $options);
$url = sprintf('%s%s', $settings['url'], $relativeURL);
+ $this->logDebug(sprintf('%s %s %s', __('Posting data') . PHP_EOL, "POST {$url}" . PHP_EOL, json_encode($data)));
return $http->post($url, $data, $options);
}
@@ -234,14 +240,18 @@ class MispConnector extends CommonConnectorTools
if (!empty($params['softError'])) {
return $response;
}
- throw new NotFoundException(__('Could not retrieve the requested resource.'));
+ $errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
+ $this->logError($errorMsg);
+ throw new NotFoundException($errorMsg);
}
}
private function postData(string $url, array $params): Response
{
if (empty($params['connection'])) {
- throw new NotFoundException(__('No connection object received.'));
+ $errorMsg = __('No connection object received.');
+ $this->logError($errorMsg);
+ throw new NotFoundException($errorMsg);
}
$url = $this->urlAppendParams($url, $params);
if (!is_string($params['body'])) {
@@ -251,7 +261,9 @@ class MispConnector extends CommonConnectorTools
if ($response->isOk()) {
return $response;
} else {
- throw new NotFoundException(__('Could not post to the requested resource. Remote returned:') . PHP_EOL . $response->getStringBody());
+ $errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
+ $this->logError($errorMsg);
+ throw new NotFoundException($errorMsg);
}
}
@@ -302,61 +314,62 @@ class MispConnector extends CommonConnectorTools
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'additionalUrlParams' => $urlParams
]
]
+ ]
+ ],
+ 'fields' => [
+ [
+ 'name' => 'Setting',
+ 'sort' => 'setting',
+ 'data_path' => 'setting',
],
- 'fields' => [
- [
- 'name' => 'Setting',
- 'sort' => 'setting',
- 'data_path' => 'setting',
+ [
+ 'name' => 'Criticality',
+ 'sort' => 'level',
+ 'data_path' => 'level',
+ 'arrayData' => [
+ 0 => 'Critical',
+ 1 => 'Recommended',
+ 2 => 'Optional'
],
- [
- 'name' => 'Criticality',
- 'sort' => 'level',
- 'data_path' => 'level',
- 'arrayData' => [
- 0 => 'Critical',
- 1 => 'Recommended',
- 2 => 'Optional'
- ],
- 'element' => 'array_lookup_field'
- ],
- [
- 'name' => __('Value'),
- 'sort' => 'value',
- 'data_path' => 'value',
- 'options' => 'options'
- ],
- [
- 'name' => __('Type'),
- 'sort' => 'type',
- 'data_path' => 'type',
- ],
- [
- 'name' => __('Error message'),
- 'sort' => 'errorMessage',
- 'data_path' => 'errorMessage',
- ]
+ 'element' => 'array_lookup_field'
],
- 'title' => false,
- 'description' => false,
- 'pull' => 'right',
- 'actions' => [
- [
- 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}',
- 'modal_params_data_path' => ['setting'],
- 'icon' => 'download',
- 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction'
- ]
+ [
+ 'name' => __('Value'),
+ 'sort' => 'value',
+ 'data_path' => 'value',
+ 'options' => 'options'
+ ],
+ [
+ 'name' => __('Type'),
+ 'sort' => 'type',
+ 'data_path' => 'type',
+ ],
+ [
+ 'name' => __('Error message'),
+ 'sort' => 'errorMessage',
+ 'data_path' => 'errorMessage',
+ ]
+ ],
+ 'title' => false,
+ 'description' => false,
+ 'pull' => 'right',
+ 'actions' => [
+ [
+ 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}',
+ 'modal_params_data_path' => ['setting'],
+ 'icon' => 'download',
+ 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction'
]
]
- ];
+ ]
+ ];
if (!empty($params['quickFilter'])) {
$needle = strtolower($params['quickFilter']);
foreach ($finalSettings['data']['data'] as $k => $v) {
@@ -392,7 +405,7 @@ class MispConnector extends CommonConnectorTools
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
@@ -482,7 +495,7 @@ class MispConnector extends CommonConnectorTools
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
@@ -558,7 +571,7 @@ class MispConnector extends CommonConnectorTools
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
@@ -626,7 +639,7 @@ class MispConnector extends CommonConnectorTools
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
@@ -819,7 +832,7 @@ class MispConnector extends CommonConnectorTools
[
'field' => 'connection_ids',
'type' => 'hidden',
- 'value' => $params['connection_ids']
+ 'value' => json_encode($params['connection_ids'])
],
[
'field' => 'method',
diff --git a/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php b/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php
index ce9b62d..eefbd7a 100644
--- a/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php
+++ b/src/Lib/default/local_tool_connectors/SkeletonConnectorExample.php
@@ -46,6 +46,22 @@ class SkeletonConnector extends CommonConnectorTools
'redirect' => 'serverSettingsAction'
]
];
+ public $settings = [
+ 'url' => [
+ 'type' => 'text'
+ ],
+ 'authkey' => [
+ 'type' => 'text'
+ ],
+ 'skip_ssl' => [
+ 'type' => 'boolean'
+ ],
+ ];
+ public $settingsPlaceholder = [
+ 'url' => 'https://your.url',
+ 'authkey' => '',
+ 'skip_ssl' => '0',
+ ];
public function health(Object $connection): array
{
@@ -87,7 +103,7 @@ class SkeletonConnector extends CommonConnectorTools
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
diff --git a/src/Model/Behavior/AuditLogBehavior.php b/src/Model/Behavior/AuditLogBehavior.php
new file mode 100644
index 0000000..55df5e8
--- /dev/null
+++ b/src/Model/Behavior/AuditLogBehavior.php
@@ -0,0 +1,213 @@
+ true,
+ 'lastpushedid' => true,
+ 'timestamp' => true,
+ 'revision' => true,
+ 'modified' => true,
+ 'date_modified' => true, // User
+ 'current_login' => true, // User
+ 'last_login' => true, // User
+ 'newsread' => true, // User
+ 'proposal_email_lock' => true, // Event
+ ];
+
+ public function initialize(array $config): void
+ {
+ $this->config = $config;
+ }
+
+ public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ $fields = $entity->extract($entity->getVisible(), true);
+ $skipFields = $this->skipFields;
+ $fieldsToFetch = array_filter($fields, function($key) use ($skipFields) {
+ return strpos($key, '_') !== 0 && !isset($skipFields[$key]);
+ }, ARRAY_FILTER_USE_KEY);
+ // Do not fetch old version when just few fields will be fetched
+ $fieldToFetch = [];
+ if (!empty($options['fieldList'])) {
+ foreach ($options['fieldList'] as $field) {
+ if (!isset($this->skipFields[$field])) {
+ $fieldToFetch[] = $field;
+ }
+ }
+ if (empty($fieldToFetch)) {
+ $this->old = null;
+ return true;
+ }
+ }
+ if ($entity->id) {
+ $this->old = $this->_table->find()->where(['id' => $entity->id])->contain($fieldToFetch)->first();
+ } else {
+ $this->old = null;
+ }
+ return true;
+ }
+
+ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ if ($entity->id) {
+ $id = $entity->id;
+ } else {
+ $id = null;
+ }
+
+ if ($entity->isNew()) {
+ $action = $entity->getConstant('ACTION_ADD');
+ } else {
+ $action = $entity->getConstant('ACTION_EDIT');
+ if (isset($entity['deleted'])) {
+ if ($entity['deleted']) {
+ $action = $entity->getConstant('ACTION_SOFT_DELETE');
+ } else if (!$entity['deleted'] && $this->old['deleted']) {
+ $action = $entity->getConstant('ACTION_UNDELETE');
+ }
+ }
+ }
+ $changedFields = $this->changedFields($entity, isset($options['fieldList']) ? $options['fieldList'] : null);
+ if (empty($changedFields)) {
+ return;
+ }
+
+ $modelTitleField = $this->_table->getDisplayField();
+ if (is_callable($modelTitleField)) {
+ $modelTitle = $modelTitleField($entity, isset($this->old) ? $this->old : []);
+ } else if (isset($entity[$modelTitleField])) {
+ $modelTitle = $entity[$modelTitleField];
+ } else if ($this->old[$modelTitleField]) {
+ $modelTitle = $this->old[$modelTitleField];
+ }
+ $this->auditLogs()->insert([
+ 'request_action' => $action,
+ 'model' => $entity->getSource(),
+ 'model_id' => $id,
+ 'model_title' => $modelTitle,
+ 'changed' => $changedFields
+ ]);
+ }
+
+ public function beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ $this->old = $this->_table->find()->where(['id' => $entity->id])->first();
+ return true;
+ }
+
+ public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ $modelTitleField = $this->_table->getDisplayField();
+ if (is_callable($modelTitleField)) {
+ $modelTitle = $modelTitleField($entity, []);
+ } else if (isset($entity[$modelTitleField])) {
+ $modelTitle = $entity[$modelTitleField];
+ }
+
+ $this->auditLogs()->insert([
+ 'request_action' => $entity->getConstant('ACTION_DELETE'),
+ 'model' => $entity->getSource(),
+ 'model_id' => $this->old->id,
+ 'model_title' => $modelTitle,
+ 'changed' => $this->changedFields($entity)
+ ]);
+ }
+
+ /**
+ * @param Model $model
+ * @param array|null $fieldsToSave
+ * @return array
+ */
+ private function changedFields(EntityInterface $entity, $fieldsToSave = null)
+ {
+ $dbFields = $this->_table->getSchema()->typeMap();
+ $changedFields = [];
+ foreach ($entity->extract($entity->getVisible()) as $key => $value) {
+ if (isset($this->skipFields[$key])) {
+ continue;
+ }
+ if (!isset($dbFields[$key])) {
+ continue;
+ }
+ if ($fieldsToSave && !in_array($key, $fieldsToSave, true)) {
+ continue;
+ }
+ if (isset($entity[$key]) && isset($this->old[$key])) {
+ $old = $this->old[$key];
+ } else {
+ $old = null;
+ }
+ // Normalize
+ if (is_bool($old)) {
+ $old = $old ? 1 : 0;
+ }
+ if (is_bool($value)) {
+ $value = $value ? 1 : 0;
+ }
+ $dbType = $dbFields[$key];
+ if ($dbType === 'integer' || $dbType === 'tinyinteger' || $dbType === 'biginteger' || $dbType === 'boolean') {
+ $value = (int)$value;
+ if ($old !== null) {
+ $old = (int)$old;
+ }
+ }
+ if ($value == $old) {
+ continue;
+ }
+ if ($key === 'password' || $key === 'authkey') {
+ $value = '*****';
+ if ($old !== null) {
+ $old = $value;
+ }
+ }
+
+ if ($old === null) {
+ $changedFields[$key] = $value;
+ } else {
+ $changedFields[$key] = [$old, $value];
+ }
+ }
+ return $changedFields;
+ }
+
+ /**
+ * @return AuditLogs
+ */
+ public function auditLogs()
+ {
+ if ($this->AuditLogs === null) {
+ $this->AuditLogs = TableRegistry::getTableLocator()->get('AuditLogs');
+ }
+ return $this->AuditLogs;
+ }
+
+ public function log()
+ {
+
+ }
+}
diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php
index d42a8c9..12bb3e2 100644
--- a/src/Model/Behavior/AuthKeycloakBehavior.php
+++ b/src/Model/Behavior/AuthKeycloakBehavior.php
@@ -98,7 +98,7 @@ class AuthKeycloakBehavior extends Behavior
{
$individual = $this->_table->Individuals->find()->where(
['id' => $data['individual_id']]
- )->contain(['Organisations'])->first();
+ )->first();
$roleConditions = [
'id' => $data['role_id']
];
@@ -106,10 +106,9 @@ class AuthKeycloakBehavior extends Behavior
$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'];
- }
+ $org = $this->_table->Organisations->find()->where([
+ ['id' => $data['organisation_id']]
+ ])->first();
$token = $this->getAdminAccessToken();
$keyCloakUser = [
'firstName' => $individual['first_name'],
@@ -118,7 +117,7 @@ class AuthKeycloakBehavior extends Behavior
'email' => $individual['email'],
'attributes' => [
'role_name' => empty($role['name']) ? Configure::read('keycloak.default_role_name') : $role['name'],
- 'org_uuid' => empty($orgs[0]) ? '' : $orgs[0]
+ 'org_uuid' => $org['uuid']
]
];
$keycloakConfig = Configure::read('keycloak');
diff --git a/src/Model/Behavior/MetaFieldsBehavior.php b/src/Model/Behavior/MetaFieldsBehavior.php
new file mode 100644
index 0000000..f558981
--- /dev/null
+++ b/src/Model/Behavior/MetaFieldsBehavior.php
@@ -0,0 +1,231 @@
+ [
+ 'className' => 'MetaFields',
+ 'foreignKey' => 'parent_id',
+ 'bindingKey' => 'id',
+ 'dependent' => true,
+ 'cascadeCallbacks' => true,
+ 'saveStrategy' => 'append',
+ 'propertyName' => 'meta_fields',
+ ],
+ 'modelAssoc' => [
+ 'foreignKey' => 'parent_id',
+ 'bindingKey' => 'id',
+ ],
+ 'metaTemplateFieldCounter' => ['counter'],
+
+ 'implementedEvents' => [
+ 'Model.beforeMarshal' => 'beforeMarshal',
+ 'Model.beforeFind' => 'beforeFind',
+ 'Model.beforeSave' => 'beforeSave',
+ ],
+ 'implementedMethods' => [
+ 'normalizeMetafields' => 'normalizeMetafields',
+ 'buildMetaFieldQuerySnippetForMatchingParent' => 'buildQuerySnippetForMatchingParent',
+ ],
+ 'implementedFinders' => [
+ 'metafieldValue' => 'findMetafieldValue',
+ ],
+ ];
+
+ private $aliasScope = null;
+
+ public function initialize(array $config): void
+ {
+ $this->bindAssociations();
+ $this->_metaTemplateFieldTable = $this->_table;
+ $this->_metaTemplateTable = $this->_table;
+ }
+
+ public function getScope()
+ {
+ if (is_null($this->aliasScope)) {
+ $this->aliasScope = Inflector::underscore(Inflector::singularize($this->_table->getAlias()));
+ }
+ return $this->aliasScope;
+
+ }
+
+ public function bindAssociations()
+ {
+ $config = $this->getConfig();
+ $metaFieldsAssoc = $config['metaFieldsAssoc'];
+ $modelAssoc = $config['modelAssoc'];
+
+ $table = $this->_table;
+ $tableAlias = $this->_table->getAlias();
+
+ $assocConditions = [
+ 'MetaFields.scope' => $this->getScope()
+ ];
+ if (!$table->hasAssociation('MetaFields')) {
+ $table->hasMany('MetaFields', array_merge(
+ $metaFieldsAssoc,
+ [
+ 'conditions' => $assocConditions
+ ]
+ ));
+ }
+
+ if (!$table->MetaFields->hasAssociation($tableAlias)) {
+ $table->MetaFields->belongsTo($tableAlias, array_merge(
+ $modelAssoc,
+ [
+ 'className' => get_class($table),
+ ]
+ ));
+ }
+ }
+
+ public function beforeMarshal($event, $data, $options)
+ {
+ $property = $this->getConfig('metaFieldsAssoc.propertyName');
+ $options['accessibleFields'][$property] = true;
+ $options['associated']['MetaFields']['accessibleFields']['id'] = true;
+
+ if (isset($data[$property])) {
+ if (!empty($data[$property])) {
+ $data[$property] = $this->normalizeMetafields($data[$property]);
+ }
+ }
+ }
+
+ public function beforeSave($event, $entity, $options)
+ {
+ if (empty($entity->metaFields)) {
+ return;
+ }
+ }
+
+ public function normalizeMetafields($metaFields)
+ {
+ return $metaFields;
+ }
+
+ /**
+ * Usage:
+ * $this->{$model}->find('metaFieldValue', [
+ * ['meta_template_id' => 1, 'field' => 'email', 'value' => '%@domain.test'],
+ * ['meta_template_id' => 1, 'field' => 'country_code', 'value' => '!LU'],
+ * ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+2'],
+ * ])
+ * $this->{$model}->find('metaFieldValue', [
+ * 'AND' => [
+ * ['meta_template_id' => 1, 'field' => 'email', 'value' => '%@domain.test'],
+ * 'OR' => [
+ * ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+1'],
+ * ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+2'],
+ * ],
+ * ],
+ * ])
+ */
+ public function findMetafieldValue(Query $query, array $filters)
+ {
+ $conditions = $this->buildQuerySnippetForMatchingParent($filters);
+ $query->where($conditions);
+ return $query;
+ }
+
+ public function buildQuerySnippetForMatchingParent(array $filters): array
+ {
+ if (empty($filters)) {
+ return [];
+ }
+ if (count(array_filter(array_keys($filters), 'is_string'))) {
+ $filters = [$filters];
+ }
+ $conjugatedFilters = $this->buildConjugatedFilters($filters);
+ $conditions = $this->buildConjugatedQuerySnippet($conjugatedFilters);
+ return $conditions;
+ }
+
+ protected function buildConjugatedFilters(array $filters): array
+ {
+ $conjugatedFilters = [];
+ foreach ($filters as $operator => $subFilters) {
+ if (is_numeric($operator)) {
+ $conjugatedFilters[] = $subFilters;
+ } else {
+ if (!empty($subFilters)) {
+ $conjugatedFilters[$operator] = $this->buildConjugatedFilters($subFilters);
+ }
+ }
+ }
+ return $conjugatedFilters;
+ }
+
+ protected function buildConjugatedQuerySnippet(array $conjugatedFilters, string $parentOperator='AND'): array
+ {
+ $conditions = [];
+ if (empty($conjugatedFilters['AND']) && empty($conjugatedFilters['OR'])) {
+ if (count(array_filter(array_keys($conjugatedFilters), 'is_string')) > 0) {
+ $conditions = $this->buildComposedQuerySnippet([$conjugatedFilters]);
+ } else {
+ $conditions = $this->buildComposedQuerySnippet($conjugatedFilters, $parentOperator);
+ }
+ } else {
+ foreach ($conjugatedFilters as $subOperator => $subFilter) {
+ $conditions[$subOperator] = $this->buildConjugatedQuerySnippet($subFilter, $subOperator);
+ }
+ }
+ return $conditions;
+ }
+
+ protected function buildComposedQuerySnippet(array $filters, string $operator='AND'): array
+ {
+ $conditions = [];
+ foreach ($filters as $filterOperator => $filter) {
+ $subQuery = $this->buildQuerySnippet($filter, true);
+ $modelAlias = $this->_table->getAlias();
+ $conditions[$operator][] = [$modelAlias . '.id IN' => $subQuery];
+ }
+ return $conditions;
+ }
+
+
+ protected function getQueryExpressionForField(QueryExpression $exp, string $field, string $value): QueryExpression
+ {
+ if (substr($value, 0, 1) == '!') {
+ $value = substr($value, 1);
+ $exp->notEq($field, $value);
+ } else if (strpos($value, '%') !== false) {
+ $exp->like($field, $value);
+ } else {
+ $exp->eq($field, $value);
+ }
+ return $exp;
+ }
+
+ protected function buildQuerySnippet(array $filter): Query
+ {
+ $whereClosure = function (QueryExpression $exp) use ($filter) {
+ foreach ($filter as $column => $value) {
+ $keyedColumn = 'MetaFields.' . $column;
+ $this->getQueryExpressionForField($exp, $keyedColumn, $value);
+ }
+ return $exp;
+ };
+
+ $foreignKey = $this->getConfig('modelAssoc.foreignKey');
+ $query = $this->_table->MetaFields->find()
+ ->select('MetaFields.' . $foreignKey)
+ ->where($whereClosure);
+ return $query;
+ }
+
+}
diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php
index 84c86b3..50bfe86 100644
--- a/src/Model/Entity/AppModel.php
+++ b/src/Model/Entity/AppModel.php
@@ -6,6 +6,28 @@ use Cake\ORM\Entity;
class AppModel extends Entity
{
+ const BROTLI_HEADER = "\xce\xb2\xcf\x81";
+ const BROTLI_MIN_LENGTH = 200;
+
+ const ACTION_ADD = 'add',
+ ACTION_EDIT = 'edit',
+ ACTION_SOFT_DELETE = 'soft_delete',
+ ACTION_DELETE = 'delete',
+ ACTION_UNDELETE = 'undelete',
+ ACTION_TAG = 'tag',
+ ACTION_TAG_LOCAL = 'tag_local',
+ ACTION_REMOVE_TAG = 'remove_tag',
+ ACTION_REMOVE_TAG_LOCAL = 'remove_local_tag',
+ ACTION_LOGIN = 'login',
+ ACTION_LOGIN_FAIL = 'login_fail',
+ ACTION_LOGOUT = 'logout';
+
+
+ public function getConstant($name)
+ {
+ return constant('self::' . $name);
+ }
+
public function getAccessibleFieldForNew(): array
{
return $this->_accessibleOnNew ?? [];
diff --git a/src/Model/Entity/AuditLog.php b/src/Model/Entity/AuditLog.php
new file mode 100644
index 0000000..74e2f41
--- /dev/null
+++ b/src/Model/Entity/AuditLog.php
@@ -0,0 +1,68 @@
+compressionEnabled = Configure::read('Cerebrate.log_compress') && function_exists('brotli_compress');
+ parent::__construct($properties, $options);
+ }
+
+ protected function _getTitle(): String
+ {
+ return $this->generateUserFriendlyTitle($this);
+ }
+
+ /**
+ * @param string $change
+ * @return array|string
+ * @throws JsonException
+ */
+ private function decodeChange($change)
+ {
+ if (substr($change, 0, 4) === self::BROTLI_HEADER) {
+ if (function_exists('brotli_uncompress')) {
+ $change = brotli_uncompress(substr($change, 4));
+ if ($change === false) {
+ return 'Compressed';
+ }
+ } else {
+ return 'Compressed';
+ }
+ }
+ return json_decode($change, true);
+ }
+
+ /**
+ * @param array $auditLog
+ * @return string
+ */
+ public function generateUserFriendlyTitle($auditLog)
+ {
+ if (in_array($auditLog['request_action'], [self::ACTION_TAG, self::ACTION_TAG_LOCAL, self::ACTION_REMOVE_TAG, self::ACTION_REMOVE_TAG_LOCAL], true)) {
+ $attached = ($auditLog['request_action'] === self::ACTION_TAG || $auditLog['request_action'] === self::ACTION_TAG_LOCAL);
+ $local = ($auditLog['request_action'] === self::ACTION_TAG_LOCAL || $auditLog['request_action'] === self::ACTION_REMOVE_TAG_LOCAL) ? __('local') : __('global');
+ if ($attached) {
+ return __('Attached %s tag "%s" to %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']);
+ } else {
+ return __('Detached %s tag "%s" from %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']);
+ }
+ }
+
+
+ $title = "{$auditLog['model']} #{$auditLog['model_id']}";
+
+ if (isset($auditLog['model_title']) && $auditLog['model_title']) {
+ $title .= ": {$auditLog['model_title']}";
+ }
+ return $title;
+ }
+}
diff --git a/src/Model/Entity/Individual.php b/src/Model/Entity/Individual.php
index a14e339..87398e2 100644
--- a/src/Model/Entity/Individual.php
+++ b/src/Model/Entity/Individual.php
@@ -16,4 +16,27 @@ class Individual extends AppModel
protected $_accessibleOnNew = [
'uuid' => true,
];
+
+ protected $_virtual = ['full_name', 'alternate_emails'];
+
+ 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);
+ }
+
+ protected function _getAlternateEmails()
+ {
+ $emails = [];
+ if (!empty($this->meta_fields)) {
+ foreach ($this->meta_fields as $metaField) {
+ if (str_contains($metaField->field, 'email')) {
+ $emails[] = $metaField;
+ }
+ }
+ }
+ return $emails;
+ }
}
diff --git a/src/Model/Entity/MailingList.php b/src/Model/Entity/MailingList.php
new file mode 100644
index 0000000..02ff121
--- /dev/null
+++ b/src/Model/Entity/MailingList.php
@@ -0,0 +1,51 @@
+ true,
+ 'id' => false,
+ 'uuid' => false,
+ 'user_id' => false,
+ ];
+
+ protected $_accessibleOnNew = [
+ 'uuid' => true,
+ 'user_id' => true,
+ ];
+
+ private $metaFieldsByParentId = [];
+
+ public function injectRegisteredEmailsIntoIndividuals()
+ {
+ if (empty($this->individuals)) {
+ return;
+ }
+ if (!empty($this->meta_fields)) {
+ foreach ($this->meta_fields as $meta_field) {
+ $this->metaFieldsByParentId[$meta_field->parent_id][] = $meta_field;
+ }
+ }
+ foreach ($this->individuals as $i => $individual) {
+ $this->individuals[$i]->mailinglist_emails = $this->collectEmailsForMailingList($individual);
+ }
+ }
+
+ protected function collectEmailsForMailingList($individual)
+ {
+ $emails = [];
+ if (!empty($individual['_joinData']) && !empty($individual['_joinData']['include_primary_email'])) {
+ $emails[] = $individual->email;
+ }
+ if (!empty($this->metaFieldsByParentId[$individual->id])) {
+ foreach ($this->metaFieldsByParentId[$individual->id] as $metaField) {
+ $emails[] = $metaField->value;
+ }
+ }
+ return $emails;
+ }
+}
diff --git a/src/Model/Entity/MetaTemplateField.php b/src/Model/Entity/MetaTemplateField.php
new file mode 100644
index 0000000..7baf30a
--- /dev/null
+++ b/src/Model/Entity/MetaTemplateField.php
@@ -0,0 +1,11 @@
+ true,
'id' => false,
- 'uuid' => false,
- ];
-
- protected $_accessibleOnNew = [
- 'uuid' => true,
];
}
diff --git a/src/Model/Entity/Outbox.php b/src/Model/Entity/Outbox.php
new file mode 100644
index 0000000..51304e6
--- /dev/null
+++ b/src/Model/Entity/Outbox.php
@@ -0,0 +1,11 @@
+user_settings)) {
+ foreach ($this->user_settings as $i => $setting) {
+ $settingsByName[$setting->name] = $setting;
+ }
+ }
+ return $settingsByName;
+ }
+
+ protected function _getUserSettingsByNameWithFallback()
+ {
+ if (!isset($this->SettingsProvider)) {
+ $this->SettingsProvider = new UserSettingsProvider();
+ }
+ $settingsByNameWithFallback = [];
+ if (!empty($this->user_settings)) {
+ foreach ($this->user_settings as $i => $setting) {
+ $settingsByNameWithFallback[$setting->name] = $setting->value;
+ }
+ }
+ $settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settingsByNameWithFallback);
+ $settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
+ return $settingsFlattened;
+ }
+
protected function _setPassword(string $password) : ?string
{
if (strlen($password) > 0) {
diff --git a/src/Model/Entity/UserSetting.php b/src/Model/Entity/UserSetting.php
new file mode 100644
index 0000000..e5d12f3
--- /dev/null
+++ b/src/Model/Entity/UserSetting.php
@@ -0,0 +1,9 @@
+belongsTo('Individuals');
+ $this->addBehavior('AuditLog');
$this->belongsTo('Organisations');
$this->addBehavior('Timestamp');
}
diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php
index 4164456..f3443f6 100644
--- a/src/Model/Table/AppTable.php
+++ b/src/Model/Table/AppTable.php
@@ -7,6 +7,9 @@ use Cake\Validation\Validator;
use Cake\Core\Configure;
use Cake\Core\Configure\Engine\PhpConfig;
use Cake\ORM\TableRegistry;
+use Cake\Utility\Hash;
+use Cake\Database\Expression\QueryExpression;
+use Cake\ORM\Query;
class AppTable extends Table
{
@@ -14,6 +17,142 @@ class AppTable extends Table
{
}
+ public function getStatisticsUsageForModel(Object $table, array $scopes, array $options=[]): array
+ {
+ $defaultOptions = [
+ 'limit' => 5,
+ 'includeOthers' => true,
+ 'ignoreNull' => true,
+ ];
+ $options = $this->getOptions($defaultOptions, $options);
+ $stats = [];
+ foreach ($scopes as $scope) {
+ $queryTopUsage = $table->find();
+ $queryTopUsage
+ ->select([
+ $scope,
+ 'count' => $queryTopUsage->func()->count('id'),
+ ]);
+ if ($queryTopUsage->getDefaultTypes()[$scope] != 'boolean') {
+ $queryTopUsage->where(function (QueryExpression $exp) use ($scope) {
+ return $exp
+ ->isNotNull($scope)
+ ->notEq($scope, '');
+ });
+ }
+ $queryTopUsage
+ ->group($scope)
+ ->order(['count' => 'DESC'])
+ ->limit($options['limit'])
+ ->page(1)
+ ->enableHydration(false);
+ $topUsage = $queryTopUsage->toList();
+ $stats[$scope] = $topUsage;
+ if (
+ !empty($options['includeOthers']) && !empty($topUsage) &&
+ $queryTopUsage->getDefaultTypes()[$scope] != 'boolean' // No need to get others as we only have 2 possibilities already considered
+ ) {
+ $queryOthersUsage = $table->find();
+ $queryOthersUsage
+ ->select([
+ 'count' => $queryOthersUsage->func()->count('id'),
+ ])
+ ->where(function (QueryExpression $exp, Query $query) use ($topUsage, $scope, $options) {
+ if (!empty($options['ignoreNull'])) {
+ return $exp
+ ->isNotNull($scope)
+ ->notEq($scope, '')
+ ->notIn($scope, Hash::extract($topUsage, "{n}.{$scope}"));
+ } else {
+ return $exp->or([
+ $query->newExpr()->isNull($scope),
+ $query->newExpr()->eq($scope, ''),
+ $query->newExpr()->notIn($scope, Hash::extract($topUsage, "{n}.{$scope}")),
+ ]);
+ }
+ })
+ ->enableHydration(false);
+ $othersUsage = $queryOthersUsage->toList();
+ if (!empty($othersUsage)) {
+ $stats[$scope][] = [
+ $scope => __('Others'),
+ 'count' => $othersUsage[0]['count'],
+ ];
+ }
+ }
+ }
+ return $stats;
+ }
+
+ private function getOptions($defaults=[], $options=[]): array
+ {
+ return array_merge($defaults, $options);
+ }
+
+ // Move this into a tool
+ public function getActivityStatisticsForModel(Object $table, int $days = 30): array
+ {
+ $statistics = [];
+ if ($table->hasBehavior('Timestamp')) {
+ if ($table->getSchema()->getColumnType('created') == 'datetime') {
+ $statistics['created'] = $this->getActivityStatistic($table, $days, 'created');
+ }
+ if ($table->getSchema()->getColumnType('modified') == 'datetime') {
+ $statistics['modified'] = $this->getActivityStatistic($table, $days, 'modified');
+ }
+ }
+ return $statistics;
+ }
+
+ public function getActivityStatistic(Object $table, int $days = 30, string $field = 'modified', bool $includeTimeline = true): array
+ {
+ $statistics = [];
+ $statistics['days'] = $days;
+ $statistics['amount'] = $table->find()->all()->count();
+ if ($table->behaviors()->has('Timestamp') && $includeTimeline) {
+ $statistics['timeline'] = $this->buildTimeline($table, $days, $field);
+ $statistics['variation'] = $table->find()->where(["{$field} >" => new \DateTime("-{$days} days")])->all()->count();
+ } else {
+ $statistics['timeline'] = [];
+ $statistics['variation'] = 0;
+ }
+ return $statistics;
+ }
+
+ public function buildTimeline(Object $table, int $days = 30, string $field = 'modified'): array
+ {
+ $timeline = [];
+ $authorizedFields = ['modified', 'created'];
+ if ($table->behaviors()->has('Timestamp')) {
+ if (!in_array($field, $authorizedFields)) {
+ throw new MethodNotAllowedException(__('Cannot construct timeline for field `{0}`', $field));
+ }
+ $days = $days - 1;
+ $query = $table->find();
+ $query->select([
+ 'count' => $query->func()->count('id'),
+ 'date' => "DATE({$field})",
+ ])
+ ->where(["{$field} >" => new \DateTime("-{$days} days")])
+ ->group(['date'])
+ ->order(['date']);
+ $data = $query->all()->toArray();
+ $interval = new \DateInterval('P1D');
+ $period = new \DatePeriod(new \DateTime("-{$days} days"), $interval, (new \DateTime())->modify( '+1 day' ));
+ 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;
+ }
+ $timeline = array_values($timeline);
+ }
+ return $timeline;
+ }
+
public function saveMetaFields($id, $input)
{
$this->MetaFields = TableRegistry::getTableLocator()->get('MetaFields');
diff --git a/src/Model/Table/AuditLogsTable.php b/src/Model/Table/AuditLogsTable.php
new file mode 100644
index 0000000..e6f6f95
--- /dev/null
+++ b/src/Model/Table/AuditLogsTable.php
@@ -0,0 +1,244 @@
+addBehavior('Timestamp');
+ $this->belongsTo('Users');
+ $this->compressionEnabled = Configure::read('Cerebrate.log_new_audit_compress') && function_exists('brotli_compress');
+ }
+
+ public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
+ {
+ if (!isset($data['request_ip'])) {
+ $ipHeader = 'REMOTE_ADDR';
+ if (isset($_SERVER[$ipHeader])) {
+ $data['request_ip'] = $_SERVER[$ipHeader];
+ } else {
+ $data['request_ip'] = '127.0.0.1';
+ }
+ }
+ foreach (['user_id', 'request_type', 'authkey_id'] as $field) {
+ if (!isset($data[$field])) {
+ if (!isset($userInfo)) {
+ $userInfo = $this->userInfo();
+ }
+ if (!empty($userInfo[$field])) {
+ $data[$field] = $userInfo[$field];
+ } else {
+ $data[$field] = 0;
+ }
+ }
+ }
+
+ if (!isset($data['request_id'] ) && isset($_SERVER['HTTP_X_REQUEST_ID'])) {
+ $data['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'];
+ }
+
+ // Truncate request_id
+ if (isset($data['request_id']) && strlen($data['request_id']) > 255) {
+ $data['request_id'] = substr($data['request_id'], 0, 255);
+ }
+
+ // Truncate model title
+ if (isset($data['model_title']) && mb_strlen($data['model_title']) > 255) {
+ $data['model_title'] = mb_substr($data['model_title'], 0, 252) . '...';
+ }
+
+ if (isset($data['changed'])) {
+ $changed = json_encode($data['changed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ if ($this->compressionEnabled && strlen($changed) >= self::BROTLI_MIN_LENGTH) {
+ $changed = self::BROTLI_HEADER . brotli_compress($changed, 4, BROTLI_TEXT);
+ }
+ $data['changed'] = $changed;
+ }
+ }
+
+ public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ $entity->request_ip = inet_pton($entity->request_ip);
+ $this->logData($entity);
+ return true;
+ }
+
+ /**
+ * @param array $data
+ * @return bool
+ */
+ private function logData(EntityInterface $entity)
+ {
+ if (Configure::read('Plugin.ZeroMQ_enable') && Configure::read('Plugin.ZeroMQ_audit_notifications_enable')) {
+ $pubSubTool = $this->getPubSubTool();
+ $pubSubTool->publish($data, 'audit', 'log');
+ }
+
+ //$this->publishKafkaNotification('audit', $data, 'log');
+
+ if (Configure::read('Plugin.ElasticSearch_logging_enable')) {
+ // send off our logs to distributed /dev/null
+ $logIndex = Configure::read("Plugin.ElasticSearch_log_index");
+ $elasticSearchClient = $this->getElasticSearchTool();
+ $elasticSearchClient->pushDocument($logIndex, "log", $data);
+ }
+
+ // write to syslogd as well if enabled
+ if ($this->syslog === null) {
+ if (Configure::read('Security.syslog')) {
+ $options = [];
+ $syslogToStdErr = Configure::read('Security.syslog_to_stderr');
+ if ($syslogToStdErr !== null) {
+ $options['to_stderr'] = $syslogToStdErr;
+ }
+ $syslogIdent = Configure::read('Security.syslog_ident');
+ if ($syslogIdent) {
+ $options['ident'] = $syslogIdent;
+ }
+ $this->syslog = new SysLog($options);
+ } else {
+ $this->syslog = false;
+ }
+ }
+ if ($this->syslog) {
+ $entry = $data['request_action'];
+ $title = $entity->generateUserFriendlyTitle();
+ if ($title) {
+ $entry .= " -- $title";
+ }
+ $this->syslog->write('info', $entry);
+ }
+ return true;
+ }
+
+ /**
+ * @return array
+ */
+ public function userInfo()
+ {
+ if ($this->user !== null) {
+ return $this->user;
+ }
+
+ $this->user = ['id' => 0, /*'org_id' => 0, */'authkey_id' => 0, 'request_type' => self::REQUEST_TYPE_DEFAULT, 'name' => ''];
+
+ $isShell = (php_sapi_name() === 'cli');
+ if ($isShell) {
+ // do not start session for shell commands and fetch user info from configuration
+ $this->user['request_type'] = self::REQUEST_TYPE_CLI;
+ $currentUserId = Configure::read('CurrentUserId');
+ if (!empty($currentUserId)) {
+ $this->user['id'] = $currentUserId;
+ $userFromDb = $this->Users->find()->where(['id' => $currentUserId])->first();
+ $this->user['name'] = $userFromDb['name'];
+ $this->user['org_id'] = $userFromDb['org_id'];
+ }
+ } else {
+ $authUser = Router::getRequest()->getSession()->read('authUser');
+ if (!empty($authUser)) {
+ $this->user['id'] = $authUser['id'];
+ $this->user['user_id'] = $authUser['id'];
+ $this->user['name'] = $authUser['name'];
+ //$this->user['org_id'] = $authUser['org_id'];
+ if (isset($authUser['logged_by_authkey']) && $authUser['logged_by_authkey']) {
+ $this->user['request_type'] = self::REQUEST_TYPE_API;
+ }
+ if (isset($authUser['authkey_id'])) {
+ $this->user['authkey_id'] = $authUser['authkey_id'];
+ }
+ }
+ }
+ return $this->user;
+ }
+
+ public function insert(array $data)
+ {
+ $logEntity = $this->newEntity($data);
+ if ($logEntity->getErrors()) {
+ throw new Exception($logEntity->getErrors());
+ } else {
+ $this->save($logEntity);
+ }
+ }
+
+ /**
+ * @param string|int $org
+ * @return array
+ */
+ public function returnDates($org = 'all')
+ {
+ $conditions = [];
+ if ($org !== 'all') {
+ $org = $this->Organisation->fetchOrg($org);
+ if (empty($org)) {
+ throw new NotFoundException('Invalid organisation.');
+ }
+ $conditions['org_id'] = $org['id'];
+ }
+
+ $dataSource = ConnectionManager::getDataSource('default')->config['datasource'];
+ if ($dataSource === 'Database/Mysql' || $dataSource === 'Database/MysqlObserver') {
+ $validDates = $this->find('all', [
+ 'recursive' => -1,
+ 'fields' => ['DISTINCT UNIX_TIMESTAMP(DATE(created)) AS Date', 'count(id) AS count'],
+ 'conditions' => $conditions,
+ 'group' => ['Date'],
+ 'order' => ['Date'],
+ ]);
+ } elseif ($dataSource === 'Database/Postgres') {
+ if (!empty($conditions['org_id'])) {
+ $condOrg = sprintf('WHERE org_id = %s', intval($conditions['org_id']));
+ } else {
+ $condOrg = '';
+ }
+ $sql = 'SELECT DISTINCT EXTRACT(EPOCH FROM CAST(created AS DATE)) AS "Date", COUNT(id) AS count
+ FROM audit_logs
+ ' . $condOrg . '
+ GROUP BY "Date" ORDER BY "Date"';
+ $validDates = $this->query($sql);
+ }
+ $data = [];
+ foreach ($validDates as $date) {
+ $data[(int)$date[0]['Date']] = (int)$date[0]['count'];
+ }
+ return $data;
+ }
+}
diff --git a/src/Model/Table/AuthKeysTable.php b/src/Model/Table/AuthKeysTable.php
index 3663af9..f5336e6 100644
--- a/src/Model/Table/AuthKeysTable.php
+++ b/src/Model/Table/AuthKeysTable.php
@@ -19,10 +19,11 @@ class AuthKeysTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
+ $this->addBehavior('AuditLog');
$this->belongsTo(
'Users'
);
- $this->setDisplayField('authkey');
+ $this->setDisplayField('comment');
}
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php
index e1993b4..b0d3dca 100644
--- a/src/Model/Table/BroodsTable.php
+++ b/src/Model/Table/BroodsTable.php
@@ -19,6 +19,7 @@ class BroodsTable extends AppTable
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
+ $this->addBehavior('AuditLog');
$this->BelongsTo(
'Organisations'
);
@@ -278,7 +279,7 @@ class BroodsTable extends AppTable
}
return $jsonReply;
}
-
+
/**
* handleSendingFailed - Handle the case if the request could not be sent or if the remote rejected the connection request
*
@@ -302,7 +303,7 @@ class BroodsTable extends AppTable
];
return $creationResult;
}
-
+
/**
* handleMessageNotCreated - Handle the case if the request was sent but the remote brood did not save the message in the inbox
*
diff --git a/src/Model/Table/EncryptionKeysTable.php b/src/Model/Table/EncryptionKeysTable.php
index 23b4867..2008e0d 100644
--- a/src/Model/Table/EncryptionKeysTable.php
+++ b/src/Model/Table/EncryptionKeysTable.php
@@ -14,6 +14,7 @@ class EncryptionKeysTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
+ $this->addBehavior('AuditLog');
$this->addBehavior('Timestamp');
$this->belongsTo(
'Individuals',
diff --git a/src/Model/Table/InboxTable.php b/src/Model/Table/InboxTable.php
index 716d246..18faf47 100644
--- a/src/Model/Table/InboxTable.php
+++ b/src/Model/Table/InboxTable.php
@@ -18,14 +18,8 @@ class InboxTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
- $this->addBehavior('Timestamp', [
- 'events' => [
- 'Model.beforeSave' => [
- 'created' => 'new'
- ]
- ]
- ]);
-
+ $this->addBehavior('Timestamp');
+ $this->addBehavior('AuditLog');
$this->belongsTo('Users');
$this->setDisplayField('title');
}
@@ -74,7 +68,7 @@ class InboxTable extends AppTable
if (empty($brood)) {
$errors[] = __('Unkown brood `{0}`', $entryData['data']['cerebrateURL']);
}
-
+
// $found = false;
// foreach ($user->individual->organisations as $organisations) {
// if ($organisations->id == $brood->organisation_id) {
diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php
index f0ba07f..0fba61f 100644
--- a/src/Model/Table/IndividualsTable.php
+++ b/src/Model/Table/IndividualsTable.php
@@ -8,14 +8,15 @@ use Cake\Validation\Validator;
class IndividualsTable extends AppTable
{
- public $metaFields = 'individual';
-
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
+ $this->addBehavior('MetaFields');
+ $this->addBehavior('AuditLog');
+
$this->hasMany(
'Alignments',
[
@@ -36,6 +37,10 @@ class IndividualsTable extends AppTable
$this->belongsToMany('Organisations', [
'through' => 'Alignments',
]);
+ $this->belongsToMany('MailingLists', [
+ 'through' => 'mailing_lists_individuals',
+ ]);
+
$this->setDisplayField('email');
}
diff --git a/src/Model/Table/InstanceTable.php b/src/Model/Table/InstanceTable.php
index 0ec2a53..0832f4d 100644
--- a/src/Model/Table/InstanceTable.php
+++ b/src/Model/Table/InstanceTable.php
@@ -18,6 +18,8 @@ class InstanceTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
+ $this->addBehavior('AuditLog');
+ $this->setDisplayField('name');
}
public function validationDefault(Validator $validator): Validator
@@ -25,43 +27,12 @@ class InstanceTable extends AppTable
return $validator;
}
- public function getStatistics($days=30): array
+ public function getStatistics(int $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;
- }
+ $statistics[$model] = $this->getActivityStatisticsForModel($table, $days);
}
return $statistics;
}
@@ -69,6 +40,18 @@ class InstanceTable extends AppTable
public function searchAll($value, $limit=5, $model=null)
{
$results = [];
+
+ // search in metafields. FIXME: To be replaced by the meta-template system
+ $metaFieldTable = TableRegistry::get('MetaFields');
+ $query = $metaFieldTable->find()->where([
+ 'value LIKE' => '%' . $value . '%'
+ ]);
+ $results['MetaFields']['amount'] = $query->count();
+ $result = $query->limit($limit)->all()->toList();
+ if (!empty($result)) {
+ $results['MetaFields']['entries'] = $result;
+ }
+
$models = $this->seachAllTables;
if (!is_null($model)) {
if (in_array($model, $this->seachAllTables)) {
@@ -81,12 +64,13 @@ class InstanceTable extends AppTable
$controller = $this->getController($tableName);
$table = TableRegistry::get($tableName);
$query = $table->find();
- $quickFilterOptions = $this->getQuickFiltersFieldsFromController($controller);
+ $quickFilters = $this->getQuickFiltersFieldsFromController($controller);
$containFields = $this->getContainFieldsFromController($controller);
- if (empty($quickFilterOptions)) {
+ if (empty($quickFilters)) {
continue; // make sure we are filtering on something
}
$params = ['quickFilter' => $value];
+ $quickFilterOptions = ['quickFilters' => $quickFilters];
$query = $controller->CRUD->setQuickFilters($params, $query, $quickFilterOptions);
if (!empty($containFields)) {
$query->contain($containFields);
diff --git a/src/Model/Table/LocalToolsTable.php b/src/Model/Table/LocalToolsTable.php
index 8f2bfea..f764ace 100644
--- a/src/Model/Table/LocalToolsTable.php
+++ b/src/Model/Table/LocalToolsTable.php
@@ -30,6 +30,7 @@ class LocalToolsTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
+ $this->addBehavior('AuditLog');
$this->addBehavior('Timestamp');
}
@@ -142,7 +143,8 @@ class LocalToolsTable extends AppTable
'connector' => $connector_type,
'connector_version' => $connector_class->version,
'connector_description' => $connector_class->description,
- 'connector_settings' => $connector_class->settings ?? []
+ 'connector_settings' => $connector_class->settings ?? [],
+ 'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [],
];
if ($includeConnections) {
$connector['connections'] = $this->healthCheck($connector_type, $connector_class);
@@ -287,6 +289,7 @@ class LocalToolsTable extends AppTable
return $jsonReply;
}
+/*
public function findConnectable($local_tool): array
{
$connectors = $this->getInterconnectors($local_tool['connector']);
@@ -296,8 +299,8 @@ class LocalToolsTable extends AppTable
$validTargets[$connector['connects'][1]] = 1;
}
}
-
}
+*/
public function fetchConnection($id): object
{
diff --git a/src/Model/Table/MailingListsTable.php b/src/Model/Table/MailingListsTable.php
new file mode 100644
index 0000000..bb8f641
--- /dev/null
+++ b/src/Model/Table/MailingListsTable.php
@@ -0,0 +1,42 @@
+addBehavior('UUID');
+ $this->addBehavior('Timestamp');
+ $this->belongsTo(
+ 'Users'
+ );
+
+ $this->belongsToMany('Individuals', [
+ 'joinTable' => 'mailing_lists_individuals',
+ ]);
+ // Change to HasMany?
+ $this->belongsToMany('MetaFields');
+
+ $this->setDisplayField('name');
+ }
+
+ public function validationDefault(Validator $validator): Validator
+ {
+ $validator
+ ->requirePresence(['name', 'releasability'], 'create');
+ return $validator;
+ }
+
+ public function buildRules(RulesChecker $rules): RulesChecker
+ {
+ return $rules;
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php
index ba4cc66..6fffc93 100644
--- a/src/Model/Table/MetaFieldsTable.php
+++ b/src/Model/Table/MetaFieldsTable.php
@@ -5,6 +5,7 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
+use Cake\ORM\RulesChecker;
class MetaFieldsTable extends AppTable
{
@@ -12,9 +13,16 @@ class MetaFieldsTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
- $this->setDisplayField('field');
+ $this->addBehavior('Timestamp');
+ $this->addBehavior('CounterCache', [
+ 'MetaTemplateFields' => ['counter']
+ ]);
+
+ $this->addBehavior('AuditLog');
$this->belongsTo('MetaTemplates');
$this->belongsTo('MetaTemplateFields');
+
+ $this->setDisplayField('field');
}
public function validationDefault(Validator $validator): Validator
@@ -28,7 +36,50 @@ class MetaFieldsTable extends AppTable
->notEmptyString('meta_template_field_id')
->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create');
- // add validation regex
+ $validator->add('value', 'validMetaField', [
+ 'rule' => 'isValidMetaField',
+ 'message' => __('The provided value doesn\'t satisfy the validation defined by the meta-fields\'s meta-template'),
+ 'provider' => 'table',
+ ]);
+
return $validator;
}
+
+ public function isValidMetaField($value, array $context)
+ {
+ $metaFieldsTable = $context['providers']['table'];
+ $entityData = $context['data'];
+ $metaTemplateField = $metaFieldsTable->MetaTemplateFields->get($entityData['meta_template_field_id']);
+ return $this->isValidMetaFieldForMetaTemplateField($value, $metaTemplateField);
+ }
+
+ public function isValidMetaFieldForMetaTemplateField($value, $metaTemplateField)
+ {
+ $typeValid = $this->isValidType($value, $metaTemplateField['type']);
+ if ($typeValid !== true) {
+ return $typeValid;
+ }
+ if (!empty($metaTemplateField['regex'])) {
+ return $this->isValidRegex($value, $metaTemplateField);
+ }
+ return true;
+ }
+
+ public function isValidType($value, string $type)
+ {
+ if (empty($value)) {
+ return __('Metafield value cannot be empty.');
+ }
+ return true;
+ }
+
+ public function isValidRegex($value, $metaTemplateField)
+ {
+
+ $re = $metaTemplateField['regex'];
+ if (!preg_match("/^$re$/m", $value)) {
+ return __('Metafield value `{0}` for `{1}` doesn\'t pass regex validation', $value, $metaTemplateField['field']);
+ }
+ return true;
+ }
}
diff --git a/src/Model/Table/MetaTemplateFieldsTable.php b/src/Model/Table/MetaTemplateFieldsTable.php
index 453690a..39d0565 100644
--- a/src/Model/Table/MetaTemplateFieldsTable.php
+++ b/src/Model/Table/MetaTemplateFieldsTable.php
@@ -11,21 +11,30 @@ class MetaTemplateFieldsTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
+
$this->BelongsTo(
'MetaTemplates'
);
$this->hasMany('MetaFields');
+
$this->setDisplayField('field');
}
+ public function beforeSave($event, $entity, $options)
+ {
+ if (empty($entity->meta_template_id)) {
+ $event->stopPropagation();
+ $event->setResult(false);
+ return;
+ }
+ }
+
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('field')
->notEmptyString('type')
- ->numeric('meta_template_id')
- ->notBlank('meta_template_id')
- ->requirePresence(['meta_template_id', 'field', 'type'], 'create');
+ ->requirePresence(['field', 'type'], 'create');
return $validator;
}
}
diff --git a/src/Model/Table/MetaTemplatesTable.php b/src/Model/Table/MetaTemplatesTable.php
index 9e01ccd..de8a5c7 100644
--- a/src/Model/Table/MetaTemplatesTable.php
+++ b/src/Model/Table/MetaTemplatesTable.php
@@ -4,11 +4,29 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
+use Cake\ORM\TableRegistry;
use Cake\Validation\Validator;
+use Cake\Utility\Hash;
+use Cake\Utility\Inflector;
+use Cake\Utility\Text;
+use Cake\Filesystem\File;
+use Cake\Filesystem\Folder;
class MetaTemplatesTable extends AppTable
{
- public $metaFields = true;
+ public const TEMPLATE_PATH = [
+ ROOT . '/libraries/default/meta_fields/',
+ ROOT . '/libraries/custom/meta_fields/'
+ ];
+
+ public const UPDATE_STRATEGY_CREATE_NEW = 'create_new';
+ public const UPDATE_STRATEGY_UPDATE_EXISTING = 'update_existing';
+ public const UPDATE_STRATEGY_KEEP_BOTH = 'keep_both';
+ public const UPDATE_STRATEGY_DELETE = 'delete_all';
+
+ public $ALLOWED_STRATEGIES = [MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW];
+
+ private $templatesOnDisk = null;
public function initialize(array $config): void
{
@@ -17,7 +35,10 @@ class MetaTemplatesTable extends AppTable
$this->hasMany(
'MetaTemplateFields',
[
- 'foreignKey' => 'meta_template_id'
+ 'foreignKey' => 'meta_template_id',
+ 'saveStrategy' => 'replace',
+ 'dependent' => true,
+ 'cascadeCallbacks' => true,
]
);
$this->setDisplayField('name');
@@ -36,40 +57,670 @@ class MetaTemplatesTable extends AppTable
return $validator;
}
- public function update()
+ public function isStrategyAllowed(string $strategy): bool
{
- $paths = [
- ROOT . '/libraries/default/meta_fields/',
- ROOT . '/libraries/custom/meta_fields/'
- ];
+ return in_array($strategy, $this->ALLOWED_STRATEGIES);
+ }
+
+ // /**
+ // * Load the template stored on the disk for the provided id and update it using the optional strategy.
+ // *
+ // * @param int $template_id
+ // * @param string|null $strategy The strategy to be used when updating templates with conflicts
+ // * @return array The update result containing potential errors and the successes
+ // */
+ // public function update($template_id, $strategy = null): array
+ // {
+ // $files_processed = [];
+ // $readErrors = [];
+ // $preUpdateChecks = [];
+ // $updatesErrors = [];
+ // $templates = $this->readTemplatesFromDisk($readErrors);
+ // foreach ($templates as $template) {
+ // $updateStatus = $this->getUpdateStatusForTemplates($template['uuid']);
+ // $preUpdateChecks[$template['uuid']] = $updateStatus;
+ // if (is_null($template_uuid) || $template_uuid == $template['uuid']) {
+ // $errors = [];
+ // $success = false;
+ // if ($updateStatus['up-to-date']) {
+ // $errors['message'] = __('Meta-template already up-to-date');
+ // $success = true;
+ // } else if ($this->isStrategyAllowed(MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) && $updateStatus['new']) {
+ // $success = $this->saveNewMetaTemplate($template, $errors);
+ // } else if ($this->isStrategyAllowed(MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING) && $updateStatus['automatically-updateable']) {
+ // $success = $this->updateMetaTemplate($template, $errors);
+ // } else if (!$updateStatus['up-to-date'] && (is_null($strategy) || !$this->isStrategyAllowed($strategy))) {
+ // $errors['message'] = __('Cannot update meta-template, update strategy not provided or not allowed');
+ // } else if (!$updateStatus['up-to-date'] && !is_null($strategy)) {
+ // $success = $this->updateMetaTemplateWithStrategyRouter($template, $strategy, $errors);
+ // } else {
+ // $errors['message'] = __('Could not update. Something went wrong.');
+ // }
+ // if ($success) {
+ // $files_processed[] = $template['uuid'];
+ // }
+ // if (!empty($errors)) {
+ // $updatesErrors[] = $errors;
+ // }
+ // }
+ // }
+ // $results = [
+ // 'read_errors' => $readErrors,
+ // 'pre_update_errors' => $preUpdateChecks,
+ // 'update_errors' => $updatesErrors,
+ // 'files_processed' => $files_processed,
+ // 'success' => !empty($files_processed),
+ // ];
+ // return $results;
+ // }
+
+ /**
+ * Load the templates stored on the disk update and create them in the database without touching at the existing ones
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return array The update result containing potential errors and the successes
+ */
+ public function updateAllTemplates(): array
+ {
+ $updatesErrors = [];
$files_processed = [];
- foreach ($paths as $path) {
+ $templatesOnDisk = $this->readTemplatesFromDisk();
+ $templatesUpdateStatus = $this->getUpdateStatusForTemplates();
+ foreach ($templatesOnDisk as $template) {
+ $errors = [];
+ $success = false;
+ $updateStatus = $templatesUpdateStatus[$template['uuid']];
+ if ($this->isStrategyAllowed(MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) && $updateStatus['new']) {
+ $success = $this->saveNewMetaTemplate($template, $errors);
+ }
+ if ($success) {
+ $files_processed[] = $template['uuid'];
+ } else {
+ $updatesErrors[] = $errors;
+ }
+ }
+ $results = [
+ 'update_errors' => $updatesErrors,
+ 'files_processed' => $files_processed,
+ 'success' => !empty($files_processed),
+ ];
+ return $results;
+ }
+
+ /**
+ * Load the template stored on the disk for the provided meta-template and update it using the optional strategy.
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @param string|null $strategy The strategy to be used when updating templates with conflicts
+ * @return array The update result containing potential errors and the successes
+ */
+ public function update($metaTemplate, $strategy = null): array
+ {
+ $files_processed = [];
+ $updatesErrors = [];
+ $templateOnDisk = $this->readTemplateFromDisk($metaTemplate->uuid);
+ $templateStatus = $this->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
+ $updateStatus = $this->computeFullUpdateStatusForMetaTemplate($templateStatus, $metaTemplate);
+ $errors = [];
+ $success = false;
+ if ($updateStatus['up-to-date']) {
+ $errors['message'] = __('Meta-template already up-to-date');
+ $success = true;
+ } else if ($this->isStrategyAllowed(MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) && $updateStatus['new']) {
+ $success = $this->saveNewMetaTemplate($templateOnDisk, $errors);
+ } else if ($this->isStrategyAllowed(MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING) && $updateStatus['automatically-updateable']) {
+ $success = $this->updateMetaTemplate($metaTemplate, $templateOnDisk, $errors);
+ } else if (!$updateStatus['up-to-date'] && (is_null($strategy) || !$this->isStrategyAllowed($strategy))) {
+ $errors['message'] = __('Cannot update meta-template, update strategy not provided or not allowed');
+ } else if (!$updateStatus['up-to-date'] && !is_null($strategy)) {
+ $success = $this->updateMetaTemplateWithStrategyRouter($metaTemplate, $templateOnDisk, $strategy, $errors);
+ } else {
+ $errors['message'] = __('Could not update. Something went wrong.');
+ }
+ if ($success) {
+ $files_processed[] = $templateOnDisk['uuid'];
+ }
+ if (!empty($errors)) {
+ $updatesErrors[] = $errors;
+ }
+ $results = [
+ 'update_errors' => $updatesErrors,
+ 'files_processed' => $files_processed,
+ 'success' => !empty($files_processed),
+ ];
+ return $results;
+ }
+
+ /**
+ * Load the templates stored on the disk update and create the one having the provided UUID in the database
+ * Will do nothing if the UUID is already known
+ *
+ * @param string $uuid
+ * @return array The update result containing potential errors and the successes
+ */
+ public function createNewTemplate(string $uuid): array
+ {
+ $templateOnDisk = $this->readTemplateFromDisk($uuid);
+ $templateStatus = $this->getUpdateStatusForTemplate($templateOnDisk);
+ $errors = [];
+ $updatesErrors = [];
+ $files_processed = [];
+ $savedMetaTemplate = null;
+ $success = false;
+ if (empty($templateStatus['new'])) {
+ $error['message'] = __('Template UUID already exists');
+ $success = true;
+ } else if ($this->isStrategyAllowed(MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW)) {
+ $success = $this->saveNewMetaTemplate($templateOnDisk, $errors, $savedMetaTemplate);
+ } else {
+ $errors['message'] = __('Could not create template. Something went wrong.');
+ }
+ if ($success) {
+ $files_processed[] = $templateOnDisk['uuid'];
+ }
+ if (!empty($errors)) {
+ $updatesErrors[] = $errors;
+ }
+ $results = [
+ 'update_errors' => $updatesErrors,
+ 'files_processed' => $files_processed,
+ 'success' => !empty($files_processed),
+ ];
+ return $results;
+ }
+
+ /**
+ * Load the templates stored on the disk and compute their update status.
+ * Only compute the result if an UUID is provided
+ *
+ * @param string|null $template_uuid
+ * @return array
+ */
+ public function getUpdateStatusForTemplates(): array
+ {
+ $errors = [];
+ $templateUpdatesStatus = [];
+ $templates = $this->readTemplatesFromDisk($errors);
+ foreach ($templates as $template) {
+ $templateUpdatesStatus[$template['uuid']] = $this->getUpdateStatusForTemplate($template);
+ }
+ return $templateUpdatesStatus;
+ }
+
+
+ /**
+ * Checks if the template is update-to-date from the provided update status
+ *
+ * @param array $updateStatus
+ * @return boolean
+ */
+ public function isUpToDate(array $updateStatus): bool
+ {
+ return !empty($updateStatus['up-to-date']) || !empty($updateStatus['new']);
+ }
+
+ /**
+ * Checks if the template is updateable automatically from the provided update status
+ *
+ * @param array $updateStatus
+ * @return boolean
+ */
+ public function isAutomaticallyUpdateable(array $updateStatus): bool
+ {
+ return !empty($updateStatus['automatically-updateable']);
+ }
+
+ /**
+ * Checks if the template is new (and not loaded in the database yet) from the provided update status
+ *
+ * @param array $updateStatus
+ * @return boolean
+ */
+ public function isNew(array $updateStatus): bool
+ {
+ return $updateStatus['new'];
+ }
+
+ /**
+ * Checks if the template has no conflicts that would prevent an automatic update from the provided update status
+ *
+ * @param array $updateStatus
+ * @return boolean
+ */
+ public function hasNoConflict(array $updateStatus): bool
+ {
+ return $this->hasConflict($updateStatus);
+ }
+
+ /**
+ * Checks if the template has conflict preventing an automatic update from the provided update status
+ *
+ * @param array $updateStatus
+ * @return boolean
+ */
+ public function hasConflict(array $updateStatus): bool
+ {
+ return empty($updateStatus['automatically-updateable']) && empty($updateStatus['up-to-date']) && empty($updateStatus['new']);
+ }
+
+ /**
+ * Checks if the metaTemplate can be updated to a newer version loaded in the database
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return boolean
+ */
+ public function isUpdateableToExistingMetaTemplate(\App\Model\Entity\MetaTemplate $metaTemplate): bool
+ {
+ $newestTemplate = $this->getNewestVersion($metaTemplate);
+ return !empty($newestTemplate);
+ }
+
+ /**
+ * Checks if the template can be removed from the database for the provided update status.
+ * A template can be removed if a newer version is already loaded in the database and no meta-fields are using it.
+ *
+ * @param array $updateStatus
+ * @return boolean
+ */
+ public function isRemovable(array $updateStatus): bool
+ {
+ return !empty($updateStatus['can-be-removed']);
+ }
+
+ /**
+ * Compute the state from the provided update status and metaTemplate
+ *
+ * @param array $updateStatus
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return array
+ */
+ public function computeFullUpdateStatusForMetaTemplate(array $updateStatus, \App\Model\Entity\MetaTemplate $metaTemplate): array
+ {
+ return [
+ 'up-to-date' => $this->isUpToDate($updateStatus),
+ 'automatically-updateable' => $this->isAutomaticallyUpdateable($updateStatus),
+ 'is-new' => $this->isNew($updateStatus),
+ 'has-conflict' => $this->hasConflict($updateStatus),
+ 'to-existing' => $this->isUpdateableToExistingMetaTemplate($metaTemplate),
+ 'can-be-removed' => $this->isRemovable($updateStatus),
+ ];
+ }
+
+ /**
+ * Get the update status of meta-templates that are up-to-date in regards to the template stored on the disk.
+ *
+ * @param array|null $updateStatus
+ * @return array The list of update status for up-to-date templates
+ */
+ public function getUpToDateTemplates($updatesStatus = null): array
+ {
+ $updatesStatus = is_null($updatesStatus) ? $this->getUpdateStatusForTemplates() : $updatesStatus;
+ foreach ($updatesStatus as $uuid => $updateStatus) {
+ if (!$this->isUpToDate($updateStatus)) {
+ unset($updatesStatus[$uuid]);
+ }
+ }
+ return $updatesStatus;
+ }
+
+ /**
+ * Get the update status of meta-templates that are not up-to-date in regards to the template stored on the disk.
+ *
+ * @param array|null $updateResult
+ * @return array The list of update status for non up-to-date templates
+ */
+ public function getNotUpToDateTemplates($updatesStatus = null): array
+ {
+ $updatesStatus = is_null($updatesStatus) ? $this->getUpdateStatusForTemplates() : $updatesStatus;
+ foreach ($updatesStatus as $uuid => $updateStatus) {
+ if ($this->isUpToDate($updateStatus)) {
+ unset($updatesStatus[$uuid]);
+ }
+ }
+ return $updatesStatus;
+ }
+
+ /**
+ * Get the update status of meta-templates that are automatically updateable in regards to the template stored on the disk.
+ *
+ * @param array|null $updateResult
+ * @return array The list of update status for non up-to-date templates
+ */
+ public function getAutomaticallyUpdateableTemplates($updatesStatus = null): array
+ {
+ $updatesStatus = is_null($updatesStatus) ? $this->getUpdateStatusForTemplates() : $updatesStatus;
+ foreach ($updatesStatus as $uuid => $updateStatus) {
+ if (!$this->isAutomaticallyUpdateable($updateStatus)) {
+ unset($updatesStatus[$uuid]);
+ }
+ }
+ return $updatesStatus;
+ }
+
+ /**
+ * Get the update status of meta-templates that are new in regards to the template stored on the disk.
+ *
+ * @param array|null $updateResult
+ * @return array The list of update status for new templates
+ */
+ public function getNewTemplates($updatesStatus = null): array
+ {
+ $updatesStatus = is_null($updatesStatus) ? $this->getUpdateStatusForTemplates() : $updatesStatus;
+ foreach ($updatesStatus as $uuid => $updateStatus) {
+ if (!$this->isNew($updateStatus)) {
+ unset($updatesStatus[$uuid]);
+ }
+ }
+ return $updatesStatus;
+ }
+
+ /**
+ * Get the update status of meta-templates that have conflict preventing an automatic update in regards to the template stored on the disk.
+ *
+ * @param array|null $updateResult
+ * @return array The list of update status for new templates
+ */
+ public function getConflictTemplates($updatesStatus = null): array
+ {
+ $updatesStatus = is_null($updatesStatus) ? $this->getUpdateStatusForTemplates() : $updatesStatus;
+ foreach ($updatesStatus as $uuid => $updateStatus) {
+ if (!$this->hasConflict($updateStatus)) {
+ unset($updatesStatus[$uuid]);
+ }
+ }
+ return $updatesStatus;
+ }
+
+ /**
+ * Get the latest (having the higher version) meta-template loaded in the database for the provided meta-template
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @param boolean $full
+ * @return \App\Model\Entity\MetaTemplate|null
+ */
+ public function getNewestVersion(\App\Model\Entity\MetaTemplate $metaTemplate, bool $full = false)
+ {
+ $query = $this->find()->where([
+ 'uuid' => $metaTemplate->uuid,
+ 'id !=' => $metaTemplate->id,
+ 'version >=' => $metaTemplate->version,
+ ])
+ ->order(['version' => 'DESC']);
+ if ($full) {
+ $query->contain(['MetaTemplateFields']);
+ }
+ $newestTemplate = $query->first();
+ return $newestTemplate;
+ }
+
+ /**
+ * Generate and return a query (to be used as a subquery) resolving to the IDs of the latest version of a saved meta-template
+ *
+ * @return \Cake\ORM\Query
+ */
+ public function genQueryForAllNewestVersionIDs(): \Cake\ORM\Query
+ {
+ /**
+ * SELECT a.id FROM meta_templates a INNER JOIN (
+ * SELECT uuid, MAX(version) maxVersion FROM meta_templates GROUP BY uuid
+ * ) b on a.uuid = b.uuid AND a.version = b.maxVersion;
+ */
+ $query = $this->find()
+ ->select([
+ 'id'
+ ])
+ ->join([
+ 't' => [
+ 'table' => '(SELECT uuid, MAX(version) AS maxVersion FROM meta_templates GROUP BY uuid)',
+ 'type' => 'INNER',
+ 'conditions' => [
+ 't.uuid = MetaTemplates.uuid',
+ 't.maxVersion = MetaTemplates.version'
+ ],
+ ],
+ ]);
+ return $query;
+ }
+
+ /**
+ * Get the update status of meta-templates that can be removed.
+ *
+ * @param array|null $updateResult
+ * @return array The list of update status for new templates
+ */
+ public function getCanBeRemovedTemplates($updatesStatus = null): array
+ {
+ $updatesStatus = is_null($updatesStatus) ? $this->getUpdateStatusForTemplates() : $updatesStatus;
+ foreach ($updatesStatus as $i => $updateStatus) {
+ if (!$this->isRemovable($updateStatus)) {
+ unset($updatesStatus[$i]);
+ }
+ }
+ return $updatesStatus;
+ }
+
+ /**
+ * Reads all template stored on the disk and parse them
+ *
+ * @param array|null $errors Contains errors while parsing the meta-templates
+ * @return array The parsed meta-templates stored on the disk
+ */
+ public function readTemplatesFromDisk(&$errors = []): array
+ {
+ if (!is_null($this->templatesOnDisk)) {
+ return $this->templatesOnDisk;
+ }
+ $templates = [];
+ $errors = [];
+ foreach (self::TEMPLATE_PATH as $path) {
if (is_dir($path)) {
$files = scandir($path);
foreach ($files as $k => $file) {
if (substr($file, -5) === '.json') {
- if ($this->loadMetaFile($path . $file) === true) {
- $files_processed[] = $file;
+ $errorMessage = '';
+ $template = $this->decodeTemplateFromDisk($path . $file, $errorMessage);
+ if (!empty($template)) {
+ $templates[] = $template;
+ } else {
+ $errors[] = $errorMessage;
}
}
}
}
}
- return $files_processed;
+ $this->templatesOnDisk = $templates;
+ return $templates;
}
- public function getTemplate($id)
+ /**
+ * Read and parse the meta-template stored on disk having the provided UUID
+ *
+ * @param string $uuid
+ * @param string $error Contains the error while parsing the meta-template
+ * @return array|null The meta-template or null if not templates matche the provided UUID
+ */
+ public function readTemplateFromDisk(string $uuid, &$error = ''): ?array
{
- $query = $this->find();
- $query->where(['id' => $id]);
- $template = $query->first();
- if (empty($template)) {
- throw new NotFoundException(__('Invalid template ID specified.'));
+ foreach (self::TEMPLATE_PATH as $path) {
+ if (is_dir($path)) {
+ $files = scandir($path);
+ foreach ($files as $k => $file) {
+ if (substr($file, -5) === '.json') {
+ $errorMessage = '';
+ $template = $this->decodeTemplateFromDisk($path . $file, $errorMessage);
+ if (!empty($template) && $template['uuid'] == $uuid) {
+ return $template;
+ }
+ }
+ }
+ }
}
- return $template;
+ $error = __('Could not find meta-template with UUID {0}', $uuid);
+ return null;
}
- public function getDefaultTemplatePerScope(String $scope = '')
+ /**
+ * Read and decode the meta-template located at the provided path
+ *
+ * @param string $filePath
+ * @param string $errorMessage
+ * @return array|null The meta-template or null if there was an error while trying to decode
+ */
+ public function decodeTemplateFromDisk(string $filePath, &$errorMessage = ''): ?array
+ {
+ $file = new File($filePath, false);
+ if ($file->exists()) {
+ $filename = $file->name();
+ $content = $file->read();
+ if (empty($content)) {
+ $errorMessage = __('Could not read template file `{0}`.', $filename);
+ return null;
+ }
+ $metaTemplate = json_decode($content, true);
+ if (empty($metaTemplate)) {
+ $errorMessage = __('Could not load template file `{0}`. Error while decoding the template\'s JSON', $filename);
+ return null;
+ }
+ if (empty($metaTemplate['uuid']) || empty($metaTemplate['version'])) {
+ $errorMessage = __('Could not load template file. Invalid template file. Missing template UUID or version');
+ return null;
+ }
+ return $metaTemplate;
+ }
+ $errorMessage = __('File does not exists');
+ return null;
+ }
+
+ /**
+ * Collect all enties having meta-fields belonging to the provided template
+ *
+ * @param integer $template_id
+ * @param integer|bool $limit The limit of entities to be returned. Pass null to be ignore the limit
+ * @return array List of entities
+ */
+ public function getEntitiesHavingMetaFieldsFromTemplate(int $metaTemplateId, $limit=10, int &$totalAmount=0): array
+ {
+ $metaTemplate = $this->get($metaTemplateId);
+ $queryParentEntities = $this->MetaTemplateFields->MetaFields->find();
+ $queryParentEntities
+ ->select(['parent_id'])
+ ->where([
+ 'meta_template_id' => $metaTemplateId
+ ])
+ ->group(['parent_id']);
+
+ $entitiesTable = $this->getTableForMetaTemplateScope($metaTemplate);
+ $entityQuery = $entitiesTable->find()
+ ->where(['id IN' => $queryParentEntities])
+ ->contain([
+ 'MetaFields' => [
+ 'conditions' => [
+ 'meta_template_id' => $metaTemplateId
+ ]
+ ]
+ ]);
+ if (!is_null($limit)) {
+ $totalAmount = $entityQuery->all()->count();
+ $entityQuery->limit($limit);
+ }
+ $entities = $entityQuery->all()->toList();
+ return $entities;
+ }
+
+ /**
+ * Get the table linked to the meta-template
+ *
+ * @param \App\Model\Entity\MetaTemplate|string $metaTemplate
+ * @return \App\Model\Table\AppTable
+ */
+ private function getTableForMetaTemplateScope($metaTemplateOrScope): \App\Model\Table\AppTable
+ {
+ if (is_string($metaTemplateOrScope)) {
+ $scope = $metaTemplateOrScope;
+ } else {
+ $scope = $metaTemplateOrScope->scope;
+ }
+ $entitiesClassName = Inflector::camelize(Inflector::pluralize($scope));
+ $entitiesTable = TableRegistry::getTableLocator()->get($entitiesClassName);
+ return $entitiesTable;
+ }
+
+ /**
+ * Get the meta-field keyed by their template_id and meta_template_id belonging to the provided entity
+ *
+ * @param integer $entity_id The entity for which the meta-fields belongs to
+ * @param array $conditions Additional conditions to be passed to the meta-fields query
+ * @return array The associated array containing the meta-fields keyed by their meta-template and meta-template-field IDs
+ */
+ public function getKeyedMetaFieldsForEntity(int $entity_id, array $conditions = []): array
+ {
+ $query = $this->MetaTemplateFields->MetaFields->find();
+ $query->where(array_merge(
+ $conditions,
+ [
+ 'MetaFields.parent_id' => $entity_id
+ ]
+ ));
+ $metaFields = $query->all();
+ $keyedMetaFields = [];
+ foreach ($metaFields as $metaField) {
+ if (empty($keyedMetaFields[$metaField->meta_template_id][$metaField->meta_template_field_id])) {
+ $keyedMetaFields[$metaField->meta_template_id][$metaField->meta_template_field_id] = [];
+ }
+ $keyedMetaFields[$metaField->meta_template_id][$metaField->meta_template_field_id][$metaField->id] = $metaField;
+ }
+ return $keyedMetaFields;
+ }
+
+ /**
+ * Insert the keyed meta-fields into the provided meta-templates
+ *
+ * @param array $keyedMetaFields An associative array containing the meta-fields keyed by their meta-template and meta-template-field IDs
+ * @param array $metaTemplates List of meta-templates
+ * @return array The list of meta-template with the meta-fields inserted
+ */
+ public function insertMetaFieldsInMetaTemplates(array $keyedMetaFields, array $metaTemplates): array
+ {
+ $merged = [];
+ foreach ($metaTemplates as $metaTemplate) {
+ $metaTemplate['meta_template_fields'] = Hash::combine($metaTemplate['meta_template_fields'], '{n}.id', '{n}');
+ $merged[$metaTemplate->id] = $metaTemplate;
+ if (isset($keyedMetaFields[$metaTemplate->id])) {
+ foreach ($metaTemplate->meta_template_fields as $j => $meta_template_field) {
+ if (isset($keyedMetaFields[$metaTemplate->id][$meta_template_field->id])) {
+ $merged[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = $keyedMetaFields[$metaTemplate->id][$meta_template_field->id];
+ } else {
+ $merged[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = [];
+ }
+ }
+ }
+ }
+ return $merged;
+ }
+
+ /**
+ * Retreive the entity associated for the provided meta-template and id
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @param integer $entity_id
+ * @return \App\Model\Entity\AppModel
+ */
+ public function getEntity(\App\Model\Entity\MetaTemplate $metaTemplate, int $entity_id): \App\Model\Entity\AppModel
+ {
+ $entitiesTable = $this->getTableForMetaTemplateScope($metaTemplate);
+ $entity = $entitiesTable->get($entity_id, [
+ 'contain' => 'MetaFields'
+ ]);
+ return $entity;
+ }
+
+ /**
+ * Collect the unique default template for each scope
+ *
+ * @param string|null $scope
+ * @return array The list of default template
+ */
+ public function getDefaultTemplatePerScope($scope = null): array
{
$query = $this->find('list', [
'keyField' => 'scope',
@@ -83,54 +734,600 @@ class MetaTemplatesTable extends AppTable
return $query->all()->toArray();
}
- public function removeDefaultFlag(String $scope)
+ /**
+ * Remove the default flag for all meta-templates belonging to the provided scope
+ *
+ * @param string $scope
+ * @return int the number of updated rows
+ */
+ public function removeDefaultFlag(string $scope): int
{
- $this->updateAll(
+ return $this->updateAll(
['is_default' => false],
['scope' => $scope]
);
}
- public function loadMetaFile(String $filePath)
+ /**
+ * Check if the provided template can be saved in the database without creating duplicate template in regards to the UUID and version
+ *
+ * @param array $template
+ * @return boolean
+ */
+ public function canBeSavedWithoutDuplicates(array $template): bool
{
- if (file_exists($filePath)) {
- $contents = file_get_contents($filePath);
- $metaTemplate = json_decode($contents, true);
- if (!empty($metaTemplate) && !empty($metaTemplate['uuid']) && !empty($metaTemplate['version'])) {
- $query = $this->find();
- $query->where(['uuid' => $metaTemplate['uuid']]);
- $template = $query->first();
- if (empty($template)) {
- $template = $this->newEntity($metaTemplate);
- $result = $this->save($template);
- if (!$result) {
- return __('Something went wrong, could not create the template.');
- }
- } else {
- if ($template->version >= $metaTemplate['version']) {
- return false;
- }
- foreach (['version', 'source', 'name', 'namespace', 'scope', 'description'] as $field) {
- $template->{$field} = $metaTemplate[$field];
- }
- $result = $this->save($template);
- if (!$result) {
- return __('Something went wrong, could not update the template.');
- return false;
- }
- }
- if ($result) {
- $this->MetaTemplateFields->deleteAll(['meta_template_id' => $template->id]);
- foreach ($metaTemplate['metaFields'] as $metaField) {
- $metaField['meta_template_id'] = $template->id;
- $metaField = $this->MetaTemplateFields->newEntity($metaField);
- $this->MetaTemplateFields->save($metaField);
- }
+ $query = $this->find()->where([
+ 'uuid' => $template['uuid'],
+ 'version' => $template['version'],
+ ]);
+ return $query->count() == 0;
+ }
+ /**
+ * Create and save the provided template in the database
+ *
+ * @param array $template The template to be saved
+ * @param array $errors The list of errors that occured during the save process
+ * @param \App\Model\Entity\MetaTemplate $savedMetaTemplate The metaTemplate entity that has just been saved
+ * @return boolean True if the save was successful, False otherwise
+ */
+ public function saveNewMetaTemplate(array $template, array &$errors = [], \App\Model\Entity\MetaTemplate &$savedMetaTemplate = null): bool
+ {
+ if (!$this->canBeSavedWithoutDuplicates($template)) {
+ $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
+ }
+ $template['meta_template_fields'] = $template['metaFields'];
+ unset($template['metaFields']);
+ $metaTemplate = $this->newEntity($template, [
+ 'associated' => ['MetaTemplateFields']
+ ]);
+ $tmp = $this->save($metaTemplate, [
+ 'associated' => ['MetaTemplateFields']
+ ]);
+ if ($tmp === false) {
+ $errors[] = new UpdateError(false, __('Could not save the template.'), $metaTemplate->getErrors());
+ return false;
+ }
+ $savedMetaTemplate = $tmp;
+ return true;
+ }
+
+ /**
+ * Update an existing meta-template and save it in the database
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate The meta-template to update
+ * @param array $template The template to use to update the existing meta-template
+ * @param array $errors
+ * @return boolean True if the save was successful, False otherwise
+ */
+ public function updateMetaTemplate(\App\Model\Entity\MetaTemplate $metaTemplate, array $template, array &$errors = []): bool
+ {
+ if (!$this->canBeSavedWithoutDuplicates($template)) {
+ $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
+ }
+ if (is_string($metaTemplate)) {
+ $errors[] = new UpdateError(false, $metaTemplate);
+ return false;
+ }
+ $metaTemplate = $this->patchEntity($metaTemplate, $template, [
+ 'associated' => ['MetaTemplateFields']
+ ]);
+ $metaTemplate = $this->save($metaTemplate, [
+ 'associated' => ['MetaTemplateFields']
+ ]);
+ if (!empty($metaTemplate)) {
+ $errors[] = new UpdateError(false, __('Could not save the template.'), $metaTemplate->getErrors());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Update an existing meta-template with the provided strategy and save it in the database
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate The meta-template to update
+ * @param array $template The template to use to update the existing meta-template
+ * @param string $strategy The strategy to use when handling update conflicts
+ * @param array $errors
+ * @return boolean True if the save was successful, False otherwise
+ */
+ public function updateMetaTemplateWithStrategyRouter(\App\Model\Entity\MetaTemplate $metaTemplate, array $template, string $strategy, array &$errors = []): bool
+ {
+ if (!$this->canBeSavedWithoutDuplicates($template)) {
+ $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
+ }
+ if (is_string($metaTemplate)) {
+ $errors[] = new UpdateError(false, $metaTemplate);
+ return false;
+ }
+ if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH) {
+ $result = $this->executeStrategyKeep($template, $metaTemplate);
+ } else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_DELETE) {
+ $result = $this->executeStrategyDeleteAll($template, $metaTemplate);
+ } else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) {
+ $result = $this->executeStrategyCreateNew($template, $metaTemplate);
+ } else {
+ $errors[] = new UpdateError(false, __('Invalid strategy {0}', $strategy));
+ return false;
+ }
+ if (is_string($result)) {
+ $errors[] = new UpdateError(false, $result);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Execute the `keep_both` update strategy by creating a new meta-template and moving non-conflicting entities to this one.
+ * Strategy:
+ * - Old template remains untouched
+ * - Create new template
+ * - Migrate all non-conflicting meta-fields for one entity to the new template
+ * - Keep all the conflicting meta-fields for one entity on the old template
+ *
+ * @param array $template
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return bool|string If the new template could be saved or the error message
+ */
+ public function executeStrategyKeep(array $template, \App\Model\Entity\MetaTemplate $metaTemplate)
+ {
+ if (!$this->canBeSavedWithoutDuplicates($template)) {
+ $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
+ }
+ $conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template);
+ $blockingConflict = Hash::extract($conflicts, '{s}.conflicts');
+ $errors = [];
+ if (empty($blockingConflict)) { // No conflict, everything can be updated without special care
+ $this->updateMetaTemplate($metaTemplate, $template, $errors);
+ return !empty($errors) ? $errors[0] : true;
+ }
+ $entities = $this->getEntitiesHavingMetaFieldsFromTemplate($metaTemplate->id, null);
+
+ $conflictingEntities = [];
+ foreach ($entities as $entity) {
+ $conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity['meta_fields'], $template);
+ if (!empty($conflicts)) {
+ $conflictingEntities[$entity->id] = $entity->id;
+ }
+ }
+ if (empty($conflictingEntities)) {
+ $this->updateMetaTemplate($metaTemplate, $template, $errors);
+ return !empty($errors) ? $errors[0] : true;
+ }
+ $template['is_default'] = $metaTemplate['is_default'];
+ $template['enabled'] = $metaTemplate['enabled'];
+ if ($metaTemplate->is_default) {
+ $metaTemplate->set('is_default', false);
+ $this->save($metaTemplate);
+ }
+ $savedMetaTemplate = null;
+ $this->saveNewMetaTemplate($template, $errors, $savedMetaTemplate);
+ if (!empty($savedMetaTemplate)) {
+ $savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}');
+ foreach ($entities as $entity) {
+ if (empty($conflictingEntities[$entity->id])) { // conflicting entities remain untouched
+ foreach ($entity['meta_fields'] as $metaField) {
+ $savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field];
+ $this->supersedeMetaFieldWithMetaTemplateField($metaField, $savedMetaTemplateField);
+ }
}
}
- return true;
+ } else {
+ return $errors[0]->message;
+ }
+ return true;
+ }
+
+ /**
+ * Execute the `delete_all` update strategy by updating the meta-template and deleting all conflicting meta-fields.
+ * Strategy:
+ * - Delete conflicting meta-fields
+ * - Update template to the new version
+ *
+ * @param array $template
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return bool|string If the new template could be saved or the error message
+ */
+ public function executeStrategyDeleteAll(array $template, \App\Model\Entity\MetaTemplate $metaTemplate)
+ {
+ if (!$this->canBeSavedWithoutDuplicates($template)) {
+ $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
+ }
+ $errors = [];
+ $conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template);
+ $blockingConflict = Hash::extract($conflicts, '{s}.conflicts');
+ if (empty($blockingConflict)) { // No conflict, everything can be updated without special care
+ $this->updateMetaTemplate($metaTemplate, $template, $errors);
+ return !empty($errors) ? $errors[0] : true;
+ }
+ $entities = $this->getEntitiesHavingMetaFieldsFromTemplate($metaTemplate->id, null);
+
+ foreach ($entities as $entity) {
+ $conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity['meta_fields'], $template);
+ $deletedCount = $this->MetaTemplateFields->MetaFields->deleteAll([
+ 'id IN' => $conflicts
+ ]);
+ }
+ $this->updateMetaTemplate($metaTemplate, $template, $errors);
+ return !empty($errors) ? $errors[0] : true;
+ }
+
+ /**
+ * Execute the `create_new` update strategy by creating a new meta-template
+ * Strategy:
+ * - Create a new meta-template
+ * - Make the new meta-template `default` and `enabled` if previous template had these states
+ * - Turn of these states on the old meta-template
+ *
+ * @param array $template
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return bool|string If the new template could be saved or the error message
+ */
+ public function executeStrategyCreateNew(array $template, \App\Model\Entity\MetaTemplate $metaTemplate)
+ {
+ if (!$this->canBeSavedWithoutDuplicates($template)) {
+ $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
+ }
+ $errors = [];
+ $template['is_default'] = $metaTemplate->is_default;
+ $template['enabled'] = $metaTemplate->enabled;
+ $savedMetaTemplate = null;
+ $success = $this->saveNewMetaTemplate($template, $errors, $savedMetaTemplate);
+ if ($success) {
+ if ($metaTemplate->is_default) {
+ $metaTemplate->set('is_default', false);
+ $metaTemplate->set('enabled', false);
+ $this->save($metaTemplate);
+ }
+ }
+ return !empty($errors) ? $errors[0] : true;
+ }
+
+ /**
+ * Supersede a meta-fields's meta-template-field with the provided one.
+ *
+ * @param \App\Model\Entity\MetaField $metaField
+ * @param \App\Model\Entity\MetaTemplateField $savedMetaTemplateField
+ * @return bool True if the replacement was a success, False otherwise
+ */
+ public function supersedeMetaFieldWithMetaTemplateField(\App\Model\Entity\MetaField $metaField, \App\Model\Entity\MetaTemplateField $savedMetaTemplateField): bool
+ {
+ $metaField->set('meta_template_id', $savedMetaTemplateField->meta_template_id);
+ $metaField->set('meta_template_field_id', $savedMetaTemplateField->id);
+ $metaField = $this->MetaTemplateFields->MetaFields->save($metaField);
+ return !empty($metaField);
+ }
+
+ /**
+ * Compute the validity of the provided meta-fields under the provided meta-template
+ *
+ * @param \App\Model\Entity\MetaField[] $metaFields
+ * @param array|\App\Model\Entity\MetaTemplate $template
+ * @return \App\Model\Entity\MetaField[] The list of conflicting meta-fields under the provided template
+ */
+ public function getMetaFieldsConflictsUnderTemplate(array $metaFields, $template): array
+ {
+ if (!is_array($template) && get_class($template) == 'App\Model\Entity\MetaTemplate') {
+ $metaTemplateFields = $template->meta_template_fields;
+ $existingMetaTemplate = true;
+ } else {
+ $metaTemplateFields = $template['metaFields'];
+ }
+ $conflicting = [];
+ $metaTemplateFieldByName = [];
+ foreach ($metaTemplateFields as $metaTemplateField) {
+ if (!is_array($template)) {
+ $metaTemplateField = $metaTemplateField->toArray();
+ }
+ $metaTemplateFieldByName[$metaTemplateField['field']] = $this->MetaTemplateFields->newEntity($metaTemplateField);
+ }
+ foreach ($metaFields as $metaField) {
+ if ($existingMetaTemplate && $metaField->meta_template_id != $template->id) {
+ continue;
+ }
+ $isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField(
+ $metaField->value,
+ $metaTemplateFieldByName[$metaField->field]
+ );
+ if ($isValid !== true) {
+ $conflicting[] = $metaField;
+ }
+ }
+ return $conflicting;
+ }
+
+ /**
+ * Compute the potential conflict that would be introduced by updating an existing meta-template-field with the provided one.
+ * This will go through all instanciation of the existing meta-template-field and checking their validity against the provided one.
+ *
+ * @param \App\Model\Entity\MetaTemplateField $metaTemplateField
+ * @param array $templateField
+ * @return array
+ */
+ public function computeExistingMetaTemplateFieldConflictForMetaTemplateField(\App\Model\Entity\MetaTemplateField $metaTemplateField, array $templateField): array
+ {
+ $result = [
+ 'automatically-updateable' => true,
+ 'conflicts' => [],
+ 'conflictingEntities' => [],
+ ];
+ if ($metaTemplateField->multiple && $templateField['multiple'] == false) { // Field is no longer multiple
+ $query = $this->MetaTemplateFields->MetaFields->find();
+ $query
+ ->enableHydration(false)
+ ->select([
+ 'parent_id',
+ 'meta_template_field_id',
+ 'count' => $query->func()->count('meta_template_field_id'),
+ ])
+ ->where([
+ 'meta_template_field_id' => $metaTemplateField->id,
+ ])
+ ->group(['parent_id'])
+ ->having(['count >' => 1]);
+ $conflictingStatus = $query->all()->toList();
+ if (!empty($conflictingStatus)) {
+ $result['automatically-updateable'] = false;
+ $result['conflicts'][] = __('This field is no longer multiple and is being that way');
+ $result['conflictingEntities'] = Hash::extract($conflictingStatus, '{n}.parent_id');
+ }
+ }
+ if (!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) {
+ $query = $this->MetaTemplateFields->MetaFields->find();
+ $query
+ ->enableHydration(false)
+ ->select([
+ 'parent_id',
+ 'scope',
+ 'meta_template_field_id',
+ ])
+ ->where([
+ 'meta_template_field_id' => $metaTemplateField->id,
+ ]);
+ $entitiesWithMetaField = $query->all()->toList();
+ if (!empty($entitiesWithMetaField)) {
+ $entitiesTable = $this->getTableForMetaTemplateScope($entitiesWithMetaField[0]['scope']);
+ $entities = $entitiesTable->find()
+ ->where(['id IN' => Hash::extract($entitiesWithMetaField, '{n}.parent_id')])
+ ->contain([
+ 'MetaFields' => [
+ 'conditions' => [
+ 'MetaFields.meta_template_field_id' => $metaTemplateField->id
+ ]
+ ]
+ ])
+ ->all()->toList();
+ $conflictingEntities = [];
+ foreach ($entities as $entity) {
+ foreach ($entity['meta_fields'] as $metaField) {
+ $isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField(
+ $metaField->value,
+ $templateField
+ );
+ if ($isValid !== true) {
+ $conflictingEntities[] = $entity->id;
+ break;
+ }
+ }
+ }
+
+ if (!empty($conflictingEntities)) {
+ $result['automatically-updateable'] = $result['automatically-updateable'] && false;
+ $result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore');
+ $result['conflictingEntities'] = $conflictingEntities;
+ }
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Check the conflict that would be introduced if the metaTemplate would be updated to the provided template
+ *
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @param \App\Model\Entity\MetaTemplate|array $template
+ * @return array
+ */
+ public function getMetaTemplateConflictsForMetaTemplate(\App\Model\Entity\MetaTemplate $metaTemplate, $template): array
+ {
+ $templateMetaFields = [];
+ if (!is_array($template) && get_class($template) == 'App\Model\Entity\MetaTemplate') {
+ $templateMetaFields = $template->meta_template_fields;
+ } else {
+ $templateMetaFields = $template['metaFields'];
+ }
+ $conflicts = [];
+ $existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field');
+ foreach ($templateMetaFields as $newMetaField) {
+ foreach ($metaTemplate->meta_template_fields as $metaField) {
+ if ($newMetaField['field'] == $metaField->field) {
+ unset($existingMetaTemplateFields[$metaField->field]);
+ $metaFieldArray = !is_array($newMetaField) && get_class($newMetaField) == 'App\Model\Entity\MetaTemplateField' ? $newMetaField->toArray() : $newMetaField;
+ $templateConflictsForMetaField = $this->computeExistingMetaTemplateFieldConflictForMetaTemplateField($metaField, $metaFieldArray);
+ if (!$templateConflictsForMetaField['automatically-updateable']) {
+ $conflicts[$metaField->field] = $templateConflictsForMetaField;
+ $conflicts[$metaField->field]['existing_meta_template_field'] = $metaField;
+ $conflicts[$metaField->field]['existing_meta_template_field']['conflicts'] = $templateConflictsForMetaField['conflicts'];
+ }
+ }
+ }
+ }
+ if (!empty($existingMetaTemplateFields)) {
+ foreach ($existingMetaTemplateFields as $field => $tmp) {
+ $conflicts[$field] = [
+ 'automatically-updateable' => false,
+ 'conflicts' => [__('This field is intended to be removed')],
+ ];
+ }
+ }
+ return $conflicts;
+ }
+
+ /**
+ * Get update status for the latest meta-template in the database for the provided template
+ *
+ * @param array $template
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate $metaTemplate
+ * @return array
+ */
+ public function getUpdateStatusForTemplate(array $template): array
+ {
+ $updateStatus = [
+ 'new' => true,
+ 'up-to-date' => false,
+ 'automatically-updateable' => false,
+ 'conflicts' => [],
+ 'template' => $template,
+ ];
+ $query = $this->find()
+ ->contain('MetaTemplateFields')
+ ->where([
+ 'uuid' => $template['uuid'],
+ ])
+ ->order(['version' => 'DESC']);
+ $metaTemplate = $query->first();
+ if (!empty($metaTemplate)) {
+ $updateStatus = array_merge(
+ $updateStatus,
+ $this->getStatusForMetaTemplate($template, $metaTemplate)
+ );
+ }
+ return $updateStatus;
+ }
+
+ /**
+ * Get update status for the meta-template stored in the database and the provided template
+ *
+ * @param array $template
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return array
+ */
+ public function getStatusForMetaTemplate(array $template, \App\Model\Entity\MetaTemplate $metaTemplate): array
+ {
+ $updateStatus = [];
+ $updateStatus['existing_template'] = $metaTemplate;
+ $updateStatus['current_version'] = $metaTemplate->version;
+ $updateStatus['next_version'] = $template['version'];
+ $updateStatus['new'] = false;
+ if ($metaTemplate->version >= $template['version']) {
+ $updateStatus['up-to-date'] = true;
+ $updateStatus['automatically-updateable'] = false;
+ $updateStatus['conflicts'][] = __('Could not update the template. Local version is equal or newer.');
+ return $updateStatus;
}
+ $conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template);
+ if (!empty($conflicts)) {
+ $updateStatus['conflicts'] = $conflicts;
+ } else {
+ $updateStatus['automatically-updateable'] = true;
+ }
+ $updateStatus['meta_field_amount'] = $this->MetaTemplateFields->MetaFields->find()->where(['meta_template_id' => $metaTemplate->id])->count();
+ $updateStatus['can-be-removed'] = empty($updateStatus['meta_field_amount']) && empty($updateStatus['to-existing']);
+ return $updateStatus;
+ }
+
+ /**
+ * Massages the meta-fields of an entity based on the input
+ * - If the keyed ID of the input meta-field is new, a new meta-field entity is created
+ * - If the input meta-field's value is empty for an existing meta-field, the existing meta-field is marked as to be deleted
+ * - If the input meta-field already exists, patch the entity and attach the validation errors
+ *
+ * @param \App\Model\Entity\AppModel $entity
+ * @param array $input
+ * @param \App\Model\Entity\MetaTemplate $metaTemplate
+ * @return array An array containing the entity with its massaged meta-fields and the meta-fields that should be deleted
+ */
+ public function massageMetaFieldsBeforeSave(\App\Model\Entity\AppModel $entity, array $input, \App\Model\Entity\MetaTemplate $metaTemplate): array
+ {
+ $metaFieldsTable = $this->MetaTemplateFields->MetaFields;
+ $className = Inflector::camelize(Inflector::pluralize($metaTemplate->scope));
+ $entityTable = TableRegistry::getTableLocator()->get($className);
+ $metaFieldsIndex = [];
+ if (!empty($entity->meta_fields)) {
+ foreach ($entity->meta_fields as $i => $metaField) {
+ $metaFieldsIndex[$metaField->id] = $i;
+ }
+ } else {
+ $entity->meta_fields = [];
+ }
+
+ $metaFieldsToDelete = [];
+ foreach ($input['MetaTemplates'] as $template_id => $template) {
+ foreach ($template['meta_template_fields'] as $meta_template_field_id => $meta_template_field) {
+ $rawMetaTemplateField = $metaTemplate->meta_template_fields[$meta_template_field_id];
+ foreach ($meta_template_field['metaFields'] as $meta_field_id => $meta_field) {
+ if ($meta_field_id == 'new') { // create new meta_field
+ $new_meta_fields = $meta_field;
+ foreach ($new_meta_fields as $new_value) {
+ if (!empty($new_value)) {
+ $metaField = $metaFieldsTable->newEmptyEntity();
+ $metaFieldsTable->patchEntity($metaField, [
+ 'value' => $new_value,
+ 'scope' => $entityTable->getBehavior('MetaFields')->getScope(),
+ 'field' => $rawMetaTemplateField->field,
+ 'meta_template_id' => $rawMetaTemplateField->meta_template_id,
+ 'meta_template_field_id' => $rawMetaTemplateField->id,
+ 'parent_id' => $entity->id,
+ 'uuid' => Text::uuid(),
+ ]);
+ $entity->meta_fields[] = $metaField;
+ $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField;
+ }
+ }
+ } else {
+ $new_value = $meta_field['value'];
+ if (!empty($new_value)) { // update meta_field and attach validation errors
+ if (!empty($metaFieldsIndex[$meta_field_id])) {
+ $index = $metaFieldsIndex[$meta_field_id];
+ $metaFieldsTable->patchEntity($entity->meta_fields[$index], [
+ 'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id
+ ], ['value']);
+ $metaFieldsTable->patchEntity(
+ $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id],
+ ['value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id],
+ ['value']
+ );
+ } else { // metafield comes from a second POST where the temporary entity has already been created
+ $metaField = $metaFieldsTable->newEmptyEntity();
+ $metaFieldsTable->patchEntity($metaField, [
+ 'value' => $new_value,
+ 'scope' => $entityTable->getBehavior('MetaFields')->getScope(), // get scope from behavior
+ 'field' => $rawMetaTemplateField->field,
+ 'meta_template_id' => $rawMetaTemplateField->meta_template_id,
+ 'meta_template_field_id' => $rawMetaTemplateField->id,
+ 'parent_id' => $entity->id,
+ 'uuid' => Text::uuid(),
+ ]);
+ $entity->meta_fields[] = $metaField;
+ $entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField;
+ }
+ } else { // Metafield value is empty, indicating the field should be removed
+ $index = $metaFieldsIndex[$meta_field_id];
+ $metaFieldsToDelete[] = $entity->meta_fields[$index];
+ unset($entity->meta_fields[$index]);
+ unset($entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id]);
+ }
+ }
+ }
+ }
+ }
+
+ $entity->setDirty('meta_fields', true);
+ return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
+ }
+}
+
+class UpdateError
+{
+ public $success;
+ public $message = '';
+ public $errors = [];
+
+ public function __construct($success = false, $message = '', $errors = [])
+ {
+ $this->success = $success;
+ $this->message = $message;
+ $this->errors = $errors;
}
}
diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php
index 254a570..b3fa792 100644
--- a/src/Model/Table/OrganisationsTable.php
+++ b/src/Model/Table/OrganisationsTable.php
@@ -9,8 +9,6 @@ use Cake\Error\Debugger;
class OrganisationsTable extends AppTable
{
- public $metaFields = 'organisation';
-
protected $_accessible = [
'id' => false
];
@@ -18,8 +16,10 @@ class OrganisationsTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
+ $this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
+ $this->addBehavior('AuditLog');
$this->hasMany(
'Alignments',
[
@@ -35,14 +35,7 @@ class OrganisationsTable extends AppTable
'conditions' => ['owner_model' => 'organisation']
]
);
- $this->hasMany(
- 'MetaFields',
- [
- 'dependent' => true,
- 'foreignKey' => 'parent_id',
- 'conditions' => ['MetaFields.scope' => 'organisation']
- ]
- );
+ $this->addBehavior('MetaFields');
$this->setDisplayField('name');
}
diff --git a/src/Model/Table/OutboxProcessorsTable.php b/src/Model/Table/OutboxProcessorsTable.php
index da26692..5812cb4 100644
--- a/src/Model/Table/OutboxProcessorsTable.php
+++ b/src/Model/Table/OutboxProcessorsTable.php
@@ -28,6 +28,7 @@ class OutboxProcessorsTable extends AppTable
if (empty($this->outboxProcessors)) {
$this->loadProcessors();
}
+ $this->addBehavior('AuditLog');
}
public function getProcessor($scope, $action=null)
@@ -87,7 +88,7 @@ class OutboxProcessorsTable extends AppTable
}
}
}
-
+
/**
* getProcessorClass
*
@@ -112,7 +113,7 @@ class OutboxProcessorsTable extends AppTable
return $e->getMessage();
}
}
-
+
/**
* createOutboxEntry
*
diff --git a/src/Model/Table/OutboxTable.php b/src/Model/Table/OutboxTable.php
index 05ed8fc..a78e0c6 100644
--- a/src/Model/Table/OutboxTable.php
+++ b/src/Model/Table/OutboxTable.php
@@ -18,15 +18,9 @@ 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->addBehavior('AuditLog');
$this->setDisplayField('title');
}
diff --git a/src/Model/Table/RemoteToolConnectionsTable.php b/src/Model/Table/RemoteToolConnectionsTable.php
index 7e8cf21..1e1ee25 100644
--- a/src/Model/Table/RemoteToolConnectionsTable.php
+++ b/src/Model/Table/RemoteToolConnectionsTable.php
@@ -18,6 +18,7 @@ class RemoteToolConnectionsTable extends AppTable
'LocalTools'
);
$this->setDisplayField('id');
+ $this->addBehavior('AuditLog');
}
public function validationDefault(Validator $validator): Validator
diff --git a/src/Model/Table/RolesTable.php b/src/Model/Table/RolesTable.php
index 3973b1b..74f290b 100644
--- a/src/Model/Table/RolesTable.php
+++ b/src/Model/Table/RolesTable.php
@@ -12,6 +12,7 @@ class RolesTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
+ $this->addBehavior('AuditLog');
$this->hasMany(
'Users',
[
diff --git a/src/Model/Table/SettingsProviderTable.php b/src/Model/Table/SettingProviders/BaseSettingsProvider.php
similarity index 53%
rename from src/Model/Table/SettingsProviderTable.php
rename to src/Model/Table/SettingProviders/BaseSettingsProvider.php
index 5483352..f9fae08 100644
--- a/src/Model/Table/SettingsProviderTable.php
+++ b/src/Model/Table/SettingProviders/BaseSettingsProvider.php
@@ -1,27 +1,27 @@
settingsConfiguration = $this->generateSettingsConfiguration();
- $this->setTable(false);
$this->error_critical = __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.');
$this->error_warning = __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.');
$this->error_info = __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.');
- $this->settingValidator = new SettingValidator();
+ if (!isset($this->settingValidator)) {
+ $this->settingValidator = new SettingValidator();
+ }
}
/**
@@ -46,151 +46,9 @@ class SettingsProviderTable extends AppTable
* redacted [optional]: Should the setting value be redacted. FIXME: To implement
* cli_only [optional]: Should this setting be modified only via the CLI.
*/
- private function generateSettingsConfiguration()
+ protected function generateSettingsConfiguration()
{
- return [
- 'Application' => [
- 'General' => [
- 'Essentials' => [
- '_description' => __('Ensentials settings required for the application to run normally.'),
- '_icon' => 'user-cog',
- 'app.baseurl' => [
- 'name' => __('Base URL'),
- 'type' => 'string',
- 'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'),
- 'default' => '',
- 'severity' => 'critical',
- 'test' => 'testBaseURL',
- ],
- 'app.uuid' => [
- 'name' => 'UUID',
- 'type' => 'string',
- 'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'),
- 'default' => '',
- 'severity' => 'critical',
- 'test' => 'testUuid',
- ],
- ],
- 'Miscellaneous' => [
- 'sc2.hero' => [
- 'description' => 'The true hero',
- 'default' => 'Sarah Kerrigan',
- 'name' => 'Hero',
- 'options' => [
- 'Jim Raynor' => 'Jim Raynor',
- 'Sarah Kerrigan' => 'Sarah Kerrigan',
- 'Artanis' => 'Artanis',
- 'Zeratul' => 'Zeratul',
- ],
- 'type' => 'select'
- ],
- 'sc2.antagonists' => [
- 'description' => 'The bad guys',
- 'default' => 'Amon',
- 'name' => 'Antagonists',
- 'options' => function($settingsProviders) {
- return [
- 'Amon' => 'Amon',
- 'Sarah Kerrigan' => 'Sarah Kerrigan',
- 'Narud' => 'Narud',
- ];
- },
- 'severity' => 'warning',
- 'type' => 'multi-select'
- ],
- ],
- 'floating-setting' => [
- 'description' => 'floaringSetting',
- // 'default' => 'A default value',
- 'name' => 'Uncategorized Setting',
- // 'severity' => 'critical',
- 'severity' => 'warning',
- // 'severity' => 'info',
- 'type' => 'integer'
- ],
- ],
- 'Network' => [
- 'Proxy' => [
- 'proxy.host' => [
- 'name' => __('Host'),
- 'type' => 'string',
- 'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
- 'test' => 'testHostname',
- ],
- 'proxy.port' => [
- 'name' => __('Port'),
- 'type' => 'integer',
- 'description' => __('The TCP port for the HTTP proxy.'),
- 'test' => 'testForRangeXY',
- ],
- 'proxy.user' => [
- 'name' => __('User'),
- 'type' => 'string',
- 'description' => __('The authentication username for the HTTP proxy.'),
- 'default' => 'admin',
- 'dependsOn' => 'proxy.host',
- ],
- 'proxy.password' => [
- 'name' => __('Password'),
- 'type' => 'string',
- 'description' => __('The authentication password for the HTTP proxy.'),
- 'default' => '',
- 'dependsOn' => 'proxy.host',
- ],
- ],
- ],
- 'UI' => [
- 'General' => [
- 'ui.bsTheme' => [
- 'description' => 'The Bootstrap theme to use for the application',
- 'default' => 'default',
- 'name' => 'UI Theme',
- 'options' => function($settingsProviders) {
- $instanceTable = TableRegistry::getTableLocator()->get('Instance');
- $themes = $instanceTable->getAvailableThemes();
- return array_combine($themes, $themes);
- },
- 'severity' => 'info',
- 'type' => 'select'
- ],
- ],
- ],
- ],
- 'Security' => [
- 'Development' => [
- 'Debugging' => [
- 'security.debug' => [
- 'name' => __('Debug Level'),
- 'type' => 'select',
- 'description' => __('The debug level of the instance'),
- 'default' => 0,
- 'options' => [
- 0 => __('Debug Off'),
- 1 => __('Debug On'),
- 2 => __('Debug On + SQL Dump'),
- ],
- 'test' => function($value, $setting, $validator) {
- $validator->range('value', [0, 3]);
- return testValidator($value, $validator);
- },
- ],
- ],
- ]
- ],
- 'Features' => [
- 'Demo Settings' => [
- 'demo.switch' => [
- 'name' => __('Switch'),
- 'type' => 'boolean',
- 'description' => __('A switch acting as a checkbox'),
- 'default' => false,
- 'test' => function() {
- return 'Fake error';
- },
- ],
- ]
- ],
- ];
+ return [];
}
/**
@@ -206,7 +64,7 @@ class SettingsProviderTable extends AppTable
}
return $settingConf;
}
-
+
/**
* mergeSettingsIntoSettingConfiguration Inject the provided settings into the configuration while performing depencency and validation checks
*
@@ -214,7 +72,7 @@ class SettingsProviderTable extends AppTable
* @param array $settings the settings
* @return void
*/
- private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
+ protected function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
{
foreach ($settingConf as $key => $value) {
if ($this->isSettingMetaKey($key)) {
@@ -249,7 +107,7 @@ class SettingsProviderTable extends AppTable
}
return $flattenedSettings;
}
-
+
/**
* getNoticesFromSettingsConfiguration Summarize the validation errors
*
@@ -277,12 +135,12 @@ class SettingsProviderTable extends AppTable
return $notices;
}
- private function isLeaf($setting)
+ protected function isLeaf($setting)
{
return !empty($setting['name']) && !empty($setting['type']);
}
- private function evaluateLeaf($setting, $settingSection)
+ protected function evaluateLeaf($setting, $settingSection)
{
$skipValidation = false;
if ($setting['type'] == 'select' || $setting['type'] == 'multi-select') {
@@ -322,7 +180,7 @@ class SettingsProviderTable extends AppTable
}
return $setting;
}
-
+
/**
* evaluateFunctionForSetting - evaluate the provided function. If function could not be evaluated, its result is defaulted to true
*
@@ -353,12 +211,6 @@ class SettingsProviderTable extends AppTable
}
}
-function testValidator($value, $validator)
-{
- $errors = $validator->validate(['value' => $value]);
- return !empty($errors) ? implode(', ', $errors['value']) : true;
-}
-
class SettingValidator
{
@@ -384,22 +236,4 @@ class SettingValidator
{
return !empty($value) ? true : __('Cannot be empty');
}
-
- public function testBaseURL($value, &$setting)
- {
- if (empty($value)) {
- return __('Cannot be empty');
- }
- if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) {
- return __('Invalid URL, please make sure that the protocol is set.');
- }
- return true;
- }
-
- public function testUuid($value, &$setting) {
- if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) {
- return __('Invalid UUID.');
- }
- return true;
- }
-}
\ No newline at end of file
+}
diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php
new file mode 100644
index 0000000..fa80078
--- /dev/null
+++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php
@@ -0,0 +1,349 @@
+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.screw' => [
+ 'name' => 'Screw',
+ 'type' => 'string',
+ 'severity' => 'info',
+ 'default' => 0,
+ 'description' => __('The misalignment allowed when validating JWT tokens between cerebrate and keycloak. Whilst crisp timings are essential for any timing push, perfect timing is only achievable by GSL participants. (given in seconds)')
+ ],
+ '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;
+ }
+}
diff --git a/src/Model/Table/SettingProviders/UserSettingsProvider.php b/src/Model/Table/SettingProviders/UserSettingsProvider.php
new file mode 100644
index 0000000..baeca50
--- /dev/null
+++ b/src/Model/Table/SettingProviders/UserSettingsProvider.php
@@ -0,0 +1,50 @@
+ [
+ __('User Interface') => [
+ 'ui.bsTheme' => [
+ 'description' => 'The Bootstrap theme to use for the application',
+ 'default' => 'default',
+ 'name' => 'UI Theme',
+ 'options' => (function () {
+ $instanceTable = TableRegistry::getTableLocator()->get('Instance');
+ $themes = $instanceTable->getAvailableThemes();
+ return array_combine($themes, $themes);
+ })(),
+ 'severity' => 'info',
+ 'type' => 'select'
+ ],
+ 'ui.sidebar.expanded' => [
+ 'name' => __('Sidebar expanded'),
+ 'type' => 'boolean',
+ 'description' => __('Should the left navigation sidebar expanded and locked.'),
+ 'default' => false,
+ 'severity' => 'info',
+ ],
+ 'ui.sidebar.include_bookmarks' => [
+ 'name' => __('Include bookmarks in the sidebar'),
+ 'type' => 'boolean',
+ 'description' => __('Should bookmarks links included in the sidebar.'),
+ 'default' => false,
+ 'severity' => 'info',
+ ],
+ ]
+ ],
+ __('Account Security') => [
+ ]
+ ];
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Table/SettingsTable.php b/src/Model/Table/SettingsTable.php
index 0235cdc..7e9bcff 100644
--- a/src/Model/Table/SettingsTable.php
+++ b/src/Model/Table/SettingsTable.php
@@ -3,20 +3,30 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
-use Cake\Validation\Validator;
use Cake\Core\Configure;
-use Cake\ORM\TableRegistry;
+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 = TableRegistry::getTableLocator()->get('SettingsProvider');
+ $this->SettingsProvider = new CerebrateSettingsProvider();
+ $this->addBehavior('AuditLog');
}
public function getSettings($full=false): array
@@ -55,6 +65,13 @@ class SettingsTable extends AppTable
$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);
@@ -78,20 +95,42 @@ class SettingsTable extends AppTable
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()
{
- return Configure::read()[$this::$CONFIG_KEY];
+ $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;
- Configure::write($this::$CONFIG_KEY, $settings);
- Configure::dump($this::$FILENAME, 'default', [$this::$CONFIG_KEY]);
+ $settings = json_encode($settings, JSON_PRETTY_PRINT);
+ file_put_contents(CONFIG . 'config.json', $settings);
+ $this->loadSettings();
return true;
}
}
diff --git a/src/Model/Table/SharingGroupsTable.php b/src/Model/Table/SharingGroupsTable.php
index ec3791e..ff39220 100644
--- a/src/Model/Table/SharingGroupsTable.php
+++ b/src/Model/Table/SharingGroupsTable.php
@@ -15,6 +15,7 @@ class SharingGroupsTable extends AppTable
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
+ $this->addBehavior('AuditLog');
$this->belongsTo(
'Users'
);
diff --git a/src/Model/Table/UserSettingsTable.php b/src/Model/Table/UserSettingsTable.php
new file mode 100644
index 0000000..bdfe535
--- /dev/null
+++ b/src/Model/Table/UserSettingsTable.php
@@ -0,0 +1,138 @@
+addBehavior('Timestamp');
+ $this->belongsTo(
+ 'Users'
+ );
+ $this->setDisplayField('name');
+
+ $this->SettingsProvider = new UserSettingsProvider();
+ }
+
+ public function validationDefault(Validator $validator): Validator
+ {
+ $validator
+ ->requirePresence(['name', 'user_id'], 'create')
+ ->notEmptyString('name', __('Please fill this field'))
+ ->notEmptyString('user_id', __('Please supply the user id to which this setting belongs to'));
+ return $validator;
+ }
+
+ public function getSettingsFromProviderForUser($user_id, $full = false): array
+ {
+ $settingsTmp = $this->getSettingsForUser($user_id)->toArray();
+ $settings = [];
+ foreach ($settingsTmp as $setting) {
+ $settings[$setting->name] = $setting->value;
+ }
+ if (empty($full)) {
+ return $settings;
+ } else {
+ $settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
+ $settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
+ $notices = $this->SettingsProvider->getNoticesFromSettingsConfiguration($settingsProvider, $settings);
+ return [
+ 'settings' => $settings,
+ 'settingsProvider' => $settingsProvider,
+ 'settingsFlattened' => $settingsFlattened,
+ 'notices' => $notices,
+ ];
+ }
+ }
+
+ public function getSettingsForUser($user_id)
+ {
+ return $this->find()->where([
+ 'user_id' => $user_id,
+ ])->all();
+ }
+
+ public function getSettingByName($user, $name)
+ {
+ return $this->find()->where([
+ 'user_id' => $user->id,
+ 'name' => $name,
+ ])->first();
+ }
+
+ public function createSetting($user, $name, $value)
+ {
+ $setting = $this->newEmptyEntity();
+ $data = [
+ 'name' => $name,
+ 'value' => $value,
+ 'user_id' => $user->id,
+ ];
+ $setting = $this->patchEntity($setting, $data);
+ $savedData = $this->save($setting);
+ return $savedData;
+ }
+
+ public function editSetting($user, $name, $value)
+ {
+ $setting = $this->getSettingByName($user, $name);
+ $setting = $this->patchEntity($setting, [
+ 'value' => $value
+ ]);
+ $savedData = $this->save($setting);
+ return $savedData;
+ }
+
+ public function saveBookmark($user, $data)
+ {
+ $setting = $this->getSettingByName($user, $this->BOOKMARK_SETTING_NAME);
+ $bookmarkData = [
+ 'label' => $data['bookmark_label'],
+ 'name' => $data['bookmark_name'],
+ 'url' => $data['bookmark_url'],
+ ];
+ if (is_null($setting)) { // setting not found, create it
+ $bookmarksData = json_encode([$bookmarkData]);
+ $result = $this->createSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData);
+ } else {
+ $bookmarksData = json_decode($setting->value);
+ $bookmarksData[] = $bookmarkData;
+ $bookmarksData = json_encode($bookmarksData);
+ $result = $this->editSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData);
+ }
+ return $result;
+ }
+
+ public function deleteBookmark($user, $data)
+ {
+ $setting = $this->getSettingByName($user, $this->BOOKMARK_SETTING_NAME);
+ $bookmarkData = [
+ 'name' => $data['bookmark_name'],
+ 'url' => $data['bookmark_url'],
+ ];
+ if (is_null($setting)) { // Can't delete something that doesn't exist
+ return null;
+ } else {
+ $bookmarksData = json_decode($setting->value, true);
+ foreach ($bookmarksData as $i => $savedBookmark) {
+ if ($savedBookmark['name'] == $bookmarkData['name'] && $savedBookmark['url'] == $bookmarkData['url']) {
+ unset($bookmarksData[$i]);
+ }
+ }
+ $bookmarksData = json_encode($bookmarksData);
+ $result = $this->editSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData);
+ }
+ return $result;
+ }
+}
diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php
index 585295d..61f06b8 100644
--- a/src/Model/Table/UsersTable.php
+++ b/src/Model/Table/UsersTable.php
@@ -12,13 +12,16 @@ use \Cake\Http\Session;
use Cake\Http\Client;
use Cake\Utility\Security;
use Cake\Core\Configure;
+use Cake\Utility\Text;
class UsersTable extends AppTable
{
public function initialize(array $config): void
{
parent::initialize($config);
+ $this->addBehavior('Timestamp');
$this->addBehavior('UUID');
+ $this->addBehavior('AuditLog');
$this->initAuthBehaviors();
$this->belongsTo(
'Individuals',
@@ -34,6 +37,20 @@ class UsersTable extends AppTable
'cascadeCallbacks' => false
]
);
+ $this->belongsTo(
+ 'Organisations',
+ [
+ 'dependent' => false,
+ 'cascadeCallbacks' => false
+ ]
+ );
+ $this->hasMany(
+ 'UserSettings',
+ [
+ 'dependent' => true,
+ 'cascadeCallbacks' => true
+ ]
+ );
$this->setDisplayField('username');
}
@@ -80,15 +97,35 @@ class UsersTable extends AppTable
return $rules;
}
+ public function test()
+ {
+ $this->Roles = TableRegistry::get('Roles');
+ $role = $this->Roles->newEntity([
+ 'name' => 'admin',
+ 'perm_admin' => 1,
+ 'perm_org_admin' => 1,
+ 'perm_sync' => 1
+ ]);
+ $this->Roles->save($role);
+ }
+
public function checkForNewInstance(): bool
{
if (empty($this->find()->first())) {
$this->Roles = TableRegistry::get('Roles');
$role = $this->Roles->newEntity([
'name' => 'admin',
- 'perm_admin' => 1
+ 'perm_admin' => 1,
+ 'perm_org_admin' => 1,
+ 'perm_sync' => 1
]);
$this->Roles->save($role);
+ $this->Organisations = TableRegistry::get('Organisations');
+ $organisation = $this->Organisations->newEntity([
+ 'name' => 'default_organisation',
+ 'uuid' => Text::uuid()
+ ]);
+ $this->Organisations->save($organisation);
$this->Individuals = TableRegistry::get('Individuals');
$individual = $this->Individuals->newEntity([
'email' => 'admin@admin.test',
@@ -100,6 +137,7 @@ class UsersTable extends AppTable
'username' => 'admin',
'password' => 'Password1234',
'individual_id' => $individual->id,
+ 'organisation_id' => $organisation->id,
'role_id' => $role->id
]);
$this->save($user);
diff --git a/src/Utility/UI/IndexSetting.php b/src/Utility/UI/IndexSetting.php
new file mode 100644
index 0000000..ac3560e
--- /dev/null
+++ b/src/Utility/UI/IndexSetting.php
@@ -0,0 +1,30 @@
+user_settings_by_name['ui.table_setting']['value']) ? json_decode($user->user_settings_by_name['ui.table_setting']['value'], true) : [];
+ return $rawSetting;
+ }
+
+ public static function getTableSetting($user, $tableId): array
+ {
+ $rawSetting = IndexSetting::getAllSetting($user);
+ if (is_object($tableId)) {
+ $tableId = IndexSetting::getIDFromTable($tableId);
+ }
+ $tableSettings = !empty($rawSetting[$tableId]) ? $rawSetting[$tableId] : [];
+ return $tableSettings;
+ }
+
+ public static function getIDFromTable(Object $table): string
+ {
+ return sprintf('%s_index', Inflector::variable(Inflector::singularize(($table->getAlias()))));
+ }
+}
\ No newline at end of file
diff --git a/src/View/AppView.php b/src/View/AppView.php
index 7636a87..9018168 100644
--- a/src/View/AppView.php
+++ b/src/View/AppView.php
@@ -44,5 +44,6 @@ class AppView extends View
$this->loadHelper('FormFieldMassage');
$this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']);
$this->loadHelper('Tags.Tag');
+ $this->loadHelper('ACL');
}
}
diff --git a/src/View/Helper/ACLHelper.php b/src/View/Helper/ACLHelper.php
new file mode 100644
index 0000000..e563e82
--- /dev/null
+++ b/src/View/Helper/ACLHelper.php
@@ -0,0 +1,25 @@
+roleAccess)) {
+ $this->roleAccess = $this->getView()->get('roleAccess');
+ }
+ if (
+ in_array($action, $this->roleAccess['*']) ||
+ (isset($this->roleAccess[$controller]) && in_array($action, $this->roleAccess[$controller]))
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php
index 287696f..5353411 100644
--- a/src/View/Helper/BootstrapHelper.php
+++ b/src/View/Helper/BootstrapHelper.php
@@ -1,4 +1,5 @@
button();
}
- public function icon($icon, $options=[])
+ public function icon($icon, $options = [])
{
$bsIcon = new BoostrapIcon($icon, $options);
return $bsIcon->icon();
@@ -98,7 +99,7 @@ class BootstrapHelper extends Helper
$bsModal = new BoostrapModal($options);
return $bsModal->modal();
}
-
+
public function card($options)
{
$bsCard = new BoostrapCard($options);
@@ -117,6 +118,12 @@ class BootstrapHelper extends Helper
return $bsCollapse->collapse();
}
+ public function accordion($options, $content)
+ {
+ $bsAccordion = new BoostrapAccordion($options, $content, $this);
+ return $bsAccordion->accordion();
+ }
+
public function progressTimeline($options)
{
$bsProgressTimeline = new BoostrapProgressTimeline($options, $this);
@@ -129,7 +136,7 @@ class BootstrapHelper extends Helper
return $bsListGroup->listGroup();
}
- public function genNode($node, $params=[], $content='')
+ public function genNode($node, $params = [], $content = '')
{
return BootstrapGeneric::genNode($node, $params, $content);
}
@@ -139,6 +146,12 @@ class BootstrapHelper extends Helper
$bsSwitch = new BoostrapSwitch($options, $this);
return $bsSwitch->switch();
}
+
+ public function dropdownMenu($options)
+ {
+ $bsDropdownMenu = new BoostrapDropdownMenu($options, $this);
+ return $bsDropdownMenu->dropdownMenu();
+ }
}
class BootstrapGeneric
@@ -171,12 +184,12 @@ class BootstrapGeneric
}
}
- public 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);
}
- protected static function openNode($node, $params=[])
+ protected static function openNode($node, $params = [])
{
return sprintf('<%s %s>', $node, BootstrapGeneric::genHTMLParams($params));
}
@@ -243,7 +256,8 @@ class BootstrapTabs extends BootstrapGeneric
];
private $bsClasses = null;
- function __construct($options) {
+ function __construct($options)
+ {
$this->allowedOptionValues = [
'justify' => [false, 'center', 'end'],
'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
@@ -278,7 +292,7 @@ class BootstrapTabs extends BootstrapGeneric
$this->options['pills'] = true;
$this->options['card'] = true;
}
-
+
if ($this->options['pills']) {
$this->bsClasses['nav'][] = 'nav-pills';
if ($this->options['vertical']) {
@@ -301,7 +315,7 @@ class BootstrapTabs extends BootstrapGeneric
$this->bsClasses['nav'][] = 'nav-justify';
}
- $activeTab = 0;
+ $activeTab = array_key_first($this->data['navs']);
foreach ($this->data['navs'] as $i => $nav) {
if (!is_array($nav)) {
$this->data['navs'][$i] = ['text' => $nav];
@@ -380,29 +394,31 @@ class BootstrapTabs extends BootstrapGeneric
"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(
- [
- ($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->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(
+ [
+ ($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');
return $html;
}
@@ -473,7 +489,8 @@ class BootstrapTabs extends BootstrapGeneric
}
}
-class BoostrapAlert extends BootstrapGeneric {
+class BoostrapAlert extends BootstrapGeneric
+{
private $defaultOptions = [
'text' => '',
'html' => null,
@@ -484,7 +501,8 @@ class BoostrapAlert extends BootstrapGeneric {
private $bsClasses = null;
- function __construct($options) {
+ function __construct($options)
+ {
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
@@ -531,11 +549,12 @@ class BoostrapAlert extends BootstrapGeneric {
private function genContent()
{
- return !is_null($this->options['html']) ? $this->options['html'] : $this->options['text'];
+ return !is_null($this->options['html']) ? $this->options['html'] : h($this->options['text']);
}
}
-class BoostrapTable extends BootstrapGeneric {
+class BoostrapTable extends BootstrapGeneric
+{
private $defaultOptions = [
'striped' => true,
'bordered' => true,
@@ -550,7 +569,8 @@ class BoostrapTable extends BootstrapGeneric {
private $bsClasses = null;
- function __construct($options, $data, $btHelper) {
+ function __construct($options, $data, $btHelper)
+ {
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, [''])
];
@@ -591,7 +611,7 @@ class BoostrapTable extends BootstrapGeneric {
$html .= $this->genCaption();
$html .= $this->genHeader();
$html .= $this->genBody();
-
+
$html .= $this->closeNode('table');
return $html;
}
@@ -631,15 +651,15 @@ class BoostrapTable extends BootstrapGeneric {
],
]);
foreach ($this->items as $i => $row) {
- $body .= $this->genRow($row);
+ $body .= $this->genRow($row, $i);
}
$body .= $this->closeNode('tbody');
return $body;
}
- private function genRow($row)
+ private function genRow($row, $rowIndex)
{
- $html = $this->openNode('tr',[
+ $html = $this->openNode('tr', [
'class' => [
!empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : ''
]
@@ -652,21 +672,21 @@ class BoostrapTable extends BootstrapGeneric {
$key = $field;
}
$cellValue = Hash::get($row, $key);
- $html .= $this->genCell($cellValue, $field, $row);
+ $html .= $this->genCell($cellValue, $field, $row, $rowIndex);
}
} else { // indexed array
- foreach ($row as $cellValue) {
- $html .= $this->genCell($cellValue, $field, $row);
+ foreach ($row as $i => $cellValue) {
+ $html .= $this->genCell($cellValue, 'index', $row, $rowIndex);
}
}
$html .= $this->closeNode('tr');
return $html;
}
- private function genCell($value, $field=[], $row=[])
+ private function genCell($value, $field=[], $row=[], $rowIndex=0)
{
if (isset($field['formatter'])) {
- $cellContent = $field['formatter']($value, $row);
+ $cellContent = $field['formatter']($value, $row, $rowIndex);
} else if (isset($field['element'])) {
$cellContent = $this->btHelper->getView()->element($field['element'], [
'data' => [$value],
@@ -688,7 +708,8 @@ class BoostrapTable extends BootstrapGeneric {
}
}
-class BoostrapListTable extends BootstrapGeneric {
+class BoostrapListTable extends BootstrapGeneric
+{
private $defaultOptions = [
'striped' => true,
'bordered' => false,
@@ -702,7 +723,8 @@ class BoostrapListTable extends BootstrapGeneric {
private $bsClasses = null;
- function __construct($options, $data, $btHelper) {
+ function __construct($options, $data, $btHelper)
+ {
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, [''])
];
@@ -743,7 +765,7 @@ class BoostrapListTable extends BootstrapGeneric {
$html .= $this->genCaption();
$html .= $this->genBody();
-
+
$html .= $this->closeNode('table');
return $html;
}
@@ -767,11 +789,11 @@ class BoostrapListTable extends BootstrapGeneric {
$rowValue = $this->genCell($field);
$rowKey = $this->genNode('th', [
'class' => [
- 'col-sm-2'
+ 'col-4 col-sm-2'
],
'scope' => 'row'
], h($field['key']));
- $row = $this->genNode('tr',[
+ $row = $this->genNode('tr', [
'class' => [
'd-flex',
!empty($field['_rowVariant']) ? "table-{$field['_rowVariant']}" : ''
@@ -780,7 +802,7 @@ class BoostrapListTable extends BootstrapGeneric {
return $row;
}
- private function genCell($field=[])
+ private function genCell($field = [])
{
if (isset($field['raw'])) {
$cellContent = h($field['raw']);
@@ -796,7 +818,7 @@ class BoostrapListTable extends BootstrapGeneric {
}
return $this->genNode('td', [
'class' => [
- 'col-sm-10',
+ 'col-8 col-sm-10',
!empty($row['_cellVariant']) ? "bg-{$row['_cellVariant']}" : ''
]
], $cellContent);
@@ -815,7 +837,8 @@ class BoostrapListTable extends BootstrapGeneric {
private function getElementPath($type)
{
- return sprintf('%s%sField',
+ return sprintf(
+ '%s%sField',
$this->options['elementsRootPath'] ?? '',
$type
);
@@ -827,7 +850,8 @@ class BoostrapListTable extends BootstrapGeneric {
}
}
-class BoostrapButton extends BootstrapGeneric {
+class BoostrapButton extends BootstrapGeneric
+{
private $defaultOptions = [
'id' => '',
'text' => '',
@@ -847,10 +871,11 @@ class BoostrapButton extends BootstrapGeneric {
private $bsClasses = [];
- function __construct($options) {
+ function __construct($options)
+ {
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
- 'size' => ['', 'sm', 'lg'],
+ 'size' => ['', 'xs', 'sm', 'lg'],
'type' => ['button', 'submit', 'reset']
];
if (empty($options['class'])) {
@@ -865,6 +890,10 @@ class BoostrapButton extends BootstrapGeneric {
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
+ if (!empty($this->options['id'])) {
+ $this->options['params']['id'] = $this->options['id'];
+ }
+
$this->bsClasses[] = 'btn';
if ($this->options['outline']) {
$this->bsClasses[] = "btn-outline-{$this->options['variant']}";
@@ -909,7 +938,7 @@ class BoostrapButton extends BootstrapGeneric {
{
if (!empty($this->options['icon'])) {
$bsIcon = new BoostrapIcon($this->options['icon'], [
- 'class' => [(!empty($this->options['title']) ? 'me-1' : '')]
+ 'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
]);
return $bsIcon->icon();
}
@@ -936,7 +965,8 @@ class BoostrapButton extends BootstrapGeneric {
}
}
-class BoostrapBadge extends BootstrapGeneric {
+class BoostrapBadge extends BootstrapGeneric
+{
private $defaultOptions = [
'text' => '',
'variant' => 'primary',
@@ -945,7 +975,8 @@ class BoostrapBadge extends BootstrapGeneric {
'class' => [],
];
- function __construct($options) {
+ function __construct($options)
+ {
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
@@ -955,6 +986,7 @@ class BoostrapBadge extends BootstrapGeneric {
private function processOptions($options)
{
$this->options = array_merge($this->defaultOptions, $options);
+ $this->options['class'] = is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']];
$this->checkOptionValidity();
}
@@ -979,13 +1011,17 @@ class BoostrapBadge extends BootstrapGeneric {
}
}
-class BoostrapIcon extends BootstrapGeneric {
+class BoostrapIcon extends BootstrapGeneric
+{
private $icon = '';
private $defaultOptions = [
'class' => [],
+ 'title' => '',
+ 'params' => [],
];
- function __construct($icon, $options=[]) {
+ function __construct($icon, $options = [])
+ {
$this->icon = $icon;
$this->processOptions($options);
}
@@ -1003,17 +1039,19 @@ class BoostrapIcon extends BootstrapGeneric {
private function genIcon()
{
- $html = $this->genNode('span', [
+ $html = $this->genNode('span', array_merge([
'class' => array_merge(
is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']],
["fa fa-{$this->icon}"]
),
- ]);
+ 'title' => h($this->options['title'])
+ ], $this->options['params']));
return $html;
}
}
-class BoostrapModal extends BootstrapGeneric {
+class BoostrapModal extends BootstrapGeneric
+{
private $defaultOptions = [
'size' => '',
'centered' => true,
@@ -1038,10 +1076,11 @@ class BoostrapModal extends BootstrapGeneric {
private $bsClasses = null;
- function __construct($options) {
+ function __construct($options)
+ {
$this->allowedOptionValues = [
'size' => ['sm', 'lg', 'xl', ''],
- 'type' => ['ok-only','confirm','confirm-success','confirm-warning','confirm-danger', 'custom'],
+ 'type' => ['ok-only', 'confirm', 'confirm-success', 'confirm-warning', 'confirm-danger', 'custom'],
'variant' => array_merge(BootstrapGeneric::$variants, ['']),
];
$this->processOptions($options);
@@ -1060,6 +1099,7 @@ class BoostrapModal extends BootstrapGeneric {
private function genModal()
{
+ $this->options['modalClass'] = !empty($this->options['modalClass']) && !is_array($this->options['modalClass']) ? [$this->options['modalClass']] : $this->options['modalClass'];
$dialog = $this->openNode('div', [
'class' => array_merge(
['modal-dialog', (!empty($this->options['size'])) ? "modal-{$this->options['size']}" : ''],
@@ -1120,7 +1160,8 @@ class BoostrapModal extends BootstrapGeneric {
return $footer;
}
- private function getFooterBasedOnType() {
+ private function getFooterBasedOnType()
+ {
if ($this->options['type'] == 'ok-only') {
return $this->getFooterOkOnly();
} else if (str_contains($this->options['type'], 'confirm')) {
@@ -1232,7 +1273,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']),
+ h(is_array($this->options['class']) ? implode(' ', $this->options['class']) : $this->options['class']),
],
], implode('', [$this->genHeader(), $this->genBody(), $this->genFooter()]));
return $card;
@@ -1284,7 +1325,8 @@ class BoostrapCard extends BootstrapGeneric
}
}
-class BoostrapSwitch extends BootstrapGeneric {
+class BoostrapSwitch extends BootstrapGeneric
+{
private $defaultOptions = [
'label' => '',
'variant' => 'primary',
@@ -1295,7 +1337,8 @@ class BoostrapSwitch extends BootstrapGeneric {
'attrs' => [],
];
- function __construct($options) {
+ function __construct($options)
+ {
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
@@ -1338,7 +1381,8 @@ class BoostrapSwitch extends BootstrapGeneric {
}
}
-class BoostrapProgress extends BootstrapGeneric {
+class BoostrapProgress extends BootstrapGeneric
+{
private $defaultOptions = [
'value' => 0,
'total' => 100,
@@ -1351,7 +1395,8 @@ class BoostrapProgress extends BootstrapGeneric {
'label' => true
];
- function __construct($options) {
+ function __construct($options)
+ {
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
@@ -1383,7 +1428,7 @@ class BoostrapProgress extends BootstrapGeneric {
$this->options['animated'] ? 'progress-bar-animated' : '',
],
'role' => "progressbar",
- 'aria-valuemin' => "0", 'aria-valuemax' => "100",'aria-valuenow' => $percentage,
+ 'aria-valuemin' => "0", 'aria-valuemax' => "100", 'aria-valuenow' => $percentage,
'style' => "${widthStyle}",
'title' => $this->options['title']
], $label);
@@ -1398,13 +1443,15 @@ class BoostrapProgress extends BootstrapGeneric {
}
}
-class BoostrapCollapse extends BootstrapGeneric {
+class BoostrapCollapse extends BootstrapGeneric
+{
private $defaultOptions = [
- 'text' => '',
+ 'title' => '',
'open' => false,
];
- function __construct($options, $content, $btHelper) {
+ function __construct($options, $content, $btHelper)
+ {
$this->allowedOptionValues = [];
$this->processOptions($options);
$this->content = $content;
@@ -1455,7 +1502,98 @@ class BoostrapCollapse extends BootstrapGeneric {
}
}
-class BoostrapProgressTimeline extends BootstrapGeneric {
+class BoostrapAccordion extends BootstrapGeneric
+{
+ private $defaultOptions = [
+ 'stayOpen' => true,
+ 'class' => [],
+ ];
+
+ function __construct($options, $content, $btHelper)
+ {
+ $this->allowedOptionValues = [];
+ $this->content = $content;
+ $this->btHelper = $btHelper;
+ $this->processOptions($options);
+ }
+
+ private function processOptions($options)
+ {
+ $this->options = array_merge($this->defaultOptions, $options);
+ $this->checkOptionValidity();
+ if (!is_array($this->options['class']) && !empty($this->options['class'])) {
+ $this->options['class'] = [$this->options['class']];
+ }
+ $this->seed = 'acc-' . mt_rand();
+ $this->contentSeeds = [];
+ foreach ($this->content as $accordionItem) {
+ $this->contentSeeds[] = mt_rand();
+ }
+ }
+
+ public function accordion()
+ {
+ return $this->genAccordion();
+ }
+
+ private function genHeader($accordionItem, $i)
+ {
+ $html = $this->openNode('h2', [
+ 'class' => ['accordion-header'],
+ 'id' => 'head-' . $this->contentSeeds[$i]
+ ]);
+ $content = !empty($accordionItem['header']['html']) ? $accordionItem['header']['html'] : h($accordionItem['header']['title'] ?? '- no title -');
+ $buttonOptions = [
+ 'class' => array_merge(['accordion-button', empty($accordionItem['_open']) ? 'collapsed' : ''], $accordionItem['header']['__class'] ?? []),
+ 'type' => 'button',
+ 'data-bs-toggle' => 'collapse',
+ 'data-bs-target' => '#body-' . $this->contentSeeds[$i],
+ 'aria-expanded' => 'false',
+ 'aria-controls' => 'body-' . $this->contentSeeds[$i],
+ ];
+ $html .= $this->genNode('button', $buttonOptions, $content);
+ $html .= $this->closeNode(('h2'));
+ return $html;
+ }
+
+ private function genBody($accordionItem, $i)
+ {
+ $content = $this->genNode('div', [
+ 'class' => ['accordion-body']
+ ], $accordionItem['body']);
+ $divOptions = [
+ 'class' => array_merge(['accordion-collapse collapse', empty($accordionItem['_open']) ? '' : 'show'], $accordionItem['body']['__class'] ?? []),
+ 'id' => 'body-' . $this->contentSeeds[$i],
+ 'aria-labelledby' => 'head-' . $this->contentSeeds[$i],
+ ];
+ if (!empty($this->options['stayOpen'])) {
+ $divOptions['data-bs-parent'] = '#' . $this->seed;
+ }
+ $html = $this->genNode('div', $divOptions, $content);
+ return $html;
+ }
+
+ private function genAccordion()
+ {
+ $html = $this->openNode('div', [
+ 'class' => array_merge(['accordion'], $this->options['class']),
+ 'id' => $this->seed
+ ]);
+ foreach ($this->content as $i => $accordionItem) {
+ $html .= $this->openNode('div', [
+ 'class' => array_merge(['accordion-item'], $accordionItem['__class'] ?? [])
+ ]);
+ $html .= $this->genHeader($accordionItem, $i);
+ $html .= $this->genBody($accordionItem, $i);
+ $html .= $this->closeNode('div');
+ }
+ $html .= $this->closeNode('div');
+ return $html;
+ }
+}
+
+class BoostrapProgressTimeline extends BootstrapGeneric
+{
private $defaultOptions = [
'steps' => [],
'selected' => 0,
@@ -1463,7 +1601,8 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
'variantInactive' => 'secondary',
];
- function __construct($options, $btHelper) {
+ function __construct($options, $btHelper)
+ {
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
'variantInactive' => BootstrapGeneric::$variants,
@@ -1490,7 +1629,7 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
!empty($step['icon']) ? h($this->btHelper->FontAwesome->getClass($step['icon'])) : '',
$this->getTextClassForVariant($this->options['variant'])
],
- ], empty($step['icon']) ? h($i+1) : '');
+ ], empty($step['icon']) ? h($i + 1) : '');
$iconContainer = $this->genNode('span', [
'class' => [
'd-flex', 'align-items-center', 'justify-content-center',
@@ -1512,7 +1651,7 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
private function getHorizontalLine($i, $nodeActive, $lineActive)
{
$stepCount = count($this->options['steps']);
- if ($i == $stepCount-1) {
+ if ($i == $stepCount - 1) {
return '';
}
$progressBar = (new BoostrapProgress([
@@ -1577,7 +1716,8 @@ class BootstrapListGroup extends BootstrapGeneric
private $bsClasses = null;
- function __construct($options, $data, $btHelper) {
+ function __construct($options, $data, $btHelper)
+ {
$this->data = $data;
$this->processOptions($options);
$this->btHelper = $btHelper;
@@ -1663,4 +1803,153 @@ class BootstrapListGroup extends BootstrapGeneric
}
return !empty($item['body']) ? h($item['body']) : '';
}
-}
\ No newline at end of file
+}
+
+
+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 = [];
+ $icon = '';
+ if (!empty($entry['icon'])) {
+ $icon = $this->btHelper->icon($entry['icon'], ['class' => 'me-2']);
+ }
+ $badge = '';
+ if (!empty($entry['badge'])) {
+ $bsBadge = new BoostrapBadge(array_merge(
+ ['class' => ['ms-auto']],
+ $entry['badge']
+ ));
+ $badge = $bsBadge->badge();
+ }
+
+ if (!empty($entry['header'])) {
+ return $this->genNode('h6', [
+ 'class' => ['dropdown-header',],
+ ], $icon . h($entry['text']) . $badge);
+ }
+
+ $classes = ['dropdown-item'];
+ $params = ['href' => '#'];
+
+ if (!empty($entry['menu'])) {
+ $classes[] = 'dropdown-toggle';
+ $classes[] = 'd-flex align-items-center';
+ $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' => 'mx-1'], h($entry['text']));
+ $content = $icon . $label . $badge;
+
+ return $this->genNode('a', array_merge([
+ 'class' => $classes,
+ ], $params), $content);
+ }
+}
diff --git a/templates/Api/index.php b/templates/Api/index.php
new file mode 100644
index 0000000..96be4b8
--- /dev/null
+++ b/templates/Api/index.php
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/templates/AuditLogs/index.php b/templates/AuditLogs/index.php
new file mode 100644
index 0000000..d20fa67
--- /dev/null
+++ b/templates/AuditLogs/index.php
@@ -0,0 +1,69 @@
+element('genericElements/IndexTable/index_table', [
+ 'data' => [
+ 'data' => $data,
+ 'top_bar' => [
+ 'children' => [
+ [
+ 'type' => 'search',
+ 'button' => __('Search'),
+ 'placeholder' => __('Enter value to search'),
+ 'data' => '',
+ 'searchKey' => 'value',
+ 'allowFilering' => true
+ ],
+ [
+ 'type' => 'table_action',
+ ]
+ ]
+ ],
+ 'fields' => [
+ [
+ 'name' => '#',
+ 'sort' => 'id',
+ 'data_path' => 'id',
+ ],
+ [
+ 'name' => __('IP'),
+ 'sort' => 'request_ip',
+ 'data_path' => 'request_ip',
+ ],
+ [
+ 'name' => __('Username'),
+ 'sort' => 'user.username',
+ 'data_path' => 'user.username',
+ ],
+ [
+ 'name' => __('Title'),
+ 'data_path' => 'title',
+ ],
+ [
+ 'name' => __('Model'),
+ 'sort' => 'model',
+ 'data_path' => 'model',
+ ],
+ [
+ 'name' => __('Model ID'),
+ 'sort' => 'model',
+ 'data_path' => 'model_id',
+ ],
+ [
+ 'name' => __('Action'),
+ 'sort' => 'request_action',
+ 'data_path' => 'request_action',
+ ],
+ [
+ 'name' => __('Changed'),
+ 'sort' => 'changed',
+ 'data_path' => 'changed',
+ 'element' => 'json'
+ ],
+ ],
+ 'title' => __('Logs'),
+ 'description' => null,
+ 'pull' => 'right',
+ 'actions' => []
+ ]
+]);
+echo '';
+?>
diff --git a/templates/AuthKeys/index.php b/templates/AuthKeys/index.php
index b5fbc35..070c3f3 100644
--- a/templates/AuthKeys/index.php
+++ b/templates/AuthKeys/index.php
@@ -16,7 +16,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
diff --git a/templates/Broods/index.php b/templates/Broods/index.php
index 43d454a..a78cf5a 100644
--- a/templates/Broods/index.php
+++ b/templates/Broods/index.php
@@ -20,7 +20,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
diff --git a/templates/Broods/preview_individuals.php b/templates/Broods/preview_individuals.php
index 85a565d..73e2199 100644
--- a/templates/Broods/preview_individuals.php
+++ b/templates/Broods/preview_individuals.php
@@ -7,7 +7,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
diff --git a/templates/Broods/preview_organisations.php b/templates/Broods/preview_organisations.php
index 5bded24..40b09c3 100644
--- a/templates/Broods/preview_organisations.php
+++ b/templates/Broods/preview_organisations.php
@@ -7,7 +7,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
diff --git a/templates/Broods/preview_sharing_groups.php b/templates/Broods/preview_sharing_groups.php
index 1348182..5a4cc58 100644
--- a/templates/Broods/preview_sharing_groups.php
+++ b/templates/Broods/preview_sharing_groups.php
@@ -7,7 +7,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'children' => [
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
diff --git a/templates/EncryptionKeys/index.php b/templates/EncryptionKeys/index.php
index 5017319..413e2d8 100644
--- a/templates/EncryptionKeys/index.php
+++ b/templates/EncryptionKeys/index.php
@@ -24,7 +24,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
diff --git a/templates/EncryptionKeys/view.php b/templates/EncryptionKeys/view.php
new file mode 100644
index 0000000..e92da92
--- /dev/null
+++ b/templates/EncryptionKeys/view.php
@@ -0,0 +1,32 @@
+element(
+ '/genericElements/SingleViews/single_view',
+ [
+ 'data' => $entity,
+ 'fields' => [
+ [
+ 'key' => __('ID'),
+ 'path' => 'id'
+ ],
+ [
+ 'key' => __('Type'),
+ 'path' => 'type'
+ ],
+ [
+ 'key' => __('Owner'),
+ 'path' => 'owner_id',
+ 'owner_model_path' => 'owner_model',
+ 'type' => 'owner'
+ ],
+ [
+ 'key' => __('Revoked'),
+ 'path' => 'revoked'
+ ],
+
+ [
+ 'key' => __('Key'),
+ 'path' => 'encryption_key'
+ ]
+ ]
+ ]
+);
diff --git a/templates/Inbox/index.php b/templates/Inbox/index.php
index f70bd90..6576ec3 100644
--- a/templates/Inbox/index.php
+++ b/templates/Inbox/index.php
@@ -29,7 +29,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
diff --git a/templates/Inbox/list_processors.php b/templates/Inbox/list_processors.php
index fd93199..3c2a7c7 100644
--- a/templates/Inbox/list_processors.php
+++ b/templates/Inbox/list_processors.php
@@ -11,7 +11,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
diff --git a/templates/Individuals/add.php b/templates/Individuals/add.php
index 935c2de..436d6fa 100644
--- a/templates/Individuals/add.php
+++ b/templates/Individuals/add.php
@@ -23,10 +23,10 @@
),
array(
'field' => 'tag_list',
- 'type' => 'tags'
+ 'type' => 'tags',
+ 'requirements' => $this->request->getParam('action') === 'edit'
),
),
- 'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
'submit' => array(
'action' => $this->request->getParam('action')
)
diff --git a/templates/Individuals/index.php b/templates/Individuals/index.php
index 4930899..5867f19 100644
--- a/templates/Individuals/index.php
+++ b/templates/Individuals/index.php
@@ -20,11 +20,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
- 'button' => __('Filter'),
+ 'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'allowFilering' => true
+ ],
+ [
+ 'type' => 'table_action',
]
]
],
@@ -69,7 +72,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'title' => __('ContactDB Individuals Index'),
'description' => __('A list of individuals known by your Cerebrate instance. This list can get populated either directly, by adding new individuals or by fetching them from trusted remote sources. Additionally, users created for the platform will always have an individual identity.'),
- 'pull' => 'right',
'actions' => [
[
'url' => '/individuals/view',
@@ -90,4 +92,4 @@ echo $this->element('genericElements/IndexTable/index_table', [
]
]);
echo '';
-?>
+?>
\ No newline at end of file
diff --git a/templates/Instance/home.php b/templates/Instance/home.php
index d576185..a37c88c 100644
--- a/templates/Instance/home.php
+++ b/templates/Instance/home.php
@@ -1,29 +1,56 @@
text($md);
+$bookmarks = !empty($loggedUser->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($loggedUser->user_settings_by_name['ui.bookmarks']['value'], true) : [];
?>
-= __('Home') ?>
+
+ = $this->Bootstrap->icon('bookmark', [
+ 'class' => ['fa-fw']
+ ]); ?>
+ = __('Bookmarks') ?>
+
- $statistics): ?>
-
+
+
+
+
= __('No bookmarks') ?>
+
+
+
+
+ = $this->Bootstrap->icon('chart-bar', [
+ 'class' => ['fa-fw']
+ ]); ?>
+ = __('Activity') ?>
+
+
+ $statisticForModel) : ?>
+
Html->link(
- h($modelForDisplay),
- $this->Url->build([
- 'controller' => $modelForDisplay,
- 'action' => 'index',
- ]),
- ['class' => 'text-white text-decoration-none fw-light stretched-link']
- );
- echo $this->element('widgets/highlight-panel', [
- 'titleHtml' => $panelTitle,
- 'number' => $statistics['amount'],
- 'variation' => $statistics['variation'] ?? '',
- 'chartData' => $statistics['timeline'] ?? []
- ]);
+ $exploded = explode('.', $modelName);
+ $modelForDisplay = $exploded[count($exploded) - 1];
+ $panelTitle = $this->Html->link(
+ h($modelForDisplay),
+ $this->Url->build([
+ 'controller' => $modelForDisplay,
+ 'action' => 'index',
+ ]),
+ ['class' => 'text-white text-decoration-none fw-light stretched-link']
+ );
+ echo $this->element('widgets/highlight-panel', [
+ 'titleHtml' => $panelTitle,
+ 'number' => $statisticForModel['created']['amount'],
+ 'variation' => $statisticForModel['created']['variation'] ?? null,
+ 'timeline' => $statisticForModel ?? []
+ ]);
?>
diff --git a/templates/Instance/save_setting.php b/templates/Instance/save_setting.php
index 6609749..7be19d8 100644
--- a/templates/Instance/save_setting.php
+++ b/templates/Instance/save_setting.php
@@ -1,7 +1,7 @@
element('genericElements/Form/genericForm', [
'data' => [
- 'description' => __('Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier.'),
+ 'description' => __('Application setting form'),
'fields' => [
[
'field' => 'name',
diff --git a/templates/Instance/search_all.php b/templates/Instance/search_all.php
index 76c46f6..bd6bb17 100644
--- a/templates/Instance/search_all.php
+++ b/templates/Instance/search_all.php
@@ -19,14 +19,25 @@
', h($tableName), $tableResult['amount']);
foreach ($tableResult['entries'] as $entry) {
- $section .= sprintf('
%s ',
- Cake\Routing\Router::URL([
- 'controller' => Cake\Utility\Inflector::pluralize($entry->getSource()),
- 'action' => 'view',
- h($entry['id'])
- ]),
- h($entry[$fieldPath])
- );
+ if ($entry->getSource() == 'MetaFields') {
+ $section .= sprintf('
%s ',
+ Cake\Routing\Router::URL([
+ 'controller' => Cake\Utility\Inflector::pluralize($entry->scope),
+ 'action' => 'view',
+ h($entry->parent_id)
+ ]),
+ sprintf('%s (%s::%s)', h($entry->value), h($entry->scope), h($entry->field))
+ );
+ } else {
+ $section .= sprintf('
%s ',
+ Cake\Routing\Router::URL([
+ 'controller' => Cake\Utility\Inflector::pluralize($entry->getSource()),
+ 'action' => 'view',
+ h($entry['id'])
+ ]),
+ h($entry[$fieldPath])
+ );
+ }
}
$remaining = $tableResult['amount'] - count($tableResult['entries']);
if ($remaining > 0) {
diff --git a/templates/Instance/settings.php b/templates/Instance/settings.php
index 0e9adf6..334b5c5 100644
--- a/templates/Instance/settings.php
+++ b/templates/Instance/settings.php
@@ -5,294 +5,69 @@ $variantFromSeverity = [
'warning' => 'warning',
'info' => 'info',
];
-$this->set('variantFromSeverity', $variantFromSeverity);
-$settingTable = genNavcard($settingsProvider, $this);
+
+$navLinks = [];
+$tabContents = [];
+
+foreach ($settingsProvider as $settingTitle => $settingContent) {
+ $navLinks[] = h($settingTitle);
+ $tabContents[] = $this->element('Settings/category', [
+ 'settings' => $settingContent,
+ 'includeScrollspy' => true,
+ ]);
+}
+
+array_unshift($navLinks, __('Settings Diagnostic'));
+$notice = $this->element('Settings/notice', [
+ 'variantFromSeverity' => $variantFromSeverity,
+ 'notices' => $notices,
+]);
+array_unshift($tabContents, $notice);
?>
=
- $this->element('Settings/search', [
- ]);
+ $this->element('Settings/search', [
+ 'settingsFlattened' => $settingsFlattened,
+ ]);
?>
- = $settingTable; ?>
-
-
- $sectionSettings) {
- if (!empty($sectionSettings)) {
- $cardContent[] = genContentForNav($sectionSettings, $appView);
- } else {
- $cardContent[] = __('No Settings available yet');
- }
- }
- array_unshift($cardNavs, __('Settings Diagnostic'));
- $notice = $appView->element('Settings/notice', [
- 'variantFromSeverity' => $appView->get('variantFromSeverity'),
- ]);
- array_unshift($cardContent, $notice);
- $tabsOptions0 = [
- // 'vertical' => true,
- // 'vertical-size' => 2,
+ false,
'pills' => false,
'justify' => 'center',
'nav-class' => ['settings-tabs'],
'data' => [
- 'navs' => $cardNavs,
- 'content' => $cardContent
+ 'navs' => $navLinks,
+ 'content' => $tabContents
]
];
- $table0 = $appView->Bootstrap->tabs($tabsOptions0);
- return $table0;
-}
-
-function genContentForNav($sectionSettings, $appView)
-{
- $groupedContent = [];
- $groupedSetting = [];
- foreach ($sectionSettings as $sectionName => $subSectionSettings) {
- if (!empty($subSectionSettings)) {
- $groupedContent[] = genSection($sectionName, $subSectionSettings, $appView);
- } else {
- $groupedContent[] = '';
- }
- if (!isLeaf($subSectionSettings)) {
- $groupedSetting[$sectionName] = array_filter( // only show grouped settings
- array_keys($subSectionSettings),
- function ($settingGroupName) use ($subSectionSettings) {
- return !isLeaf($subSectionSettings[$settingGroupName]) && !empty($subSectionSettings[$settingGroupName]);
- }
- );
- }
- }
- $contentHtml = implode('', $groupedContent);
- $scrollspyNav = $appView->element('Settings/scrollspyNav', [
- 'groupedSetting' => $groupedSetting
- ]);
- $mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)';
- $container = '
';
- $container .= "
{$scrollspyNav}
";
- $container .= "
{$contentHtml}
";
- $container .= '
';
- return $container;
-}
-
-function genSection($sectionName, $subSectionSettings, $appView)
-{
- $sectionContent = [];
- $sectionContent[] = '
';
- if (isLeaf($subSectionSettings)) {
- $panelHTML = $appView->element('Settings/panel', [
- 'sectionName' => $sectionName,
- 'panelName' => $sectionName,
- 'panelSettings' => $subSectionSettings,
- ]);
- $sectionContent[] = $panelHTML;
- } else {
- if (count($subSectionSettings) > 0) {
- $sectionContent[] = sprintf('
%s ', getResolvableID($sectionName), h($sectionName));
- }
- foreach ($subSectionSettings as $panelName => $panelSettings) {
- if (!empty($panelSettings)) {
- $panelHTML = $appView->element('Settings/panel', [
- 'sectionName' => $sectionName,
- 'panelName' => $panelName,
- 'panelSettings' => $panelSettings,
- ]);
- $sectionContent[] = $panelHTML;
- } else {
- $sectionContent[] = '';
- }
- }
- }
- $sectionContent[] = '';
- return implode('', $sectionContent);
-}
-
-function isLeaf($setting)
-{
- return !empty($setting['name']) && !empty($setting['type']);
-}
-
-function getResolvableID($sectionName, $panelName=false)
-{
- $id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName)));
- if (!empty($panelName)) {
- $id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName));
- }
- return $id;
-}
-?>
-
-
+ echo $this->Bootstrap->tabs($tabsOptions);
+ echo $this->Html->script('settings');
+ ?>
+
\ No newline at end of file
diff --git a/templates/element/charts/generic.php b/templates/element/charts/generic.php
new file mode 100644
index 0000000..488577c
--- /dev/null
+++ b/templates/element/charts/generic.php
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/element/charts/pie.php b/templates/element/charts/pie.php
new file mode 100644
index 0000000..6dd4475
--- /dev/null
+++ b/templates/element/charts/pie.php
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/element/flash/success.php b/templates/element/flash/success.php
index 1e866d3..6d6bf96 100644
--- a/templates/element/flash/success.php
+++ b/templates/element/flash/success.php
@@ -24,4 +24,4 @@ if (!isset($params['escape']) || $params['escape'] !== false) {
-
+
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/Fields/dropdownField.php b/templates/element/genericElements/Form/Fields/dropdownField.php
index b5c64b6..a00f0e9 100644
--- a/templates/element/genericElements/Form/Fields/dropdownField.php
+++ b/templates/element/genericElements/Form/Fields/dropdownField.php
@@ -2,6 +2,12 @@
$controlParams = [
'options' => $fieldData['options'],
'empty' => $fieldData['empty'] ?? false,
+ 'value' => $fieldData['value'] ?? null,
+ 'multiple' => $fieldData['multiple'] ?? false,
+ 'disabled' => $fieldData['disabled'] ?? false,
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select'
];
+ if (!empty($fieldData['label'])) {
+ $controlParams['label'] = $fieldData['label'];
+ }
echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $fieldData);
diff --git a/templates/element/genericElements/Form/Fields/genericField.php b/templates/element/genericElements/Form/Fields/genericField.php
index 38f05a4..4174c5b 100644
--- a/templates/element/genericElements/Form/Fields/genericField.php
+++ b/templates/element/genericElements/Form/Fields/genericField.php
@@ -1,5 +1,7 @@
FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
?>
diff --git a/templates/element/genericElements/Form/Fields/passwordField.php b/templates/element/genericElements/Form/Fields/passwordField.php
new file mode 100644
index 0000000..6831ce6
--- /dev/null
+++ b/templates/element/genericElements/Form/Fields/passwordField.php
@@ -0,0 +1,6 @@
+FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
+?>
diff --git a/templates/element/genericElements/Form/fieldScaffold.php b/templates/element/genericElements/Form/fieldScaffold.php
index 4258ce0..32b339b 100644
--- a/templates/element/genericElements/Form/fieldScaffold.php
+++ b/templates/element/genericElements/Form/fieldScaffold.php
@@ -8,7 +8,9 @@
$fieldTemplate = $fieldData['type'] . 'Field';
}
if (empty($fieldData['label'])) {
- $fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']);
+ if (!isset($fieldData['label']) || $fieldData['label'] !== false) {
+ $fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']);
+ }
}
if (!empty($fieldDesc[$fieldData['field']])) {
$fieldData['label'] .= $this->element(
@@ -30,10 +32,9 @@
} else {
$params['class'] = '';
}
- if (empty($fieldData['type']) || $fieldData['type'] !== 'checkbox' ) {
+ if (empty($fieldData['type']) || ($fieldData['type'] !== 'checkbox' && $fieldData['type'] !== 'radio')) {
$params['class'] .= ' form-control';
}
- //$params['class'] = sprintf('form-control %s', $params['class']);
foreach ($fieldData as $k => $fd) {
if (!isset($simpleFieldWhitelist) || in_array($k, $simpleFieldWhitelist) || strpos($k, 'data-') === 0) {
$params[$k] = $fd;
@@ -47,7 +48,6 @@
$temp = '' . $temp . ' ';
}
echo $temp;
- // $fieldsArrayForPersistence []= $modelForForm . \Cake\Utility\Inflector::camelize($fieldData['field']);
} else {
echo $fieldData;
}
diff --git a/templates/element/genericElements/Form/formLayouts/formDefault.php b/templates/element/genericElements/Form/formLayouts/formDefault.php
new file mode 100644
index 0000000..7953082
--- /dev/null
+++ b/templates/element/genericElements/Form/formLayouts/formDefault.php
@@ -0,0 +1,38 @@
+
+
+ = empty($data['title']) ? sprintf('%s %s', $actionName, $modelName) : h($data['title']) ?>
+
+ = $formCreate ?>
+ = $ajaxFlashMessage ?>
+
+
+ = $data['description'] ?>
+
+
+
+ = $fieldsString ?>
+
+
+
+
+ =
+ $this->Bootstrap->accordion(
+ [
+ 'class' => 'mb-3'
+ ],
+ [
+ [
+ '_open' => true,
+ 'header' => [
+ 'title' => __('Meta fields')
+ ],
+ 'body' => $metaTemplateString,
+ ],
+ ]
+ );
+ ?>
+
+
+ = $this->element('genericElements/Form/submitButton', $submitButtonData); ?>
+ = $formEnd; ?>
+
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/formLayouts/formRaw.php b/templates/element/genericElements/Form/formLayouts/formRaw.php
new file mode 100644
index 0000000..371cb0a
--- /dev/null
+++ b/templates/element/genericElements/Form/formLayouts/formRaw.php
@@ -0,0 +1,28 @@
+
+
+ = $data['description'] ?>
+
+
+= $ajaxFlashMessage ?>
+= $formCreate ?>
+= $fieldsString ?>
+
+
+ =
+ $this->Bootstrap->accordion(
+ [
+ 'class' => 'mb-3'
+ ],
+ [
+ [
+ '_open' => true,
+ 'header' => [
+ 'title' => __('Meta fields')
+ ],
+ 'body' => $metaTemplateString,
+ ],
+ ]
+ );
+ ?>
+
+= $formEnd; ?>
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php
index 2e509ba..446cc48 100644
--- a/templates/element/genericElements/Form/genericForm.php
+++ b/templates/element/genericElements/Form/genericForm.php
@@ -1,5 +1,5 @@
Form->setConfig('errorClass', 'is-invalid');
$modelForForm = empty($data['model']) ?
h(\Cake\Utility\Inflector::singularize(\Cake\Utility\Inflector::classify($this->request->getParam('controller')))) :
@@ -19,13 +20,13 @@
$entity = isset($entity) ? $entity : null;
$fieldsString = '';
$simpleFieldWhitelist = [
- 'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required'
+ 'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required', 'templates', 'options', 'value', 'checked'
];
- //$fieldsArrayForPersistence = array();
if (empty($data['url'])) {
$data['url'] = ["controller" => $this->request->getParam('controller'), "action" => $this->request->getParam('url')];
}
$formRandomValue = Cake\Utility\Security::randomString(8);
+ $initSelect2 = false;
$formCreate = $this->Form->create($entity, ['id' => 'form-' . $formRandomValue]);
$default_template = [
'inputContainer' => '{{content}}
',
@@ -36,7 +37,9 @@
'select' => '{{content}} ',
'checkbox' => ' ',
'checkboxFormGroup' => '{{label}}',
- 'formGroup' => '{{label}}
{{input}}{{error}}
',
+ 'radio' => ' ',
+ 'radioWrapper' => '{{label}}',
+ 'formGroup' => '{{label}} {{input}}{{error}}
',
'nestingLabel' => '{{hidden}}{{text}}
{{input}}
',
'option' => '{{text}} ',
'optgroup' => '{{content}} ',
@@ -52,6 +55,7 @@
continue;
}
}
+ $initSelect2 = $initSelect2 || (!empty($fieldData['type']) && $fieldData['type'] == 'dropdown' && !empty($fieldData['select2']));
$formTemplate = $default_template;
if (!empty($fieldData['floating-label'])) {
$formTemplate['inputContainer'] = '{{content}}
';
@@ -59,6 +63,9 @@
$formTemplate['formGroup'] = '{{input}}{{label}}';
$fieldData['placeholder'] = !empty($fieldData['label']) ? $fieldData['label'] : h($fieldData['field']);
}
+ if (!empty($data['templates'])) {
+ $formTemplate = array_merge($formTemplate, $data['templates']);
+ }
// we reset the template each iteration as individual fields might override the defaults.
$this->Form->setConfig($formTemplate);
$this->Form->setTemplates($formTemplate);
@@ -66,7 +73,8 @@
continue;
}
$fieldsString .= $this->element(
- 'genericElements/Form/fieldScaffold', [
+ 'genericElements/Form/fieldScaffold',
+ [
'fieldData' => $fieldData,
'form' => $this->Form,
'simpleFieldWhitelist' => $simpleFieldWhitelist
@@ -74,10 +82,11 @@
);
}
}
- if (!empty($data['metaTemplates']) && $data['metaTemplates']->count() > 0) {
+ $metaTemplateString = '';
+ if (!empty($entity['MetaTemplates']) && count($entity['MetaTemplates']) > 0) {
$metaTemplateString = $this->element(
- 'genericElements/Form/metaTemplateScaffold', [
- 'metaTemplatesData' => $data['metaTemplates'],
+ 'genericElements/Form/metaTemplateScaffold',
+ [
'form' => $this->Form,
]
);
@@ -100,82 +109,42 @@
$actionName = h(\Cake\Utility\Inflector::humanize($this->request->getParam('action')));
$modelName = h(\Cake\Utility\Inflector::humanize(\Cake\Utility\Inflector::singularize($this->request->getParam('controller'))));
if (!empty($ajax)) {
- echo $this->element('genericElements/genericModal', [
+ $seedModal = 'mseed-' . mt_rand();
+ echo $this->Bootstrap->modal([
'title' => empty($data['title']) ? sprintf('%s %s', $actionName, $modelName) : h($data['title']),
- 'body' => sprintf(
- '%s%s%s%s%s%s',
- empty($data['description']) ? '' : sprintf(
- '%s
',
- $data['description']
- ),
- $ajaxFlashMessage,
- $formCreate,
- $fieldsString,
- empty($metaTemplateString) ? '' : $this->element(
- 'genericElements/accordion_scaffold', [
- 'children' => [
- [
- 'body' => $metaTemplateString,
- 'title' => 'Meta fields'
- ]
- ]
- ]
- ),
- $formEnd
- ),
- 'actionButton' => $this->element('genericElements/Form/submitButton', $submitButtonData),
- 'class' => 'modal-lg'
+ 'bodyHtml' => $this->element('genericElements/Form/formLayouts/formRaw', [
+ 'formCreate' => $formCreate,
+ 'ajaxFlashMessage' => $ajaxFlashMessage,
+ 'fieldsString' => $fieldsString,
+ 'formEnd' => $formEnd,
+ 'metaTemplateString' => $metaTemplateString,
+ ]),
+ 'size' => !empty($fieldsString) ? 'xl' : 'lg',
+ 'type' => 'confirm',
+ 'modalClass' => $seedModal,
]);
} else if (!empty($raw)) {
- echo sprintf(
- '%s%s%s%s%s%s',
- empty($data['description']) ? '' : sprintf(
- '%s
',
- $data['description']
- ),
- $ajaxFlashMessage,
- $formCreate,
- $fieldsString,
- empty($metaTemplateString) ? '' : $this->element(
- 'genericElements/accordion_scaffold', [
- 'children' => [
- [
- 'body' => $metaTemplateString,
- 'title' => 'Meta fields'
- ]
- ]
- ]
- ),
- $formEnd
- );
+ echo $this->element('genericElements/Form/formLayouts/formDefault', [
+ 'actionName' => $actionName,
+ 'modelName' => $modelName,
+ 'submitButtonData' => $submitButtonData,
+ 'formCreate' => $formCreate,
+ 'ajaxFlashMessage' => $ajaxFlashMessage,
+ 'fieldsString' => $fieldsString,
+ 'formEnd' => $formEnd,
+ 'metaTemplateString' => $metaTemplateString,
+ ]);
} else {
- echo sprintf(
- '%s%s %s%s%s%s%s%s%s%s%s',
- empty($ajax) ? '' : '',
- empty($data['title']) ? sprintf('%s %s', $actionName, $modelName) : h($data['title']),
- $formCreate,
- $ajaxFlashMessage,
- empty($data['description']) ? '' : sprintf(
- '
%s
',
- $data['description']
- ),
- sprintf('
%s
', $fieldsString),
- empty($metaTemplateString) ? '' : $this->element(
- 'genericElements/accordion_scaffold', [
- 'children' => [
- [
- 'body' => $metaTemplateString,
- 'title' => 'Meta fields',
- ]
- ],
- 'class' => 'mb-2'
- ]
- ),
- $this->element('genericElements/Form/submitButton', $submitButtonData),
- $formEnd,
- '
',
- empty($ajax) ? '
' : ''
- );
+ echo $this->element('genericElements/Form/formLayouts/formDefault', [
+ 'actionName' => $actionName,
+ 'modelName' => $modelName,
+ 'submitButtonData' => $submitButtonData,
+ 'formCreate' => $formCreate,
+ 'ajaxFlashMessage' => $ajaxFlashMessage,
+ 'fieldsString' => $fieldsString,
+ 'formEnd' => $formEnd,
+ 'metaTemplateString' => $metaTemplateString,
+ ]);
}
?>
+
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/metaTemplateForm.php b/templates/element/genericElements/Form/metaTemplateForm.php
new file mode 100644
index 0000000..0a4d934
--- /dev/null
+++ b/templates/element/genericElements/Form/metaTemplateForm.php
@@ -0,0 +1,77 @@
+ '{{content}}
',
+ 'inputContainerError' => '{{content}}
',
+ 'formGroup' => '{{label}} {{input}}{{error}}
',
+ 'error' => '{{content}}
',
+ 'errorList' => '',
+ 'errorItem' => '{{text}} ',
+];
+$this->Form->setTemplates($default_template);
+$backupTemplates = $this->Form->getTemplates();
+
+$fieldsHtml = '';
+foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
+ $metaTemplateField->label = Inflector::humanize($metaTemplateField->field);
+ if (!empty($metaTemplateField->metaFields)) {
+ if (!empty($metaTemplateField->multiple)) {
+ $fieldsHtml .= $this->element(
+ 'genericElements/Form/multiFieldScaffold',
+ [
+ 'metaFieldsEntities' => $metaTemplateField->metaFields,
+ 'metaTemplateField' => $metaTemplateField,
+ 'multiple' => !empty($metaTemplateField->multiple),
+ 'form' => $this->Form,
+ ]
+ );
+ } else {
+ $metaField = reset($metaTemplateField->metaFields);
+ $fieldData = [
+ 'label' => $metaTemplateField->label,
+ ];
+ if (isset($metaField->id)) {
+ $fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, $metaField->id);
+ } else {
+ $fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, array_key_first($metaTemplateField->metaFields));
+ }
+ $this->Form->setTemplates($backupTemplates);
+ $fieldsHtml .= $this->element(
+ 'genericElements/Form/fieldScaffold',
+ [
+ 'fieldData' => $fieldData,
+ 'metaTemplateField' => $metaTemplateField,
+ 'form' => $this->Form
+ ]
+ );
+ }
+ } else {
+ if (!empty($metaTemplateField->multiple)) {
+ $fieldsHtml .= $this->element(
+ 'genericElements/Form/multiFieldScaffold',
+ [
+ 'metaFieldsEntities' => [],
+ 'metaTemplateField' => $metaTemplateField,
+ 'multiple' => !empty($metaTemplateField->multiple),
+ 'form' => $this->Form,
+ ]
+ );
+ } else {
+ $this->Form->setTemplates($backupTemplates);
+ $fieldData = [
+ 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
+ 'label' => $metaTemplateField->label,
+ ];
+ $fieldsHtml .= $this->element(
+ 'genericElements/Form/fieldScaffold',
+ [
+ 'fieldData' => $fieldData,
+ 'form' => $this->Form
+ ]
+ );
+ }
+ }
+}
+echo $fieldsHtml;
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/metaTemplateScaffold.php b/templates/element/genericElements/Form/metaTemplateScaffold.php
index 614dc2a..00dc9be 100644
--- a/templates/element/genericElements/Form/metaTemplateScaffold.php
+++ b/templates/element/genericElements/Form/metaTemplateScaffold.php
@@ -1,32 +1,24 @@
Form->getTemplates();
$tabData = [];
-foreach($metaTemplatesData as $i => $metaTemplate) {
- if ($metaTemplate->is_default) {
- $tabData['navs'][$i] = [
- 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate])
- ];
- } else {
- $tabData['navs'][$i] = [
- 'text' => $metaTemplate->name
- ];
- }
+foreach ($entity->MetaTemplates as $i => $metaTemplate) {
+ $tabData['navs'][$i] = [
+ 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate])
+ ];
$fieldsHtml = '';
- foreach ($metaTemplate->meta_template_fields as $metaField) {
- $metaField->label = Inflector::humanize($metaField->field);
- $metaField->field = sprintf('%s.%s.%s', 'metaFields', $metaField->meta_template_id, $metaField->field);
- $fieldsHtml .= $this->element(
- 'genericElements/Form/fieldScaffold', [
- 'fieldData' => $metaField->toArray(),
- 'form' => $this->Form
- ]
- );
- }
+ $fieldsHtml .= $this->element(
+ 'genericElements/Form/metaTemplateForm',
+ [
+ 'metaTemplate' => $metaTemplate,
+ ]
+ );
$tabData['content'][$i] = $fieldsHtml;
}
+$this->Form->setTemplates($backupTemplates);
echo $this->Bootstrap->Tabs([
'pills' => true,
'data' => $tabData,
- 'nav-class' => ['pb-1']
-]);
\ No newline at end of file
+ 'nav-class' => ['shadow mb-3 p-2 rounded'],
+ 'content-class' => ['pt-2 px-3']
+]);
diff --git a/templates/element/genericElements/Form/multiFieldButton.php b/templates/element/genericElements/Form/multiFieldButton.php
new file mode 100644
index 0000000..c7c778b
--- /dev/null
+++ b/templates/element/genericElements/Form/multiFieldButton.php
@@ -0,0 +1,71 @@
+
+
+ %s
%s ', $this->Bootstrap->icon('plus'), __('Add another {0}', h($metaTemplateFieldName)));
+ $content = sprintf(
+ '%s%s',
+ $this->Bootstrap->button([
+ 'nodeType' => 'a',
+ 'icon' => 'plus',
+ 'variant' => 'secondary',
+ 'size' => 'xs',
+ ]),
+ $this->Bootstrap->button([
+ 'nodeType' => 'a',
+ 'text' => __('Add another {0}', h($metaTemplateFieldName)),
+ 'variant' => 'link',
+ 'class' => ['link-secondary'],
+ 'size' => 'xs',
+ ])
+ );
+ ?>
+ =
+ $this->Bootstrap->button([
+ 'id' => $seed,
+ 'html' => $content,
+ 'variant' => 'link',
+ 'size' => 'xs',
+ ]);
+ ?>
+
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/multiFieldMetaFieldTemplate.php b/templates/element/genericElements/Form/multiFieldMetaFieldTemplate.php
new file mode 100644
index 0000000..162b894
--- /dev/null
+++ b/templates/element/genericElements/Form/multiFieldMetaFieldTemplate.php
@@ -0,0 +1,15 @@
+ false,
+ 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.{count}', $metaTemplateField['meta_template_id'], $metaTemplateField['id']),
+ 'class' => 'metafield-template',
+ ];
+ echo $this->element(
+ 'genericElements/Form/fieldScaffold',
+ [
+ 'fieldData' => $fieldData,
+ 'form' => $form
+ ]
+ );
+}
diff --git a/templates/element/genericElements/Form/multiFieldScaffold.php b/templates/element/genericElements/Form/multiFieldScaffold.php
new file mode 100644
index 0000000..3a73125
--- /dev/null
+++ b/templates/element/genericElements/Form/multiFieldScaffold.php
@@ -0,0 +1,92 @@
+ '{{content}}
',
+ 'inputContainerError' => '{{content}}
',
+ 'formGroup' => '{{label}} {{input}}{{error}}
',
+];
+$form->setTemplates($default_template);
+
+$fieldsHtml = '';
+$labelPrintedOnce = false;
+if (!empty($metaFieldsEntities)) {
+ foreach ($metaFieldsEntities as $i => $metaFieldsEntity) {
+ $metaFieldsEntity->label = Inflector::humanize($metaFieldsEntity->field);
+ $fieldData = [
+ 'label' => $metaFieldsEntity->label,
+ 'field' => sprintf(
+ 'MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value',
+ $metaFieldsEntity->meta_template_id,
+ $metaFieldsEntity->meta_template_field_id,
+ $metaFieldsEntity->id
+ ),
+ ];
+ if($metaFieldsEntity->isNew()) {
+ $fieldData['field'] = sprintf(
+ 'MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value',
+ $metaFieldsEntity->meta_template_id,
+ $metaFieldsEntity->meta_template_field_id,
+ $i
+ );
+ $fieldData['class'] = 'new-metafield';
+ }
+ if ($labelPrintedOnce) { // Only the first input can have a label
+ $fieldData['label'] = false;
+ }
+ $labelPrintedOnce = true;
+ $fieldsHtml .= $this->element(
+ 'genericElements/Form/fieldScaffold',
+ [
+ 'fieldData' => $fieldData,
+ 'form' => $form
+ ]
+ );
+ }
+}
+if (!empty($metaTemplateField) && !empty($multiple)) { // Add multiple field button
+ $metaTemplateField->label = Inflector::humanize($metaTemplateField->field);
+ $emptyMetaFieldInput = '';
+ if (empty($metaFieldsEntities)) { // Include editable field for meta-template not containing a meta-field
+ $emptyMetaFieldInput = $this->element(
+ 'genericElements/Form/fieldScaffold',
+ [
+ 'fieldData' => [
+ 'label' => $metaTemplateField->label,
+ 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
+ 'class' => 'new-metafield',
+ ],
+ 'form' => $form,
+ ]
+ );
+ }
+ $emptyInputForSecurityComponent = $this->element(
+ 'genericElements/Form/fieldScaffold',
+ [
+ 'fieldData' => [
+ 'label' => false,
+ 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new[]', $metaTemplateField->meta_template_id, $metaTemplateField->id),
+ 'value' => '',
+ ],
+ 'form' => $form,
+ ]
+ );
+ $multiFieldButtonHtml = sprintf(
+ '',
+ $this->element(
+ 'genericElements/Form/multiFieldButton',
+ [
+ 'metaTemplateFieldName' => $metaTemplateField->field,
+ ]
+ )
+ );
+ $fieldsHtml .= $emptyMetaFieldInput;
+ $fieldsHtml .= sprintf('%s
', $emptyInputForSecurityComponent);
+ $fieldsHtml .= $multiFieldButtonHtml;
+}
+?>
+
+
+ = $fieldsHtml; ?>
+
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/submitButton.php b/templates/element/genericElements/Form/submitButton.php
index 55062dd..be9352e 100644
--- a/templates/element/genericElements/Form/submitButton.php
+++ b/templates/element/genericElements/Form/submitButton.php
@@ -11,7 +11,8 @@
} else {
echo $this->Form->button(empty($text) ? __('Submit') : h($text), [
'class' => 'btn btn-' . (empty($type) ? 'primary' : h($type)),
- 'type' => 'submit'
+ 'type' => 'submit',
+ 'data-form-id' => '#form-' . h($formRandomValue)
]);
}
?>
diff --git a/templates/element/genericElements/IndexTable/Fields/actions.php b/templates/element/genericElements/IndexTable/Fields/actions.php
index c280aad..543cb2e 100644
--- a/templates/element/genericElements/IndexTable/Fields/actions.php
+++ b/templates/element/genericElements/IndexTable/Fields/actions.php
@@ -101,12 +101,13 @@
$action['onclick'] = sprintf('UI.submissionModalForIndex(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue);
}
echo sprintf(
- ' ',
+ ' ',
$url,
empty($action['title']) ? '' : h($action['title']),
empty($action['title']) ? '' : h($action['title']),
empty($action['dbclickAction']) ? '' : 'class="dblclickActionElement"',
empty($action['onclick']) ? '' : sprintf('onClick="%s"', $action['onclick']),
+ empty($action['variant']) ? 'outline-dark' : h($action['variant']),
$this->FontAwesome->getClass($action['icon'])
);
}
diff --git a/templates/element/genericElements/IndexTable/Fields/generic_field.php b/templates/element/genericElements/IndexTable/Fields/generic_field.php
index abc8eca..b3c150e 100644
--- a/templates/element/genericElements/IndexTable/Fields/generic_field.php
+++ b/templates/element/genericElements/IndexTable/Fields/generic_field.php
@@ -2,14 +2,16 @@
$data = $this->Hash->extract($row, $field['data_path']);
if (is_array($data)) {
if (count($data) > 1) {
- $data = implode(', ', $data);
+ $data = implode(' ', array_map('h', $data));
} else {
if (count($data) > 0) {
- $data = $data[0];
+ $data = h($data[0]);
} else {
$data = '';
}
}
+ } else {
+ $data = h($data);
}
if (is_bool($data)) {
$data = sprintf(
@@ -17,7 +19,6 @@
$data ? 'check' : 'times'
);
} else {
- $data = h($data);
if (!empty($field['options'])) {
$options = $this->Hash->extract($row, $field['options']);
if (!empty($options)) {
diff --git a/templates/element/genericElements/IndexTable/Fields/json.php b/templates/element/genericElements/IndexTable/Fields/json.php
index 6c14248..2aca821 100644
--- a/templates/element/genericElements/IndexTable/Fields/json.php
+++ b/templates/element/genericElements/IndexTable/Fields/json.php
@@ -1,9 +1,12 @@
Hash->extract($row, $field['data_path']));
+ $data = $this->Hash->extract($row, $field['data_path']);
// I feed dirty for this...
if (is_array($data) && count($data) === 1 && isset($data[0])) {
$data = $data[0];
}
+ if (!is_array($data)) {
+ $data = json_decode($data, true);
+ }
echo sprintf(
'
',
h($k)
diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php
new file mode 100644
index 0000000..1f92f0e
--- /dev/null
+++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php
@@ -0,0 +1,141 @@
+request->getQuery('statistics_entry_amount', 5);
+$statistics_pie_include_remaining = $this->request->getQuery('statistics_include_remainging', true);
+if (is_string($statistics_pie_include_remaining)) {
+ $statistics_pie_include_remaining = $statistics_pie_include_remaining == 'true' ? true : false;
+}
+$statistics_pie_ignore_null = $this->request->getQuery('statistics_ignore_null', true);
+if (is_string($statistics_pie_ignore_null)) {
+ $statistics_pie_ignore_null = $statistics_pie_ignore_null == 'true' ? true : false;
+}
+
+$seedPiechart = 's-' . mt_rand();
+foreach ($statistics['usage'] as $scope => $graphData) {
+ $pieChart = $this->element('charts/pie', [
+ 'data' => $graphData,
+ 'chartOptions' => [
+ 'chart' => [
+ 'height' => '80px',
+ 'sparkline' => [
+ 'enabled' => true,
+ ]
+ ],
+ 'plotOptions' => [
+ 'pie' => [
+ 'customScale' => 0.9,
+ ]
+ ],
+ ],
+ ]);
+ $titleHtml = sprintf(
+ '%s%s ',
+ Inflector::Pluralize(Inflector::Humanize(h($scope))),
+ $this->Bootstrap->button([
+ 'variant' => 'link',
+ 'icon' => 'cog',
+ 'size' => 'xs',
+ 'nodeType' => 'a',
+ 'onclick' => '',
+ 'class' => ['btn-statistics-pie-configurator-' . $seedPiechart],
+ 'params' => [
+ 'data-bs-toggle' => 'popover',
+ ]
+ ])
+ );
+ $panelHtml = sprintf(
+ '%s%s
',
+ $titleHtml,
+ $pieChart
+ );
+ $statPie = $this->Bootstrap->card([
+ 'variant' => 'secondary',
+ 'bodyHTML' => $panelHtml,
+ 'bodyClass' => 'py-1 px-2',
+ 'class' => ['shadow-sm', 'h-100']
+ ]);
+ $statisticsHtml .= sprintf('%s
', $statPie);
+}
+?>
+
+= $statisticsHtml ?>
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_scaffold.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_scaffold.php
new file mode 100644
index 0000000..251994b
--- /dev/null
+++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_scaffold.php
@@ -0,0 +1,16 @@
+element('genericElements/IndexTable/Statistics/index_statistic_timestamp', [
+ 'timeline' => $statistics,
+ ]);
+}
+if (!empty($statistics['usage'])) {
+ $statisticsHtml .= $this->element('genericElements/IndexTable/Statistics/index_statistic_field_amount', [
+ 'statistics' => $statistics,
+ ]);
+}
+$statisticsHtml = sprintf('', $statisticsHtml);
+echo sprintf('%s
', $statisticsHtml);
+?>
diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php
new file mode 100644
index 0000000..c202890
--- /dev/null
+++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php
@@ -0,0 +1,140 @@
+ $entry['time'], 'y' => $entry['count']];
+ }
+}
+if (!empty($timeline['modified']['timeline'])) {
+ $series[1]['name'] = __('Modified');
+ foreach ($timeline['modified']['timeline'] as $entry) {
+ $series[1]['data'][] = ['x' => $entry['time'], 'y' => $entry['count']];
+ }
+}
+
+$panelControlHtml = sprintf(
+ '
+ %s %s %s
+
',
+ $title,
+ $subTitle,
+ $this->Bootstrap->button([
+ 'variant' => 'link',
+ 'icon' => 'cog',
+ 'size' => 'xs',
+ 'nodeType' => 'a',
+ 'onclick' => '',
+ 'class' => ['btn-statistics-days-configurator-' . $seed,],
+ 'params' => [
+ 'data-bs-toggle' => 'popover',
+ ]
+ ])
+);
+$createdNumber = empty($timeline['created']) ? '' : sprintf(
+ '%s %s
',
+ __('{0} Created', $timeline['created']['variation']),
+ $this->Bootstrap->icon('plus', ['class' => ['fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
+ $timeline['created']['variation']
+);
+$modifiedNumber = empty($timeline['modified']) ? '' : sprintf(
+ '%s %s
',
+ __('{0} Modified', $timeline['modified']['variation']),
+ $this->Bootstrap->icon('edit', ['class' => ['fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
+ $timeline['modified']['variation']
+);
+$activityNumbers = sprintf('%s%s
', $createdNumber, $modifiedNumber);
+
+$leftContent = sprintf(
+ '%s%s',
+ $panelControlHtml,
+ $activityNumbers
+);
+$rightContent = sprintf('%s
', $this->element('charts/bar', [
+ 'series' => $series,
+ 'chartOptions' => array_merge(
+ [
+ 'chart' => [
+ 'height' => 60,
+ ],
+ 'stroke' => [
+ 'width' => 2,
+ 'curve' => 'smooth',
+ ],
+ ],
+ !empty($chartOptions) ? $chartOptions : []
+ )
+]));
+$cardContent = sprintf(
+ '',
+ $leftContent,
+ $rightContent
+);
+
+$card = $this->Bootstrap->card([
+ 'variant' => 'secondary',
+ 'bodyHTML' => $cardContent,
+ 'bodyClass' => 'py-1 px-2',
+ 'class' => ['shadow-sm', 'h-100']
+]);
+
+?>
+
+= $card ?>
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/IndexTable/headers.php b/templates/element/genericElements/IndexTable/headers.php
index e38714a..b949d12 100644
--- a/templates/element/genericElements/IndexTable/headers.php
+++ b/templates/element/genericElements/IndexTable/headers.php
@@ -21,8 +21,14 @@
}
}
+ if (!empty($header['element']) && $header['element'] === 'selector') {
+ $columnName = 'row-selector';
+ } else {
+ $columnName = h(\Cake\Utility\Inflector::variable(!empty($header['name']) ? $header['name'] : \Cake\Utility\Inflector::humanize($header['data_path'])));
+ }
$headersHtml .= sprintf(
- '%s ',
+ '%s ',
+ $columnName,
$header_data
);
}
diff --git a/templates/element/genericElements/IndexTable/index_table.php b/templates/element/genericElements/IndexTable/index_table.php
index fc2a066..114c9d9 100644
--- a/templates/element/genericElements/IndexTable/index_table.php
+++ b/templates/element/genericElements/IndexTable/index_table.php
@@ -1,5 +1,7 @@
element('/genericElements/IndexTable/index_table', [
* 'top_bar' => (
* // search/filter bar information compliant with ListTopBar
@@ -12,117 +14,155 @@
* ),
* 'title' => optional title,
* 'description' => optional description,
+ * 'index_statistics' => optional statistics to be displayed for the index,
* 'primary_id_path' => path to each primary ID (extracted and passed as $primary to fields)
* ));
*
*/
- $tableRandomValue = Cake\Utility\Security::randomString(8);
- echo '';
- if (!empty($data['title'])) {
- echo sprintf('
%s ', h($data['title']));
- }
- if (!empty($data['description'])) {
- echo sprintf(
- '
%s
',
- empty($data['description']) ? '' : h($data['description'])
- );
- }
- echo '
';
- if (!empty($data['html'])) {
- echo sprintf('
%s
', $data['html']);
- }
- $skipPagination = isset($data['skip_pagination']) ? $data['skip_pagination'] : 0;
- if (!$skipPagination) {
- $paginationData = !empty($data['paginatorOptions']) ? $data['paginatorOptions'] : [];
- echo $this->element(
- '/genericElements/IndexTable/pagination',
- [
- 'paginationOptions' => $paginationData,
- 'tableRandomValue' => $tableRandomValue
- ]
- );
- echo $this->element(
- '/genericElements/IndexTable/pagination_links'
- );
- }
- $multiSelectData = getMultiSelectData($data['top_bar']);
- if (!empty($multiSelectData)) {
- $multiSelectField = [
- 'element' => 'selector',
- 'class' => 'short',
- 'data' => $multiSelectData['data']
+
+$newMetaFields = [];
+if (!empty($requestedMetaFields)) { // Create mapping for new index table fields on the fly
+ foreach ($requestedMetaFields as $requestedMetaField) {
+ $template_id = $requestedMetaField['template_id'];
+ $meta_template_field_id = $requestedMetaField['meta_template_field_id'];
+ $newMetaFields[] = [
+ 'name' => $meta_templates[$template_id]['meta_template_fields'][$meta_template_field_id]['field'],
+ 'data_path' => "MetaTemplates.{$template_id}.meta_template_fields.{$meta_template_field_id}.metaFields.{n}.value",
+ 'element' => 'generic_field',
+ '_metafield' => true,
+ '_automatic_field' => true,
];
- array_unshift($data['fields'], $multiSelectField);
}
- if (!empty($data['top_bar'])) {
- echo $this->element(
- '/genericElements/ListTopBar/scaffold',
- [
- 'data' => $data['top_bar'],
- 'tableRandomValue' => $tableRandomValue
- ]
- );
- }
- $rows = '';
- $row_element = isset($data['row_element']) ? $data['row_element'] : 'row';
- $options = isset($data['options']) ? $data['options'] : [];
- $actions = isset($data['actions']) ? $data['actions'] : [];
- if ($this->request->getParam('prefix') === 'Open') {
- $actions = [];
- }
- $dblclickActionArray = !empty($actions) ? $this->Hash->extract($actions, '{n}[dbclickAction]') : [];
- $dbclickAction = '';
- foreach ($data['data'] as $k => $data_row) {
- $primary = null;
- if (!empty($data['primary_id_path'])) {
- $primary = $this->Hash->extract($data_row, $data['primary_id_path'])[0];
- }
- if (!empty($dblclickActionArray)) {
- $dbclickAction = sprintf("changeLocationFromIndexDblclick(%s)", $k);
- }
- $rows .= sprintf(
- '
%s ',
- h($k),
- empty($dbclickAction) ? '' : 'ondblclick="' . $dbclickAction . '"',
- empty($primary) ? '' : 'data-primary-id="' . $primary . '"',
- empty($data['row_modifier']) ? '' : h($data['row_modifier']($data_row)),
- empty($data['class']) ? '' : h($data['row_class']),
- $this->element(
- '/genericElements/IndexTable/' . $row_element,
- [
- 'k' => $k,
- 'row' => $data_row,
- 'fields' => $data['fields'],
- 'options' => $options,
- 'actions' => $actions,
- 'primary' => $primary,
- 'tableRandomValue' => $tableRandomValue
+}
+$data['fields'] = array_merge($data['fields'], $newMetaFields);
+
+$tableRandomValue = Cake\Utility\Security::randomString(8);
+echo '
';
+if (!empty($data['title'])) {
+ echo Text::insert(
+ '
:title :help ',
+ [
+ 'title' => h($data['title']),
+ 'help' => $this->Bootstrap->icon('info', [
+ 'class' => ['fs-6', 'align-text-top',],
+ 'title' => empty($data['description']) ? '' : h($data['description']),
+ 'params' => [
+ 'data-bs-toggle' => 'tooltip',
]
- )
- );
+ ]),
+ ]
+ );
+}
+
+if(!empty($notice)) {
+ echo $this->Bootstrap->alert($notice);
+}
+
+if (!empty($modelStatistics)) {
+ echo $this->element('genericElements/IndexTable/Statistics/index_statistic_scaffold', [
+ 'statistics' => $modelStatistics,
+ ]);
+}
+
+
+echo '
';
+if (!empty($data['html'])) {
+ echo sprintf('
%s
', $data['html']);
+}
+$skipPagination = isset($data['skip_pagination']) ? $data['skip_pagination'] : 0;
+if (!$skipPagination) {
+ $paginationData = !empty($data['paginatorOptions']) ? $data['paginatorOptions'] : [];
+ echo $this->element(
+ '/genericElements/IndexTable/pagination',
+ [
+ 'paginationOptions' => $paginationData,
+ 'tableRandomValue' => $tableRandomValue
+ ]
+ );
+ echo $this->element(
+ '/genericElements/IndexTable/pagination_links'
+ );
+}
+$multiSelectData = getMultiSelectData($data['top_bar']);
+if (!empty($multiSelectData)) {
+ $multiSelectField = [
+ 'element' => 'selector',
+ 'class' => 'short',
+ 'data' => $multiSelectData['data']
+ ];
+ array_unshift($data['fields'], $multiSelectField);
+}
+if (!empty($data['top_bar'])) {
+ echo $this->element(
+ '/genericElements/ListTopBar/scaffold',
+ [
+ 'data' => $data['top_bar'],
+ 'table_data' => $data,
+ 'tableRandomValue' => $tableRandomValue
+ ]
+ );
+}
+$rows = '';
+$row_element = isset($data['row_element']) ? $data['row_element'] : 'row';
+$options = isset($data['options']) ? $data['options'] : [];
+$actions = isset($data['actions']) ? $data['actions'] : [];
+if ($this->request->getParam('prefix') === 'Open') {
+ $actions = [];
+}
+$dblclickActionArray = !empty($actions) ? $this->Hash->extract($actions, '{n}[dbclickAction]') : [];
+$dbclickAction = '';
+foreach ($data['data'] as $k => $data_row) {
+ $primary = null;
+ if (!empty($data['primary_id_path'])) {
+ $primary = $this->Hash->extract($data_row, $data['primary_id_path'])[0];
}
- $tbody = '
' . $rows . ' ';
- echo sprintf(
- '
',
- $tableRandomValue,
- $tableRandomValue,
+ if (!empty($dblclickActionArray)) {
+ $dbclickAction = sprintf("changeLocationFromIndexDblclick(%s)", $k);
+ }
+ $rows .= sprintf(
+ '
%s ',
+ h($k),
+ empty($dbclickAction) ? '' : 'ondblclick="' . $dbclickAction . '"',
+ empty($primary) ? '' : 'data-primary-id="' . $primary . '"',
+ empty($data['row_modifier']) ? '' : h($data['row_modifier']($data_row)),
+ empty($data['class']) ? '' : h($data['row_class']),
$this->element(
- '/genericElements/IndexTable/headers',
+ '/genericElements/IndexTable/' . $row_element,
[
+ 'k' => $k,
+ 'row' => $data_row,
'fields' => $data['fields'],
- 'paginator' => $this->Paginator,
- 'actions' => (empty($actions) ? false : true),
+ 'options' => $options,
+ 'actions' => $actions,
+ 'primary' => $primary,
'tableRandomValue' => $tableRandomValue
]
- ),
- $tbody
+ )
);
- if (!$skipPagination) {
- echo $this->element('/genericElements/IndexTable/pagination_counter', $paginationData);
- echo $this->element('/genericElements/IndexTable/pagination_links');
- }
- echo '
';
- echo '
';
+}
+$tbody = '
' . $rows . ' ';
+echo sprintf(
+ '
',
+ $tableRandomValue,
+ $tableRandomValue,
+ h($this->Url->build(['action' => $this->request->getParam('action'),])),
+ $this->element(
+ '/genericElements/IndexTable/headers',
+ [
+ 'fields' => $data['fields'],
+ 'paginator' => $this->Paginator,
+ 'actions' => (empty($actions) ? false : true),
+ 'tableRandomValue' => $tableRandomValue
+ ]
+ ),
+ $tbody
+);
+if (!$skipPagination) {
+ echo $this->element('/genericElements/IndexTable/pagination_counter', $paginationData);
+ echo $this->element('/genericElements/IndexTable/pagination_links');
+}
+echo '
';
+echo '
';
?>
$metaTemplate) {
+ foreach ($metaTemplate['meta_template_fields'] as $metaTemplateField) {
+ $filteringItems[h($metaTemplate->name)][] = ['id' => $metaTemplateField->id, 'name' => h($metaTemplateField->field), 'template_id' => $template_id];
+ }
+}
+
+$filteringForm = $this->Bootstrap->table(
+ [
+ 'small' => true,
+ 'striped' => false,
+ 'hover' => false,
+ 'tableClass' => ['indexMetaFieldsFilteringTable'],
+ ],
+ [
+ 'fields' => [
+ __('Meta Field'),
+ __('Operator'),
+ [
+ 'labelHtml' => sprintf(
+ '%s %s',
+ __('Value'),
+ sprintf('', __('Supports strict matches and LIKE matches with the `%` character.
Example: `%.com`'))
+ )
+ ],
+ __('Action')
+ ],
+ 'items' => []
+ ]
+);
+?>
+
+= $filteringForm; ?>
+
\ No newline at end of file
diff --git a/templates/element/genericElements/IndexTable/row.php b/templates/element/genericElements/IndexTable/row.php
index c019110..b426534 100644
--- a/templates/element/genericElements/IndexTable/row.php
+++ b/templates/element/genericElements/IndexTable/row.php
@@ -31,7 +31,7 @@
);
}
$rowHtml .= sprintf(
- '%s ',
+ '%s ',
(empty($field['id'])) ? '' : sprintf('id="%s"', $field['id']),
(empty($field['class'])) ? '' : sprintf(' class="%s"', $field['class']),
(empty($field['style'])) ? '' : sprintf(' style="%s"', $field['style']),
@@ -42,6 +42,10 @@
h(implode(', ', $field['data_path'])) :
(h($field['data_path']))
),
+ sprintf(
+ ' data-columnname="%s"',
+ h(\Cake\Utility\Inflector::variable(!empty($field['name']) ? $field['name'] : \Cake\Utility\Inflector::humanize($field['data_path'])))
+ ),
(empty($field['encode_raw_value']) || empty($field['data_path'])) ? '' : sprintf(' data-value="%s"', (h($this->Hash->extract($row, $field['data_path'])[0]))),
(empty($field['ondblclick'])) ? '' : sprintf(' ondblclick="%s"', $field['ondblclick']),
$valueField
diff --git a/templates/element/genericElements/ListTopBar/group_search.php b/templates/element/genericElements/ListTopBar/group_search.php
index db94351..6119c6f 100644
--- a/templates/element/genericElements/ListTopBar/group_search.php
+++ b/templates/element/genericElements/ListTopBar/group_search.php
@@ -15,12 +15,22 @@
if (!empty($data['quickFilter'])) {
$quickFilter = $data['quickFilter'];
}
+ if (!empty($quickFilterForMetaField['enabled'])) {
+ $quickFilter[] = [
+ 'MetaField\'s value' => !empty($quickFilterForMetaField['wildcard_search'])
+ ];
+ }
$filterEffective = !empty($quickFilter); // No filters will be picked up, thus rendering the filtering useless
$filteringButton = '';
if (!empty($data['allowFilering'])) {
$activeFilters = !empty($activeFilters) ? $activeFilters : [];
+ $numberActiveFilters = count($activeFilters);
+ if (!empty($activeFilters['filteringMetaFields'])) {
+ $numberActiveFilters += count($activeFilters['filteringMetaFields']) - 1;
+ }
$buttonConfig = [
'icon' => 'filter',
+ 'variant' => $numberActiveFilters > 0 ? 'warning' : 'primary',
'params' => [
'title' => __('Filter index'),
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
@@ -29,8 +39,8 @@
if (count($activeFilters) > 0) {
$buttonConfig['badge'] = [
'variant' => 'light',
- 'text' => count($activeFilters),
- 'title' => __n('There is {0} active filter', 'There are {0} active filters', count($activeFilters), count($activeFilters))
+ 'text' => $numberActiveFilters,
+ 'title' => __n('There is {0} active filter', 'There are {0} active filters', $numberActiveFilters, $numberActiveFilters)
];
}
$filteringButton = $this->Bootstrap->button($buttonConfig);
@@ -58,7 +68,8 @@
$filterEffective ? '' : 'disabled="disabled"'
);
echo sprintf(
- '%s%s
',
+ '%s%s
',
+ $filterEffective ? '' : 'd-none',
h($tableRandomValue),
$input,
$button
diff --git a/templates/element/genericElements/ListTopBar/group_table_action.php b/templates/element/genericElements/ListTopBar/group_table_action.php
new file mode 100644
index 0000000..e9de073
--- /dev/null
+++ b/templates/element/genericElements/ListTopBar/group_table_action.php
@@ -0,0 +1,85 @@
+element('/genericElements/ListTopBar/group_table_action/hiddenColumns', [
+ 'table_data' => $table_data,
+ 'tableSettings' => $tableSettings,
+ 'table_setting_id' => $data['table_setting_id'],
+]);
+
+$metaTemplateColumnMenu = [];
+if (!empty($meta_templates)) {
+ $metaTemplateColumnMenu[] = ['header' => true, 'text' => __('Meta Templates'), 'icon' => 'object-group',];
+ foreach ($meta_templates as $meta_template) {
+ $numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0;
+ $metaTemplateColumnMenu[] = [
+ 'text' => $meta_template->name,
+ 'badge' => [
+ 'text' => $numberActiveMetaField,
+ 'variant' => 'secondary',
+ 'title' => __n('{0} meta-field active for this meta-template', '{0} meta-fields active for this meta-template', $numberActiveMetaField, $numberActiveMetaField),
+ ],
+ 'keepOpen' => true,
+ 'menu' => [
+ [
+ 'html' => $this->element('/genericElements/ListTopBar/group_table_action/hiddenMetaColumns', [
+ 'tableSettings' => $tableSettings,
+ 'table_setting_id' => $data['table_setting_id'],
+ 'meta_template' => $meta_template,
+ ])
+ ]
+ ],
+ ];
+ }
+}
+$indexColumnMenu = array_merge(
+ [['header' => true, 'text' => sprintf('%s\'s fields', $this->request->getParam('controller'))]],
+ [['html' => $availableColumnsHtml]],
+ $metaTemplateColumnMenu
+);
+
+$compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_action/compactDisplay', [
+ 'table_data' => $table_data,
+ 'tableSettings' => $tableSettings,
+ 'table_setting_id' => $data['table_setting_id'],
+ 'compactDisplay' => $compactDisplay
+]);
+?>
+
+ Bootstrap->dropdownMenu([
+ 'dropdown-class' => 'ms-1',
+ 'alignment' => 'end',
+ 'direction' => 'down',
+ 'toggle-button' => [
+ 'icon' => 'sliders-h',
+ 'variant' => 'primary',
+ ],
+ 'submenu_alignment' => 'end',
+ 'submenu_direction' => 'start',
+ 'params' => [
+ 'data-table-random-value' => $tableRandomValue,
+ 'data-table_setting_id' => $data['table_setting_id'],
+ ],
+ 'menu' => [
+ [
+ 'text' => __('Show/hide columns'),
+ 'icon' => 'eye-slash',
+ 'keepOpen' => true,
+ 'menu' => $indexColumnMenu,
+ ],
+ [
+ 'html' => $compactDisplayHtml,
+ ]
+ ]
+ ]);
+ ?>
+
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/compactDisplay.php b/templates/element/genericElements/ListTopBar/group_table_action/compactDisplay.php
new file mode 100644
index 0000000..312ad37
--- /dev/null
+++ b/templates/element/genericElements/ListTopBar/group_table_action/compactDisplay.php
@@ -0,0 +1,46 @@
+
+
+
+
+ = __('Compact display') ?>
+ >
+
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php b/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
new file mode 100644
index 0000000..3a95b82
--- /dev/null
+++ b/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
@@ -0,0 +1,143 @@
+
+
+
+ %s
+
+ ',
+ h(\Cake\Utility\Inflector::variable($fieldName)),
+ h(\Cake\Utility\Inflector::variable($fieldName)),
+ $isVisible ? 'checked' : '',
+ h(\Cake\Utility\Inflector::variable($fieldName)),
+ h($fieldName)
+ );
+}
+
+$availableColumnsHtml = $this->Bootstrap->genNode('form', [
+ 'class' => ['visible-column-form', 'px-2 py-1'],
+], $availableColumnsHtml);
+echo $availableColumnsHtml;
+?>
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/hiddenMetaColumns.php b/templates/element/genericElements/ListTopBar/group_table_action/hiddenMetaColumns.php
new file mode 100644
index 0000000..8415dce
--- /dev/null
+++ b/templates/element/genericElements/ListTopBar/group_table_action/hiddenMetaColumns.php
@@ -0,0 +1,33 @@
+meta_template_fields as $j => $meta_template_field) {
+ $fieldName = $meta_template_field['field'];
+ $fieldId = "metatemplate-{$meta_template_field->meta_template_id}-{$meta_template_field->id}";
+ $isVisible = false;
+ if (!empty($tableSettings['visible_meta_column']) && !empty($tableSettings['visible_meta_column'][$meta_template_field->meta_template_id])) {
+ $isVisible = in_array($meta_template_field->id, $tableSettings['visible_meta_column'][$meta_template_field->meta_template_id]);
+ }
+ $availableMetaColumnsHtml .= sprintf(
+ '
+
+
+ %s
+
+
',
+ h($fieldId),
+ h($fieldId),
+ $isVisible ? 'checked' : '',
+ h($fieldId),
+ h($fieldName)
+ );
+ }
+}
+
+$availableMetaColumnsHtml = $this->Bootstrap->genNode('form', [
+ 'class' => ['visible-meta-column-form', 'px-2 py-1'],
+], $availableMetaColumnsHtml);
+echo $availableMetaColumnsHtml;
+?>
diff --git a/templates/element/genericElements/ListTopBar/scaffold.php b/templates/element/genericElements/ListTopBar/scaffold.php
index 675a660..8bd056b 100644
--- a/templates/element/genericElements/ListTopBar/scaffold.php
+++ b/templates/element/genericElements/ListTopBar/scaffold.php
@@ -1,8 +1,13 @@
element('/genericElements/ListTopBar/group_' . (empty($group['type']) ? 'simple' : h($group['type'])), array('data' => $group, 'tableRandomValue' => $tableRandomValue));
+ $groups .= $this->element('/genericElements/ListTopBar/group_' . (empty($group['type']) ? 'simple' : h($group['type'])), array(
+ 'data' => $group,
+ 'tableRandomValue' => $tableRandomValue,
+ 'table_data' => $table_data,
+ ));
$hasGroupSearch = $hasGroupSearch || (!empty($group['type']) && $group['type'] == 'search');
}
$tempClass = "btn-toolbar";
diff --git a/templates/element/genericElements/MetaTemplates/metaTemplateNav.php b/templates/element/genericElements/MetaTemplates/metaTemplateNav.php
index 9f9e36d..11c16c1 100644
--- a/templates/element/genericElements/MetaTemplates/metaTemplateNav.php
+++ b/templates/element/genericElements/MetaTemplates/metaTemplateNav.php
@@ -1,4 +1,12 @@
= h($metaTemplate->name) ?>
-
+ =
+ $this->Bootstrap->badge([
+ 'variant' => !empty($metaTemplate['hasNewerVersion']) ? 'warning' : 'primary',
+ 'text' => sprintf('v%s', h($metaTemplate->version))
+ ])
+ ?>
+ is_default)): ?>
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/SingleViews/Fields/ownerField.php b/templates/element/genericElements/SingleViews/Fields/ownerField.php
new file mode 100644
index 0000000..4f213a7
--- /dev/null
+++ b/templates/element/genericElements/SingleViews/Fields/ownerField.php
@@ -0,0 +1,6 @@
+element('/genericElements/IndexTable/Fields/owner', [
+ 'field' => $field,
+ 'row' => $data
+]);
+?>
diff --git a/templates/element/genericElements/SingleViews/metafields_panel.php b/templates/element/genericElements/SingleViews/metafields_panel.php
index ed718b1..e426679 100644
--- a/templates/element/genericElements/SingleViews/metafields_panel.php
+++ b/templates/element/genericElements/SingleViews/metafields_panel.php
@@ -1,34 +1,63 @@
[],
'content' => []
];
-foreach($data['metaTemplates'] as $metaTemplate) {
+foreach($data['MetaTemplates'] as $metaTemplate) {
if (!empty($metaTemplate->meta_template_fields)) {
- if ($metaTemplate->is_default) {
- $tabData['navs'][] = [
- 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate])
- ];
- } else {
- $tabData['navs'][] = [
- 'text' => $metaTemplate->name
- ];
- }
+ $tabData['navs'][] = [
+ 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate])
+ ];
$fields = [];
foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
- $metaField = $metaTemplateField->meta_fields[0];
- $fields[] = [
- 'key' => $metaField->field,
- 'raw' => $metaField->value
- ];
+ $labelPrintedOnce = false;
+ if (!empty($metaTemplateField->metaFields)) {
+ foreach ($metaTemplateField->metaFields as $metaField) {
+ $fields[] = [
+ 'key' => !$labelPrintedOnce ? $metaField->field : '',
+ 'raw' => $metaField->value
+ ];
+ $labelPrintedOnce = true;
+ }
+ }
}
$listTable = $this->Bootstrap->listTable([
'hover' => false,
'elementsRootPath' => '/genericElements/SingleViews/Fields/'
],[
'item' => false,
- 'fields' => $fields
+ 'fields' => $fields,
+ 'caption' => __n(
+ 'This meta-template contains {0} meta-field',
+ 'This meta-template contains {0} meta-fields',
+ count($fields),
+ count($fields)
+ )
]);
+ if (!empty($metaTemplate['hasNewerVersion']) && !empty($fields)) {
+ $listTable = $this->Bootstrap->alert([
+ 'html' => sprintf(
+ '%s
%s
',
+ __('These meta-fields are registered under an outdated template. Newest template is {0}, current is {1}.', $metaTemplate['hasNewerVersion']->version, $metaTemplate->version),
+ $this->Bootstrap->button([
+ 'text' => __('Migrate to version {0}', $metaTemplate['hasNewerVersion']->version),
+ 'variant' => 'success',
+ 'nodeType' => 'a',
+ 'params' => [
+ 'href' => Router::url([
+ 'controller' => 'metaTemplates',
+ 'action' => 'migrateOldMetaTemplateToNewestVersionForEntity',
+ $metaTemplate->id,
+ $data->id,
+ ])
+ ]
+ ])
+ ),
+ 'variant' => 'warning',
+ ]) . $listTable;
+ }
$tabData['content'][] = $listTable;
}
}
diff --git a/templates/element/genericElements/SingleViews/single_view.php b/templates/element/genericElements/SingleViews/single_view.php
index 90bcbc1..f1812b0 100644
--- a/templates/element/genericElements/SingleViews/single_view.php
+++ b/templates/element/genericElements/SingleViews/single_view.php
@@ -36,7 +36,7 @@
'tableClass' => 'col-sm-8',
'elementsRootPath' => '/genericElements/SingleViews/Fields/'
];
- if (!empty($data['metaTemplates']) && (empty($skip_meta_templates)) && !empty($combinedFieldsView)) {
+ if (!empty($data['MetaTemplates']) && (empty($skip_meta_templates)) && !empty($combinedFieldsView)) {
$listTableOptions['tableClass'] = '';
}
$listTable = $this->Bootstrap->listTable($listTableOptions,[
@@ -45,7 +45,7 @@
]);
$metafieldsPanel = '';
- if (!empty($data['metaTemplates']) && (empty($skip_meta_templates))) {
+ if (!empty($data['MetaTemplates']) && (empty($skip_meta_templates))) {
$metaFieldsData = [
'data' => $data,
];
@@ -78,11 +78,11 @@
__('{0} view', \Cake\Utility\Inflector::singularize(\Cake\Utility\Inflector::humanize($this->request->getParam('controller')))) :
$title;
echo sprintf(
- "
+ "
%s
%s%s
-
%s
-
%s
+
%s
+
%s
%s
",
$tableRandomValue,
diff --git a/templates/element/layouts/header/header-breadcrumb.php b/templates/element/layouts/header/header-breadcrumb.php
index 5c4c619..9ac92fa 100644
--- a/templates/element/layouts/header/header-breadcrumb.php
+++ b/templates/element/layouts/header/header-breadcrumb.php
@@ -1,104 +1,121 @@
request->getParam('controller');
- $action = $this->request->getParam('action');
- $curentPath = "{$controller}{$action}";
+use Cake\Core\Configure;
+use Cake\Routing\Router;
- $breadcrumbLinks = '';
- $breadcrumbAction = '';
- $this->Breadcrumbs->setTemplates([
- 'wrapper' => sprintf(
- ''
- ),
- 'item' => '{{separator}}',
- 'itemWithoutLink' => '{{separator}}',
- 'separator' => ''
- ]);
+$controller = $this->request->getParam('controller');
+$action = $this->request->getParam('action');
+$curentPath = "{$controller}{$action}";
+$entity = !empty($entity) ? $entity : [];
- if (!empty($breadcrumb)) {
- foreach ($breadcrumb as $i => $entry) {
- if (!empty($entry['textGetter'])) {
+$breadcrumbLinks = '';
+$breadcrumbAction = '';
+$this->Breadcrumbs->setTemplates([
+ 'wrapper' => sprintf(
+ ''
+ ),
+ 'item' => '{{separator}}',
+ 'itemWithoutLink' => '{{separator}}',
+ 'separator' => ''
+]);
+
+if (!empty($breadcrumb)) {
+ foreach ($breadcrumb as $i => $entry) {
+ if (!empty($entry['textGetter'])) {
+ if (is_array($entry['textGetter']) && !empty($entry['textGetter']['path'])) {
+ $data = !empty(${$entry['textGetter']['varname']}) ? ${$entry['textGetter']['varname']} : $entity;
+ $entry['label'] = Cake\Utility\Hash::get($data, $entry['textGetter']['path']);
+ } else {
$entry['label'] = Cake\Utility\Hash::get($entity, $entry['textGetter']);
}
- if (!empty($entry['url_vars'])) {
- $entry['url'] = $this->DataFromPath->buildStringFromDataPath($entry['url'], $entity, $entry['url_vars']);
- }
- $this->Breadcrumbs->add(h($entry['label']), Router::url($entry['url']), [
- 'title' => h($entry['label']),
- 'templateVars' => [
- 'linkClass' => $i == 0 ? 'fw-light' : '',
- 'icon' => ($i == 0 && !empty($entry['icon'])) ? $this->FontAwesome->getClass(h($entry['icon'])) : ''
- ]
- ]);
}
-
- $lastCrumb = $breadcrumb[count($breadcrumb)-1];
+ if (empty($entry['label'])) {
+ $entry['label'] = "[{$entry['textGetter']}]";
+ }
+ if (!empty($entry['url_vars'])) {
+ $entry['url'] = $this->DataFromPath->buildStringFromDataPath($entry['url'], $entity, $entry['url_vars']);
+ }
+ $this->Breadcrumbs->add(h($entry['label']), Router::url($entry['url']), [
+ 'title' => h($entry['label']),
+ 'templateVars' => [
+ 'linkClass' => $i == 0 ? 'fw-light' : '',
+ 'icon' => ($i == 0 && !empty($entry['icon'])) ? $this->FontAwesome->getClass(h($entry['icon'])) : ''
+ ]
+ ]);
+ }
- if (!empty($lastCrumb['links'])) {
- foreach ($lastCrumb['links'] as $i => $linkEntry) {
- $active = $linkEntry['route_path'] == $lastCrumb['route_path'];
- if (!empty($linkEntry['url_vars'])) {
- $linkEntry['url'] = $this->DataFromPath->buildStringFromDataPath($linkEntry['url'], $entity, $linkEntry['url_vars']);
- }
- $breadcrumbLinks .= sprintf('
%s ',
- $active ? 'secondary' : 'outline-secondary',
- Router::url($linkEntry['url']),
- h($linkEntry['label'])
- );
+ $lastCrumb = $breadcrumb[count($breadcrumb) - 1];
+
+ if (!empty($lastCrumb['links'])) {
+ // dd($lastCrumb['links']);
+ foreach ($lastCrumb['links'] as $i => $linkEntry) {
+ $active = $linkEntry['route_path'] == $lastCrumb['route_path'];
+ if (!empty($linkEntry['url_vars'])) {
+ $linkEntry['url'] = $this->DataFromPath->buildStringFromDataPath($linkEntry['url'], $entity, $linkEntry['url_vars']);
}
- }
- if (!empty($lastCrumb['actions'])) {
- foreach ($lastCrumb['actions'] as $i => $actionEntry) {
- if (!empty($actionEntry['url_vars'])) {
- $actionEntry['url'] = $this->DataFromPath->buildStringFromDataPath($actionEntry['url'], $entity, $actionEntry['url_vars']);
- }
- $breadcrumbAction .= sprintf('
%s',
- Router::url($actionEntry['url']),
- !empty($entry['icon']) ? $this->FontAwesome->getClass(h($actionEntry['icon'])) : '',
- h($actionEntry['label'])
- );
+ if (!empty($linkEntry['selfLink'])) {
+ $url = Router::url(null);
+ } else {
+ $url = Router::url($linkEntry['url']);
}
+ $breadcrumbLinks .= sprintf(
+ '
%s ',
+ $active ? 'secondary' : 'outline-secondary',
+ $url,
+ h($linkEntry['label'])
+ );
}
}
+ $badgeNumber = 0;
+ if (!empty($lastCrumb['actions'])) {
+ foreach ($lastCrumb['actions'] as $i => $actionEntry) {
+ if (!empty($actionEntry['url_vars'])) {
+ $actionEntry['url'] = $this->DataFromPath->buildStringFromDataPath($actionEntry['url'], $entity, $actionEntry['url_vars']);
+ }
+ if (!empty($actionEntry['badge'])) {
+ $badgeNumber += 1;
+ }
+ $breadcrumbAction .= sprintf(
+ '
%s%s',
+ !empty($actionEntry['variant']) ? sprintf('dropdown-item-%s', $actionEntry['variant']) : '',
+ sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url']))),
+ !empty($actionEntry['icon']) ? $this->FontAwesome->getClass(h($actionEntry['icon'])) : '',
+ h($actionEntry['label']),
+ !empty($actionEntry['badge']) ? $this->Bootstrap->badge($actionEntry['badge']) : ''
+ );
+ }
+ }
+}
?>
Breadcrumbs->render(
- [],
- ['separator' => '']
- );
+echo $this->Breadcrumbs->render(
+ [],
+ ['separator' => '']
+);
?>
-
-
-
-
-
-
\ No newline at end of file
diff --git a/templates/element/layouts/sidebar/bookmark-add.php b/templates/element/layouts/sidebar/bookmark-add.php
new file mode 100644
index 0000000..69dd353
--- /dev/null
+++ b/templates/element/layouts/sidebar/bookmark-add.php
@@ -0,0 +1,12 @@
+Bootstrap->button([
+ 'nodeType' => 'a',
+ 'icon' => 'plus',
+ 'title' => __('Add new bookmark'),
+ 'variant' => 'primary',
+ 'size' => 'sm',
+ 'class' => 'mb-1',
+ 'params' => [
+ 'id' => 'btn-add-bookmark',
+ ]
+]);
diff --git a/templates/element/layouts/sidebar/bookmark-entry.php b/templates/element/layouts/sidebar/bookmark-entry.php
new file mode 100644
index 0000000..f2d01c1
--- /dev/null
+++ b/templates/element/layouts/sidebar/bookmark-entry.php
@@ -0,0 +1,30 @@
+Bootstrap->button([
+ 'nodeType' => 'a',
+ 'text' => h($label),
+ 'title' => h($name),
+ 'variant' => 'dark',
+ 'outline' => !$active,
+ 'size' => 'sm',
+ 'icon' => h($icon),
+ 'class' => ['mb-1'],
+ 'params' => [
+ 'href' => h($url),
+ ]
+ ]);
+?>
diff --git a/templates/element/layouts/sidebar/category.php b/templates/element/layouts/sidebar/category.php
index 7c2be88..eaa4d00 100644
--- a/templates/element/layouts/sidebar/category.php
+++ b/templates/element/layouts/sidebar/category.php
@@ -1,4 +1,4 @@
-
+
= h($label) ?>
diff --git a/templates/element/layouts/sidebar/entry.php b/templates/element/layouts/sidebar/entry.php
index a7125d4..59da1ee 100644
--- a/templates/element/layouts/sidebar/entry.php
+++ b/templates/element/layouts/sidebar/entry.php
@@ -29,7 +29,12 @@
?>
-
diff --git a/templates/element/widgets/highlight-panel.php b/templates/element/widgets/highlight-panel.php
index 4aae72f..bbb1645 100644
--- a/templates/element/widgets/highlight-panel.php
+++ b/templates/element/widgets/highlight-panel.php
@@ -1,42 +1,86 @@
0) {
- $variationIcon = 'arrow-up';
- $variationClass = 'bg-success';
-} else {
- $variationIcon = 'arrow-down';
- $variationClass = 'bg-danger';
+if (!is_null($variation)) {
+ if ($variation == 0) {
+ $variationIcon = $this->FontAwesome->getClass('minus');
+ } elseif ($variation > 0) {
+ $variationIcon = 'trends-arrow-up-white fs-6';
+ $variationClass = 'bg-success';
+ } else {
+ $variationIcon = 'trends-arrow-up-white fs-6 fa-rotate-180 fa-flip-vertical';
+ $variationClass = 'bg-danger';
+ }
}
-$variationHtml = sprintf(' %s
',
- $variationClass,
- $this->FontAwesome->getClass($variationIcon),
- !empty($variation) ? h($variation) : ''
-);
+$series = [];
+$statistics_day_number = '';
+if (!empty($timeline['created']['timeline'])) {
+ $statistics_day_number = $timeline['created']['days'];
+ $i = count($series);
+ $series[$i]['name'] = __('Created');
+ $series[$i]['type'] = !empty($chartType) ? $chartType : 'column';
+ foreach ($timeline['created']['timeline'] as $entry) {
+ $series[$i]['data'][] = ['x' => $entry['time'], 'y' => $entry['count']];
+ }
+}
+if (!empty($timeline['modified']['timeline'])) {
+ $statistics_day_number = empty($statistics_day_number) ? $timeline['modified']['days'] : $statistics_day_number;
+ $i = count($series);
+ $series[$i]['name'] = __('Modified');
+ $series[$i]['type'] = !empty($chartType) ? $chartType : 'line';
+ foreach ($timeline['modified']['timeline'] as $entry) {
+ $series[$i]['data'][] = ['x' => $entry['time'], 'y' => $entry['count']];
+ }
+}
+
+$variationHtml = '';
+if (!is_null($variation)) {
+ $variationHtml = sprintf(
+ ' %s
',
+ $variationClass,
+ $variationIcon,
+ !is_null($variation) ? h($variation) : ''
+ );
+}
$titleHtml = isset($title) ? h($title) : ($titleHtml ?? '');
-$leftContent = sprintf('%s
%s %s',
+$leftContent = sprintf(
+ '%s
%s %s %s',
$titleHtml,
h($number ?? ''),
+ __('Past {0} days', $statistics_day_number),
$variationHtml
);
$rightContent = sprintf('%s
', $this->element('charts/bar', [
- 'chartData' => $chartData,
- 'chartOptions' => [
-
- ]
+ 'series' => $series,
+ 'chartOptions' => array_merge(
+ [
+ 'chart' => [
+ 'height' => '90px',
+ ],
+ 'stroke' => [
+ 'width' => [0, 2],
+ 'curve' => 'smooth',
+ ],
+ ],
+ !empty($chartOptions) ? $chartOptions : []
+ )
]));
-
-$cardContent = sprintf('', $leftContent, $rightContent);
+$cardContent = sprintf(
+ '',
+ $panelClasses ?? '',
+ $panelStyle ?? '',
+ $leftContent,
+ $rightContent
+);
echo $this->Bootstrap->card([
'variant' => 'secondary',
'bodyHTML' => $cardContent,
'bodyClass' => 'p-3',
- 'class' => 'grow-on-hover shadow-sm'
+ 'class' => ['shadow-sm', (empty($panelNoGrow) ? 'grow-on-hover' : '')]
]);
?>
diff --git a/templates/genericTemplates/delete.php b/templates/genericTemplates/delete.php
index f2b7449..b652eb0 100644
--- a/templates/genericTemplates/delete.php
+++ b/templates/genericTemplates/delete.php
@@ -18,7 +18,7 @@ $form = $this->element('genericElements/Form/genericForm', [
]);
$formHTML = sprintf('%s
', $form);
-$bodyMessage = !empty($deletionText) ? __($deletionText) : __('Are you sure you want to delete {0} #{1}?', h(Cake\Utility\Inflector::singularize($this->request->getParam('controller'))), h($id));
+$bodyMessage = !empty($deletionText) ? h($deletionText) : __('Are you sure you want to delete {0} #{1}?', h(Cake\Utility\Inflector::singularize($this->request->getParam('controller'))), h($id));
$bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage);
echo $this->Bootstrap->modal([
diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php
index d51a0b2..33fb379 100644
--- a/templates/genericTemplates/filters.php
+++ b/templates/genericTemplates/filters.php
@@ -1,6 +1,13 @@
$fieldName,
+ ];
+}, $filters);
+
$filteringForm = $this->Bootstrap->table(
[
'small' => true,
@@ -9,41 +16,70 @@ $filteringForm = $this->Bootstrap->table(
'tableClass' => ['indexFilteringTable'],
],
[
- 'fields' => [
- __('Field'),
- __('Operator'),
- [
- 'labelHtml' => sprintf('%s %s',
- __('Value'),
- sprintf('', __('Supports strict matches and LIKE matches with the `%` character.
Example: `%.com`'))
- )
+ 'fields' => [
+ [
+ 'key' => 'fieldname', 'label' => __('Field'), 'formatter' => function ($field, $row) {
+ return sprintf('%s ', h($field), h($field));
+ }
+ ],
+ [
+ 'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) {
+ $options = [
+ sprintf('%s ', '=', '='),
+ sprintf('%s ', '!=', '!='),
+ ];
+ return sprintf('%s ', implode('', $options));
+ }
+ ],
+ [
+ 'key' => 'value',
+ 'labelHtml' => sprintf(
+ '%s %s',
+ __('Value'),
+ sprintf('', __('Supports strict matches and LIKE matches with the `%` character.
Example: `%.com`'))
+ ),
+ 'formatter' => function ($field, $row) {
+ return sprintf(' ');
+ }
+ ],
],
- __('Action')
- ],
- 'items' => []
-]);
+ 'items' => $tableItems
+ ]
+);
+$filteringMetafields = '';
+if ($metaFieldsEnabled) {
+ $helpText = $this->Bootstrap->genNode('sup', [
+ 'class' => ['ms-1 fa fa-info'],
+ 'title' => __('Include help'),
+ 'data-bs-toggle' => 'tooltip',
+ ]);
+ $filteringMetafields = $this->Bootstrap->genNode('h5', [], __('Meta Fields') . $helpText);
+ $filteringMetafields .= $this->element('genericElements/IndexTable/metafield_filtering', $metaTemplates);
+}
+
+$filteringTags = '';
if ($taggingEnabled) {
$helpText = $this->Bootstrap->genNode('sup', [
'class' => ['ms-1 fa fa-info'],
'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character).
Example: `!exportable`, `%able`'),
'data-bs-toggle' => 'tooltip',
]);
- $filteringTags = $this->Bootstrap->genNode('h5', [], __('Tags') . $helpText);
+ $filteringTags = $this->Bootstrap->genNode('h5', [
+ 'class' => 'mt-2'
+ ], __('Tags') . $helpText);
$filteringTags .= $this->Tag->tags([], [
'allTags' => $allTags,
'picker' => true,
'editable' => false,
]);
-} else {
- $filteringTags = '';
}
-$modalBody = sprintf('%s%s', $filteringForm, $filteringTags);
+$modalBody = implode('', [$filteringForm, $filteringMetafields, $filteringTags]);
echo $this->Bootstrap->modal([
'title' => __('Filtering options for {0}', Inflector::singularize($this->request->getParam('controller'))),
- 'size' => 'lg',
+ 'size' => !empty($metaFieldsEnabled) ? 'xl' : 'lg',
'type' => 'confirm',
'bodyHtml' => $modalBody,
'confirmText' => __('Filter'),
@@ -61,7 +97,7 @@ echo $this->Bootstrap->modal([
const controller = '= $this->request->getParam('controller') ?>';
const action = 'index';
const $tbody = modalObject.$modal.find('table.indexFilteringTable tbody')
- const $rows = $tbody.find('tr:not(#controlRow)')
+ const $rows = $tbody.find('tr')
const activeFilters = {}
$rows.each(function() {
const rowData = getDataFromRow($(this))
@@ -69,10 +105,20 @@ echo $this->Bootstrap->modal([
if (rowData['operator'] == '!=') {
fullFilter += ' !='
}
- activeFilters[fullFilter] = rowData['value']
+ if (rowData['value'].length > 0) {
+ activeFilters[fullFilter] = rowData['value']
+ }
})
- $select = modalObject.$modal.find('select.tag-input')
- activeFilters['filteringTags'] = $select.select2('data').map(tag => tag.text)
+ if (modalObject.$modal.find('table.indexMetaFieldsFilteringTable').length > 0) {
+ let metaFieldFilters = modalObject.$modal.find('table.indexMetaFieldsFilteringTable')[0].getFiltersFunction()
+ // activeFilters['filteringMetaFields'] = metaFieldFilters !== undefined ? metaFieldFilters : [];
+ metaFieldFilters = metaFieldFilters !== undefined ? metaFieldFilters : []
+ for (let [metaFieldPath, metaFieldValue] of Object.entries(metaFieldFilters)) {
+ activeFilters[metaFieldPath] = metaFieldValue
+ }
+ }
+ $selectTag = modalObject.$modal.find('.tag-container select.select2-input')
+ activeFilters['filteringTags'] = $selectTag.select2('data').map(tag => tag.text)
const searchParam = jQuery.param(activeFilters);
const url = `/${controller}/${action}?${searchParam}`
@@ -85,8 +131,6 @@ echo $this->Bootstrap->modal([
function initFilteringTable($filteringTable) {
const $controlRow = $filteringTable.find('#controlRow')
- $filteringTable.find('tbody').empty()
- addControlRow($filteringTable)
const randomValue = getRandomValue()
const activeFilters = Object.assign({}, $(`#toggleFilterButton-${randomValue}`).data('activeFilters'))
const tags = activeFilters['filteringTags'] !== undefined ? Object.assign({}, activeFilters)['filteringTags'] : []
@@ -100,9 +144,9 @@ echo $this->Bootstrap->modal([
} else if (fieldParts.length > 2) {
console.error('Field contains multiple spaces. ' + field)
}
- addFilteringRow($filteringTable, field, value, operator)
+ setFilteringValues($filteringTable, field, value, operator)
}
- $select = $filteringTable.closest('.modal-body').find('select.tag-input')
+ $select = $filteringTable.closest('.modal-body').find('select.select2-input')
let passedTags = []
tags.forEach(tagname => {
const existingOption = $select.find('option').filter(function() {
@@ -118,97 +162,17 @@ echo $this->Bootstrap->modal([
.trigger('change')
}
- function addControlRow($filteringTable) {
- const availableFilters = = json_encode($filters) ?>;
- const $selectField = $(' ').addClass('fieldSelect form-select form-select-sm')
- availableFilters.forEach(filter => {
- $selectField.append($(' ').text(filter))
- });
- const $selectOperator = $(' ').addClass('fieldOperator form-select form-select-sm')
- .append([
- $(' ').text('=').val('='),
- $(' ').text('!=').val('!='),
- ])
- const $row = $(' ').attr('id', 'controlRow')
- .append(
- $(' ').append($selectField),
- $(' ').append($selectOperator),
- $(' ').append(
- $(' ').attr('type', 'text').addClass('fieldValue form-control form-control-sm')
- ),
- $(' ').append(
- $(' ').attr('type', 'button').addClass('btn btn-sm btn-primary')
- .append($(' ').addClass('fa fa-plus'))
- .click(addFiltering)
- )
- )
- $filteringTable.append($row)
- }
-
- function addFilteringRow($filteringTable, field, value, operator) {
- const $selectOperator = $(' ').addClass('fieldOperator form-select form-select-sm')
- .append([
- $(' ').text('=').val('='),
- $(' ').text('!=').val('!='),
- ]).val(operator)
- const $row = $(' ')
- .append(
- $(' ').text(field).addClass('fieldName').data('fieldName', field),
- $(' ').append($selectOperator),
- $(' ').append(
- $(' ').attr('type', 'text').addClass('fieldValue form-control form-control-sm').val(value)
- ),
- $(' ').append(
- $(' ').attr('type', 'button').addClass('btn btn-sm btn-danger')
- .append($(' ').addClass('fa fa-trash'))
- .click(removeSelf)
- )
- )
- $filteringTable.append($row)
- const $controlRow = $filteringTable.find('#controlRow')
- disableOptionFromSelect($controlRow, field)
- }
-
- function addFiltering() {
- const $table = $(this).closest('table.indexFilteringTable')
- const $controlRow = $table.find('#controlRow')
- const field = $controlRow.find('select.fieldSelect').val()
- const value = $controlRow.find('input.fieldValue').val()
- const operator = $controlRow.find('input.fieldOperator').val()
- addFilteringRow($table, field, value, operator)
- $controlRow.find('input.fieldValue').val('')
- $controlRow.find('select.fieldSelect').val('')
- }
-
- function removeSelf() {
- const $row = $(this).closest('tr')
- const $controlRow = $row.closest('table.indexFilteringTable').find('#controlRow')
- const field = $row.data('fieldName')
- $row.remove()
- enableOptionFromSelect($controlRow, field)
- }
-
- function disableOptionFromSelect($controlRow, optionName) {
- $controlRow.find('select.fieldSelect option').each(function() {
- const $option = $(this)
- if ($option.text() == optionName) {
- $option.prop('disabled', true)
- }
- });
- }
-
- function enableOptionFromSelect($controlRow, optionName) {
- $controlRow.find('select.fieldSelect option').each(function() {
- const $option = $(this)
- if ($option.text() == optionName) {
- $option.prop('disabled', false)
- }
- });
+ function setFilteringValues($filteringTable, field, value, operator) {
+ $row = $filteringTable.find('td > span.fieldName').filter(function() {
+ return $(this).data('fieldname') == field
+ }).closest('tr')
+ $row.find('.fieldOperator').val(operator)
+ $row.find('.fieldValue').val(value)
}
function getDataFromRow($row) {
const rowData = {};
- rowData['name'] = $row.find('td.fieldName').data('fieldName')
+ rowData['name'] = $row.find('td > span.fieldName').data('fieldname')
rowData['operator'] = $row.find('select.fieldOperator').val()
rowData['value'] = $row.find('input.fieldValue').val()
return rowData
diff --git a/templates/layout/default.php b/templates/layout/default.php
index 865002d..59d2efe 100644
--- a/templates/layout/default.php
+++ b/templates/layout/default.php
@@ -16,6 +16,8 @@
use Cake\Core\Configure;
$cakeDescription = 'Cerebrate';
+
+$sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expanded']['value'];
?>
@@ -41,6 +43,7 @@ $cakeDescription = 'Cerebrate';
= $this->Html->script('bootstrap-helper.js') ?>
= $this->Html->script('api-helper.js') ?>
= $this->Html->script('select2.min.js') ?>
+ = $this->Html->script('table-settings.js') ?>
= $this->Html->script('CodeMirror/codemirror.js') ?>
= $this->Html->script('CodeMirror/mode/javascript/javascript') ?>
= $this->Html->script('CodeMirror/addon/hint/show-hint') ?>
@@ -49,6 +52,7 @@ $cakeDescription = 'Cerebrate';
= $this->Html->script('CodeMirror/addon/lint/json-lint') ?>
= $this->Html->script('CodeMirror/addon/edit/matchbrackets') ?>
= $this->Html->script('CodeMirror/addon/edit/closebrackets') ?>
+ = $this->Html->script('CodeMirror/addon/display/placeholder') ?>
= $this->Html->css('CodeMirror/codemirror') ?>
= $this->Html->css('CodeMirror/codemirror-additional') ?>
= $this->Html->css('CodeMirror/addon/hint/show-hint') ?>
@@ -72,7 +76,7 @@ $cakeDescription = 'Cerebrate';
= $this->element('layouts/header') ?>
-