diff --git a/message/classes/api.php b/message/classes/api.php index 2b3bcee12d5..4ba966e86cc 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -802,8 +802,11 @@ class api { // If not set, the context is always context_user. if (is_null($conversation->contextid)) { $convcontext = \context_user::instance($userid); + // We'll need to check the capability to delete messages for all users in context system when contextid is null. + $contexttodeletemessageforall = \context_system::instance(); } else { $convcontext = \context::instance_by_id($conversation->contextid); + $contexttodeletemessageforall = $convcontext; } $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]); @@ -819,6 +822,8 @@ class api { // Add the most recent message information. $conv->messages = []; + // Add if the user has to allow delete messages for all users in the conversation. + $conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage', $contexttodeletemessageforall); if ($conversation->smallmessage) { $msg = new \stdClass(); $msg->id = $conversation->messageid; @@ -983,6 +988,9 @@ class api { $ismuted = true; } + // Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation. + $deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid); + return (object) [ 'id' => $conversation->id, 'name' => $conversation->name, @@ -995,7 +1003,8 @@ class api { 'unreadcount' => $unreadcount, 'ismuted' => $ismuted, 'members' => $members, - 'messages' => $messages['messages'] + 'messages' => $messages['messages'], + 'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext) ]; } @@ -3324,4 +3333,60 @@ class api { $DB->delete_records('messages', ['conversationid' => $conversationid]); } } + + /** + * Checks if a user can delete a message for all users. + * + * @param int $userid the user id of who we want to delete the message for all users + * @param int $messageid The message id + * @return bool Returns true if a user can delete the message for all users, false otherwise. + */ + public static function can_delete_message_for_all_users(int $userid, int $messageid) : bool { + global $DB; + + $sql = "SELECT mc.id, mc.contextid + FROM {message_conversations} mc + INNER JOIN {messages} m + ON mc.id = m.conversationid + WHERE m.id = :messageid"; + $conversation = $DB->get_record_sql($sql, ['messageid' => $messageid]); + + if (!empty($conversation->contextid)) { + return has_capability('moodle/site:deleteanymessage', + \context::instance_by_id($conversation->contextid), $userid); + } + + return has_capability('moodle/site:deleteanymessage', \context_system::instance(), $userid); + } + /** + * Delete a message for all users. + * + * This function does not verify any permissions. + * + * @param int $messageid The message id + * @return void + */ + public static function delete_message_for_all_users(int $messageid) { + global $DB, $USER; + + if (!$DB->record_exists('messages', ['id' => $messageid])) { + return false; + } + + // Get all members in the conversation where the message belongs. + $membersql = "SELECT mcm.id, mcm.userid + FROM {message_conversation_members} mcm + INNER JOIN {messages} m + ON mcm.conversationid = m.conversationid + WHERE m.id = :messageid"; + $params = [ + 'messageid' => $messageid + ]; + $members = $DB->get_records_sql($membersql, $params); + if ($members) { + foreach ($members as $member) { + self::delete_message($member->userid, $messageid); + } + } + } } diff --git a/message/tests/api_test.php b/message/tests/api_test.php index a94c6ca71cb..8f9a6452394 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -6897,6 +6897,177 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $this->assertEquals(1, $DB->count_records('messages', ['id' => $g2m4])); } + /** + * Tests the user can delete message for all users as a teacher. + */ + public function test_can_delete_message_for_all_users_teacher() { + global $DB; + $this->resetAfterTest(true); + + // Create fake data to test it. + list($teacher, $student1, $student2, $convgroup, $convindividual) = $this->create_delete_message_test_data(); + + // Allow Teacher can delete messages for all. + $editingteacher = $DB->get_record('role', ['shortname' => 'editingteacher']); + assign_capability('moodle/site:deleteanymessage', CAP_ALLOW, $editingteacher->id, context_system::instance()); + + // Set as the first user. + $this->setUser($teacher); + + // Send a message to private conversation and in a group conversation. + $messageidind = \core_message\tests\helper::send_fake_message_to_conversation($teacher, $convindividual->id); + $messageidgrp = \core_message\tests\helper::send_fake_message_to_conversation($teacher, $convgroup->id); + + // Teacher cannot delete message for everyone in a private conversation. + $this->assertFalse(\core_message\api::can_delete_message_for_all_users($teacher->id, $messageidind)); + + // Teacher can delete message for everyone in a group conversation. + $this->assertTrue(\core_message\api::can_delete_message_for_all_users($teacher->id, $messageidgrp)); + } + + /** + * Tests the user can delete message for all users as a student. + */ + public function test_can_delete_message_for_all_users_student() { + $this->resetAfterTest(true); + + // Create fake data to test it. + list($teacher, $student1, $student2, $convgroup, $convindividual) = $this->create_delete_message_test_data(); + + // Set as the first user. + $this->setUser($student1); + + // Send a message to private conversation and in a group conversation. + $messageidind = \core_message\tests\helper::send_fake_message_to_conversation($teacher, $convindividual->id); + $messageidgrp = \core_message\tests\helper::send_fake_message_to_conversation($teacher, $convgroup->id); + + // Student1 cannot delete message for everyone in a private conversation. + $this->assertFalse(\core_message\api::can_delete_message_for_all_users($student1->id, $messageidind)); + + // Student1 cannot delete message for everyone in a group conversation. + $this->assertFalse(\core_message\api::can_delete_message_for_all_users($student1->id, $messageidgrp)); + } + + /** + * Tests tdelete message for all users in group conversation. + */ + public function test_delete_message_for_all_users_group_conversation() { + global $DB; + $this->resetAfterTest(true); + + // Create fake data to test it. + list($teacher, $student1, $student2, $convgroup, $convindividual) = $this->create_delete_message_test_data(); + + // Send 3 messages to a group conversation. + $mgid1 = \core_message\tests\helper::send_fake_message_to_conversation($teacher, $convgroup->id); + $mgid2 = \core_message\tests\helper::send_fake_message_to_conversation($student1, $convgroup->id); + $mgid3 = \core_message\tests\helper::send_fake_message_to_conversation($student2, $convgroup->id); + + // Delete message 1 for all users. + \core_message\api::delete_message_for_all_users($mgid1); + + // Get the messages to check if the message 1 was deleted for teacher. + $convmessages1 = \core_message\api::get_conversation_messages($teacher->id, $convgroup->id); + // Only has to remains 2 messages. + $this->assertCount(2, $convmessages1['messages']); + // Check if no one of the two messages is message 1. + foreach ($convmessages1['messages'] as $message) { + $this->assertNotEquals($mgid1, $message->id); + } + + // Get the messages to check if the message 1 was deleted for student1. + $convmessages2 = \core_message\api::get_conversation_messages($student1->id, $convgroup->id); + // Only has to remains 2 messages. + $this->assertCount(2, $convmessages2['messages']); + // Check if no one of the two messages is message 1. + foreach ($convmessages2['messages'] as $message) { + $this->assertNotEquals($mgid1, $message->id); + } + + // Get the messages to check if the message 1 was deleted for student2. + $convmessages3 = \core_message\api::get_conversation_messages($student2->id, $convgroup->id); + // Only has to remains 2 messages. + $this->assertCount(2, $convmessages3['messages']); + // Check if no one of the two messages is message 1. + foreach ($convmessages3['messages'] as $message) { + $this->assertNotEquals($mgid1, $message->id); + } + } + + /** + * Tests delete message for all users in private conversation. + */ + public function test_delete_message_for_all_users_individual_conversation() { + global $DB; + $this->resetAfterTest(true); + + // Create fake data to test it. + list($teacher, $student1, $student2, $convgroup, $convindividual) = $this->create_delete_message_test_data(); + + // Send 2 messages in a individual conversation. + $mid1 = \core_message\tests\helper::send_fake_message_to_conversation($teacher, $convindividual->id); + $mid2 = \core_message\tests\helper::send_fake_message_to_conversation($student1, $convindividual->id); + + // Delete the first message for all users. + \core_message\api::delete_message_for_all_users($mid1); + + // Get the messages to check if the message 1 was deleted for teacher. + $convmessages1 = \core_message\api::get_conversation_messages($teacher->id, $convindividual->id); + // Only has to remains 1 messages for teacher. + $this->assertCount(1, $convmessages1['messages']); + // Check the one messages remains not is the first message. + $this->assertNotEquals($mid1, $convmessages1['messages'][0]->id); + + // Get the messages to check if the message 1 was deleted for student1. + $convmessages2 = \core_message\api::get_conversation_messages($student1->id, $convindividual->id); + // Only has to remains 1 messages for student1. + $this->assertCount(1, $convmessages2['messages']); + // Check the one messages remains not is the first message. + $this->assertNotEquals($mid1, $convmessages2['messages'][0]->id); + } + + /** + * Helper to seed the database with initial state with data. + */ + protected function create_delete_message_test_data() { + // Create some users. + $teacher = self::getDataGenerator()->create_user(); + $student1 = self::getDataGenerator()->create_user(); + $student2 = self::getDataGenerator()->create_user(); + + // Create a course and enrol the users. + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); + $this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student'); + + // Create a group and added the users into. + $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); + groups_add_member($group1->id, $teacher->id); + groups_add_member($group1->id, $student1->id); + groups_add_member($group1->id, $student2->id); + + // Create a group conversation linked with the course. + $convgroup = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [$teacher->id, $student1->id, $student2->id], + 'Group test delete for everyone', \core_message\api::MESSAGE_CONVERSATION_ENABLED, + 'core_group', + 'groups', + $group1->id, + context_course::instance($course->id)->id + ); + + // Create and individual conversation. + $convindividual = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [$teacher->id, $student1->id] + ); + + return [$teacher, $student1, $student2, $convgroup, $convindividual]; + } + /** * Comparison function for sorting contacts. *