Merge branch 'refactor-metatemplates' into develop-unstable

pull/93/head
Sami Mokaddem 2022-01-20 13:56:18 +01:00
commit 8dd330fcea
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
326 changed files with 24266 additions and 6715 deletions

52
.github/workflows/docker-publish.yml vendored Normal file
View File

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

9
.gitignore vendored
View File

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

View File

@ -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:

View File

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
class UserSettings extends AbstractMigration
{
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
public function change()
{
$table = $this->table('user_settings', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci',
]);
$table
->addColumn('id', 'integer', [
'autoIncrement' => true,
'limit' => 10,
'signed' => false,
])
->addPrimaryKey('id')
->addColumn('name', 'string', [
'default' => null,
'null' => false,
'limit' => 255,
'comment' => 'The name of the user setting',
])
->addColumn('value', 'text', [
'default' => null,
'null' => true,
'limit' => MysqlAdapter::TEXT_LONG,
'comment' => 'The value of the user setting',
])
->addColumn('user_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10,
])
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addForeignKey('user_id', 'users', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']);
$table->addIndex('name')
->addIndex('user_id')
->addIndex('created')
->addIndex('modified');
$table->create();
}
}

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
class MailingLists extends AbstractMigration
{
/**
* Change Method.
*
* More information on this method is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
* @return void
*/
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
public function change()
{
$mailinglists = $this->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();
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
class MoreMetaFieldColumns extends AbstractMigration
{
public function change()
{
$metaFieldsTable = $this->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`
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
final class AuditLogs extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
public function change(): void
{
$exists = $this->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();
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
final class UserOrg extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$exists = $this->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();
}
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
final class AuditChanged extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$exists = $this->table('audit_logs')->hasColumn('change');
if ($exists) {
$this->table('audit_logs')
->renameColumn('change', 'changed')
->update();
}
}
}

View File

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

View File

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

View File

@ -0,0 +1 @@
{}

1
debian/install vendored
View File

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

79
docker/Dockerfile Normal file
View File

@ -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<Directory /var/www/html/>\n\t\tAllowOverride all\n\t</Directory>' /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" ]

30
docker/README.md Normal file
View File

@ -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) \
.
```

29
docker/docker-compose.yml Normal file
View File

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

28
docker/entrypoint.sh Executable file
View File

@ -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 "$@"

View File

@ -0,0 +1,3 @@
RewriteEngine on
RewriteRule ^$ webroot/ [L]
RewriteRule (.*) webroot/$1 [L]

44
docker/etc/app_local.php Normal file
View File

@ -0,0 +1,44 @@
<?php
$db = [
'username' => 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
]
];

View File

@ -0,0 +1,3 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]

186
docker/wait-for-it.sh Executable file
View File

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

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

1
logs/.gitkeep Normal file
View File

@ -0,0 +1 @@

View File

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
>
<phpunit colors="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/bootstrap.php">
<php>
<ini name="memory_limit" value="-1"/>
<ini name="apc.enable_cli" value="1"/>
<ini name="memory_limit" value="-1" />
<ini name="apc.enable_cli" value="1" />
<env name="WIREMOCK_HOST" value="localhost" />
<env name="WIREMOCK_PORT" value="8080" />
<env name="OPENAPI_SPEC" value="webroot/docs/openapi.yaml" />
<env name="SKIP_DB_MIGRATIONS" value="0" />
</php>
<!-- Add any additional test suites you want to run here -->
@ -15,17 +14,17 @@
<testsuite name="app">
<directory>tests/TestCase/</directory>
</testsuite>
<!-- Add plugin test suites here. -->
<testsuite name="controller">
<directory>./tests/TestCase/Controller</directory>
</testsuite>
<testsuite name="api">
<directory>./tests/TestCase/Api</directory>
</testsuite>
</testsuites>
<!-- Setup a listener for fixtures -->
<listeners>
<listener class="Cake\TestSuite\Fixture\FixtureInjector">
<arguments>
<object class="Cake\TestSuite\Fixture\FixtureManager"/>
</arguments>
</listener>
</listeners>
<extensions>
<extension class="\Cake\TestSuite\Fixture\PHPUnitExtension" />
</extensions>
<!-- Ignore vendor tests in code coverage reports -->
<filter>
@ -37,4 +36,4 @@
</exclude>
</whitelist>
</filter>
</phpunit>
</phpunit>

View File

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

View File

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

View File

@ -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([

View File

@ -12,7 +12,6 @@
'type' => 'color',
),
),
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
'submit' => array(
'action' => $this->request->getParam('action')
)

View File

@ -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'

View File

@ -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 = $('<div></div>').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%',

View File

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

View File

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

View File

@ -0,0 +1,11 @@
{
"format": "json",
"mapping": {
"name": "{n}.Organisation.name",
"uuid": "{n}.Organisation.uuid",
"nationality": "{n}.Organisation.nationality"
},
"sourceHeaders": {
"Authorization": "~~YOUR_API_KEY_HERE~~"
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
class ApiController extends AppController
{
/**
* Controller action for displaying built-in Redoc UI
*
* @return \Cake\Http\Response|null|void Renders view
*/
public function index()
{
$url = '/docs/openapi.yaml';
$this->set('url', $url);
}
}

View File

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

View File

@ -0,0 +1,41 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Core\Configure;
class AuditLogsController extends AppController
{
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title'];
public $quickFilterFields = ['model', 'request_action', 'model_title'];
public $containFields = ['Users'];
public function index()
{
$this->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();
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,16 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class ApiNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('Api', 'index', [
'label' => __('API'),
'url' => '/api/index',
'icon' => 'code'
]);
}
}

View File

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

View File

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

View File

@ -0,0 +1,44 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class InboxNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('Inbox', 'index', $this->bcf->defaultCRUD('Inbox', 'index'));
$this->bcf->addRoute('Inbox', 'view', $this->bcf->defaultCRUD('Inbox', 'view'));
$this->bcf->addRoute('Inbox', 'discard', [
'label' => __('Discard request'),
'icon' => 'trash',
'url' => '/inbox/discard/{{id}}',
'url_vars' => ['id' => 'id'],
]);
$this->bcf->addRoute('Inbox', 'process', [
'label' => __('Process request'),
'icon' => 'cogs',
'url' => '/inbox/process/{{id}}',
'url_vars' => ['id' => 'id'],
]);
}
public function addParents()
{
$this->bcf->addParent('Inbox', 'view', 'Inbox', 'index');
$this->bcf->addParent('Inbox', 'discard', 'Inbox', 'index');
$this->bcf->addParent('Inbox', 'process', 'Inbox', 'index');
}
public function addLinks()
{
$this->bcf->addSelfLink('Inbox', 'view');
}
public function addActions()
{
$this->bcf->addAction('Inbox', 'view', 'Inbox', 'process');
$this->bcf->addAction('Inbox', 'view', 'Inbox', 'discard');
}
}

View File

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

View File

@ -0,0 +1,26 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class InstanceNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('Instance', 'home', [
'label' => __('Home'),
'url' => '/',
'icon' => 'home'
]);
$this->bcf->addRoute('Instance', 'settings', [
'label' => __('Settings'),
'url' => '/instance/settings',
'icon' => 'cogs'
]);
$this->bcf->addRoute('Instance', 'migrationIndex', [
'label' => __('Database Migration'),
'url' => '/instance/migrationIndex',
'icon' => 'database'
]);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class LocalToolsNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('LocalTools', 'viewConnector', [
'label' => __('View'),
'textGetter' => 'connector',
'url' => '/localTools/viewConnector/{{connector}}',
'url_vars' => ['connector' => 'connector'],
]);
$this->bcf->addRoute('LocalTools', 'broodTools', [
'label' => __('Brood Tools'),
'url' => '/localTools/broodTools/{{id}}',
'url_vars' => ['id' => 'id'],
]);
}
public function addParents()
{
$this->bcf->addParent('LocalTools', 'viewConnector', 'LocalTools', 'index');
}
public function addLinks()
{
$passedData = $this->request->getParam('pass');
if (!empty($passedData[0])) {
$brood_id = $passedData[0];
$this->bcf->addParent('LocalTools', 'broodTools', 'Broods', 'view', [
'textGetter' => [
'path' => 'name',
'varname' => 'broodEntity',
],
'url' => "/broods/view/{$brood_id}",
]);
$this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'view', [
'url' => "/broods/view/{$brood_id}",
]);
$this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'edit', [
'url' => "/broods/view/{$brood_id}",
]);
}
$this->bcf->addSelfLink('LocalTools', 'broodTools');
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class MetaTemplatesNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('MetaTemplates', 'index', $this->bcf->defaultCRUD('MetaTemplates', 'index'));
$this->bcf->addRoute('MetaTemplates', 'view', $this->bcf->defaultCRUD('MetaTemplates', 'view'));
$this->bcf->addRoute('MetaTemplates', 'enable', [
'label' => __('Enable'),
'icon' => 'check-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');
}
}

