From 7983fb830e5418a340092a115ea35be3d41e830a Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 5 Oct 2018 13:22:59 +0200 Subject: [PATCH] MDL-63289 message: Bypass privacy user preferences for teachers Created capability 'moodle/site:messageanyuser' to allow bypass user privacy preferences for messaging participants in a course, even when some of them has blocked the teacher. --- lang/en/role.php | 1 + lib/db/access.php | 11 ++++ message/classes/api.php | 101 +++++++++++++++++++++++++++++-------- message/tests/api_test.php | 76 ++++++++++++++++++++++++++-- version.php | 2 +- 5 files changed, 164 insertions(+), 27 deletions(-) diff --git a/lang/en/role.php b/lang/en/role.php index 1255ad8ea95..5f26748fa96 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -409,6 +409,7 @@ $string['site:import'] = 'Import other courses into a course'; $string['site:maintenanceaccess'] = 'Access site while in maintenance mode'; $string['site:manageallmessaging'] = 'Add, remove, block and unblock contacts for any user'; $string['site:manageblocks'] = 'Manage blocks on a page'; +$string['site:messageanyuser'] = 'Bypass user privacy preferences for messaging any user'; $string['site:mnetloginfromremote'] = 'Login from a remote application via MNet'; $string['site:mnetlogintoremote'] = 'Roam to a remote application via MNet'; $string['site:readallmessages'] = 'Read all messages on site'; diff --git a/lib/db/access.php b/lib/db/access.php index 1652534349e..406c46d1d9a 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2410,5 +2410,16 @@ $capabilities = array( 'archetypes' => array( ) ), + // Allow message any user, regardlesss of the privacy preferences for messaging. + 'moodle/site:messageanyuser' => array( + 'riskbitmask' => RISK_SPAM, + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), ); diff --git a/message/classes/api.php b/message/classes/api.php index d9b507edb40..5c1b04f6982 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -939,32 +939,17 @@ class api { $sender = $USER; } - if (!has_capability('moodle/site:sendmessage', \context_system::instance(), $sender)) { - return false; - } - - // The recipient blocks messages from non-contacts and the - // sender isn't a contact. - if (self::is_user_non_contact_blocked($recipient, $sender)) { - return false; - } - - $senderid = null; - if ($sender !== null && isset($sender->id)) { - $senderid = $sender->id; - } - $systemcontext = \context_system::instance(); - if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) { + if (!has_capability('moodle/site:sendmessage', $systemcontext, $sender)) { + return false; + } + + if (has_capability('moodle/site:readallmessages', $systemcontext, $sender->id)) { return true; } - // The recipient has specifically blocked this sender. - if (self::is_blocked($recipient->id, $senderid)) { - return false; - } - - return true; + // Check if the recipient can be messaged by the sender. + return (self::can_contact_user($recipient, $sender)); } /** @@ -1687,6 +1672,78 @@ class api { return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid, 'userid' => $userid]); + } + /** + * Checks if the sender can message the recipient. + * + * @param \stdClass $recipient The user object. + * @param \stdClass $sender The user object. + * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise. + */ + protected static function can_contact_user(\stdClass $recipient, \stdClass $sender) : bool { + global $CFG; + + if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $sender->id)) { + // The sender has the ability to contact any user across the entire site. + return true; + } + + // The initial value of $cancontact is null to indicate that a value has not been determined. + $cancontact = null; + + if (self::is_blocked($recipient->id, $sender->id)) { + // The recipient has specifically blocked this sender. + $cancontact = false; + } + + $sharedcourses = null; + if (null === $cancontact) { + // There are three user preference options: + // - Site: Allow anyone not explicitly blocked to contact me; + // - Course members: Allow anyone I am in a course with to contact me; and + // - Contacts: Only allow my contacts to contact me. + // + // The Site option is only possible when the messagingallusers site setting is also enabled. + + $privacypreference = self::get_user_privacy_messaging_preference($recipient->id); + if (self::MESSAGE_PRIVACY_SITE === $privacypreference) { + // The user preference is to allow any user to contact them. + // No need to check anything else. + $cancontact = true; + } else { + // This user only allows their own contacts, and possibly course peers, to contact them. + // If the users are contacts then we can avoid the more expensive shared courses check. + $cancontact = self::is_contact($sender->id, $recipient->id); + + if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) { + // The users are not contacts and the user allows course member messaging. + // Check whether these two users share any course together. + $sharedcourses = enrol_get_shared_courses($recipient->id, $sender->id, true); + $cancontact = (!empty($sharedcourses)); + } + } + } + + if (false === $cancontact) { + // At the moment the users cannot contact one another. + // Check whether the messageanyuser capability applies in any of the shared courses. + // This is intended to allow teachers to message students regardless of message settings. + + // Note: You cannot use empty($sharedcourses) here because this may be an empty array. + if (null === $sharedcourses) { + $sharedcourses = enrol_get_shared_courses($recipient->id, $sender->id, true); + } + + foreach ($sharedcourses as $course) { + // Note: enrol_get_shared_courses will preload any shared context. + if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $sender->id)) { + $cancontact = true; + break; + } + } + } + + return $cancontact; } } diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 8be0f3f6070..3bcb7d76025 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -1352,7 +1352,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase { /** * Tests the user can't post a message without proper capability. */ - public function test_can_post_message_without_cap() { + public function test_can_post_message_without_sendmessage_cap() { global $DB; // Create some users. @@ -1441,15 +1441,83 @@ class core_message_api_testcase extends core_message_messagelib_testcase { // Set as the first user. $this->setUser($user1); - // Set the second user's preference to receive messages from everybody. As site-wide messaging setting - // is disabled by default, the value will be changed to MESSAGE_PRIVACY_COURSEMEMBER. - set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_SITE, $user2->id); + // By default, user only can be messaged by contacts and members of any of his/her courses. $this->assertFalse(\core_message\api::can_post_message($user2)); // Enable site-wide messagging privacy setting. The user will be able to receive messages from everybody. 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 we can send user2 a message. $this->assertTrue(\core_message\api::can_post_message($user2)); + + // 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. + $this->assertFalse(\core_message\api::can_post_message($user2)); + + // Enrol users to the same course. + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + // Check that we can send user2 a message because they are sharing a course. + $this->assertTrue(\core_message\api::can_post_message($user2)); + + // Set the second user's preference to receive messages only from contacts. + set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $user2->id); + // Check that now the user2 can't be contacted because user1 is not their contact. + $this->assertFalse(\core_message\api::can_post_message($user2)); + + // Make contacts user1 and user2. + \core_message\api::add_contact($user2->id, $user1->id); + // Check that we can send user2 a message because they are contacts. + $this->assertTrue(\core_message\api::can_post_message($user2)); + } + + /** + * Tests the user with the messageanyuser capability can post a message. + */ + public function test_can_post_message_with_messageanyuser_cap() { + global $DB; + + // Create some users. + $teacher1 = self::getDataGenerator()->create_user(); + $student1 = self::getDataGenerator()->create_user(); + $student2 = self::getDataGenerator()->create_user(); + + // Create users not enrolled in any course. + $user1 = self::getDataGenerator()->create_user(); + + // Create a course. + $course1 = $this->getDataGenerator()->create_course(); + + // Enrol the users in the course. + $this->getDataGenerator()->enrol_user($teacher1->id, $course1->id, 'editingteacher'); + $this->getDataGenerator()->enrol_user($student1->id, $course1->id, 'student'); + $this->getDataGenerator()->enrol_user($student2->id, $course1->id, 'student'); + + // Set some student preferences to not receive messages from non-contacts. + set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $student1->id); + + // Check that we can send student1 a message because teacher has the messageanyuser cap by default. + $this->assertTrue(\core_message\api::can_post_message($student1, $teacher1)); + // Check that the teacher can't contact user1 because it's not his teacher. + $this->assertFalse(\core_message\api::can_post_message($user1, $teacher1)); + + // Remove the messageanyuser capability from the course1 for teachers. + $coursecontext = context_course::instance($course1->id); + $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); + assign_capability('moodle/site:messageanyuser', CAP_PROHIBIT, $teacherrole->id, $coursecontext->id); + $coursecontext->mark_dirty(); + + // Check that we can't send user1 a message because they are not contacts. + $this->assertFalse(\core_message\api::can_post_message($student1, $teacher1)); + // However, teacher can message student2 because they are sharing a course. + $this->assertTrue(\core_message\api::can_post_message($student2, $teacher1)); } /** diff --git a/version.php b/version.php index d10fd0eef92..82f0af37444 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018101900.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018101900.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.