diff --git a/libraries/default/InboxProcessors/NotificationInboxProcessor.php b/libraries/default/InboxProcessors/NotificationInboxProcessor.php
new file mode 100644
index 0000000..081b424
--- /dev/null
+++ b/libraries/default/InboxProcessors/NotificationInboxProcessor.php
@@ -0,0 +1,97 @@
+description = __('Notify when data has been changed');
+ $this->severity = $this->Inbox::SEVERITY_PRIMARY;
+ }
+
+ protected function addValidatorRules($validator)
+ {
+ return $validator
+ ->requirePresence('original', 'changed')
+ ->isArray('original', __('The `original` data must be provided'))
+ ->isArray('changed', __('The `changed` data must be provided'));
+ }
+
+ public function create($requestData) {
+ $this->validateRequestData($requestData);
+ return parent::create($requestData);
+ }
+
+ public function getViewVariables($request)
+ {
+ $request = $this->__decodeMoreFields($request);
+ return [
+ 'data' => [
+ 'original' => $request->data['original'],
+ 'changed' => $request->data['changed'],
+ 'summaryTemplate' => $request->data['summaryTemplate'],
+ 'summaryMessage' => $request->data['summaryMessage'],
+ 'entityType' => $request->data['entityType'],
+ 'entityDisplayField' => $request->data['entityDisplayField'],
+ 'entityViewURL' => $request->data['entityViewURL'],
+ ],
+ ];
+ }
+
+ public function process($id, $requestData, $inboxRequest)
+ {
+ $this->discard($id, $requestData);
+ return $this->genActionResult(
+ [],
+ true,
+ __('Notification acknowledged'),
+ []
+ );
+ }
+
+ public function discard($id, $requestData)
+ {
+ return parent::discard($id, $requestData);
+ }
+
+ private function __decodeMoreFields($request)
+ {
+ $decodedRequest = $request;
+ $decodedRequest->data['original'] = $this->__decodeMoreChangedFields($request->data['original']);
+ $decodedRequest->data['changed'] = $this->__decodeMoreChangedFields($request->data['changed']);
+ return $decodedRequest;
+ }
+
+ private function __decodeMoreChangedFields(array $fieldData): array
+ {
+ array_walk($fieldData, function(&$fieldValue, $fieldName) {
+ if ($fieldName === 'meta_fields') {
+ $fieldValue = json_decode($fieldValue, true);
+ }
+ });
+ return $fieldData;
+ }
+}
\ No newline at end of file
diff --git a/libraries/default/InboxProcessors/templates/Notification/DataChange.php b/libraries/default/InboxProcessors/templates/Notification/DataChange.php
new file mode 100644
index 0000000..8cf79b8
--- /dev/null
+++ b/libraries/default/InboxProcessors/templates/Notification/DataChange.php
@@ -0,0 +1,79 @@
+%s',
+ h($data['entityViewURL']),
+ h($data['entityDisplayField'])
+ )
+ );
+} else {
+ $changedSummary = '';
+}
+
+$form = $this->element('genericElements/Form/genericForm', [
+ 'entity' => null,
+ 'ajax' => false,
+ 'raw' => true,
+ 'data' => [
+ 'model' => 'Inbox',
+ 'fields' => [],
+ 'submit' => [
+ 'action' => $this->request->getParam('action')
+ ]
+ ]
+]);
+
+echo $this->Bootstrap->modal([
+ 'title' => __('Acknowledge notification'),
+ 'size' => 'xl',
+ 'type' => 'confirm',
+ 'bodyHtml' => sprintf(
+ '
%s
+ ',
+ $form,
+ $changedSummary,
+ $this->Bootstrap->card([
+ 'headerText' => __('Original values'),
+ 'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['original']]])
+ ]),
+ $this->Bootstrap->card([
+ 'headerText' => __('Changed values'),
+ 'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['changed']]])
+ ])
+ ),
+ 'confirmText' => __('Acknowledge & Discard'),
+ 'confirmIcon' => 'check',
+ // 'confirmFunction' => 'submitAck'
+]);
+?>
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php
index 9e8aef8..238dc46 100644
--- a/src/Controller/AppController.php
+++ b/src/Controller/AppController.php
@@ -122,6 +122,7 @@ class AppController extends Controller
$this->set('loggedUser', $this->ACL->getUser());
$this->set('roleAccess', $this->ACL->getRoleAccess(false, false));
}
+ Configure::write('loggedUser', $user);
} else if ($this->ParamHandler->isRest()) {
throw new MethodNotAllowedException(__('Invalid user credentials.'));
}
diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php
index 602f637..7d7e612 100644
--- a/src/Controller/Component/CRUDComponent.php
+++ b/src/Controller/Component/CRUDComponent.php
@@ -457,6 +457,7 @@ class CRUDComponent extends Component
}
$entity->setDirty('meta_fields', true);
+ $entity->_metafields_to_delete = $metaFieldsToDelete;
return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
}
diff --git a/src/Model/Behavior/NotifyAdminsBehavior.php b/src/Model/Behavior/NotifyAdminsBehavior.php
new file mode 100644
index 0000000..62db8fb
--- /dev/null
+++ b/src/Model/Behavior/NotifyAdminsBehavior.php
@@ -0,0 +1,270 @@
+ [
+ 'notifySiteAdmins' => 'notifySiteAdmins',
+ 'notifySiteAdminsForEntity' => 'notifySiteAdminsForEntity',
+ ]
+ ];
+
+ /** @var AuditLog|null */
+ private $Inbox;
+ /** @var User|null */
+ private $Users;
+ /** @var InboxProcessors|null */
+ private $InboxProcessors;
+
+ public function initialize(array $config): void
+ {
+ $this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
+ if($this->table() instanceof UsersTable) {
+ $this->Users = $this->table();
+ } else {
+ $this->Users = TableRegistry::getTableLocator()->get('Users');
+ }
+ }
+
+ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+ {
+ if (!$this->isNotificationAllowed($event, $entity, $options)) {
+ return;
+ }
+ $this->notifySiteAdminsForEntityChange($entity);
+ }
+
+ public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+ {
+ if (!$this->isNotificationAllowed($event, $entity, $options)) {
+ return;
+ }
+ $this->notifySiteAdminsForEntityDeletion($entity);
+ }
+
+ public function isNotificationAllowed(EventInterface $event, EntityInterface $entity, ArrayObject $options): bool
+ {
+ $loggedUser = Configure::read('loggedUser');
+ if (empty($loggedUser) || !empty($loggedUser['role']['perm_admin']) || !empty($loggedUser['role']['perm_sync'])) {
+ return false;
+ }
+ return true;
+ }
+
+ public function notifySiteAdminsForEntityChange(EntityInterface $entity): void
+ {
+ $watchedFields = !empty($this->getConfig()['fields']) ? $this->getConfig()['fields'] : $entity->getVisible();
+ $originalFields = [];
+ $changedFields = $entity->extract($watchedFields);
+ $titleTemplate = 'New {0} `{1}` created';
+ $title = __(
+ $titleTemplate,
+ Inflector::singularize($entity->getSource()),
+ $entity->get($this->table()->getDisplayField())
+ );
+ $message = __('New {0}', Inflector::singularize($entity->getSource()));
+ if (!$entity->isNew()) {
+ $originalFields = $this->_getOriginalChangedFields($entity, $watchedFields);
+ if ($entity->table()->hasBehavior('MetaFields') && !empty($entity->meta_fields)) {
+ $originalFields['meta_fields'] = array_merge(
+ $originalFields['meta_fields'],
+ $this->_getDeletedMetafields($entity)
+ );
+ }
+ $changedFields = $entity->extract(array_keys($originalFields));
+ if ($entity->table()->hasBehavior('MetaFields') && !empty($entity->meta_fields)) {
+ $originalFields['meta_fields'] = array_map(function($metaField) {
+ $originalValues = $metaField->getOriginalValues($metaField->getVisible());
+ $metaField->set($originalValues);
+ return $metaField;
+ }, $originalFields['meta_fields']);
+ $originalFields['meta_fields'] = $this->_massageMetaField($entity, $originalFields['meta_fields']);
+ $changedFields['meta_fields'] = $this->_massageMetaField($entity, $changedFields['meta_fields']);
+ }
+ $titleTemplate = '{0} {1} modified';
+ $title = __(
+ $titleTemplate,
+ Inflector::singularize($entity->getSource()),
+ $entity->get($this->table()->getDisplayField())
+ );
+ $message = __n(
+ '{0} field was updated',
+ '{0} fields were updated',
+ count($changedFields),
+ count($changedFields)
+ );
+ }
+ $data = [
+ 'original' => $this->_serializeFields($originalFields),
+ 'changed' => $this->_serializeFields($changedFields),
+ 'summaryTemplate' => $titleTemplate,
+ 'summaryMessage' => $message,
+ 'entityType' => Inflector::singularize($entity->getSource()),
+ 'entityDisplayField' => $entity->get($this->table()->getDisplayField()),
+ 'entityViewURL' => Router::url([
+ 'controller' => Inflector::underscore($entity->getSource()),
+ 'action' => 'view',
+ $entity->id
+ ]),
+ ];
+ $this->notifySiteAdmins($entity->getSource(), $title, $message, $data);
+ }
+
+ public function notifySiteAdminsForEntityDeletion(EntityInterface $entity): void
+ {
+ $watchedFields = !empty($this->getConfig()['fields']) ? $this->getConfig()['fields'] : $entity->getVisible();
+ $originalFields = $entity->extract($watchedFields);
+ $changedFields = [];
+ $titleTemplate = 'Deleted {0} `{1}`';
+ $title = __(
+ $titleTemplate,
+ Inflector::singularize($entity->getSource()),
+ $entity->get($this->table()->getDisplayField())
+ );
+ $message = __('{0} deleted', Inflector::singularize($entity->getSource()));
+ $data = [
+ 'original' => $this->_serializeFields($originalFields),
+ 'changed' => $this->_serializeFields($changedFields),
+ 'summaryTemplate' => $titleTemplate,
+ 'summaryMessage' => $message,
+ 'entityType' => Inflector::singularize($entity->getSource()),
+ 'entityDisplayField' => $entity->get($this->table()->getDisplayField()),
+ 'entityViewURL' => Router::url([
+ 'controller' => Inflector::underscore($entity->getSource()),
+ 'action' => 'view',
+ $entity->id
+ ]),
+ ];
+ $this->notifySiteAdmins($entity->getSource(), $title, $message, $data);
+ }
+
+ /**
+ * Create a message in each site-admin users
+ *
+ * @param string $title A quick summary of what that notification is about
+ * @param string $origin The origin of the notification. For example, could be the model source
+ * @param string $message Even more free text
+ * @param array $data data used to generate the view when processing.
+ * Must contain: `original` array of the data before the change
+ * Must contain: `changed` array of the data after the change
+ * Optional: `summary` A text summarizing the change
+ * Optional: `summaryTemplate`, `summaryMessage`, `entityType`, `entityDisplayField`, `entityViewURL` text used to build a summary of the change
+ * @return void
+ */
+ public function notifySiteAdmins(
+ string $origin,
+ string $title,
+ string $message = '',
+ array $data
+ ): void {
+ $this->InboxProcessors = $this->InboxProcessors ?: TableRegistry::getTableLocator()->get('InboxProcessors');
+ $processor = $this->InboxProcessors->getProcessor('Notification', 'DataChange');
+ $siteAdmins = $this->_getSiteAdmins();
+ foreach ($siteAdmins as $siteAdmin) {
+ $notificationData = [
+ 'origin' => $origin,
+ 'title' => $title,
+ 'user_id' => $siteAdmin->id,
+ 'message' => $message,
+ 'data' => $data,
+ ];
+ $processor->create($notificationData);
+ }
+ }
+
+ protected function _getSiteAdmins(): array
+ {
+ return $this->Users->find()
+ ->matching('Roles', function(Query $q) {
+ return $q
+ ->where(['Roles.perm_admin' => true]);
+ })
+ ->all()->toList();
+ }
+
+ protected function _getOriginalChangedFields($entity, array $fields): array
+ {
+ $originalChangedFields = $entity->extractOriginalChanged($fields);
+ $originalChangedFields = array_map(function ($fieldValue) {
+ if (is_array($fieldValue)) {
+ return array_filter(array_map(function($fieldValue) {
+ if (is_subclass_of($fieldValue, 'App\Model\Entity\AppModel')) {
+ if (!empty($fieldValue->extractOriginalChanged($fieldValue->getVisible()))) {
+ return $fieldValue;
+ } else {
+ return null;
+ }
+ }
+ return $fieldValue;
+ }, $fieldValue), fn ($v) => !is_null($v));
+ } else if (is_subclass_of($fieldValue, 'App\Model\Entity\AppModel')) {
+ return $fieldValue->extractOriginalChanged($fieldValue);
+ }
+ return $fieldValue;
+ }, $originalChangedFields);
+ return $originalChangedFields;
+ }
+
+ protected function _massageMetaField(AppModel $entity, array $metaFields): array
+ {
+ $massaged = [];
+ foreach ($metaFields as $metaField) {
+ foreach ($entity->MetaTemplates as $template) {
+ $templateDisplayName = sprintf('%s (v%s)', $template->name, $template->version);
+ foreach ($template['meta_template_fields'] as $field) {
+ if ($metaField->meta_template_id == $template->id && $metaField->meta_template_field_id == $field->id) {
+ if (!empty($massaged[$templateDisplayName][$field['field']])) {
+ if (!is_array($massaged[$templateDisplayName][$field['field']])) {
+ $massaged[$templateDisplayName][$field['field']] = [$massaged[$templateDisplayName][$field['field']]];
+ }
+ $massaged[$templateDisplayName][$field['field']][] = $metaField['value'];
+ } else {
+ $massaged[$templateDisplayName][$field['field']] = $metaField['value'];
+ }
+ break 2;
+ }
+ }
+ }
+ }
+ return $massaged;
+ }
+
+ protected function _getDeletedMetafields(AppModel $entity): array
+ {
+ return $entity->_metafields_to_delete ?? [];
+ }
+
+ protected function _serializeFields($fields): array
+ {
+ return array_map(function ($fieldValue) {
+ if (is_bool($fieldValue)) {
+ return empty($fieldValue) ? 0 : 1;
+ } else if (is_array($fieldValue)) {
+ return json_encode($fieldValue);
+ } else if (is_object($fieldValue)) {
+ switch (get_class($fieldValue)) {
+ case 'Cake\I18n\FrozenTime':
+ return $fieldValue->i18nFormat('yyyy-mm-dd HH:mm:ss');
+ }
+ } else {
+ return strval($fieldValue);
+ }
+ }, $fields);
+ }
+}