View File

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

View File

@ -0,0 +1,43 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class OutboxNavigation extends BaseNavigation
{
function addRoutes()
{
$this->bcf->addRoute('Outbox', 'index', $this->bcf->defaultCRUD('Outbox', 'index'));
$this->bcf->addRoute('Outbox', 'view', $this->bcf->defaultCRUD('Outbox', 'view'));
$this->bcf->addRoute('Outbox', 'discard', [
'label' => __('Discard request'),
'icon' => 'trash',
'url' => '/outbox/discard/{{id}}',
'url_vars' => ['id' => 'id'],
]);
$this->bcf->addRoute('Outbox', 'process', [
'label' => __('Process request'),
'icon' => 'cogs',
'url' => '/outbox/process/{{id}}',
'url_vars' => ['id' => 'id'],
]);
}
public function addParents()
{
$this->bcf->addParent('Outbox', 'view', 'Outbox', 'index');
$this->bcf->addParent('Outbox', 'discard', 'Outbox', 'index');
$this->bcf->addParent('Outbox', 'process', 'Outbox', 'index');
}
public function addLinks()
{
$this->bcf->addSelfLink('Outbox', 'view');
}
public function addActions()
{
$this->bcf->addAction('Outbox', 'view', 'Outbox', 'process');
$this->bcf->addAction('Outbox', 'view', 'Outbox', 'discard');
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class UserSettingsNavigation extends BaseNavigation
{
public function addLinks()
{
$bcf = $this->bcf;
$request = $this->request;
$this->bcf->addLink('UserSettings', 'index', 'Users', 'view', function ($config) use ($bcf, $request) {
if (!empty($request->getQuery('Users_id'))) {
$user_id = h($request->getQuery('Users_id'));
$linkData = [
'label' => __('View user [{0}]', h($user_id)),
'url' => sprintf('/users/view/%s', h($user_id))
];
return $linkData;
}
return null;
});
$this->bcf->addLink('UserSettings', 'index', 'Users', 'edit', function ($config) use ($bcf, $request) {
if (!empty($request->getQuery('Users_id'))) {
$user_id = h($request->getQuery('Users_id'));
$linkData = [
'label' => __('Edit user [{0}]', h($user_id)),
'url' => sprintf('/users/edit/%s', h($user_id))
];
return $linkData;
}
return null;
});
if (!empty($request->getQuery('Users_id'))) {
$this->bcf->addSelfLink('UserSettings', 'index');
}
if ($this->request->getParam('controller') == 'UserSettings' && $this->request->getParam('action') == 'index') {
if (!empty($this->request->getQuery('Users_id'))) {
$user_id = $this->request->getQuery('Users_id');
$this->bcf->addParent('UserSettings', 'index', 'Users', 'view', [
'textGetter' => [
'path' => 'username',
'varname' => 'settingsForUser',
],
'url' => "/users/view/{$user_id}"
]);
}
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace BreadcrumbNavigation;
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
class UsersNavigation extends BaseNavigation
{
public function addRoutes()
{
$this->bcf->addRoute('Users', 'settings', [
'label' => __('User settings'),
'url' => '/users/settings/',
'icon' => 'user-cog'
]);
}
public function addParents()
{
// $this->bcf->addParent('Users', 'settings', 'Users', 'view');
}
public function addLinks()
{
$bcf = $this->bcf;
$request = $this->request;
$passedData = $this->request->getParam('pass');
$this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
if (!empty($passedData[0])) {
$user_id = $passedData[0];
$linkData = [
'label' => __('Account settings', h($user_id)),
'url' => sprintf('/users/settings/%s', h($user_id))
];
return $linkData;
}
return [];
});
$this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
if (!empty($passedData[0])) {
$user_id = $passedData[0];
$linkData = [
'label' => __('User Setting [{0}]', h($user_id)),
'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id))
];
return $linkData;
}
return [];
});
$this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
if (!empty($passedData[0])) {
$user_id = $passedData[0];
$linkData = [
'label' => __('Account settings', h($user_id)),
'url' => sprintf('/users/settings/%s', h($user_id))
];
return $linkData;
}
return [];
});
$this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
if (!empty($passedData[0])) {
$user_id = $passedData[0];
$linkData = [
'label' => __('User Setting [{0}]', h($user_id)),
'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id))
];
return $linkData;
}
return [];
});
$this->bcf->addLink('Users', 'settings', 'Users', 'view', function ($config) use ($bcf, $request, $passedData) {
if (!empty($passedData[0])) {
$user_id = $passedData[0];
$linkData = [
'label' => __('View user', h($user_id)),
'url' => sprintf('/users/view/%s', h($user_id))
];
return $linkData;
}
return [];
});
$this->bcf->addSelfLink('Users', 'settings', [
'label' => __('Account settings')
]);
}
}

