From 8e3bf150d9bbdbee1e6434fdecb5007e7a3a63d2 Mon Sep 17 00:00:00 2001 From: Jake Dallimore Date: Thu, 1 Nov 2018 10:26:28 +0800 Subject: [PATCH] MDL-63724 core_message: add can_send_message_to_conversation() to api --- message/classes/api.php | 44 +++++ message/tests/api_test.php | 374 +++++++++++++++++++++++++++++++++++++ 2 files changed, 418 insertions(+) diff --git a/message/classes/api.php b/message/classes/api.php index 43fa325726f..70619370210 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -1431,6 +1431,50 @@ class api { return (self::can_contact_user($recipient, $sender)); } + /** + * Determines if a user is permitted to send a message to a given conversation. + * If no sender is provided then it defaults to the logged in user. + * + * @param int $userid the id of the user on which the checks will be applied. + * @param int $conversationid the id of the conversation we wish to check. + * @return bool true if the user can send a message to the conversation, false otherwise. + * @throws \moodle_exception + */ + public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool { + global $DB; + + $systemcontext = \context_system::instance(); + if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) { + return false; + } + + if (!self::is_user_in_conversation($userid, $conversationid)) { + return false; + } + + // User can post messages and is in the conversation, but we need to check the conversation type to + // know whether or not to check the user privacy settings via can_contact_user(). + $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST); + if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) { + return true; + } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { + // Get the other user in the conversation. + $members = self::get_conversation_members($userid, $conversationid); + $otheruser = array_filter($members, function($member) use($userid) { + return $member->id != $userid; + }); + $otheruser = reset($otheruser); + + // Spoof the stdClass for now - can_contact_user() only uses the ids. + $user = (object) ['id' => $userid]; + $otheruser = (object) ['id' => $otheruser->id]; + + return self::can_contact_user($otheruser, $user); + } else { + throw new \moodle_exception("Invalid conversation type '$conversation->type'."); + } + } + /** * Get the messaging preference for a user. * If the user has not any messaging privacy preference: diff --git a/message/tests/api_test.php b/message/tests/api_test.php index dceda6f79cb..2067dc4997c 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -2877,6 +2877,380 @@ class core_message_api_testcase extends core_message_messagelib_testcase { $this->assertTrue(\core_message\api::can_post_message($student2, $teacher1)); } + /** + * Verify the expected behaviour of the can_send_message_to_conversation() method for authenticated users with default settings. + */ + public function test_can_send_message_to_conversation_basic() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Create a group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // For group conversations, there are no user privacy checks, so only membership in the conversation is needed. + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + + // For individual conversations, the default privacy setting of 'only contacts and course members' applies. + // Users are not in the same course, nor are they contacts, so messages cannot be sent. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + + // Enrol the users into the same course. + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + + // After enrolling users to the course, they should be able to message them with the default privacy setting. + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + } + + /** + * Verify the behaviour of can_send_message_to_conversation() for authenticated users without the sendmessage capability. + */ + public function test_can_send_message_to_conversation_sendmessage_cap() { + global $DB; + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Enrol the users into the same course. + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + $this->getDataGenerator()->enrol_user($user3->id, $course->id); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // Default settings - user1 can send a message to both conversations. + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + + // Remove the capability to send a message. + $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); + unassign_capability('moodle/site:sendmessage', $roleids['user'], context_system::instance()); + + // Verify that a user cannot send a message to either an individual or a group conversation. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + } + + /** + * Verify the behaviour of can_send_message_to_conversation() for authenticated users without the messageanyuser capability. + */ + public function test_can_send_message_to_conversation_messageanyuser_cap() { + global $DB; + + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Enrol the users into the same course. + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + $this->getDataGenerator()->enrol_user($user3->id, $course->id); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // Update the message preference for user2, so they can only be messaged by contacts. + set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $user2->id); + + // Verify that the user cannot be contacted in the individual conversation and that groups are unaffected. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + + // Assign the 'messageanyuser' capability to user1 at system context. + $systemcontext = context_system::instance(); + $authenticateduser = $DB->get_record('role', ['shortname' => 'user']); + assign_capability('moodle/site:messageanyuser', CAP_ALLOW, $authenticateduser->id, $systemcontext->id); + + // Check that user1 can now message user2 due to the capability, and that group conversations is again unaffected. + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + } + + /** + * Test verifying that users cannot send messages to conversations they are not a part of. + */ + public function test_can_post_message_to_conversation_non_member() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + $user4 = self::getDataGenerator()->create_user(); + + // Enrol the users into the same course. + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + $this->getDataGenerator()->enrol_user($user3->id, $course->id); + $this->getDataGenerator()->enrol_user($user4->id, $course->id); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Create a group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // Verify, non members cannot send a message. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $gc1->id)); + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $ic1->id)); + } + + /** + * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to contacts only. + */ + public function test_can_send_message_to_conversation_privacy_contacts_only() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Create a group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // Set the message privacy preference to 'contacts only' for user 2. + set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $user2->id); + + // Verify that user1 cannot send a message to the individual conversation, but that the group conversation is unaffected. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + + // Now, simulate a contact request (and approval) between user1 and user2. + \core_message\api::create_contact_request($user1->id, $user2->id); + \core_message\api::confirm_contact_request($user1->id, $user2->id); + + // Verify user1 can now message user2 again via their individual conversation. + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + } + + /** + * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to contacts / course members. + */ + public function test_can_send_message_to_conversation_privacy_contacts_course() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Set the message privacy preference to 'contacts + course members' for user 2. + set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_COURSEMEMBER, $user2->id); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Create a group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // Verify that users in a group conversation can message one another (i.e. privacy controls ignored). + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + + // Verify that user1 can not message user2 unless they are either contacts, or share a course. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + + // Enrol the users into the same course. + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + $this->getDataGenerator()->enrol_user($user3->id, $course->id); + + // Verify that user1 can send a message to user2, based on the shared course, without being a contact. + $this->assertFalse(\core_message\api::is_contact($user1->id, $user2->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + } + + /** + * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to any user. + */ + public function test_can_send_message_to_conversation_privacy_sitewide() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Create a group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // By default, the messaging privacy dictates that users can only be contacted by contacts, and members of their courses. + // Verify also, that groups are not restricted in this way. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + + // Enable site-wide messagging privacy setting. + // This enables a privacy option for users, allowing them to choose to be contactable by anybody on the site. + set_config('messagingallusers', true); + + // Set the second user's preference to receive messages from everybody. + set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_SITE, $user2->id); + + // Check that user1 can send user2 a message, and that the group conversation is unaffected. + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + + // Disable site-wide messagging privacy setting. The user will be able to receive messages from contacts + // and members sharing a course with her. + set_config('messagingallusers', false); + + // As site-wide messaging setting is disabled, the value for user2 will be changed to MESSAGE_PRIVACY_COURSEMEMBER. + // Verify also that the group conversation is unaffected. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + } + + /** + * Test verifying the behaviour of the can_send_message_to_conversation method when a user is blocked. + */ + public function test_can_send_message_to_conversation_when_blocked() { + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + + // Create an individual conversation between user1 and user2. + $ic1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $user1->id, + $user2->id + ] + ); + + // Create a group conversation between and users 1, 2 and 3. + $gc1 = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + [ + $user1->id, + $user2->id, + $user3->id + ] + ); + + // Enrol the users into the same course. + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + $this->getDataGenerator()->enrol_user($user3->id, $course->id); + + // Block the second user. + \core_message\api::block_user($user1->id, $user2->id); + + // Check that user2 can not send user1 a message in their individual conversation. + $this->assertFalse(\core_message\api::can_send_message_to_conversation($user2->id, $ic1->id)); + + // Verify that group conversations are unaffected. + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id)); + $this->assertTrue(\core_message\api::can_send_message_to_conversation($user2->id, $gc1->id)); + } + /** * Tests get_user_privacy_messaging_preference method. */