Message: Enable soft-delete in message_rel_user - refs BT#21988

pull/5805/head
Angel Fernando Quiroz Campos 2 months ago
parent 8eded5234f
commit da69428831
No known key found for this signature in database
GPG Key ID: B284841AE3E562CD
  1. 14
      assets/vue/services/message.js
  2. 25
      assets/vue/views/message/MessageList.vue
  3. 28
      assets/vue/views/message/MessageShow.vue
  4. 30
      src/CoreBundle/Entity/Listener/MessageListener.php
  5. 17
      src/CoreBundle/Entity/Message.php
  6. 15
      src/CoreBundle/Entity/MessageRelUser.php
  7. 26
      src/CoreBundle/Migrations/Schema/V200/Version20240918002830.php
  8. 56
      src/CoreBundle/Security/Authorization/Voter/MessageRelUserVoter.php
  9. 5
      src/CoreBundle/State/MessageProcessor.php

@ -1,6 +1,5 @@
import makeService from "./api"
import baseService from "./baseService"
import axios from "axios"
// MIGRATION IN PROGRESS. makeService is deprecated
// if you use some method in this service you should try to refactor it with new baseService defining async functions
@ -23,18 +22,7 @@ async function countUnreadMessages(params) {
return await baseService.get(`/api/messages?${queryParams}`)
}
async function deleteMessageForUser(messageId, userId) {
return await axios.patch(`/api/messages/${messageId}/delete-for-user`,
{ userId: userId },
{
headers: {
'Content-Type': 'application/json'
}
})
}
export const messageService = {
create,
countUnreadMessages,
deleteMessageForUser,
};
}