View File

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

View File

@ -0,0 +1,162 @@
<?php
namespace SidemenuNavigation;
use Cake\Core\Configure;
class Sidemenu {
private $iconTable;
private $request;
public function __construct($iconTable, $request)
{
$this->iconTable = $iconTable;
$this->request = $request;
}
public function get(): array
{
return [
__('ContactDB') => [
'Individuals' => [
'label' => __('Individuals'),
'icon' => $this->iconTable['Individuals'],
'url' => '/individuals/index',
],
'Organisations' => [
'label' => __('Organisations'),
'icon' => $this->iconTable['Organisations'],
'url' => '/organisations/index',
],
'EncryptionKeys' => [
'label' => __('Encryption keys'),
'icon' => $this->iconTable['EncryptionKeys'],
'url' => '/encryptionKeys/index',
]
],
__('Trust Circles') => [
'SharingGroups' => [
'label' => __('Sharing Groups'),
'icon' => $this->iconTable['SharingGroups'],
'url' => '/sharingGroups/index',
],
'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'))
]
]
];
}
}

View File

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

View File

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

View File

@ -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',

View File

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

View File

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

View File

@ -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']);

View File

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

View File

@ -0,0 +1,301 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use App\Model\Entity\Individual;
use Cake\Utility\Inflector;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\ORM\Query;
use Cake\ORM\Entity;
use Exception;
class MailingListsController extends AppController
{
public $filterFields = ['MailingLists.uuid', 'MailingLists.name', 'description', 'releasability'];
public $quickFilterFields = ['MailingLists.uuid', ['MailingLists.name' => 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');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

@ -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',

View File

@ -0,0 +1,213 @@
<?php
namespace App\Model\Behavior;
use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\Utility\Text;
use Cake\Utility\Security;
use \Cake\Http\Session;
use Cake\Core\Configure;
use Cake\ORM\TableRegistry;
use App\Model\Table\AuditLogTable;
class AuditLogBehavior extends Behavior
{
/** @var array */
private $config;
/** @var array|null */
private $old;
/** @var AuditLog|null */
private $AuditLogs;
// Hash is faster that in_array
private $skipFields = [
'id' => 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()
{
}
}

View File

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

View File

@ -0,0 +1,231 @@
<?php
namespace App\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\Utility\Inflector;
use Cake\Database\Expression\QueryExpression;
use function PHPSTORM_META\type;
class MetaFieldsBehavior extends Behavior
{
protected $_defaultConfig = [
'metaFieldsAssoc' => [
'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;
}
}

View File

@ -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 ?? [];

View File

@ -0,0 +1,68 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
use Cake\Core\Configure;
class AuditLog extends AppModel
{
private $compressionEnabled = false;
public function __construct(array $properties = [], array $options = [])
{
$this->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;
}
}

View File

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

View File

@ -0,0 +1,51 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
class MailingList extends AppModel
{
protected $_accessible = [
'*' => 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;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class MetaTemplateField extends AppModel
{
}

View File

@ -10,10 +10,5 @@ class Organisation extends AppModel
protected $_accessible = [
'*' => true,
'id' => false,
'uuid' => false,
];
protected $_accessibleOnNew = [
'uuid' => true,
];
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class Outbox extends AppModel
{
}

View File

@ -6,9 +6,42 @@ use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
use Authentication\PasswordHasher\DefaultPasswordHasher;
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'UserSettingsProvider.php');
use App\Settings\SettingsProvider\UserSettingsProvider;
class User extends AppModel
{
protected $_hidden = ['password', 'confirm_password'];
protected $_virtual = ['user_settings_by_name', 'user_settings_by_name_with_fallback'];
protected function _getUserSettingsByName()
{
$settingsByName = [];
if (!empty($this->user_settings)) {
foreach ($this->user_settings as $i => $setting) {
$settingsByName[$setting->name] = $setting;
}
}
return $settingsByName;
}
protected function _getUserSettingsByNameWithFallback()
{
if (!isset($this->SettingsProvider)) {
$this->SettingsProvider = new UserSettingsProvider();
}
$settingsByNameWithFallback = [];
if (!empty($this->user_settings)) {
foreach ($this->user_settings as $i => $setting) {
$settingsByNameWithFallback[$setting->name] = $setting->value;
}
}
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settingsByNameWithFallback);
$settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
return $settingsFlattened;
}
protected function _setPassword(string $password) : ?string
{
if (strlen($password) > 0) {

View File

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

View File

@ -12,6 +12,7 @@ class AlignmentsTable extends AppTable
{
parent::initialize($config);
$this->belongsTo('Individuals');
$this->addBehavior('AuditLog');
$this->belongsTo('Organisations');
$this->addBehavior('Timestamp');
}

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