[ 'Model.afterSave' => 'afterSave', 'Model.afterDelete' => 'afterDelete', 'Model.beforeDelete' => 'beforeDelete', ], 'implementedMethods' => [ '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 beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void { if ($entity->table()->hasBehavior('MetaFields') && !isset($entity->meta_fields)) { $entity = $entity->table()->loadInto($entity, ['MetaFields']); } } 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(Configure::read('inbox.data_change_notify_for_all', false)) && (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->_getOriginalFields($entity, $watchedFields); $changedFields = $this->_getChangedFields($entity, $originalFields); if ( $entity->table()->hasBehavior('Timestamp') && count($changedFields) == 1 && !empty($changedFields['modified']) ) { return; // A not watched field has changed } $changeAmount = $this->_computeChangeAmount($entity, $originalFields, $changedFields); $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', $changeAmount, $changeAmount ); } if ($entity->table()->hasBehavior('MetaFields')) { $originalFields['meta_fields'] = $this->_massageMetaField($entity, $originalFields['meta_fields'] ?? []); $changedFields['meta_fields'] = $this->_massageMetaField($entity, $changedFields['meta_fields'] ?? []); if (empty($originalFields['meta_fields']) && empty($changedFields['meta_fields'])) { unset($originalFields['meta_fields']); unset($changedFields['meta_fields']); } } $originalFields = $entity->isNew() ? [] : $originalFields; $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, $data, $message); } 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, $data, $message); } /** * 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, array $data, string $message = '' ): 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 _getOriginalFields(AppModel $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); if ($entity->table()->hasBehavior('MetaFields')) { // Include deleted meta-fields $originalChangedFields['meta_fields'] = array_merge( $originalChangedFields['meta_fields'] ?? [], $this->_getDeletedMetafields($entity) ); // Restore original values of meta-fields as the entity has been saved with the changes if (!empty($entity->meta_fields)) { $originalChangedFields['meta_fields'] = array_map(function ($metaField) { $originalValues = $metaField->getOriginalValues($metaField->getVisible()); $originalMetafield = $metaField->table()->newEntity($metaField->toArray()); $originalMetafield->set($originalValues); return $originalMetafield; }, $originalChangedFields['meta_fields']); } } return $originalChangedFields; } protected function _getChangedFields(AppModel $entity, array $originalFields): array { $changedFields = array_filter( $entity->extract(array_keys($originalFields)), fn ($v) => !is_null($v) ); if ($entity->table()->hasBehavior('MetaFields')) { $changedMetafields = $entity->extractOriginalChanged($entity->getVisible())['meta_fields'] ?? []; $changedFields['meta_fields'] = array_filter($changedMetafields, function($metaField) { return !empty($metaField->getDirty()); }); } return $changedFields; } 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 _computeChangeAmount(AppModel $entity, array $originalFields, array $changedFields): int { $amount = count($changedFields); if ($entity->table()->hasBehavior('MetaFields')) { $amount -= 1; // `meta_fields` key was counted without checking at the content } if (!empty($originalFields['meta_fields']) && !empty($changedFields['meta_fields'])) { $amount += count(array_intersect_key($originalFields['meta_fields'] ?? [], $changedFields['meta_fields'] ?? [])); // Add changed fields $amount += count(array_diff_key($changedFields['meta_fields'] ?? [], $originalFields['meta_fields'] ?? [])); // Add new fields $amount += count(array_diff_key($originalFields['meta_fields'] ?? [], $changedFields['meta_fields'] ?? [])); // Add deleted fields } return $amount; } 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); } }