@ -202,7 +202,6 @@ import SectionHeader from "../../components/layout/SectionHeader.vue"
import InputGroup from "primevue/inputgroup"
import InputText from "primevue/inputtext"
import BaseAppLink from "../../components/basecomponents/BaseAppLink.vue"
import { messageService } from "../../services/message"
import messageRelUserService from "../../services/messagereluser"
import { useMessageReceiverFormatter } from "../../composables/message/messageFormatter"
@ -435,25 +434,15 @@ function findMyReceiver(message) {
return receivers.find(({ receiver }) => receiver["@id"] === securityStore.user["@id"])
}
function extractUserId(apiId) {
return apiId.split("/").pop()
}
async function deleteMessage(message) {
try {
const userId = extractUserId(securityStore.user["@id"])
const messageId = extractUserId(message["@id"])
if (message.sender["@id"] === securityStore.user["@id"]) {
await messageService.deleteMessageForUser(messageId, userId)
} else {
const myReceiver = findMyReceiver(message)
if (myReceiver) {
await store.dispatch("messagereluser/del", myReceiver)
}
}
const myReceiver = findMyReceiver(message)
if (myReceiver) {
await store.dispatch("messagereluser/del", myReceiver)
notification.showSuccessNotification(t("Message deleted"))
notification.showSuccessNotification(t("Message deleted"))
}
await messageRelUserStore.findUnreadCount()
loadMessages()
} catch (e) {
@ -464,7 +453,7 @@ async function deleteMessage(message) {
function showDlgConfirmDeleteSingle({ data }) {
confirm.require({
header: t("Confirmation"),
message: t("Are you sure you want to delete %s?", [ data.title ]),
message: t("Are you sure you want to delete %s?", [data.title]),
accept: async () => {
await deleteMessage(data)
},

@ -146,7 +146,6 @@ import BaseCard from "../../components/basecomponents/BaseCard.vue"
import BaseAvatarList from "../../components/basecomponents/BaseAvatarList.vue"
import BaseIcon from "../../components/basecomponents/BaseIcon.vue"
import SectionHeader from "../../components/layout/SectionHeader.vue"
import { messageService } from "../../services/message"
import { useNotification } from "../../composables/notification"
import { useMessageReceiverFormatter } from "../../composables/message/messageFormatter"
@ -178,7 +177,7 @@ const notification = useNotification()
store.dispatch("message/load", id).then((responseItem) => {
item.value = responseItem
myReceiver.value = findMyReceiver(responseItem, securityStore.user["@id"])
myReceiver.value = findMyReceiver(responseItem)
// Change to read.
if (myReceiver.value && false === myReceiver.value.read) {
@ -188,30 +187,21 @@ store.dispatch("message/load", id).then((responseItem) => {
}
})
function extractUserId(apiId) {
return apiId.split("/").pop()
}
function findMyReceiver(message, userId) {
function findMyReceiver(message) {
const receivers = [...message.receiversTo, ...message.receiversCc]
return receivers.find(({ receiver }) => receiver["@id"] === userId)
return receivers.find(({ receiver }) => receiver["@id"] === securityStore.user["@id"])
}
async function deleteMessage(message) {
try {
const userId = extractUserId(securityStore.user["@id"])
const messageId = extractUserId(message["@id"])
if (message.sender["@id"] === securityStore.user["@id"]) {
await messageService.deleteMessageForUser(messageId, userId)
} else {
const myReceiver = findMyReceiver(message, securityStore.user["@id"])
if (myReceiver) {
await store.dispatch("messagereluser/del", myReceiver)
}
const myReceiver = findMyReceiver(message)
if (myReceiver) {
await store.dispatch("messagereluser/del", myReceiver)
notification.showSuccessNotification(t("Message deleted"))
}
notification.showSuccessNotification(t("Message deleted"))
await messageRelUserStore.findUnreadCount()
await router.push({ name: "MessageList" })
} catch (e) {

@ -0,0 +1,30 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity\Listener;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\MessageRelUser;
use Doctrine\ORM\Event\PostLoadEventArgs;
class MessageListener
{
public function postLoad(Message $message, PostLoadEventArgs $args): void
{
$om = $args->getObjectManager();
$messageRelUserRepo = $om->getRepository(MessageRelUser::class);
$softDeleteable = $om->getFilters()->enable('softdeleteable');
$softDeleteable->disableForEntity(MessageRelUser::class);
$message->setReceiversFromArray(
$messageRelUserRepo->findBy(['message' => $message])
);
$softDeleteable->enableForEntity(Message::class);
}
}

@ -14,9 +14,9 @@ use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Chamilo\CoreBundle\Entity\Listener\MessageListener;
use Chamilo\CoreBundle\Filter\SearchOrFilter;
use Chamilo\CoreBundle\Repository\MessageRepository;
use Chamilo\CoreBundle\State\MessageByGroupStateProvider;
@ -35,6 +35,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Index(columns: ['group_id'], name: 'idx_message_group')]
#[ORM\Index(columns: ['msg_type'], name: 'idx_message_type')]
#[ORM\Entity(repositoryClass: MessageRepository::class)]
#[ORM\EntityListeners([MessageListener::class])]
#[ApiResource(
operations: [
new Get(security: "is_granted('VIEW', object)"),
@ -52,13 +53,6 @@ use Symfony\Component\Validator\Constraints as Assert;
provider: MessageByGroupStateProvider::class
),
new Post(securityPostDenormalize: "is_granted('CREATE', object)"),
new Patch(
uriTemplate: '/messages/{id}/delete-for-user',
inputFormats: ['json' => ['application/json']],
security: "is_granted('ROLE_USER')",
output: false,
processor: MessageProcessor::class,
),
],
normalizationContext: [
'groups' => ['message:read'],
@ -200,6 +194,13 @@ class Message
return $this->receivers;
}
public function setReceiversFromArray(array $receivers): self
{
$this->receivers = new ArrayCollection($receivers);
return $this;
}
#[Groups(['message:read'])]
public function getReceiversTo(): array
{

@ -9,15 +9,27 @@ namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Put;
use Chamilo\CoreBundle\Entity\Listener\MessageStatusListener;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
operations: [
new Get(security: "is_granted('VIEW', object)"),
new Put(security: "is_granted('EDIT', object)"),
new Patch(security: "is_granted('EDIT', object)"),
new Delete(security: "is_granted('DELETE', object)"),
],
normalizationContext: [
'groups' => ['message_rel_user:read'],
],
@ -34,6 +46,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\UniqueConstraint(name: 'message_receiver', columns: ['message_id', 'user_id', 'receiver_type'])]
#[ORM\Entity]
#[ORM\EntityListeners([MessageStatusListener::class])]
#[Gedmo\SoftDeleteable(timeAware: true)]
#[ApiFilter(
filterClass: SearchFilter::class,
properties: [
@ -46,6 +59,8 @@ use Symfony\Component\Validator\Constraints as Assert;
)]
class MessageRelUser
{
use SoftDeleteableEntity;
// Type indicating the message is sent to the main recipient
public const TYPE_TO = 1;
// Type indicating the message is sent as a carbon copy (CC) to the recipient

@ -0,0 +1,26 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
final class Version20240918002830 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Make soft-deleteable message_rel_user';
}
/**
* @inheritDoc
*/
public function up(Schema $schema): void
{
$this->addSql("ALTER TABLE message_rel_user ADD deleted_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime)'");
}
}

@ -0,0 +1,56 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Security\Authorization\Voter;
use Chamilo\CoreBundle\Entity\MessageRelUser;
use Chamilo\CoreBundle\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class MessageRelUserVoter extends Voter
{
public const DELETE = 'DELETE';
public const VIEW = 'VIEW';
public const EDIT = 'EDIT';
public function __construct(
private readonly Security $security
) {
}
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::DELETE, self::VIEW, self::EDIT])
&& $subject instanceof MessageRelUser;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
if ($this->security->isGranted('ROLE_ADMIN')) {
return true;
}
assert($user instanceof User);
assert($subject instanceof MessageRelUser);
$message = $subject->getMessage();
$isReceiver = $message->hasUserReceiver($user);
return match ($attribute) {
self::VIEW, self::EDIT, self::DELETE => $isReceiver,
default => false,
};
}
}

@ -8,7 +8,6 @@ namespace Chamilo\CoreBundle\State;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\State\ProcessorInterface;
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\MessageAttachment;
@ -40,10 +39,6 @@ final class MessageProcessor implements ProcessorInterface
return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
}
if ($operation instanceof Patch && str_contains($operation->getUriTemplate(), 'delete-for-user')) {
return $this->processDeleteForUser($data);
}
/** @var Message $message */
$message = $this->persistProcessor->process($data, $operation, $uriVariables, $context);

Loading…
Cancel
Save