diff --git a/assets/vue/services/message.js b/assets/vue/services/message.js index 3053ed7d20..7c3a1951ba 100644 --- a/assets/vue/services/message.js +++ b/assets/vue/services/message.js @@ -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, -}; +} diff --git a/assets/vue/views/message/MessageList.vue b/assets/vue/views/message/MessageList.vue index 290aedc912..297c3a2a3d 100644 --- a/assets/vue/views/message/MessageList.vue +++ b/assets/vue/views/message/MessageList.vue @@ -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) }, diff --git a/assets/vue/views/message/MessageShow.vue b/assets/vue/views/message/MessageShow.vue index d39a145f15..594057d9c0 100644 --- a/assets/vue/views/message/MessageShow.vue +++ b/assets/vue/views/message/MessageShow.vue @@ -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) { diff --git a/src/CoreBundle/Entity/Listener/MessageListener.php b/src/CoreBundle/Entity/Listener/MessageListener.php new file mode 100644 index 0000000000..cb8e96c998 --- /dev/null +++ b/src/CoreBundle/Entity/Listener/MessageListener.php @@ -0,0 +1,30 @@ +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); + } +} \ No newline at end of file diff --git a/src/CoreBundle/Entity/Message.php b/src/CoreBundle/Entity/Message.php index 9be10bc916..ca7b8d0240 100644 --- a/src/CoreBundle/Entity/Message.php +++ b/src/CoreBundle/Entity/Message.php @@ -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 { diff --git a/src/CoreBundle/Entity/MessageRelUser.php b/src/CoreBundle/Entity/MessageRelUser.php index 639db38687..0b4715a534 100644 --- a/src/CoreBundle/Entity/MessageRelUser.php +++ b/src/CoreBundle/Entity/MessageRelUser.php @@ -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 diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240918002830.php b/src/CoreBundle/Migrations/Schema/V200/Version20240918002830.php new file mode 100644 index 0000000000..994cd1fafe --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240918002830.php @@ -0,0 +1,26 @@ +addSql("ALTER TABLE message_rel_user ADD deleted_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime)'"); + } +} \ No newline at end of file diff --git a/src/CoreBundle/Security/Authorization/Voter/MessageRelUserVoter.php b/src/CoreBundle/Security/Authorization/Voter/MessageRelUserVoter.php new file mode 100644 index 0000000000..eeb7269b21 --- /dev/null +++ b/src/CoreBundle/Security/Authorization/Voter/MessageRelUserVoter.php @@ -0,0 +1,56 @@ +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, + }; + } +} diff --git a/src/CoreBundle/State/MessageProcessor.php b/src/CoreBundle/State/MessageProcessor.php index 57a06b2134..9efb64434f 100644 --- a/src/CoreBundle/State/MessageProcessor.php +++ b/src/CoreBundle/State/MessageProcessor.php @@ -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);