Skip to content

Commit

Permalink
MDL-63289 message: Bypass privacy user preferences for teachers
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sarjona committed Oct 20, 2018
1 parent cc486e6 commit 7983fb8
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 25 deletions.
1 change: 1 addition & 0 deletions lang/en/role.php
Expand Up @@ -409,6 +409,7 @@
$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';
Expand Down
11 changes: 11 additions & 0 deletions lib/db/access.php
Expand Up @@ -2410,5 +2410,16 @@
'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
)
),

);
97 changes: 77 additions & 20 deletions message/classes/api.php
Expand Up @@ -939,32 +939,17 @@ public static function can_post_message($recipient, $sender = null) {
$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)) {
$systemcontext = \context_system::instance();
if (!has_capability('moodle/site:sendmessage', $systemcontext, $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: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));
}

/**
Expand Down Expand Up @@ -1687,6 +1672,78 @@ public static function is_user_in_conversation(int $userid, int $conversationid)

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;
}
}
76 changes: 72 additions & 4 deletions message/tests/api_test.php
Expand Up @@ -1352,7 +1352,7 @@ public function test_can_post_message() {
/**
* 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.
Expand Down Expand Up @@ -1441,15 +1441,83 @@ public function test_can_post_message_site_messaging_setting() {
// 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));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion version.php
Expand Up @@ -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.

Expand Down

0 comments on commit 7983fb8

Please sign in to comment.