2022-10-26 17:10:04 +02:00
|
|
|
<?php
|
|
|
|
namespace App\Model\Behavior;
|
|
|
|
|
|
|
|
use ArrayObject;
|
|
|
|
use App\Model\Entity\AppModel;
|
|
|
|
use App\Model\Entity\MetaField;
|
|
|
|
use Cake\Core\Configure;
|
|
|
|
use App\Model\Table\UsersTable;
|
|
|
|
use Cake\ORM\Behavior;
|
|
|
|
use Cake\Datasource\EntityInterface;
|
|
|
|
use Cake\Event\EventInterface;
|
|
|
|
use Cake\ORM\TableRegistry;
|
|
|
|
use Cake\ORM\Query;
|
|
|
|
use Cake\Utility\Inflector;
|
|
|
|
use Cake\Utility\Hash;
|
|
|
|
use Cake\Routing\Router;
|
|
|
|
|
|
|
|
class NotifyAdminsBehavior extends Behavior
|
|
|
|
{
|
|
|
|
/** @var array */
|
|
|
|
protected $_defaultConfig = [
|
2022-10-27 11:07:21 +02:00
|
|
|
'implementedEvents' => [
|
|
|
|
'Model.afterSave' => 'afterSave',
|
|
|
|
'Model.afterDelete' => 'afterDelete',
|
|
|
|
'Model.beforeDelete' => 'beforeDelete',
|
|
|
|
],
|
2022-10-26 17:10:04 +02:00
|
|
|
'implementedMethods' => [
|
|
|
|
'notifySiteAdmins' => 'notifySiteAdmins',
|
|
|
|
'notifySiteAdminsForEntity' => 'notifySiteAdminsForEntity',
|
2022-10-27 11:07:21 +02:00
|
|
|
],
|
2022-10-26 17:10:04 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
/** @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);
|
|
|
|
}
|
|
|
|
|
2022-10-27 11:07:21 +02:00
|
|
|
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']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-26 17:10:04 +02:00
|
|
|
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');
|
2023-12-13 15:01:23 +01:00
|
|
|
if (
|
|
|
|
empty(Configure::read('inbox.data_change_notify_for_all', false)) &&
|
|
|
|
(empty($loggedUser) || !empty($loggedUser['role']['perm_admin']) || !empty($loggedUser['role']['perm_sync']))
|
|
|
|
) {
|
2022-10-26 17:10:04 +02:00
|
|
|
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);
|
2022-10-27 10:12:06 +02:00
|
|
|
|
2022-10-26 17:10:04 +02:00
|
|
|
$titleTemplate = 'New {0} `{1}` created';
|
|
|
|
$title = __(
|
|
|
|
$titleTemplate,
|
|
|
|
Inflector::singularize($entity->getSource()),
|
|
|
|
$entity->get($this->table()->getDisplayField())
|
|
|
|
);
|
|
|
|
$message = __('New {0}', Inflector::singularize($entity->getSource()));
|
2022-10-27 10:12:06 +02:00
|
|
|
|
2022-10-26 17:10:04 +02:00
|
|
|
if (!$entity->isNew()) {
|
2022-10-27 10:12:06 +02:00
|
|
|
$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
|
2022-10-26 17:10:04 +02:00
|
|
|
}
|
2022-10-27 10:12:06 +02:00
|
|
|
|
|
|
|
$changeAmount = $this->_computeChangeAmount($entity, $originalFields, $changedFields);
|
2022-10-26 17:10:04 +02:00
|
|
|
$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',
|
2022-10-27 10:12:06 +02:00
|
|
|
$changeAmount,
|
|
|
|
$changeAmount
|
2022-10-26 17:10:04 +02:00
|
|
|
);
|
|
|
|
}
|
2022-10-27 10:12:06 +02:00
|
|
|
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']);
|
|
|
|
}
|
|
|
|
}
|
2022-12-08 10:25:13 +01:00
|
|
|
$originalFields = $entity->isNew() ? [] : $originalFields;
|
2022-10-26 17:10:04 +02:00
|
|
|
$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
|
|
|
|
]),
|
|
|
|
];
|
2022-12-13 11:00:30 +01:00
|
|
|
$this->notifySiteAdmins($entity->getSource(), $title, $data, $message);
|
2022-10-26 17:10:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
]),
|
|
|
|
];
|
2022-12-13 11:00:30 +01:00
|
|
|
$this->notifySiteAdmins($entity->getSource(), $title, $data, $message);
|
2022-10-26 17:10:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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,
|
2022-12-13 11:00:30 +01:00
|
|
|
array $data,
|
|
|
|
string $message = ''
|
2022-10-26 17:10:04 +02:00
|
|
|
): 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();
|
|
|
|
}
|
|
|
|
|
2022-10-27 10:12:06 +02:00
|
|
|
protected function _getOriginalFields(AppModel $entity, array $fields): array
|
2022-10-26 17:10:04 +02:00
|
|
|
{
|
|
|
|
$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);
|
2022-10-27 10:12:06 +02:00
|
|
|
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']);
|
|
|
|
}
|
|
|
|
}
|
2022-10-26 17:10:04 +02:00
|
|
|
return $originalChangedFields;
|
|
|
|
}
|
|
|
|
|
2022-10-27 10:12:06 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-10-26 17:10:04 +02:00
|
|
|
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 ?? [];
|
|
|
|
}
|
|
|
|
|
2022-10-27 10:12:06 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-10-26 17:10:04 +02:00
|
|
|
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':
|
2023-12-12 09:02:43 +01:00
|
|
|
return $fieldValue->i18nFormat('yyyy-MM-dd HH:mm:ss');
|
2022-10-26 17:10:04 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return strval($fieldValue);
|
|
|
|
}
|
|
|
|
}, $fields);
|
|
|
|
}
|
|
|
|
}
|