From 6bd9d7d2f0cf78918c27a5afca008c21fedf037b Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 17 Aug 2022 13:46:59 +0200 Subject: [PATCH 01/55] chg: [error handler] changed to conform with 4.4 --- config/bootstrap.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/bootstrap.php b/config/bootstrap.php index 9b8dd98..739122d 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -35,8 +35,8 @@ use Cake\Cache\Cache; use Cake\Core\Configure; use Cake\Core\Configure\Engine\PhpConfig; use Cake\Datasource\ConnectionManager; -use Cake\Error\ConsoleErrorHandler; -use Cake\Error\ErrorHandler; +use Cake\Error\ErrorTrap; +use Cake\Error\ExceptionTrap; use Cake\Filesystem\File; use Cake\Http\ServerRequest; use Cake\Log\Log; @@ -132,9 +132,9 @@ ini_set('intl.default_locale', Configure::read('App.defaultLocale')); */ $isCli = PHP_SAPI === 'cli'; if ($isCli) { - (new ConsoleErrorHandler(Configure::read('Error')))->register(); + (new ErrorTrap(Configure::read('Error')))->register(); } else { - (new ErrorHandler(Configure::read('Error')))->register(); + (new ExceptionTrap(Configure::read('Error')))->register(); } /* From 60d8a8f6555f0577951c112884eb68cbe00e87d0 Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 17 Aug 2022 13:49:11 +0200 Subject: [PATCH 02/55] fix: [deprecation] toList() queries updated --- src/Controller/Component/FloodProtectionComponent.php | 2 +- src/Model/Table/AppTable.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/Component/FloodProtectionComponent.php b/src/Controller/Component/FloodProtectionComponent.php index 91668b6..fe632ad 100644 --- a/src/Controller/Component/FloodProtectionComponent.php +++ b/src/Controller/Component/FloodProtectionComponent.php @@ -34,7 +34,7 @@ class FloodProtectionComponent extends Component public function check(string $action, int $limit = 5, int $expiration_time = 300): bool { - $results = $this->FloodProtections->find('all')->where(['request_action' => $action, 'remote_ip' => $this->remote_ip, 'expiration' > time()])->toList(); + $results = $this->FloodProtections->find()->where(['request_action' => $action, 'remote_ip' => $this->remote_ip, 'expiration' > time()])->all()->toList(); if (count($results) >= $limit) { throw new TooManyRequestsException(__('Too many {0} requests have been issued ({1} requests allowed ever {2} seconds)', [$action, $limit, $expiration_time])); } diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php index 2de0b1d..527d290 100644 --- a/src/Model/Table/AppTable.php +++ b/src/Model/Table/AppTable.php @@ -47,7 +47,7 @@ class AppTable extends Table ->limit($options['limit']) ->page(1) ->enableHydration(false); - $topUsage = $queryTopUsage->toList(); + $topUsage = $queryTopUsage->all()->toList(); $stats[$scope] = $topUsage; if ( !empty($options['includeOthers']) && !empty($topUsage) && From a5c9f683164286671d6ffec4db9f0b61f9a668d8 Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 17 Aug 2022 13:49:52 +0200 Subject: [PATCH 03/55] fix: [deprecation] futher toList() call updated --- src/Model/Table/AppTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php index 527d290..70ea76f 100644 --- a/src/Model/Table/AppTable.php +++ b/src/Model/Table/AppTable.php @@ -73,7 +73,7 @@ class AppTable extends Table } }) ->enableHydration(false); - $othersUsage = $queryOthersUsage->toList(); + $othersUsage = $queryOthersUsage->all()->toList(); if (!empty($othersUsage)) { $stats[$scope][] = [ $scope => __('Others'), From cbb737e18ecce07821652e890743b97e5bb63ecf Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 17 Aug 2022 14:00:38 +0200 Subject: [PATCH 04/55] fix: [deprecation] pagination component's use removed to comply with 4.4 requirements --- src/Controller/AlignmentsController.php | 3 +-- src/Controller/Component/CRUDComponent.php | 3 +-- src/Controller/Component/CustomPaginationComponent.php | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Controller/AlignmentsController.php b/src/Controller/AlignmentsController.php index a8dd626..a264fb1 100644 --- a/src/Controller/AlignmentsController.php +++ b/src/Controller/AlignmentsController.php @@ -23,8 +23,7 @@ class AlignmentsController extends AppController $alignments = $query->all(); return $this->RestResponse->viewData($alignments, 'json'); } else { - $this->loadComponent('Paginator'); - $alignments = $this->Paginator->paginate($query); + $alignments = $this->paginate($query); $this->set('data', $alignments); $this->set('metaGroup', 'ContactDB'); } diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 5de6000..353d75f 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -100,8 +100,7 @@ class CRUDComponent extends Component if ($this->metaFieldsSupported()) { $query = $this->includeRequestedMetaFields($query); } - $this->Controller->loadComponent('Paginator'); - $data = $this->Controller->Paginator->paginate($query, $this->Controller->paginate ?? []); + $data = $this->Controller->paginate($query, $this->Controller->paginate ?? []); if (isset($options['afterFind'])) { $function = $options['afterFind']; if (is_callable($function)) { diff --git a/src/Controller/Component/CustomPaginationComponent.php b/src/Controller/Component/CustomPaginationComponent.php index 0f8871a..fe6869a 100644 --- a/src/Controller/Component/CustomPaginationComponent.php +++ b/src/Controller/Component/CustomPaginationComponent.php @@ -7,6 +7,7 @@ use Cake\Controller\ComponentRegistry; use Cake\Http\Exception\NotFoundException; use InvalidArgumentException; use Cake\Controller\Component\PaginatorComponent; +use Cake\Datasource\Pagination\NumericPaginator; use Cake\Utility\Hash; class CustomPaginationComponent extends Component From b9e5b76766dd812a64f5e3959719022b0a0e378f Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 19 Aug 2022 13:00:19 +0200 Subject: [PATCH 05/55] new: [component] APIRearrange component added - alter the data's format before passing it back via the RestResponseComponent - to be used to clean up UI specific artifacts / junk - also to maintain compability between versions/tools --- .../Component/APIRearrangeComponent.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Controller/Component/APIRearrangeComponent.php diff --git a/src/Controller/Component/APIRearrangeComponent.php b/src/Controller/Component/APIRearrangeComponent.php new file mode 100644 index 0000000..cd10ec6 --- /dev/null +++ b/src/Controller/Component/APIRearrangeComponent.php @@ -0,0 +1,106 @@ + 'rearrangeOrganisation' + ]; + + public function rearrange(object $data): object + { + if (get_class($data) === 'Cake\ORM\ResultSet') { + $data->each(function ($value, $key) { + $value = $this->rearrangeEntity($value); + }); + } else { + $data = $this->rearrangeEntity($data); + } + return $data; + } + + private function rearrangeEntity(object $entity): object + { + $entityClass = get_class($entity); + if (isset($this->rearrangeFunctions[$entityClass])) { + $entity = $this->{$this->rearrangeFunctions[$entityClass]}($entity); + } + return $entity; + } + + private function rearrangeOrganisation(object $entity): object + { + if (!empty($entity['tags'])) { + $entity['tags'] = $this->rearrangeTags($entity['tags']); + } + if (!empty($entity['alignments'])) { + $entity['alignments'] = $this->rearrangeAlignments($entity['alignments']); + } + if (!empty($entity['meta_fields'])) { + $entity = $this->rearrangeMetaFields($entity); + } + if (!empty($entity['MetaTemplates'])) { + unset($entity['MetaTemplates']); + } + return $entity; + } + + private function rearrangeMetaFields(object $entity): object + { + $entity['meta_fields'] = []; + foreach ($entity['MetaTemplates'] as $template) { + foreach ($template['meta_template_fields'] as $field) { + if ($field['counter'] > 0) { + foreach ($field['metaFields'] as $metaField) { + if (!empty($entity['meta_fields'][$template['name']][$field['field']])) { + if (!is_array($entity['meta_fields'][$template['name']])) { + $entity['meta_fields'][$template['name']][$field['field']] = [$entity['meta_fields'][$template['name']][$field['field']]]; + } + $entity['meta_fields'][$template['name']][$field['field']][] = $metaField['value']; + } else { + $entity['meta_fields'][$template['name']][$field['field']] = $metaField['value']; + } + } + } + } + } + return $entity; + } + + private function rearrangeTags(array $tags): array + { + foreach ($tags as &$tag) { + unset($tag['_joinData']); + } + return $tags; + } + + private function rearrangeAlignments(array $alignments): array + { + $rearrangedAlignments = []; + $validAlignmentTypes = ['individual', 'organisation']; + foreach ($alignments as $alignment) { + //debug($alignment); + foreach ($validAlignmentTypes as $type) { + if (isset($alignment[$type])) { + $alignment[$type]['type'] = $alignment['type']; + $rearrangedAlignments[$type][] = $alignment[$type]; + } + } + } + return $rearrangedAlignments; + } +} \ No newline at end of file From 3e0d015f69ff6d7ad6494bdbef10b4afaef77328 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 19 Aug 2022 13:01:47 +0200 Subject: [PATCH 06/55] fix: [meta] template loading reworked - no more crappy string numeric keys among others --- src/Controller/Component/CRUDComponent.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 353d75f..c530c6c 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -381,7 +381,6 @@ class CRUDComponent extends Component } 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) { @@ -622,7 +621,7 @@ class CRUDComponent extends Component 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; + $data[$metaField->meta_template_id][$metaField->meta_template_field_id][] = $metaField; } return $data; } @@ -643,6 +642,9 @@ class CRUDComponent extends Component $metaTemplates[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = []; } } + if (!empty($metaTemplates[$metaTemplate->id]->meta_template_fields)) { + $metaTemplates[$metaTemplate->id]->meta_template_fields = array_values($metaTemplates[$metaTemplate->id]->meta_template_fields); + } } else { if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) { unset($metaTemplates[$i]); @@ -653,7 +655,7 @@ class CRUDComponent extends Component $metaTemplates[$i]['hasNewerVersion'] = $newestTemplate; } } - $data['MetaTemplates'] = $metaTemplates; + $data['MetaTemplates'] = array_values($metaTemplates); return $data; } From d96353ee4ff9db64d3a1dc78b8a28cd5ba8c0a36 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 19 Aug 2022 13:02:25 +0200 Subject: [PATCH 07/55] chg: [APIRearrange] component tied into rest response --- src/Controller/Component/RestResponseComponent.php | 5 ++++- src/Model/Behavior/MetaFieldsBehavior.php | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index 50fb40c..d384ba2 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -8,7 +8,7 @@ use Cake\Utility\Inflector; class RestResponseComponent extends Component { - public $components = ['ACL']; + public $components = ['ACL', 'APIRearrange']; public $headers = []; @@ -558,6 +558,9 @@ class RestResponseComponent extends Component if (!empty($errors)) { $data['errors'] = $errors; } + if (!$raw) { + $data = $this->APIRearrange->rearrange($data); + } return $this->__sendResponse($data, 200, $format, $raw, $download, $headers); } diff --git a/src/Model/Behavior/MetaFieldsBehavior.php b/src/Model/Behavior/MetaFieldsBehavior.php index 6f3a46e..173439d 100644 --- a/src/Model/Behavior/MetaFieldsBehavior.php +++ b/src/Model/Behavior/MetaFieldsBehavior.php @@ -111,7 +111,6 @@ class MetaFieldsBehavior extends Behavior $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]); From 1077251f8babcf07c61d2e7fe09ad29522cefe60 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 23 Aug 2022 11:05:07 +0200 Subject: [PATCH 08/55] fix: [keycloak] fixed encoding issue with urlencoded usernames created in keycloak --- src/Model/Behavior/AuthKeycloakBehavior.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index ae35957..7f02d12 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -409,7 +409,11 @@ class AuthKeycloakBehavior extends Behavior ] ]); } - $newUser = $this->restApiRequest('%s/admin/realms/%s/users?username=' . urlencode($user['username']), [], 'get'); + $newUser = $this->restApiRequest( + '%s/admin/realms/%s/users?username=' . $this->urlencodeEscapeForSprintf(urlencode($user['username'])), + [], + 'get' + ); $users = json_decode($newUser->getStringBody(), true); if (empty($users[0]['id'])) { return false; @@ -527,4 +531,9 @@ class AuthKeycloakBehavior extends Behavior } return $changed; } + + private function urlencodeEscapeForSprintf(string $input): string + { + return str_replace('%', '%%', $input); + } } From 095dd4513c34e6aaa316296b068a5468ad2da338 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 23 Aug 2022 11:42:30 +0200 Subject: [PATCH 09/55] chg: [rearrange] moved to Entity --- .../Component/APIRearrangeComponent.php | 83 +------------------ .../Component/RestResponseComponent.php | 2 +- src/Model/Entity/AppModel.php | 48 +++++++++++ src/Model/Entity/Organisation.php | 16 ++++ 4 files changed, 68 insertions(+), 81 deletions(-) diff --git a/src/Controller/Component/APIRearrangeComponent.php b/src/Controller/Component/APIRearrangeComponent.php index cd10ec6..0665206 100644 --- a/src/Controller/Component/APIRearrangeComponent.php +++ b/src/Controller/Component/APIRearrangeComponent.php @@ -15,92 +15,15 @@ use Cake\Routing\Router; class APIRearrangeComponent extends Component { - - private $rearrangeFunctions = [ - 'App\Model\Entity\Organisation' => 'rearrangeOrganisation' - ]; - - public function rearrange(object $data): object + public function rearrangeForAPI(object $data): object { if (get_class($data) === 'Cake\ORM\ResultSet') { $data->each(function ($value, $key) { - $value = $this->rearrangeEntity($value); + $value->rearrangeForAPI(); }); } else { - $data = $this->rearrangeEntity($data); + $data->rearrangeForAPI(); } return $data; } - - private function rearrangeEntity(object $entity): object - { - $entityClass = get_class($entity); - if (isset($this->rearrangeFunctions[$entityClass])) { - $entity = $this->{$this->rearrangeFunctions[$entityClass]}($entity); - } - return $entity; - } - - private function rearrangeOrganisation(object $entity): object - { - if (!empty($entity['tags'])) { - $entity['tags'] = $this->rearrangeTags($entity['tags']); - } - if (!empty($entity['alignments'])) { - $entity['alignments'] = $this->rearrangeAlignments($entity['alignments']); - } - if (!empty($entity['meta_fields'])) { - $entity = $this->rearrangeMetaFields($entity); - } - if (!empty($entity['MetaTemplates'])) { - unset($entity['MetaTemplates']); - } - return $entity; - } - - private function rearrangeMetaFields(object $entity): object - { - $entity['meta_fields'] = []; - foreach ($entity['MetaTemplates'] as $template) { - foreach ($template['meta_template_fields'] as $field) { - if ($field['counter'] > 0) { - foreach ($field['metaFields'] as $metaField) { - if (!empty($entity['meta_fields'][$template['name']][$field['field']])) { - if (!is_array($entity['meta_fields'][$template['name']])) { - $entity['meta_fields'][$template['name']][$field['field']] = [$entity['meta_fields'][$template['name']][$field['field']]]; - } - $entity['meta_fields'][$template['name']][$field['field']][] = $metaField['value']; - } else { - $entity['meta_fields'][$template['name']][$field['field']] = $metaField['value']; - } - } - } - } - } - return $entity; - } - - private function rearrangeTags(array $tags): array - { - foreach ($tags as &$tag) { - unset($tag['_joinData']); - } - return $tags; - } - - private function rearrangeAlignments(array $alignments): array - { - $rearrangedAlignments = []; - $validAlignmentTypes = ['individual', 'organisation']; - foreach ($alignments as $alignment) { - //debug($alignment); - foreach ($validAlignmentTypes as $type) { - if (isset($alignment[$type])) { - $alignment[$type]['type'] = $alignment['type']; - $rearrangedAlignments[$type][] = $alignment[$type]; - } - } - } - return $rearrangedAlignments; - } } \ No newline at end of file diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index d384ba2..0ca239a 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -559,7 +559,7 @@ class RestResponseComponent extends Component $data['errors'] = $errors; } if (!$raw) { - $data = $this->APIRearrange->rearrange($data); + $data = $this->APIRearrange->rearrangeForAPI($data); } return $this->__sendResponse($data, 200, $format, $raw, $download, $headers); } diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php index 50bfe86..1e54623 100644 --- a/src/Model/Entity/AppModel.php +++ b/src/Model/Entity/AppModel.php @@ -32,4 +32,52 @@ class AppModel extends Entity { return $this->_accessibleOnNew ?? []; } + + public function rearrangeForAPI(): void + { + } + + public function rearrangeMetaFields(): void + { + $this->meta_fields = []; + foreach ($this->MetaTemplates as $template) { + foreach ($template['meta_template_fields'] as $field) { + if ($field['counter'] > 0) { + foreach ($field['metaFields'] as $metaField) { + if (!empty($this->meta_fields[$template['name']][$field['field']])) { + if (!is_array($this->meta_fields[$template['name']])) { + $this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]]; + } + $this->meta_fields[$template['name']][$field['field']][] = $metaField['value']; + } else { + $this->meta_fields[$template['name']][$field['field']] = $metaField['value']; + } + } + } + } + } + } + + public function rearrangeTags(array $tags): array + { + foreach ($tags as &$tag) { + unset($tag['_joinData']); + } + return $tags; + } + + public function rearrangeAlignments(array $alignments): array + { + $rearrangedAlignments = []; + $validAlignmentTypes = ['individual', 'organisation']; + foreach ($alignments as $alignment) { + foreach ($validAlignmentTypes as $type) { + if (isset($alignment[$type])) { + $alignment[$type]['type'] = $alignment['type']; + $rearrangedAlignments[$type][] = $alignment[$type]; + } + } + } + return $rearrangedAlignments; + } } diff --git a/src/Model/Entity/Organisation.php b/src/Model/Entity/Organisation.php index a56d3c0..bbfe0eb 100644 --- a/src/Model/Entity/Organisation.php +++ b/src/Model/Entity/Organisation.php @@ -16,4 +16,20 @@ class Organisation extends AppModel protected $_accessibleOnNew = [ 'created' => true ]; + + public function rearrangeForAPI(): void + { + if (!empty($this->tags)) { + $this->tags = $this->rearrangeTags($this->tags); + } + if (!empty($this->alignments)) { + $this->alignments = $this->rearrangeAlignments($this->alignments); + } + if (!empty($this->meta_fields)) { + $this->rearrangeMetaFields(); + } + if (!empty($this->MetaTemplates)) { + unset($this->MetaTemplates); + } + } } From 8bc3088e1219d991be55ea31515a4f776671aac5 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 23 Aug 2022 14:50:13 +0200 Subject: [PATCH 10/55] fix: [revert] meta fields unindexing - required for the saving of vchanges --- src/Controller/Component/CRUDComponent.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index c530c6c..693b2ab 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -413,6 +413,7 @@ class CRUDComponent extends Component $metaFieldsTable->patchEntity($entity->meta_fields[$index], [ 'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id ], ['value']); + debug($entity); $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], @@ -621,7 +622,7 @@ class CRUDComponent extends Component 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; + $data[$metaField->meta_template_id][$metaField->meta_template_field_id][$metaField->id] = $metaField; } return $data; } @@ -655,7 +656,7 @@ class CRUDComponent extends Component $metaTemplates[$i]['hasNewerVersion'] = $newestTemplate; } } - $data['MetaTemplates'] = array_values($metaTemplates); + $data['MetaTemplates'] = $metaTemplates; return $data; } From 94bfafb743208333bc881f2e42fa0d827331be7f Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 23 Aug 2022 16:02:52 +0200 Subject: [PATCH 11/55] fix: [meta template] fixes --- src/Controller/Component/CRUDComponent.php | 4 ---- src/Model/Entity/AppModel.php | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 693b2ab..cbd1d26 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -413,7 +413,6 @@ class CRUDComponent extends Component $metaFieldsTable->patchEntity($entity->meta_fields[$index], [ 'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id ], ['value']); - debug($entity); $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], @@ -643,9 +642,6 @@ class CRUDComponent extends Component $metaTemplates[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = []; } } - if (!empty($metaTemplates[$metaTemplate->id]->meta_template_fields)) { - $metaTemplates[$metaTemplate->id]->meta_template_fields = array_values($metaTemplates[$metaTemplate->id]->meta_template_fields); - } } else { if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) { unset($metaTemplates[$i]); diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php index 1e54623..a94e78a 100644 --- a/src/Model/Entity/AppModel.php +++ b/src/Model/Entity/AppModel.php @@ -45,7 +45,7 @@ class AppModel extends Entity if ($field['counter'] > 0) { foreach ($field['metaFields'] as $metaField) { if (!empty($this->meta_fields[$template['name']][$field['field']])) { - if (!is_array($this->meta_fields[$template['name']])) { + if (!is_array($this->meta_fields[$template['name']][$field['field']])) { $this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]]; } $this->meta_fields[$template['name']][$field['field']][] = $metaField['value']; From d35a6745054977b1398e368c3cc78c414bbdd45d Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 24 Aug 2022 11:39:56 +0200 Subject: [PATCH 12/55] chg: [navigation] added keycloak self management - also some changes to the navigation system --- src/Controller/Component/Navigation/Users.php | 17 +++++++++++++++++ src/Controller/Component/Navigation/base.php | 1 + .../Component/NavigationComponent.php | 12 ++++++++++++ .../layouts/header/header-breadcrumb.php | 6 +++++- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/Navigation/Users.php b/src/Controller/Component/Navigation/Users.php index 21fb76b..0b2030a 100644 --- a/src/Controller/Component/Navigation/Users.php +++ b/src/Controller/Component/Navigation/Users.php @@ -1,6 +1,7 @@ bcf; $request = $this->request; $passedData = $this->request->getParam('pass'); + $currentUserId = $this->currentUserId; $currentUser = $this->currentUser; $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData, $currentUser) { if (!empty($passedData[0])) { @@ -69,6 +71,21 @@ class UsersNavigation extends BaseNavigation } return []; }); + if ( + !empty(Configure::read('keycloak.enabled')) && + !empty(Configure::read('keycloak.provider.baseUrl')) && + !empty(Configure::read('keycloak.provider.realm')) && + $currentUserId == $passedData[0] + ) { + $url = sprintf( + '%s/realms/%s/account', + Configure::read('keycloak.provider.baseUrl'), + Configure::read('keycloak.provider.realm') + ); + foreach (['edit', 'view', 'settings'] as $sourceAction) { + $this->bcf->addCustomLink('Users', $sourceAction, $url, __('Manage KeyCloak Account')); + } + } $this->bcf->addLink('Users', 'settings', 'Users', 'view', function ($config) use ($bcf, $request, $passedData) { if (!empty($passedData[0])) { diff --git a/src/Controller/Component/Navigation/base.php b/src/Controller/Component/Navigation/base.php index 30f904a..daf39ce 100644 --- a/src/Controller/Component/Navigation/base.php +++ b/src/Controller/Component/Navigation/base.php @@ -12,6 +12,7 @@ class BaseNavigation { $this->bcf = $bcf; $this->request = $request; + $this->currentUserId = $this->request->getAttribute('identity')->getIdentifier(); $this->viewVars = $viewVars; } diff --git a/src/Controller/Component/NavigationComponent.php b/src/Controller/Component/NavigationComponent.php index 9df48be..86da134 100644 --- a/src/Controller/Component/NavigationComponent.php +++ b/src/Controller/Component/NavigationComponent.php @@ -346,6 +346,18 @@ class BreadcrumbFactory $this->endpoints[$sourceController][$sourceAction]['links'] = $links; } + public function addCustomLink(string $sourceController, string $sourceAction, string $targetUrl, string $label, $overrides = []) + { + $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true); + $links = array_merge($routeSourceConfig['links'] ?? [], [[ + 'url' => $targetUrl, + 'icon' => 'link', + 'label' => $label, + 'route_path' => 'foo:bar' + ]]); + $this->endpoints[$sourceController][$sourceAction]['links'] = $links; + } + public function addAction(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = []) { $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true); diff --git a/templates/element/layouts/header/header-breadcrumb.php b/templates/element/layouts/header/header-breadcrumb.php index 9ac92fa..4f9560e 100644 --- a/templates/element/layouts/header/header-breadcrumb.php +++ b/templates/element/layouts/header/header-breadcrumb.php @@ -49,7 +49,11 @@ if (!empty($breadcrumb)) { if (!empty($lastCrumb['links'])) { // dd($lastCrumb['links']); foreach ($lastCrumb['links'] as $i => $linkEntry) { - $active = $linkEntry['route_path'] == $lastCrumb['route_path']; + if (empty($linkEntry['route_path'])) { + $active = false; + } else { + $active = $linkEntry['route_path'] == $lastCrumb['route_path']; + } if (!empty($linkEntry['url_vars'])) { $linkEntry['url'] = $this->DataFromPath->buildStringFromDataPath($linkEntry['url'], $entity, $linkEntry['url_vars']); } From 4c1ce31d507659e3b2c70a40d8c340a004cdbdda Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 24 Aug 2022 11:42:38 +0200 Subject: [PATCH 13/55] fix: [unauthed] users internal error fixed --- src/Controller/Component/Navigation/base.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/Navigation/base.php b/src/Controller/Component/Navigation/base.php index daf39ce..9db4e80 100644 --- a/src/Controller/Component/Navigation/base.php +++ b/src/Controller/Component/Navigation/base.php @@ -12,7 +12,9 @@ class BaseNavigation { $this->bcf = $bcf; $this->request = $request; - $this->currentUserId = $this->request->getAttribute('identity')->getIdentifier(); + if (!empty($this->request->getAttribute('identity')->getIdentifier())) { + $this->currentUserId = $this->request->getAttribute('identity')->getIdentifier(); + } $this->viewVars = $viewVars; } From fac19e0a3ca1e6b9445f1062b2e6216f48b8a453 Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 24 Aug 2022 11:43:36 +0200 Subject: [PATCH 14/55] fix: [exception] speculative fix to a check causing a 500 --- src/Controller/Component/Navigation/base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Component/Navigation/base.php b/src/Controller/Component/Navigation/base.php index 9db4e80..782ee0c 100644 --- a/src/Controller/Component/Navigation/base.php +++ b/src/Controller/Component/Navigation/base.php @@ -12,7 +12,7 @@ class BaseNavigation { $this->bcf = $bcf; $this->request = $request; - if (!empty($this->request->getAttribute('identity')->getIdentifier())) { + if (!empty($this->request->getAttribute('identity'))) { $this->currentUserId = $this->request->getAttribute('identity')->getIdentifier(); } $this->viewVars = $viewVars; From 3857de8499468597eed7d7042647f1f4d5728011 Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 24 Aug 2022 14:47:40 +0200 Subject: [PATCH 15/55] fix: [notice] errors when not logged in removed --- src/Controller/Component/Navigation/Users.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/Navigation/Users.php b/src/Controller/Component/Navigation/Users.php index 0b2030a..4b1837b 100644 --- a/src/Controller/Component/Navigation/Users.php +++ b/src/Controller/Component/Navigation/Users.php @@ -25,7 +25,7 @@ class UsersNavigation extends BaseNavigation $bcf = $this->bcf; $request = $this->request; $passedData = $this->request->getParam('pass'); - $currentUserId = $this->currentUserId; + $currentUserId = empty($this->currentUserId) ? null : $this->currentUserId; $currentUser = $this->currentUser; $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData, $currentUser) { if (!empty($passedData[0])) { @@ -75,6 +75,7 @@ class UsersNavigation extends BaseNavigation !empty(Configure::read('keycloak.enabled')) && !empty(Configure::read('keycloak.provider.baseUrl')) && !empty(Configure::read('keycloak.provider.realm')) && + !empty($passedData[0]) && $currentUserId == $passedData[0] ) { $url = sprintf( From 370995ab505f450abb61674f2c4744ac48d25cb3 Mon Sep 17 00:00:00 2001 From: iglocska Date: Sun, 18 Sep 2022 18:16:34 +0200 Subject: [PATCH 16/55] fix: [audit log] error due to compressible fields not being streams when compression not enabled --- src/Controller/AuditLogsController.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Controller/AuditLogsController.php b/src/Controller/AuditLogsController.php index e72327d..00d545e 100644 --- a/src/Controller/AuditLogsController.php +++ b/src/Controller/AuditLogsController.php @@ -8,6 +8,7 @@ use Cake\ORM\TableRegistry; use \Cake\Database\Expression\QueryExpression; use Cake\Http\Exception\UnauthorizedException; use Cake\Core\Configure; +use PhpParser\Node\Stmt\Echo_; class AuditLogsController extends AppController { @@ -22,8 +23,10 @@ class AuditLogsController extends AppController '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']); + $request_ip = is_resource($data['request_ip']) ? stream_get_contents($data['request_ip']) : $data['request_ip']; + $change = is_resource($data['change']) ? stream_get_contents($data['change']) : $data['change']; + $data['request_ip'] = inet_ntop($request_ip); + $data['changed'] = $change; return $data; } ]); From 85e8a350916844fdf07b3aa527c09efac3720b7f Mon Sep 17 00:00:00 2001 From: iglocska Date: Sun, 18 Sep 2022 18:27:00 +0200 Subject: [PATCH 17/55] fix: [api rearrange] shouldn't trigger when dealing with arrays --- src/Controller/Component/RestResponseComponent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index 0ca239a..db5072b 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -558,7 +558,7 @@ class RestResponseComponent extends Component if (!empty($errors)) { $data['errors'] = $errors; } - if (!$raw) { + if (!$raw && is_object($data)) { $data = $this->APIRearrange->rearrangeForAPI($data); } return $this->__sendResponse($data, 200, $format, $raw, $download, $headers); From 09ff4eba5358136fd10e6669b66a5c0ede6bfda3 Mon Sep 17 00:00:00 2001 From: iglocska Date: Sun, 18 Sep 2022 18:27:39 +0200 Subject: [PATCH 18/55] fix: [xss] resolved in the genericField of the single view - as reported by SK-CERT --- .../genericElements/SingleViews/Fields/genericField.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/element/genericElements/SingleViews/Fields/genericField.php b/templates/element/genericElements/SingleViews/Fields/genericField.php index f2f4f5d..ad99eea 100644 --- a/templates/element/genericElements/SingleViews/Fields/genericField.php +++ b/templates/element/genericElements/SingleViews/Fields/genericField.php @@ -22,7 +22,9 @@ if (!empty($field['url'])) { '%s', $baseurl, h($field['url']), - $string + h($string) ); +} else { + $string = h($string); } echo $string; From 822c96dbf0f6b66ae542161c9513537300522939 Mon Sep 17 00:00:00 2001 From: iglocska Date: Sun, 18 Sep 2022 18:32:43 +0200 Subject: [PATCH 19/55] fix: [single view generic field] allow for unsanitised raw input --- .../element/genericElements/SingleViews/Fields/genericField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/element/genericElements/SingleViews/Fields/genericField.php b/templates/element/genericElements/SingleViews/Fields/genericField.php index ad99eea..0223543 100644 --- a/templates/element/genericElements/SingleViews/Fields/genericField.php +++ b/templates/element/genericElements/SingleViews/Fields/genericField.php @@ -24,7 +24,7 @@ if (!empty($field['url'])) { h($field['url']), h($string) ); -} else { +} else if (empty($field['raw'])) { $string = h($string); } echo $string; From 10ea126a9397a6b69eaa67ca9ccb5004a37cde1f Mon Sep 17 00:00:00 2001 From: iglocska Date: Sun, 18 Sep 2022 18:51:05 +0200 Subject: [PATCH 20/55] fix: [security] KeyCloak login getUser fixes - removed dead code - tightened check on the user profile, if the KC user's email address and that of the Cerebrate user disagree, block the authentication - as reported by SK-CERT --- src/Model/Behavior/AuthKeycloakBehavior.php | 48 ++------------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 7f02d12..f5a0916 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -30,7 +30,7 @@ class AuthKeycloakBehavior extends Behavior $raw_profile_payload = $profile->access_token->getJwt()->getPayload(); $user = $this->extractProfileData($raw_profile_payload); if (!$user) { - throw new \RuntimeException('Unable to save new user'); + throw new \RuntimeException('Unable to authenticate user. The KeyCloak and Cerebrate states of the user differ. This could be due to a missing synchronisation of the data.'); } return $user; @@ -50,50 +50,10 @@ class AuthKeycloakBehavior extends Behavior $fields[$field] = $mapping[$field]; } } - $user = [ - 'individual' => [ - 'email' => $profile_payload[$fields['email']], - 'first_name' => $profile_payload[$fields['first_name']], - 'last_name' => $profile_payload[$fields['last_name']] - ], - 'user' => [ - 'username' => $profile_payload[$fields['username']], - ], - 'organisation' => [ - 'uuid' => $profile_payload[$fields['org_uuid']], - ], - 'role' => [ - 'name' => $profile_payload[$fields['role_name']], - ] - ]; - //$user['user']['individual_id'] = $this->_table->captureIndividual($user); - //$user['user']['role_id'] = $this->_table->captureRole($user); - $existingUser = $this->_table->find()->where(['username' => $user['user']['username']])->first(); - /* - if (empty($existingUser)) { - $user['user']['password'] = Security::randomString(16); - $existingUser = $this->_table->newEntity($user['user']); - if (!$this->_table->save($existingUser)) { - return false; - } - } else { - $dirty = false; - if ($user['user']['individual_id'] != $existingUser['individual_id']) { - $existingUser['individual_id'] = $user['user']['individual_id']; - $dirty = true; - } - if ($user['user']['role_id'] != $existingUser['role_id']) { - $existingUser['role_id'] = $user['user']['role_id']; - $dirty = true; - } - $existingUser; - if ($dirty) { - if (!$this->_table->save($existingUser)) { - return false; - } - } + $existingUser = $this->_table->find()->where(['username' => $profile_payload[$fields['username']]])->first(); + if ($existingUser['individual']['email'] !== $profile_payload[$fields['email']]) { + return false; } - */ return $existingUser; } From 254fdc3b84f865e354ac63f7d3676a91bd6b0279 Mon Sep 17 00:00:00 2001 From: iglocska Date: Sun, 18 Sep 2022 19:26:24 +0200 Subject: [PATCH 21/55] chg: [security] keycloak enabled - disallow multiple users from being created for the same individual - as reported by SK-CERT --- src/Controller/UsersController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 45a3bc6..8405b3f 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -90,6 +90,12 @@ class UsersController extends AppController if (empty($data['individual_id'])) { throw new MethodNotAllowedException(__('No valid individual found. Either supply it in the request or set the individual_id to a valid value.')); } + if (Configure::read('keycloak.enabled')) { + $existingUserForIndividual = $this->Users->find()->where(['individual_id' => $data['individual_id']])->first(); + if (!empty($existingUserForIndividual)) { + throw new MethodNotAllowedException(__('Invalid individual selected - when KeyCloak is enabled, only one user account may be assigned to an individual.')); + } + } $this->Users->enrollUserRouter($data); return $data; } From 07a8d1dfcb20df8d1ba0f1a9acad4c2ed58d7ffa Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 00:24:29 +0200 Subject: [PATCH 22/55] chg: [dead variable] removed --- src/Model/Behavior/AuthKeycloakBehavior.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index f5a0916..d9a7829 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -212,7 +212,6 @@ class AuthKeycloakBehavior extends Behavior { $keycloakRoles = $this->getAllRoles($clientId); $keycloakRolesParsed = Hash::extract($keycloakRoles, '{n}.name'); - $rolesToAdd = []; $scopeString = $scope . ':'; $modified = 0; foreach ($roles as $role) { From af1e2fd6325fcff883190cacb6ad76592a10e38f Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 00:25:15 +0200 Subject: [PATCH 23/55] new: [security] Bruteforce protection added - logins allow for 5 attempts every 5 minutes - Code ported and updated from MISP - As reported by SK-CERT --- .../Migrations/20220918000000_bruteforces.php | 46 +++++++++++++ src/Controller/UsersController.php | 60 +++++++++------- src/Model/Entity/Bruteforce.php | 11 +++ src/Model/Table/BruteforcesTable.php | 69 +++++++++++++++++++ 4 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 config/Migrations/20220918000000_bruteforces.php create mode 100644 src/Model/Entity/Bruteforce.php create mode 100644 src/Model/Table/BruteforcesTable.php diff --git a/config/Migrations/20220918000000_bruteforces.php b/config/Migrations/20220918000000_bruteforces.php new file mode 100644 index 0000000..aefdad5 --- /dev/null +++ b/config/Migrations/20220918000000_bruteforces.php @@ -0,0 +1,46 @@ +hasTable('bruteforces'); + if (!$exists) { + $table = $this->table('bruteforces', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci', + ]); + $table + ->addColumn('user_ip', 'string', [ + 'null' => false, + 'length' => 45, + ]) + ->addColumn('username', 'string', [ + 'null' => false, + 'length' => 191, + 'collation' => 'utf8mb4_unicode_ci' + ]) + ->addColumn('expiration', 'datetime', [ + 'null' => false + ]) + ->addIndex('user_ip') + ->addIndex('username') + ->addIndex('expiration'); + $table->create(); + } + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 8405b3f..b0d35d2 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -297,30 +297,44 @@ class UsersController extends AppController public function login() { - $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); + $blocked = false; + if ($this->request->is('post')) { + $BruteforceTable = TableRegistry::getTableLocator()->get('Bruteforces'); + $input = $this->request->getData(); + $blocked = $BruteforceTable->isBlocklisted($_SERVER['REMOTE_ADDR'], $input['username']); + if ($blocked) { + $this->Authentication->logout(); + $this->Flash->error(__('Too many attempts, brute force protection triggered. Wait 5 minutes before trying again.')); + $this->redirect(['controller' => 'users', 'action' => 'login']); + } } - 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')); + if (!$blocked) { + $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()) { + $BruteforceTable->insert($_SERVER['REMOTE_ADDR'], $input['username']); + $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'); } diff --git a/src/Model/Entity/Bruteforce.php b/src/Model/Entity/Bruteforce.php new file mode 100644 index 0000000..ed0001e --- /dev/null +++ b/src/Model/Entity/Bruteforce.php @@ -0,0 +1,11 @@ +setDisplayField('email'); + $this->logModel = TableRegistry::getTableLocator()->get('AuditLogs'); + } + + public function insert($ip, $username) + { + $expire = 300; + $amount = 5; + $expire = time() + $expire; + $expire = date('Y-m-d H:i:s', $expire); + $bruteforceEntry = $this->newEntity([ + 'user_ip' => $ip, + 'username' => trim(strtolower($username)), + 'expiration' => $expire + ]); + $this->save($bruteforceEntry); + $title = 'Failed login attempt using username ' . $username . ' from IP: ' . $ip . '.'; + if ($this->isBlocklisted($ip, $username)) { + $title .= 'This has tripped the bruteforce protection after ' . $amount . ' failed attempts. The user is now blocklisted for ' . $expire . ' seconds.'; + } + $this->logModel->insert([ + 'request_action' => 'login_fail', + 'model' => 'Users', + 'model_id' => 0, + 'model_title' => 'bruteforce_block', + 'changed' => [] + ]); + } + + public function clean() + { + $expire = date('Y-m-d H:i:s', time()); + $this->deleteAll(['expiration <=' => $expire]); + } + + public function isBlocklisted($ip, $username) + { + // first remove old expired rows + $this->clean(); + // count + $count = $this->find('all', [ + 'conditions' => [ + 'user_ip' => $ip, + 'username' => trim($username) + ] + ])->count(); + if ($count >= 5) { + return true; + } else { + return false; + } + } +} From a9eccb3097ad7371e927f10bcaafc278de333ebb Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 01:11:18 +0200 Subject: [PATCH 24/55] fix: [security] X-FRAME-OPTIONS: DENY added to all responses - as reported by SK-CERT --- src/Controller/AppController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index dc5b81f..d7a06ef 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -149,6 +149,7 @@ class AppController extends Controller if ($this->modelClass == 'Tags.Tags') { $this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate'); } + $this->response = $this->response->withHeader('X-Frame-Options', 'DENY'); } if (mt_rand(1, 50) === 1) { $this->FloodProtection->cleanup(); From 9a50a5693e2ab17d7a2af19a2302b89bef185abd Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 01:12:14 +0200 Subject: [PATCH 25/55] fix: [users] added uniqueness to usernames - added upgrade script with removal of duplicate usernames - added unique index to username field - massaging the usernames before insertion (trim + lowercasing) - As reported by SK-CERT --- .../20220918000001_unique_usernames.php | 35 +++++++++++++++++++ src/Model/Table/UsersTable.php | 11 ++++-- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 config/Migrations/20220918000001_unique_usernames.php diff --git a/config/Migrations/20220918000001_unique_usernames.php b/config/Migrations/20220918000001_unique_usernames.php new file mode 100644 index 0000000..ce41c1c --- /dev/null +++ b/config/Migrations/20220918000001_unique_usernames.php @@ -0,0 +1,35 @@ +table('users'); + $exists = $table->hasIndexByName('users', 'username'); + $this->execute('DELETE FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY LOWER(username));'); + if (!$exists) { + $table->addIndex( + [ + 'username' + ], + [ + 'unique' => true + ] + )->save(); + } + } +} diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 61f06b8..b4caebe 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -7,12 +7,14 @@ use Cake\ORM\Table; use Cake\Validation\Validator; use Cake\ORM\RulesChecker; use Cake\ORM\TableRegistry; -use \Cake\Datasource\EntityInterface; -use \Cake\Http\Session; +use Cake\Event\EventInterface; +use Cake\Datasource\EntityInterface; +use Cake\Http\Session; use Cake\Http\Client; use Cake\Utility\Security; use Cake\Core\Configure; use Cake\Utility\Text; +use ArrayObject; class UsersTable extends AppTable { @@ -54,6 +56,11 @@ class UsersTable extends AppTable $this->setDisplayField('username'); } + public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options) + { + $data['username'] = trim(mb_strtolower($data['username'])); + } + private function initAuthBehaviors() { if (!empty(Configure::read('keycloak'))) { From 5e0ab5cc38c764f67874da614d42f4b5396bee3e Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 01:22:53 +0200 Subject: [PATCH 26/55] new: [users] username validation added - >5 && <50 in length required - trim username to test to avoid whitespace names - as reported by SK-CERT --- src/Model/Table/UsersTable.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index b4caebe..cc440a2 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -94,6 +94,16 @@ class UsersTable extends AppTable 'message' => __('Password confirmation missing or not matching the password.') ] ]) + ->add('username', [ + 'username_policy' => [ + 'rule' => function($value, $context) { + if (mb_strlen(trim($value)) < 5 || mb_strlen(trim($value)) > 50) { + return __('Invalid username length. Make sure that you provide a username of at least 5 and up to 50 characters in length.'); + } + return true; + } + ] + ]) ->requirePresence(['username'], 'create') ->notEmptyString('username', 'Please fill this field'); return $validator; From ca65c4b68e7e69f4be77707df1e6b4dd91b216e0 Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 01:39:38 +0200 Subject: [PATCH 27/55] fix: [alignments] added an index view template - Can't see any usefulness in this, but why not - As reported by SK-CERT --- templates/Alignments/index.php | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 templates/Alignments/index.php diff --git a/templates/Alignments/index.php b/templates/Alignments/index.php new file mode 100644 index 0000000..fea9fce --- /dev/null +++ b/templates/Alignments/index.php @@ -0,0 +1,43 @@ +element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'children' => [ + + ], + ], + 'fields' => [ + [ + 'name' => '#', + 'sort' => 'id', + 'data_path' => 'id', + ], + [ + 'name' => __('Individual'), + 'data_path' => 'individual.email', + 'url' => '/individuals/view/{{0}}', + 'url_vars' => ['individual.id'] + ], + [ + 'name' => __('Organisation'), + 'data_path' => 'organisation.name', + 'url' => '/organisations/view/{{0}}', + 'url_vars' => ['organisation.id'] + ], + [ + 'name' => __('Type'), + 'sort' => 'type', + 'data_path' => 'type' + ], + ], + 'title' => __('User index'), + 'description' => __('The list of enrolled users in this Cerebrate instance. All of the users have or at one point had access to the system.'), + 'pull' => 'right', + 'actions' => [ + + ] + ] +]); +echo ''; +?> From 4c0c6ef4ac070f15adc568bc2a7844605b6c8412 Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 01:46:57 +0200 Subject: [PATCH 28/55] fix: [counter graphs] fixed to disallow invalid interval entries - as reported by SK-CERT --- src/Controller/Component/CRUDComponent.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index cbd1d26..0e15ecd 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -144,8 +144,17 @@ class CRUDComponent extends Component if (is_string($statIgnoreNull)) { $statIgnoreNull = $statIgnoreNull == 'true' ? true : false; } + $statistics_entry_amount = $this->request->getQuery('statistics_entry_amount'); + if ( + !is_numeric($statistics_entry_amount) || + intval($statistics_entry_amount) <= 0 + ) { + $statistics_entry_amount = 5; + } else { + $statistics_entry_amount = intval($statistics_entry_amount); + } $statsOptions = [ - 'limit' => !is_numeric($this->request->getQuery('statistics_entry_amount')) ? 5 : $this->request->getQuery('statistics_entry_amount'), + 'limit' => $statistics_entry_amount, 'includeOthers' => $statIncludeRemaining, 'ignoreNull' => $statIgnoreNull, ]; From 3b215a5ec058371fbfbc63001eee586c1cc6d89b Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 01:59:23 +0200 Subject: [PATCH 29/55] fix: [alignments] fixed invalid urls in alignment fields lacking a / - as reported by SK-CERT --- .../element/genericElements/IndexTable/Fields/alignments.php | 4 ++-- .../genericElements/SingleViews/Fields/alignmentField.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/element/genericElements/IndexTable/Fields/alignments.php b/templates/element/genericElements/IndexTable/Fields/alignments.php index 875c480..15c29e1 100644 --- a/templates/element/genericElements/IndexTable/Fields/alignments.php +++ b/templates/element/genericElements/IndexTable/Fields/alignments.php @@ -8,7 +8,7 @@ if ($field['scope'] === 'individuals') { '
%s @ %s
', h($alignment['type']), sprintf( - '%s', + '%s', $baseurl, h($alignment['organisation']['id']), h($alignment['organisation']['name']) @@ -28,7 +28,7 @@ if ($field['scope'] === 'individuals') { '
[%s] %s
', h($alignment['type']), sprintf( - '%s', + '%s', $baseurl, h($alignment['individual']['id']), h($alignment['individual']['email']) diff --git a/templates/element/genericElements/SingleViews/Fields/alignmentField.php b/templates/element/genericElements/SingleViews/Fields/alignmentField.php index ffd605a..1832ec0 100644 --- a/templates/element/genericElements/SingleViews/Fields/alignmentField.php +++ b/templates/element/genericElements/SingleViews/Fields/alignmentField.php @@ -14,7 +14,7 @@ if ($field['scope'] === 'individuals') { '
%s @ %s
', h($alignment['type']), sprintf( - '%s', + '%s', $baseurl, h($alignment['organisation']['id']), h($alignment['organisation']['name']) @@ -34,7 +34,7 @@ if ($field['scope'] === 'individuals') { '
[%s] %s
', h($alignment['type']), sprintf( - '%s', + '%s', $baseurl, h($alignment['individual']['id']), h($alignment['individual']['email']) From fd6d3466d70969248e4fef6dc098ca9bf92459f1 Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 02:14:57 +0200 Subject: [PATCH 30/55] fix: [authkey] should only be used in a rest context - otherwise some weird authentication snafus can happen - as reported by SK-CERT --- src/Controller/AppController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index d7a06ef..9e8aef8 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -99,8 +99,8 @@ class AppController extends Controller { $this->loadModel('Users'); $this->Users->checkForNewInstance(); - $this->authApiUser(); if ($this->ParamHandler->isRest()) { + $this->authApiUser(); $this->Security->setConfig('unlockedActions', [$this->request->getParam('action')]); } $this->ACL->setPublicInterfaces(); From 760badd26894acd8f729e5bd8398a5bcb66740cf Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 02:17:36 +0200 Subject: [PATCH 31/55] fix: [alignments] missing contains added --- src/Controller/AlignmentsController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/AlignmentsController.php b/src/Controller/AlignmentsController.php index a264fb1..dc92f0c 100644 --- a/src/Controller/AlignmentsController.php +++ b/src/Controller/AlignmentsController.php @@ -23,6 +23,7 @@ class AlignmentsController extends AppController $alignments = $query->all(); return $this->RestResponse->viewData($alignments, 'json'); } else { + $this->paginate['contain'] = ['Individuals', 'Organisations']; $alignments = $this->paginate($query); $this->set('data', $alignments); $this->set('metaGroup', 'ContactDB'); From f37cea1cade8826c02e8b0a2ff6c23a61dfbbca5 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 20 Sep 2022 11:13:24 +0200 Subject: [PATCH 32/55] fix: [migration:unique_usernames] Table 'users' is specified twice, both as a target and as a separate source --- config/Migrations/20220918000001_unique_usernames.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/Migrations/20220918000001_unique_usernames.php b/config/Migrations/20220918000001_unique_usernames.php index ce41c1c..41ec565 100644 --- a/config/Migrations/20220918000001_unique_usernames.php +++ b/config/Migrations/20220918000001_unique_usernames.php @@ -20,7 +20,7 @@ final class UniqueUserNames extends AbstractMigration { $table = $this->table('users'); $exists = $table->hasIndexByName('users', 'username'); - $this->execute('DELETE FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY LOWER(username));'); + $this->execute('DELETE FROM users WHERE id NOT IN (SELECT MIN(id) FROM (select * from users) AS u2 GROUP BY LOWER(u2.username));'); if (!$exists) { $table->addIndex( [ From 8d26be28a25f8dda26a37d1b0c914b943d7e6242 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 20 Sep 2022 15:31:31 +0200 Subject: [PATCH 33/55] chg: [auditlogs:index] Reverse sort by ID --- src/Controller/AuditLogsController.php | 1 + src/Controller/Component/CRUDComponent.php | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/Controller/AuditLogsController.php b/src/Controller/AuditLogsController.php index 00d545e..912586b 100644 --- a/src/Controller/AuditLogsController.php +++ b/src/Controller/AuditLogsController.php @@ -20,6 +20,7 @@ class AuditLogsController extends AppController { $this->CRUD->index([ 'contain' => $this->containFields, + 'order' => ['AuditLogs.id' => 'DESC'], 'filters' => $this->filterFields, 'quickFilters' => $this->quickFilterFields, 'afterFind' => function($data) { diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 0e15ecd..fdb0d60 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -69,6 +69,9 @@ class CRUDComponent extends Component if (!empty($options['fields'])) { $query->select($options['fields']); } + if (!empty($options['order'])) { + $query->order($options['order']); + } if ($this->Controller->ParamHandler->isRest()) { $data = $query->all(); if (isset($options['hidden'])) { From efe917c8242778c4a53a1cf69fcc83bd5536e9e8 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 21 Sep 2022 10:05:55 +0200 Subject: [PATCH 34/55] fix: [authKeycloakBehavior] Typo preventing roles to be saved --- src/Model/Behavior/AuthKeycloakBehavior.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index d9a7829..7aeed31 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -444,7 +444,7 @@ class AuthKeycloakBehavior extends Behavior $toRemove = array_diff($keycloakUserRoles, array_keys($userRoles)); $changed = false; foreach ($toRemove as $k => $role) { - if (substr($role, 0, strlen('Organisation:')) !== 'Organisation:' && substr($role, 0, strlen('Role:') !== 'Role:')) { + if (substr($role, 0, strlen('Organisation:')) !== 'Organisation:' && substr($role, 0, strlen('Role:')) !== 'Role:') { unset($toRemove[$k]); } else { $toRemove[$k] = $assignedRolesParsed[$role]; From 69fee0249867f519e36082c45143834343411482 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 21 Sep 2022 10:06:33 +0200 Subject: [PATCH 35/55] fix: [authKeycloakBehavior] Re-indexing array preventing roles to be parsed by keycloak --- src/Model/Behavior/AuthKeycloakBehavior.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 7aeed31..4d5f509 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -472,6 +472,7 @@ class AuthKeycloakBehavior extends Behavior $toAdd[$k] = $userRoles[$name]; } if (!empty($toAdd)) { + $toAdd = array_values($toAdd); $response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'] . '/role-mappings/clients/' . $clientId, $toAdd, 'post'); if (!$response->isOk()) { $this->_table->auditLogs()->insert([ From 2c87b1e5009fe2acec72f19b16243b1981198184 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 21 Sep 2022 10:07:51 +0200 Subject: [PATCH 36/55] fix: [authKeycloakBehavior] Added missing association preventing user to log via keycloak --- src/Model/Behavior/AuthKeycloakBehavior.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 4d5f509..57e947f 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -50,7 +50,10 @@ class AuthKeycloakBehavior extends Behavior $fields[$field] = $mapping[$field]; } } - $existingUser = $this->_table->find()->where(['username' => $profile_payload[$fields['username']]])->first(); + $existingUser = $this->_table->find() + ->where(['username' => $profile_payload[$fields['username']]]) + ->contain('Individuals') + ->first(); if ($existingUser['individual']['email'] !== $profile_payload[$fields['email']]) { return false; } From f2db6b3b5e699ab25f4b3d239a8cceb09258c581 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 21 Sep 2022 10:08:40 +0200 Subject: [PATCH 37/55] chg: [users:add] Missing comma --- templates/Users/add.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Users/add.php b/templates/Users/add.php index c6999dc..b83f84e 100644 --- a/templates/Users/add.php +++ b/templates/Users/add.php @@ -87,7 +87,7 @@ 'field' => 'disabled', 'type' => 'checkbox', 'label' => 'Disable' - ] + ], ], 'submit' => [ 'action' => $this->request->getParam('action') From 80277e4bdf1c938dcaeeb409d8f3352344d346eb Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 21 Sep 2022 10:09:12 +0200 Subject: [PATCH 38/55] chg: [command:keycloakSync] Make sure User model is loaded --- src/Command/KeycloakSyncCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Command/KeycloakSyncCommand.php b/src/Command/KeycloakSyncCommand.php index 0902563..efe14e6 100644 --- a/src/Command/KeycloakSyncCommand.php +++ b/src/Command/KeycloakSyncCommand.php @@ -13,6 +13,7 @@ class KeycloakSyncCommand extends Command public function execute(Arguments $args, ConsoleIo $io) { if (!empty(Configure::read('keycloak'))) { + $this->loadModel('Users'); $results = $this->fetchTable()->syncWithKeycloak(); $tableData = [ ['Changes to', 'Count'] From 37094e0abb161279ea3ca4ca113bbc5d36321cfa Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 21 Sep 2022 10:10:02 +0200 Subject: [PATCH 39/55] fix: [user:validation] Allow user edition when `username` is not set --- src/Model/Table/UsersTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index cc440a2..3a642bb 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -105,7 +105,7 @@ class UsersTable extends AppTable ] ]) ->requirePresence(['username'], 'create') - ->notEmptyString('username', 'Please fill this field'); + ->notEmptyString('username', __('Please fill this field'), 'create'); return $validator; } From 21403995e3f30a46e0569fc65028ff810ae7cbec Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 21 Sep 2022 10:11:09 +0200 Subject: [PATCH 40/55] new: [user:edit] Added keycloak updates when a user gets modified --- src/Model/Behavior/AuthKeycloakBehavior.php | 24 +++++++++++++++++++++ src/Model/Table/UsersTable.php | 15 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 57e947f..1d76e13 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -145,6 +145,30 @@ class AuthKeycloakBehavior extends Behavior return true; } + /** + * handleUserUpdate + * + * @param \App\Model\Entity\User $user + * @return boolean If the update was a success + */ + public function handleUserUpdate(\App\Model\Entity\User $user): bool + { + $user['individual'] = $this->_table->Individuals->find()->where([ + 'id' => $user['individual_id'] + ])->first(); + $user['role'] = $this->_table->Roles->find()->where([ + 'id' => $user['role_id'] + ])->first(); + $user['organisation'] = $this->_table->Organisations->find()->where([ + 'id' => $user['organisation_id'] + ])->first(); + + $users = [$user->toArray()]; + $clientId = $this->getClientId(); + $changes = $this->syncUsers($users, $clientId); + return !empty($changes); + } + private function getAdminAccessToken() { $keycloakConfig = Configure::read('keycloak'); diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 3a642bb..7584216 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -61,6 +61,12 @@ class UsersTable extends AppTable $data['username'] = trim(mb_strtolower($data['username'])); } + public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) + { + $success = $this->handleUserUpdateRouter($entity); + return $success; + } + private function initAuthBehaviors() { if (!empty(Configure::read('keycloak'))) { @@ -208,4 +214,13 @@ class UsersTable extends AppTable $this->enrollUser($data); } } + + public function handleUserUpdateRouter(\App\Model\Entity\User $user): bool + { + if (!empty(Configure::read('keycloak'))) { + $success = $this->handleUserUpdate($user); + return $success; + } + return true; + } } From 96041cc71affc146aba7c81462dc1e90c6aa393d Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 29 Sep 2022 17:54:58 +0200 Subject: [PATCH 41/55] chg: [genericIndex:select_visible_columns] Show meta-template versions --- src/View/Helper/BootstrapHelper.php | 6 +++++- .../genericElements/ListTopBar/group_table_action.php | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index f2c1f96..12ccad9 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -2012,7 +2012,11 @@ class BoostrapDropdownMenu extends BootstrapGeneric $params['data-open-form-id'] = mt_rand(); } - $label = $this->genNode('span', ['class' => 'mx-1'], h($entry['text'])); + $labelContent = sprintf('%s%s', + h($entry['text']), + !empty($entry['sup']) ? $this->genNode('sup', ['class' => 'ms-1 text-muted'], $entry['sup']) : '' + ); + $label = $this->genNode('span', ['class' => 'mx-1'], $labelContent); $content = $icon . $label . $badge; return $this->genNode('a', array_merge([ diff --git a/templates/element/genericElements/ListTopBar/group_table_action.php b/templates/element/genericElements/ListTopBar/group_table_action.php index e9de073..fdb799c 100644 --- a/templates/element/genericElements/ListTopBar/group_table_action.php +++ b/templates/element/genericElements/ListTopBar/group_table_action.php @@ -22,6 +22,7 @@ if (!empty($meta_templates)) { $numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0; $metaTemplateColumnMenu[] = [ 'text' => $meta_template->name, + 'sup' => $meta_template->version, 'badge' => [ 'text' => $numberActiveMetaField, 'variant' => 'secondary', From c65978f8f236eaf41a6f7440df4ed3d47699217b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 08:59:36 +0200 Subject: [PATCH 42/55] fix: [behavior:authKeycloak] Correctly check if the user was saved --- src/Model/Behavior/AuthKeycloakBehavior.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 1d76e13..4d17b61 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -115,7 +115,7 @@ class AuthKeycloakBehavior extends Behavior foreach ($roles as $role) { $rolesParsed[$role['name']] = $role['id']; } - if ($this->createUser($user, $clientId, $rolesParsed)) { + if (!$this->createUser($user, $clientId, $rolesParsed)) { $logChange = [ 'username' => $user['username'], 'individual_id' => $user['individual']['id'], From a091edbf22bdec1a79de97d404732d2f48bf15b9 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 09:00:49 +0200 Subject: [PATCH 43/55] fix: [user:beforeSave] Only call the user-update callback if the user is not new --- src/Model/Table/UsersTable.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 7584216..789c9aa 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -63,7 +63,9 @@ class UsersTable extends AppTable public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) { - $success = $this->handleUserUpdateRouter($entity); + if (!$entity->isNew()) { + $success = $this->handleUserUpdateRouter($entity); + } return $success; } From 455daba4d43a8b4ada89248032382cd37a32df8a Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 14:06:46 +0200 Subject: [PATCH 44/55] fix: [navigation:meta-template] Correctly show badge for new templates --- src/Controller/Component/Navigation/MetaTemplates.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Component/Navigation/MetaTemplates.php b/src/Controller/Component/Navigation/MetaTemplates.php index ca83ef9..769bc36 100644 --- a/src/Controller/Component/Navigation/MetaTemplates.php +++ b/src/Controller/Component/Navigation/MetaTemplates.php @@ -68,7 +68,7 @@ class MetaTemplatesNavigation extends BaseNavigation public function addActions() { $totalUpdateCount = 0; - if (!empty($this->viewVars['updateableTemplates']['not-up-to-date']) && !empty($this->viewVars['updateableTemplates']['new'])) { + 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; From 0f27435251422572be6783dcb0cf35eabfab7711 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 14:07:41 +0200 Subject: [PATCH 45/55] fix: [metaTemplates] Correctly show update message --- src/Controller/MetaTemplatesController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/MetaTemplatesController.php b/src/Controller/MetaTemplatesController.php index 0a863e0..2595385 100644 --- a/src/Controller/MetaTemplatesController.php +++ b/src/Controller/MetaTemplatesController.php @@ -27,9 +27,9 @@ class MetaTemplatesController extends AppController 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']); + $message = __n('{0} templates updated.', 'The template has been updated.', empty($template_id), count($result['files_processed'])); } else { - $message = __n('{0} templates could not be updated.', 'The template could not be updated.', empty($template_id), $result['files_processed']); + $message = __n('{0} templates could not be updated.', 'The template could not be updated.', empty($template_id), count($result['files_processed'])); } $this->CRUD->setResponseForController('updateAllTemplate', $result['success'], $message, $result['files_processed'], $result['update_errors'], ['redirect' => $this->referer()]); $responsePayload = $this->CRUD->getResponsePayload(); From ddfc83af6fecde20b980d00d83fd3937b022bec3 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 14:14:38 +0200 Subject: [PATCH 46/55] chg: [navigation:socialProvider] Improved UI for SSO profile management --- src/Controller/Component/Navigation/Users.php | 1 + src/View/Helper/SocialProviderHelper.php | 5 ++++ .../element/layouts/header/header-profile.php | 27 +++++++++++++++++++ webroot/css/layout.css | 4 +++ 4 files changed, 37 insertions(+) diff --git a/src/Controller/Component/Navigation/Users.php b/src/Controller/Component/Navigation/Users.php index 4b1837b..c14ec70 100644 --- a/src/Controller/Component/Navigation/Users.php +++ b/src/Controller/Component/Navigation/Users.php @@ -72,6 +72,7 @@ class UsersNavigation extends BaseNavigation return []; }); if ( + !empty($this->loggedUser['social_profile']) && !empty(Configure::read('keycloak.enabled')) && !empty(Configure::read('keycloak.provider.baseUrl')) && !empty(Configure::read('keycloak.provider.realm')) && diff --git a/src/View/Helper/SocialProviderHelper.php b/src/View/Helper/SocialProviderHelper.php index df5c52b..effb8c5 100644 --- a/src/View/Helper/SocialProviderHelper.php +++ b/src/View/Helper/SocialProviderHelper.php @@ -13,6 +13,11 @@ class SocialProviderHelper extends Helper 'keycloak' => '/img/keycloak_logo.png', ]; + public function hasSocialProfile($identity): bool + { + return !empty($identity['social_profile']); + } + public function getIcon($identity) { if (!empty($identity['social_profile'])) { diff --git a/templates/element/layouts/header/header-profile.php b/templates/element/layouts/header/header-profile.php index 146aa3e..197eb8e 100644 --- a/templates/element/layouts/header/header-profile.php +++ b/templates/element/layouts/header/header-profile.php @@ -1,5 +1,6 @@
@@ -29,6 +30,32 @@ use Cake\Routing\Router; + SocialProvider->hasSocialProfile($this->request->getAttribute('identity'))) && + !empty(Configure::read('keycloak.enabled')) && + !empty(Configure::read('keycloak.provider.baseUrl')) && + !empty(Configure::read('keycloak.provider.realm')) && + !empty($this->request->getAttribute('identity')['id']) + ): + ?> + + SocialProvider->getIcon($this->request->getAttribute('identity')))): ?> + SocialProvider->getIcon($this->request->getAttribute('identity')) ?> + + + + + + diff --git a/webroot/css/layout.css b/webroot/css/layout.css index 4625b2b..9762b1b 100644 --- a/webroot/css/layout.css +++ b/webroot/css/layout.css @@ -513,6 +513,10 @@ ul.sidebar-elements > li.category > span.category-divider > hr { padding: 2px; border-radius: 0 0 0 5px; } +.header-breadcrumb-children .dropdown-menu .dropdown-item { + display: flex; + align-items: center; +} .header-breadcrumb-children .dropdown-menu .dropdown-item > i { min-width: 25px; } From 815e3e06718ed78c6bbf1905cd917620894cfad0 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 14:15:08 +0200 Subject: [PATCH 47/55] fix: [metaTemplates:updateAll] Fixed missing form preventing to update --- templates/MetaTemplates/update_all.php | 28 +++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/templates/MetaTemplates/update_all.php b/templates/MetaTemplates/update_all.php index 1e592cc..141cca5 100644 --- a/templates/MetaTemplates/update_all.php +++ b/templates/MetaTemplates/update_all.php @@ -1,4 +1,5 @@ $status) { if (!empty($status['new'])) { $tableHtml .= sprintf('%s', __('N/A')); } else { - $tableHtml .= sprintf('%s', + $tableHtml .= sprintf( + '%s', Router::url(['controller' => 'MetaTemplates', 'action' => 'view', 'plugin' => null, h($status['existing_template']->id)]), h($status['existing_template']->id) ); @@ -29,7 +31,8 @@ foreach ($templatesUpdateStatus as $uuid => $status) { if (!empty($status['new'])) { $tableHtml .= sprintf('%s', h($uuid)); } else { - $tableHtml .= sprintf('%s', + $tableHtml .= sprintf( + '%s', Router::url(['controller' => 'MetaTemplates', 'action' => 'view', 'plugin' => null, h($status['existing_template']->id)]), h($status['existing_template']->name) ); @@ -37,7 +40,8 @@ foreach ($templatesUpdateStatus as $uuid => $status) { if (!empty($status['new'])) { $tableHtml .= sprintf('%s', __('N/A')); } else { - $tableHtml .= sprintf('%s %s %s', + $tableHtml .= sprintf( + '%s %s %s', h($status['current_version']), $this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']), h($status['next_version']) @@ -107,6 +111,12 @@ if (empty($numberOfSkippedUpdates) && empty($numberOfUpdates)) { } $bodyHtml .= $tableHtml; +$form = sprintf( + '
%s%s
', + $this->Form->create(null), + $this->Form->end() +); +$bodyHtml .= $form; echo $this->Bootstrap->modal([ 'title' => h($title), @@ -117,3 +127,15 @@ echo $this->Bootstrap->modal([ 'confirmFunction' => 'updateMetaTemplate', ]); ?> + + \ No newline at end of file From 41a241cadad2c9b8d0be6dc3ac575674e52ef495 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 21 Oct 2022 15:25:52 +0200 Subject: [PATCH 48/55] new: [pgp] library ported from MISP - added proper view elements for encryption keys - added key information extraction --- composer.json | 13 +- src/Controller/EncryptionKeysController.php | 32 ++- src/Lib/Tools/CryptGpgExtended.php | 199 ++++++++++++++ src/Lib/Tools/GpgTool.php | 248 ++++++++++++++++++ src/Model/Table/EncryptionKeysTable.php | 91 +++++++ templates/EncryptionKeys/index.php | 11 +- templates/EncryptionKeys/view.php | 81 +++--- .../IndexTable/Fields/pgp_key.php | 5 + .../SingleViews/Fields/keyField.php | 5 + templates/element/genericElements/key.php | 21 ++ 10 files changed, 670 insertions(+), 36 deletions(-) create mode 100644 src/Lib/Tools/CryptGpgExtended.php create mode 100644 src/Lib/Tools/GpgTool.php create mode 100644 templates/element/genericElements/IndexTable/Fields/pgp_key.php create mode 100644 templates/element/genericElements/SingleViews/Fields/keyField.php create mode 100644 templates/element/genericElements/key.php diff --git a/composer.json b/composer.json index 17e726e..6e2a48d 100644 --- a/composer.json +++ b/composer.json @@ -13,11 +13,12 @@ "cakephp/migrations": "^3.0", "cakephp/plugin-installer": "^1.2", "erusev/parsedown": "^1.7", - "mobiledetect/mobiledetectlib": "^2.8" + "mobiledetect/mobiledetectlib": "^2.8", + "pear/crypt_gpg": "^1.6" }, "require-dev": { "cakephp/bake": "^2.0.3", - "cakephp/cakephp-codesniffer": "~4.0.0", + "cakephp/cakephp-codesniffer": "^4.0", "cakephp/debug_kit": "^4.0", "cebe/php-openapi": "^1.6", "fzaninotto/faker": "^1.9", @@ -68,7 +69,11 @@ }, "prefer-stable": true, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "cakephp/plugin-installer": true + } }, "minimum-stability": "dev" -} \ No newline at end of file +} diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index d0376db..39453b2 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -21,6 +21,8 @@ class EncryptionKeysController extends AppController public function index() { + $this->EncryptionKeys->initializeGpg(); + $Model = $this->EncryptionKeys; $this->CRUD->index([ 'quickFilters' => $this->quickFilterFields, 'filters' => $this->filterFields, @@ -31,6 +33,20 @@ class EncryptionKeysController extends AppController ], 'contain' => $this->containFields, 'statisticsFields' => $this->statisticsFields, + 'afterFind' => function($data) use ($Model) { + if ($data['type'] === 'pgp') { + $keyInfo = $Model->verifySingleGPG($data); + $data['status'] = __('OK'); + $data['fingerprint'] = __('N/A'); + if (!$keyInfo[0]) { + $data['status'] = $keyInfo[2]; + } + if (!empty($keyInfo[4])) { + $data['fingerprint'] = $keyInfo[4]; + } + } + return $data; + } ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -155,8 +171,22 @@ class EncryptionKeysController extends AppController public function view($id = false) { + $this->EncryptionKeys->initializeGpg(); + $Model = $this->EncryptionKeys; $this->CRUD->view($id, [ - 'contain' => ['Individuals', 'Organisations'] + 'contain' => ['Individuals', 'Organisations'], + 'afterFind' => function($data) use ($Model) { + if ($data['type'] === 'pgp') { + $keyInfo = $Model->verifySingleGPG($data); + if (!$keyInfo[0]) { + $data['pgp_error'] = $keyInfo[2]; + } + if (!empty($keyInfo[4])) { + $data['pgp_fingerprint'] = $keyInfo[4]; + } + } + return $data; + } ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { diff --git a/src/Lib/Tools/CryptGpgExtended.php b/src/Lib/Tools/CryptGpgExtended.php new file mode 100644 index 0000000..266f6f7 --- /dev/null +++ b/src/Lib/Tools/CryptGpgExtended.php @@ -0,0 +1,199 @@ +getFileName(); + throw new Exception("Crypt_GPG class from '$classPath' is too old, at least version 1.6.1 is required."); + } + parent::__construct($options); + } + + /** + * Export the smallest public key possible from the keyring. + * + * This removes all signatures except the most recent self-signature on each user ID. This option is the same as + * running the --edit-key command "minimize" before export except that the local copy of the key is not modified. + * + * The exported key remains on the keyring. To delete the public key, use + * {@link Crypt_GPG::deletePublicKey()}. + * + * If more than one key fingerprint is available for the specified + * $keyId (for example, if you use a non-unique uid) only the + * first public key is exported. + * + * @param string $keyId either the full uid of the public key, the email + * part of the uid of the public key or the key id of + * the public key. For example, + * "Test User (example) ", + * "test@example.com" or a hexadecimal string. + * @param boolean $armor optional. If true, ASCII armored data is returned; + * otherwise, binary data is returned. Defaults to + * true. + * + * @return string the public key data. + * + * @throws Crypt_GPG_KeyNotFoundException if a public key with the given + * $keyId is not found. + * + * @throws Crypt_GPG_Exception if an unknown or unexpected error occurs. + * Use the debug option and file a bug report if these + * exceptions occur. + */ + public function exportPublicKeyMinimal($keyId, $armor = true) + { + $fingerprint = $this->getFingerprint($keyId); + + if ($fingerprint === null) { + throw new \Crypt_GPG_KeyNotFoundException( + 'Key not found: ' . $keyId, + self::ERROR_KEY_NOT_FOUND, + $keyId + ); + } + + $keyData = ''; + $operation = '--export'; + $operation .= ' ' . escapeshellarg($fingerprint); + + $arguments = array('--export-options', 'export-minimal'); + if ($armor) { + $arguments[] = '--armor'; + } + + $this->engine->reset(); + $this->engine->setPins($this->passphrases); + $this->engine->setOutput($keyData); + $this->engine->setOperation($operation, $arguments); + $this->engine->run(); + + return $keyData; + } + + /** + * Return key info without importing it when GPG supports --import-options show-only, otherwise just import and + * then return details. + * + * @param string $key + * @return Crypt_GPG_Key[] + * @throws Crypt_GPG_Exception + * @throws Crypt_GPG_InvalidOperationException + */ + public function keyInfo($key) + { + $version = $this->engine->getVersion(); + if (version_compare($version, '2.1.23', 'le')) { + $importResult = $this->importKey($key); + $keys = []; + foreach ($importResult['fingerprints'] as $fingerprint) { + foreach ($this->getKeys($fingerprint) as $key) { + $keys[] = $key; + } + } + return $keys; + } + + $input = $this->_prepareInput($key, false, false); + + $output = ''; + $this->engine->reset(); + $this->engine->setInput($input); + $this->engine->setOutput($output); + $this->engine->setOperation('--import', ['--import-options', 'show-only', '--with-colons']); + $this->engine->run(); + + $keys = []; + $key = null; // current key + $subKey = null; // current sub-key + + foreach (explode(PHP_EOL, $output) as $line) { + $lineExp = explode(':', $line); + + if ($lineExp[0] === 'pub') { + // new primary key means last key should be added to the array + if ($key !== null) { + $keys[] = $key; + } + + $key = new \Crypt_GPG_Key(); + + $subKey = \Crypt_GPG_SubKey::parse($line); + $key->addSubKey($subKey); + + } elseif ($lineExp[0] === 'sub') { + $subKey = \Crypt_GPG_SubKey::parse($line); + $key->addSubKey($subKey); + + } elseif ($lineExp[0] === 'fpr') { + $fingerprint = $lineExp[9]; + + // set current sub-key fingerprint + $subKey->setFingerprint($fingerprint); + + } elseif ($lineExp[0] === 'uid') { + $string = stripcslashes($lineExp[9]); // as per documentation + $userId = new \Crypt_GPG_UserId($string); + + if ($lineExp[1] === 'r') { + $userId->setRevoked(true); + } + + $key->addUserId($userId); + } + } + + // add last key + if ($key !== null) { + $keys[] = $key; + } else { + throw new \Crypt_GPG_Exception("Key data provided, but gpg process output could not be parsed: $output"); + } + + return $keys; + } + + /** + * @param string $key + * @return string + * @throws Crypt_GPG_Exception + * @throws Crypt_GPG_InvalidOperationException + */ + public function enarmor($key) + { + $input = $this->_prepareInput($key, false, false); + + $armored = ''; + $this->engine->reset(); + $this->engine->setInput($input); + $this->engine->setOutput($armored); + $this->engine->setOperation('--enarmor'); + $this->engine->run(); + + return $armored; + } + + /** + * @param mixed $data + * @param bool $isFile + * @param bool $allowEmpty + * @return resource|string|null + * @throws Crypt_GPG_FileException + * @throws Crypt_GPG_NoDataException + */ + protected function _prepareInput($data, $isFile = false, $allowEmpty = true) + { + if ($isFile && $data instanceof TmpFileTool) { + return $data->resource(); + } + + return parent::_prepareInput($data, $isFile, $allowEmpty); + } +} diff --git a/src/Lib/Tools/GpgTool.php b/src/Lib/Tools/GpgTool.php new file mode 100644 index 0000000..e6da4b1 --- /dev/null +++ b/src/Lib/Tools/GpgTool.php @@ -0,0 +1,248 @@ + $homedir, + 'gpgconf' => Configure::read('GnuPG.gpgconf'), + 'binary' => Configure::read('GnuPG.binary') ?: '/usr/bin/gpg', + ]; + return new CryptGpgExtended($options); + } + + public function __construct(CryptGpgExtended $gpg = null) + { + $this->gpg = $gpg; + } + + /** + * @param string $search + * @return array + * @throws Exception + */ + public function searchGpgKey($search) + { + $uri = 'https://openpgp.circl.lu/pks/lookup?search=' . urlencode($search) . '&op=index&fingerprint=on&options=mr'; + try { + $response = $this->keyServerLookup($uri); + } catch (HttpSocketHttpException $e) { + if ($e->getCode() === 404) { + return []; + } + throw $e; + } + return $this->extractKeySearch($response->body); + } + + /** + * @param string $fingerprint + * @return string|null + * @throws Exception + */ + public function fetchGpgKey($fingerprint) + { + $uri = 'https://openpgp.circl.lu/pks/lookup?search=0x' . urlencode($fingerprint) . '&op=get&options=mr'; + try { + $response = $this->keyServerLookup($uri); + } catch (HttpSocketHttpException $e) { + if ($e->getCode() === 404) { + return null; + } + throw $e; + } + + $key = $response->body; + + if ($this->gpg) { + $fetchedFingerprint = $this->validateGpgKey($key); + if (strtolower($fingerprint) !== strtolower($fetchedFingerprint)) { + throw new Exception("Requested fingerprint do not match with fetched key fingerprint ($fingerprint != $fetchedFingerprint)"); + } + } + + return $key; + } + + /** + * Validates PGP key + * @param string $keyData + * @return string Primary key fingerprint + * @throws Exception + */ + public function validateGpgKey($keyData) + { + if (!$this->gpg instanceof CryptGpgExtended) { + throw new InvalidArgumentException("Valid CryptGpgExtended instance required."); + } + $fetchedKeyInfo = $this->gpg->keyInfo($keyData); + if (count($fetchedKeyInfo) !== 1) { + throw new Exception("Multiple keys found"); + } + $primaryKey = $fetchedKeyInfo[0]->getPrimaryKey(); + if (empty($primaryKey)) { + throw new Exception("No primary key found"); + } + $this->gpg->importKey($keyData); + return $primaryKey->getFingerprint(); + } + + /** + * @param string $body + * @return array + */ + private function extractKeySearch($body) + { + $final = array(); + $lines = explode("\n", $body); + foreach ($lines as $line) { + $parts = explode(":", $line); + + if ($parts[0] === 'pub') { + if (!empty($temp)) { + $final[] = $temp; + $temp = array(); + } + + if (strpos($parts[6], 'r') !== false || strpos($parts[6], 'd') !== false || strpos($parts[6], 'e') !== false) { + continue; // skip if key is expired, revoked or disabled + } + + $temp = array( + 'fingerprint' => $parts[1], + 'key_id' => substr($parts[1], -8), + 'date' => date('Y-m-d', $parts[4]), + ); + + } else if ($parts[0] === 'uid' && !empty($temp)) { + $temp['address'] = urldecode($parts[1]); + } + } + + if (!empty($temp)) { + $final[] = $temp; + } + + return $final; + } + + /** + * @see https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-10 + * @param string $email + * @return string + * @throws Exception + */ + public function wkd($email) + { + if (!$this->gpg instanceof CryptGpgExtended) { + throw new InvalidArgumentException("Valid CryptGpgExtended instance required."); + } + + $parts = explode('@', $email); + if (count($parts) !== 2) { + throw new InvalidArgumentException("Invalid e-mail address provided."); + } + + list($localPart, $domain) = $parts; + $localPart = strtolower($localPart); + $localPartHash = $this->zbase32(sha1($localPart, true)); + + $advancedUrl = "https://openpgpkey.$domain/.well-known/openpgpkey/" . strtolower($domain) . "/hu/$localPartHash"; + try { + $response = $this->keyServerLookup($advancedUrl); + return $this->gpg->enarmor($response->body()); + } catch (Exception $e) { + // pass, continue to direct method + } + + $directUrl = "https://$domain/.well-known/openpgpkey/hu/$localPartHash"; + try { + $response = $this->keyServerLookup($directUrl); + } catch (HttpSocketHttpException $e) { + if ($e->getCode() === 404) { + throw new NotFoundException("Key not found"); + } + throw $e; + } + return $this->gpg->enarmor($response->body()); + } + + /** + * Converts data to zbase32 string. + * + * @see http://philzimmermann.com/docs/human-oriented-base-32-encoding.txt + * @param string $data + * @return string + */ + private function zbase32($data) + { + $chars = 'ybndrfg8ejkmcpqxot1uwisza345h769'; // lower-case + $res = ''; + $remainder = 0; + $remainderSize = 0; + + for ($i = 0; $i < strlen($data); $i++) { + $b = ord($data[$i]); + $remainder = ($remainder << 8) | $b; + $remainderSize += 8; + while ($remainderSize > 4) { + $remainderSize -= 5; + $c = $remainder & (31 << $remainderSize); + $c >>= $remainderSize; + $res .= $chars[$c]; + } + } + if ($remainderSize > 0) { + // remainderSize < 5: + $remainder <<= (5 - $remainderSize); + $c = $remainder & 31; + $res .= $chars[$c]; + } + return $res; + } + + /** + * @param string $uri + * @return HttpSocketResponseExtended + * @throws HttpSocketHttpException + * @throws Exception + */ + private function keyServerLookup($uri) + { + App::uses('SyncTool', 'Tools'); + $syncTool = new SyncTool(); + $HttpSocket = $syncTool->createHttpSocket(['compress' => true]); + $response = $HttpSocket->get($uri); + if (!$response->isOk()) { + throw new HttpSocketHttpException($response, $uri); + } + return $response; + } +} diff --git a/src/Model/Table/EncryptionKeysTable.php b/src/Model/Table/EncryptionKeysTable.php index 2008e0d..0015e1d 100644 --- a/src/Model/Table/EncryptionKeysTable.php +++ b/src/Model/Table/EncryptionKeysTable.php @@ -10,6 +10,9 @@ use ArrayObject; class EncryptionKeysTable extends AppTable { + + public $gpg = null; + public function initialize(array $config): void { parent::initialize($config); @@ -56,4 +59,92 @@ class EncryptionKeysTable extends AppTable ->requirePresence(['type', 'encryption_key', 'owner_id', 'owner_model'], 'create'); return $validator; } + + /** + * 0 - true if key is valid + * 1 - User e-mail + * 2 - Error message + * 3 - Not used + * 4 - Key fingerprint + * 5 - Key fingerprint + * @param \App\Model\Entity\EncryptionKey $encryptionKey + * @return array + */ + public function verifySingleGPG(\App\Model\Entity\EncryptionKey $encryptionKey): array + { + $result = [0 => false, 1 => null]; + + $gpg = $this->initializeGpg(); + if (!$gpg) { + $result[2] = 'GnuPG is not configured on this system.'; + return $result; + } + + try { + $currentTimestamp = time(); + $keys = $gpg->keyInfo($encryptionKey['encryption_key']); + if (count($keys) !== 1) { + $result[2] = 'Multiple or no key found'; + return $result; + } + + $key = $keys[0]; + $result[4] = $key->getPrimaryKey()->getFingerprint(); + $result[5] = $result[4]; + + $sortedKeys = ['valid' => 0, 'expired' => 0, 'noEncrypt' => 0]; + foreach ($key->getSubKeys() as $subKey) { + $expiration = $subKey->getExpirationDate(); + if ($expiration != 0 && $currentTimestamp > $expiration) { + $sortedKeys['expired']++; + continue; + } + if (!$subKey->canEncrypt()) { + $sortedKeys['noEncrypt']++; + continue; + } + $sortedKeys['valid']++; + } + if (!$sortedKeys['valid']) { + $result[2] = 'The user\'s PGP key does not include a valid subkey that could be used for encryption.'; + if ($sortedKeys['expired']) { + $result[2] .= ' ' . __n('Found %s subkey that have expired.', 'Found %s subkeys that have expired.', $sortedKeys['expired'], $sortedKeys['expired']); + } + if ($sortedKeys['noEncrypt']) { + $result[2] .= ' ' . __n('Found %s subkey that is sign only.', 'Found %s subkeys that are sign only.', $sortedKeys['noEncrypt'], $sortedKeys['noEncrypt']); + } + } else { + $result[0] = true; + } + } catch (\Exception $e) { + $result[2] = $e->getMessage(); + } + return $result; + } + + + /** + * Initialize GPG. Returns `null` if initialization failed. + * + * @return null|CryptGpgExtended + */ + public function initializeGpg() + { + require_once(ROOT . '/src/Lib/Tools/GpgTool.php'); + if ($this->gpg !== null) { + if ($this->gpg === false) { // initialization failed + return null; + } + return $this->gpg; + } + + try { + $this->gpg = \App\Lib\Tools\GpgTool::initializeGpg(); + return $this->gpg; + } catch (\Exception $e) { + //$this->logException("GPG couldn't be initialized, GPG encryption and signing will be not available.", $e, LOG_NOTICE); + $this->gpg = false; + return null; + } + } } diff --git a/templates/EncryptionKeys/index.php b/templates/EncryptionKeys/index.php index 413e2d8..1d9e241 100644 --- a/templates/EncryptionKeys/index.php +++ b/templates/EncryptionKeys/index.php @@ -48,6 +48,14 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'owner_model_path' => 'owner_model', 'element' => 'owner' ], + [ + 'name' => __('Revoked'), + 'data_path' => 'fingerprint' + ], + [ + 'name' => __('Status'), + 'data_path' => 'status' + ], [ 'name' => __('Revoked'), 'sort' => 'revoked', @@ -56,7 +64,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ ], [ 'name' => __('Key'), - 'data_path' => 'encryption_key' + 'data_path' => 'encryption_key', + 'element' => 'pgp_key' ], ], 'title' => __('Encryption key Index'), diff --git a/templates/EncryptionKeys/view.php b/templates/EncryptionKeys/view.php index e92da92..da95f99 100644 --- a/templates/EncryptionKeys/view.php +++ b/templates/EncryptionKeys/view.php @@ -1,32 +1,53 @@ element( - '/genericElements/SingleViews/single_view', - [ - 'data' => $entity, - 'fields' => [ - [ - 'key' => __('ID'), - 'path' => 'id' - ], - [ - 'key' => __('Type'), - 'path' => 'type' - ], - [ - 'key' => __('Owner'), - 'path' => 'owner_id', - 'owner_model_path' => 'owner_model', - 'type' => 'owner' - ], - [ - 'key' => __('Revoked'), - 'path' => 'revoked' - ], - - [ - 'key' => __('Key'), - 'path' => 'encryption_key' - ] + $fields = [ + [ + 'key' => __('ID'), + 'path' => 'id' + ], + [ + 'key' => __('Type'), + 'path' => 'type' + ], + [ + 'key' => __('Owner'), + 'path' => 'owner_id', + 'owner_model_path' => 'owner_model', + 'type' => 'owner' + ], + [ + 'key' => __('Revoked'), + 'path' => 'revoked', + 'type' => 'boolean' + ], + [ + 'key' => __('Key'), + 'path' => 'encryption_key', + 'type' => 'key' ] - ] -); + ]; + if ($entity['type'] === 'pgp') { + if (!empty($entity['pgp_fingerprint'])) { + $fields[] = [ + 'key' => __('Fingerprint'), + 'path' => 'pgp_fingerprint' + ]; + } + if (!empty($entity['pgp_error'])) { + $fields[] = [ + 'key' => __('PGP Status'), + 'path' => 'pgp_error' + ]; + } else { + $fields[] = [ + 'key' => __('PGP Status'), + 'raw' => __('OK') + ]; + } + } + echo $this->element( + '/genericElements/SingleViews/single_view', + [ + 'data' => $entity, + 'fields' => $fields + ] + ); diff --git a/templates/element/genericElements/IndexTable/Fields/pgp_key.php b/templates/element/genericElements/IndexTable/Fields/pgp_key.php new file mode 100644 index 0000000..fb29755 --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/pgp_key.php @@ -0,0 +1,5 @@ +element('/genericElements/key', ['value' => $value, 'description' => $description ?? null]); +?> diff --git a/templates/element/genericElements/SingleViews/Fields/keyField.php b/templates/element/genericElements/SingleViews/Fields/keyField.php new file mode 100644 index 0000000..4a76e09 --- /dev/null +++ b/templates/element/genericElements/SingleViews/Fields/keyField.php @@ -0,0 +1,5 @@ +element('/genericElements/key', ['value' => $value, 'description' => $description ?? null]); +?> diff --git a/templates/element/genericElements/key.php b/templates/element/genericElements/key.php new file mode 100644 index 0000000..572b8d3 --- /dev/null +++ b/templates/element/genericElements/key.php @@ -0,0 +1,21 @@ +%s', + __('N/A') + ); + } else { + echo sprintf( + '
%s%s
', + !empty($description) ? + sprintf( + '%s', + h($description) + ) : '', + sprintf( + '
%s
', + h($value) + ) + ); + } +?> \ No newline at end of file From 5389f02b4f8cd9025cf4c17e78a654a57c4af98b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 15:29:45 +0200 Subject: [PATCH 49/55] new: [scss:boostrap-additional] Added `btn-outline-text` to ease integration with themes --- .../IndexTable/Fields/actions.php | 2 +- .../additional/bootstrap-additional.css | 25 +++++++++++++++++++ webroot/css/themes/theme-darkly.css | 25 +++++++++++++++++++ webroot/css/themes/theme-default.css | 25 +++++++++++++++++++ webroot/css/themes/theme-flatly.css | 25 +++++++++++++++++++ webroot/css/themes/theme-minty.css | 25 +++++++++++++++++++ webroot/css/themes/theme-quartz.css | 25 +++++++++++++++++++ webroot/css/themes/theme-slate.css | 25 +++++++++++++++++++ webroot/css/themes/theme-vapor.css | 25 +++++++++++++++++++ .../scss/additional/bootstrap-additional.scss | 4 +++ 10 files changed, 205 insertions(+), 1 deletion(-) diff --git a/templates/element/genericElements/IndexTable/Fields/actions.php b/templates/element/genericElements/IndexTable/Fields/actions.php index 379fb58..570ff63 100644 --- a/templates/element/genericElements/IndexTable/Fields/actions.php +++ b/templates/element/genericElements/IndexTable/Fields/actions.php @@ -107,7 +107,7 @@ empty($action['title']) ? '' : h($action['title']), empty($action['dbclickAction']) ? '' : 'class="dblclickActionElement"', empty($action['onclick']) ? '' : sprintf('onClick="%s"', $action['onclick']), - empty($action['variant']) ? 'outline-dark' : h($action['variant']), + empty($action['variant']) ? 'outline-text' : h($action['variant']), $this->FontAwesome->getClass($action['icon']) ); } diff --git a/webroot/css/themes/additional/bootstrap-additional.css b/webroot/css/themes/additional/bootstrap-additional.css index 4b53697..2654c1e 100644 --- a/webroot/css/themes/additional/bootstrap-additional.css +++ b/webroot/css/themes/additional/bootstrap-additional.css @@ -237,6 +237,31 @@ background-color: #212529; } +.btn-outline-text { + color: #212529; + border-color: #212529; +} +.btn-outline-text:hover { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #212529; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/css/themes/theme-darkly.css b/webroot/css/themes/theme-darkly.css index 5d202e4..f0ab02c 100644 --- a/webroot/css/themes/theme-darkly.css +++ b/webroot/css/themes/theme-darkly.css @@ -237,6 +237,31 @@ background-color: #303030; } +.btn-outline-text { + color: #fff; + border-color: #fff; +} +.btn-outline-text:hover { + color: #000; + background-color: #fff; + border-color: #fff; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #000; + background-color: #fff; + border-color: #fff; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #fff; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/css/themes/theme-default.css b/webroot/css/themes/theme-default.css index 1cd760e..632df9b 100644 --- a/webroot/css/themes/theme-default.css +++ b/webroot/css/themes/theme-default.css @@ -237,6 +237,31 @@ background-color: #212529; } +.btn-outline-text { + color: #212529; + border-color: #212529; +} +.btn-outline-text:hover { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #212529; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/css/themes/theme-flatly.css b/webroot/css/themes/theme-flatly.css index b2df7b7..a136080 100644 --- a/webroot/css/themes/theme-flatly.css +++ b/webroot/css/themes/theme-flatly.css @@ -237,6 +237,31 @@ background-color: #7b8a8b; } +.btn-outline-text { + color: #212529; + border-color: #212529; +} +.btn-outline-text:hover { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #212529; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/css/themes/theme-minty.css b/webroot/css/themes/theme-minty.css index 3205729..f118a7d 100644 --- a/webroot/css/themes/theme-minty.css +++ b/webroot/css/themes/theme-minty.css @@ -237,6 +237,31 @@ background-color: #7b8a8b; } +.btn-outline-text { + color: #212529; + border-color: #212529; +} +.btn-outline-text:hover { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #fff; + background-color: #212529; + border-color: #212529; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #212529; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/css/themes/theme-quartz.css b/webroot/css/themes/theme-quartz.css index f88e622..8853db3 100644 --- a/webroot/css/themes/theme-quartz.css +++ b/webroot/css/themes/theme-quartz.css @@ -237,6 +237,31 @@ background-color: #212529; } +.btn-outline-text { + color: #fff; + border-color: #fff; +} +.btn-outline-text:hover { + color: #000; + background-color: #fff; + border-color: #fff; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #000; + background-color: #fff; + border-color: #fff; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #fff; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/css/themes/theme-slate.css b/webroot/css/themes/theme-slate.css index 70309e0..be86d33 100644 --- a/webroot/css/themes/theme-slate.css +++ b/webroot/css/themes/theme-slate.css @@ -237,6 +237,31 @@ background-color: #272b30; } +.btn-outline-text { + color: #aaa; + border-color: #aaa; +} +.btn-outline-text:hover { + color: #fff; + background-color: #aaa; + border-color: #aaa; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(170, 170, 170, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #fff; + background-color: #aaa; + border-color: #aaa; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(170, 170, 170, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #aaa; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/css/themes/theme-vapor.css b/webroot/css/themes/theme-vapor.css index 2c7266f..be21737 100644 --- a/webroot/css/themes/theme-vapor.css +++ b/webroot/css/themes/theme-vapor.css @@ -237,6 +237,31 @@ background-color: #170229; } +.btn-outline-text { + color: #32fbe2; + border-color: #32fbe2; +} +.btn-outline-text:hover { + color: #fff; + background-color: #32fbe2; + border-color: #32fbe2; +} +.btn-check:focus + .btn-outline-text, .btn-outline-text:focus { + box-shadow: 0 0 0 0.25rem rgba(50, 251, 226, 0.5); +} +.btn-check:checked + .btn-outline-text, .btn-check:active + .btn-outline-text, .btn-outline-text:active, .btn-outline-text.active, .btn-outline-text.dropdown-toggle.show { + color: #fff; + background-color: #32fbe2; + border-color: #32fbe2; +} +.btn-check:checked + .btn-outline-text:focus, .btn-check:active + .btn-outline-text:focus, .btn-outline-text:active:focus, .btn-outline-text.active:focus, .btn-outline-text.dropdown-toggle.show:focus { + box-shadow: 0 0 0 0.25rem rgba(50, 251, 226, 0.5); +} +.btn-outline-text:disabled, .btn-outline-text.disabled { + color: #32fbe2; + background-color: transparent; +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; diff --git a/webroot/theme/scss/additional/bootstrap-additional.scss b/webroot/theme/scss/additional/bootstrap-additional.scss index 9b234dd..6c0675d 100644 --- a/webroot/theme/scss/additional/bootstrap-additional.scss +++ b/webroot/theme/scss/additional/bootstrap-additional.scss @@ -66,6 +66,10 @@ $toast-color-level: 70% !default; } } +.btn-outline-text { + @include button-outline-variant($body-color); +} + /* Progress Timeline */ .progress-timeline { padding: 0.2em 0.2em 0.5em 0.2em; From cfae8cb91435a14161f933f4575b9933fdebd267 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Oct 2022 15:36:08 +0200 Subject: [PATCH 50/55] chg: [indexTable:indexStatistic] better support of themes --- .../IndexTable/Statistics/index_statistic_field_amount.php | 2 +- .../IndexTable/Statistics/index_statistic_timestamp.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php index 1f92f0e..123da84 100644 --- a/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php +++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php @@ -57,7 +57,7 @@ foreach ($statistics['usage'] as $scope => $graphData) { 'bodyClass' => 'py-1 px-2', 'class' => ['shadow-sm', 'h-100'] ]); - $statisticsHtml .= sprintf('
%s
', $statPie); + $statisticsHtml .= sprintf('
%s
', $statPie); } ?> diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php index c202890..bd0d43b 100644 --- a/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php +++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php @@ -95,7 +95,7 @@ $card = $this->Bootstrap->card([ ?> -
+