diff --git a/favourites/classes/privacy/provider.php b/favourites/classes/privacy/provider.php index 44a076f6862ec..99757f09c2772 100644 --- a/favourites/classes/privacy/provider.php +++ b/favourites/classes/privacy/provider.php @@ -172,9 +172,11 @@ public static function get_favourites_info_for_user(int $userid, \context $conte * @param \context $context The context to which deletion is scoped. * @param string $component The favourite's component name. * @param string $itemtype The favourite's itemtype. + * @param int $itemid Optional itemid associated with component. * @throws \dml_exception if any errors are encountered during deletion. */ - public static function delete_favourites_for_all_users(\context $context, string $component, string $itemtype) { + public static function delete_favourites_for_all_users(\context $context, string $component, string $itemtype, + int $itemid = 0) { global $DB; $params = [ @@ -184,6 +186,11 @@ public static function delete_favourites_for_all_users(\context $context, string ]; $select = "component = :component AND itemtype =:itemtype AND contextid = :contextid"; + + if (!empty($itemid)) { + $select .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } $DB->delete_records_select('favourite', $select, $params); } @@ -193,10 +200,11 @@ public static function delete_favourites_for_all_users(\context $context, string * @param \core_privacy\local\request\approved_userlist $userlist The approved contexts and user information * to delete information for. * @param string $itemtype The favourite's itemtype. + * @param int $itemid Optional itemid associated with component. * @throws \dml_exception if any errors are encountered during deletion. */ public static function delete_favourites_for_userlist(\core_privacy\local\request\approved_userlist $userlist, - string $itemtype) { + string $itemtype, int $itemid = 0) { global $DB; $userids = $userlist->get_userids(); @@ -217,6 +225,11 @@ public static function delete_favourites_for_userlist(\core_privacy\local\reques $params += $userparams; $select = "component = :component AND itemtype = :itemtype AND contextid = :contextid AND userid $usersql"; + if (!empty($itemid)) { + $select .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } + $DB->delete_records_select('favourite', $select, $params); } @@ -224,12 +237,14 @@ public static function delete_favourites_for_userlist(\core_privacy\local\reques * Delete all favourites for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. - * @param string $component - * @param string $itemtype + * @param string $component The favourite's component name. + * @param string $itemtype The favourite's itemtype. + * @param int $itemid Optional itemid associated with component. * @throws \coding_exception * @throws \dml_exception */ - public static function delete_favourites_for_user(approved_contextlist $contextlist, string $component, string $itemtype) { + public static function delete_favourites_for_user(approved_contextlist $contextlist, string $component, string $itemtype, + int $itemid = 0) { global $DB; $userid = $contextlist->get_user()->id; @@ -244,6 +259,12 @@ public static function delete_favourites_for_user(approved_contextlist $contextl $params += $inparams; $select = "userid = :userid AND component = :component AND itemtype =:itemtype AND contextid $insql"; + + if (!empty($itemid)) { + $select .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } + $DB->delete_records_select('favourite', $select, $params); } } diff --git a/group/classes/privacy/provider.php b/group/classes/privacy/provider.php index 86e441cc24ab1..33f172fac5e53 100644 --- a/group/classes/privacy/provider.php +++ b/group/classes/privacy/provider.php @@ -68,6 +68,8 @@ public static function get_metadata(collection $collection) : collection { 'timeadded' => 'privacy:metadata:groups:timeadded', ], 'privacy:metadata:groups'); + $collection->link_subsystem('core_message', 'privacy:metadata:core_message'); + return $collection; } @@ -88,7 +90,7 @@ public static function export_groups(\context $context, string $component, array $subcontext[] = get_string('groups', 'core_group'); - $sql = "SELECT gm.id, gm.timeadded, gm.userid, g.name + $sql = "SELECT gm.id, gm.timeadded, gm.userid, g.name, gm.groupid FROM {groups_members} gm JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = :courseid @@ -107,7 +109,7 @@ public static function export_groups(\context $context, string $component, array $groups = $DB->get_records_sql($sql, $params); - $groups = array_map(function($group) { + $groupstoexport = array_map(function($group) { return (object) [ 'name' => format_string($group->name), 'timeadded' => transform::datetime($group->timeadded), @@ -117,8 +119,14 @@ public static function export_groups(\context $context, string $component, array if (!empty($groups)) { \core_privacy\local\request\writer::with_context($context) ->export_data($subcontext, (object) [ - 'groups' => $groups, + 'groups' => $groupstoexport, ]); + + foreach ($groups as $group) { + // Export associated conversations to this group. + \core_message\privacy\provider::export_conversations($USER->id, 'core_group', 'groups', + $context, [], $group->groupid); + } } } @@ -148,6 +156,13 @@ public static function delete_groups_for_all_users(\context $context, string $co $params['itemid'] = $itemid; } + // Delete the group conversations. + $groups = $DB->get_records_select('groups_members', $select, $params); + foreach ($groups as $group) { + \core_message\privacy\provider::delete_conversations_for_all_users($context, 'core_group', 'groups', $group->groupid); + } + + // Remove members from the group. $DB->delete_records_select('groups_members', $select, $params); // Purge the group and grouping cache for users. @@ -191,6 +206,13 @@ public static function delete_groups_for_user(approved_contextlist $contextlist, $params['itemid'] = $itemid; } + // Delete the group conversations. + $groups = $DB->get_records_select('groups_members', $select, $params); + foreach ($groups as $group) { + \core_message\privacy\provider::delete_conversations_for_user($contextlist, 'core_group', 'groups', $group->groupid); + } + + // Remove members from the group. $DB->delete_records_select('groups_members', $select, $params); // Invalidate the group and grouping cache for the user. @@ -227,6 +249,9 @@ public static function get_group_members_in_context(userlist $userlist, string $ } $userlist->add_from_sql('userid', $sql, $params); + + // Get the users with some group conversation in this context. + \core_message\privacy\provider::add_conversations_in_context($userlist, 'core_group', 'groups', $itemid); } /** @@ -258,6 +283,12 @@ public static function delete_groups_for_users(approved_userlist $userlist, stri $params['itemid'] = $itemid; } + // Delete the group conversations for these users. + $groups = $DB->get_records_select('groups_members', $select, $params); + foreach ($groups as $group) { + \core_message\privacy\provider::delete_conversations_for_users($userlist, 'core_group', 'groups', $group->groupid); + } + $DB->delete_records_select('groups_members', $select, $params); // Invalidate the group and grouping cache for the user. @@ -294,6 +325,9 @@ public static function get_contexts_for_group_member(int $userid, string $compon $contextlist->add_from_sql($sql, $params); + // Get the contexts where the userid has group conversations. + \core_message\privacy\provider::add_contexts_for_conversations($contextlist, $userid, 'core_group', 'groups', $itemid); + return $contextlist; } diff --git a/group/tests/privacy_provider_test.php b/group/tests/privacy_provider_test.php index 19590293b0f10..6fd01fa4fbc4f 100644 --- a/group/tests/privacy_provider_test.php +++ b/group/tests/privacy_provider_test.php @@ -45,10 +45,9 @@ public function test_get_metadata() { $collection = new collection('core_group'); $newcollection = provider::get_metadata($collection); $itemcollection = $newcollection->get_collection(); - $this->assertCount(1, $itemcollection); - - $table = reset($itemcollection); + $this->assertCount(2, $itemcollection); + $table = array_shift($itemcollection); $this->assertEquals('groups_members', $table->get_name()); $this->assertEquals('privacy:metadata:groups', $table->get_summary()); @@ -56,6 +55,10 @@ public function test_get_metadata() { $this->assertArrayHasKey('groupid', $privacyfields); $this->assertArrayHasKey('userid', $privacyfields); $this->assertArrayHasKey('timeadded', $privacyfields); + + $table = array_shift($itemcollection); + $this->assertEquals('core_message', $table->get_name()); + $this->assertEquals('privacy:metadata:core_message', $table->get_summary()); } /** @@ -508,6 +511,145 @@ public function test_delete_groups_for_user_for_component() { ); } + /** + * Test for provider::delete_groups_for_users() to delete group memberships of a component. + */ + public function test_delete_groups_for_users_for_component() { + global $DB; + + $this->resetAfterTest(); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); + $group1b = $this->getDataGenerator()->create_group(array('courseid' => $course1->id)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); + $group2b = $this->getDataGenerator()->create_group(array('courseid' => $course2->id)); + $group3a = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); + $group3b = $this->getDataGenerator()->create_group(array('courseid' => $course3->id)); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'self'); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id, null, 'self'); + $this->getDataGenerator()->enrol_user($user1->id, $course3->id, null, 'self'); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'self'); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id, null, 'self'); + $this->getDataGenerator()->enrol_user($user2->id, $course3->id, null, 'self'); + + $this->getDataGenerator()->create_group_member( + array('groupid' => $group1a->id, 'userid' => $user1->id, 'component' => 'enrol_self')); + $this->getDataGenerator()->create_group_member( + array('groupid' => $group1b->id, 'userid' => $user2->id, 'component' => 'enrol_self')); + $this->getDataGenerator()->create_group_member( + array('groupid' => $group2a->id, 'userid' => $user1->id, 'component' => 'enrol_self')); + $this->getDataGenerator()->create_group_member( + array('groupid' => $group2b->id, 'userid' => $user2->id, 'component' => 'enrol_self')); + $this->getDataGenerator()->create_group_member(array('groupid' => $group3a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group3b->id, 'userid' => $user2->id)); + + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course2->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course2->id]) + ); + $this->assertEquals( + 3, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE gm.userid = ?", [$user1->id]) + ); + + // Delete user1 and user2 from groups in course1. + $coursecontext1 = context_course::instance($course1->id); + $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'core_group', + [$user1->id, $user2->id]); + provider::delete_groups_for_users($approveduserlist, 'enrol_self'); + + $this->assertEquals( + 0, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course2->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course3->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE gm.userid = ?", [$user1->id]) + ); + + // Delete user1 and user2 from course3. + $coursecontext3 = context_course::instance($course3->id); + $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext3, 'core_group', + [$user1->id, $user2->id]); + provider::delete_groups_for_users($approveduserlist, 'enrol_self'); + $this->assertEquals( + 0, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course1->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course2->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE g.courseid = ?", [$course3->id]) + ); + $this->assertEquals( + 2, + $DB->count_records_sql("SELECT COUNT(gm.id) + FROM {groups_members} gm + JOIN {groups} g ON gm.groupid = g.id + WHERE gm.userid = ?", [$user1->id]) + ); + } + /** * Test for provider::delete_groups_for_user() to check deleting from cache. */ diff --git a/lang/en/group.php b/lang/en/group.php index cabf387f1da08..aed3fcbfc3e69 100644 --- a/lang/en/group.php +++ b/lang/en/group.php @@ -175,6 +175,7 @@ $string['potentialmembers'] = 'Potential members: {$a}'; $string['potentialmembs'] = 'Potential members'; $string['printerfriendly'] = 'Printer-friendly display'; +$string['privacy:metadata:core_message'] = 'The group conversations'; $string['privacy:metadata:groups'] = 'A record of group membership.'; $string['privacy:metadata:groups:groupid'] = 'The ID of the group.'; $string['privacy:metadata:groups:timeadded'] = 'The timestamp indicating when the user was added to the group.'; diff --git a/lang/en/message.php b/lang/en/message.php index 33e227f764afe..573e51e80ae3e 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -143,6 +143,7 @@ $string['permitted'] = 'Permitted'; $string['privacy'] = 'Privacy'; $string['privacy_desc'] = 'You can restrict who can message you'; +$string['privacy:metadata:core_favourites'] = 'The conversations starred by the user'; $string['privacy:metadata:messages'] = 'Messages'; $string['privacy:metadata:messages:conversationid'] = 'The ID of the conversation'; $string['privacy:metadata:messages:fullmessage'] = 'The full message'; @@ -189,6 +190,7 @@ $string['privacy:metadata:notifications:useridto'] = 'The ID of the user who received the notification'; $string['privacy:metadata:preference:core_message_settings'] = 'Settings related to messaging'; $string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\''; +$string['privacy:export:conversationprefix'] = 'Conversation: '; $string['processorsettings'] = 'Processor settings'; $string['removecontact'] = 'Remove contact'; $string['removecontactconfirm'] = 'Are you sure you want to remove {$a} from your contacts?'; diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index d68905b835956..2b98306074edf 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2834,5 +2834,25 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2018111900.01); } + if ($oldversion < 2018112000.00) { + // Update favourited conversations, so they are saved in the proper context instead of the system. + $sql = "SELECT f.*, mc.contextid as conversationctx + FROM {favourite} f + JOIN {message_conversations} mc + ON mc.id = f.itemid"; + $favouritedconversations = $DB->get_records_sql($sql); + foreach ($favouritedconversations as $fc) { + if (empty($fc->conversationctx)) { + $conversationidctx = \context_user::instance($fc->userid)->id; + } else { + $conversationidctx = $fc->conversationctx; + } + + $DB->set_field('favourite', 'contextid', $conversationidctx, ['id' => $fc->id]); + } + + upgrade_main_savepoint(true, 2018112000.00); + } + return true; } diff --git a/message/classes/api.php b/message/classes/api.php index 8212c44fcaf34..3545f46aed9ae 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -927,16 +927,24 @@ public static function get_conversation( * @throws \moodle_exception if the user or conversation don't exist. */ public static function set_favourite_conversation(int $conversationid, int $userid) : favourite { + global $DB; + if (!self::is_user_in_conversation($userid, $conversationid)) { throw new \moodle_exception("Conversation doesn't exist or user is not a member"); } - $systemcontext = \context_system::instance(); - $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); - if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $systemcontext)) { - return $favourite; + // Get the context for this conversation. + $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]); + $userctx = \context_user::instance($userid); + if (empty($conversation->contextid)) { + // When the conversation hasn't any contextid value defined, the favourite will be added to the user context. + $conversationctx = $userctx; } else { - return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $systemcontext); + // If the contextid is defined, the favourite will be added there. + $conversationctx = \context::instance_by_id($conversation->contextid); } + + $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx); + return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx); } /** @@ -947,8 +955,21 @@ public static function set_favourite_conversation(int $conversationid, int $user * @throws \moodle_exception if the favourite does not exist for the user. */ public static function unset_favourite_conversation(int $conversationid, int $userid) { - $ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); - $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, \context_system::instance()); + global $DB; + + // Get the context for this conversation. + $conversation = $DB->get_records('message_conversations', ['id' => $conversationid]); + $userctx = \context_user::instance($userid); + if (empty($conversation->contextid)) { + // When the conversation hasn't any contextid value defined, the favourite will be added to the user context. + $conversationctx = $userctx; + } else { + // If the contextid is defined, the favourite will be added there. + $conversationctx = \context::instance_by_id($conversation->contextid); + } + + $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx); + $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx); } /** diff --git a/message/classes/privacy/provider.php b/message/classes/privacy/provider.php index b3a0e8aaa0c13..8e5441bf526e3 100644 --- a/message/classes/privacy/provider.php +++ b/message/classes/privacy/provider.php @@ -41,10 +41,20 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements + // The messaging subsystem contains data. \core_privacy\local\metadata\provider, + + // The messaging subsystem provides all the messages at user context - i.e. individual ones. \core_privacy\local\request\subsystem\provider, + + // This plugin has some sitewide user preferences to export. \core_privacy\local\request\user_preference_provider, - \core_privacy\local\request\core_userlist_provider { + + // This plugin is capable of determining which users have data within it. + \core_privacy\local\request\core_userlist_provider, + + // The messaging subsystem provides a data service to other components. + \core_privacy\local\request\subsystem\plugin_provider { /** * Return the fields which contain personal data. @@ -147,6 +157,9 @@ public static function get_metadata(collection $items) : collection { $items->add_user_preference('core_message_messageprovider_settings', 'privacy:metadata:preference:core_message_settings'); + // Add favourite conversations. + $items->link_subsystem('core_favourites', 'privacy:metadata:core_favourites'); + return $items; } @@ -196,8 +209,18 @@ public static function get_contexts_for_userid(int $userid) : contextlist { $hasdata = false; $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]); - $hasdata = $hasdata || $DB->record_exists('message_conversation_members', ['userid' => $userid]); - $hasdata = $hasdata || $DB->record_exists('messages', ['useridfrom' => $userid]); + $sql = "SELECT mc.id + FROM {message_conversations} mc + JOIN {message_conversation_members} mcm + ON (mcm.conversationid = mc.id AND mcm.userid = :userid) + WHERE mc.contextid IS NULL"; + $hasdata = $hasdata || $DB->record_exists_sql($sql, ['userid' => $userid]); + $sql = "SELECT mc.id + FROM {message_conversations} mc + JOIN {messages} m + ON (m.conversationid = mc.id AND m.useridfrom = :useridfrom) + WHERE mc.contextid IS NULL"; + $hasdata = $hasdata || $DB->record_exists_sql($sql, ['useridfrom' => $userid]); $hasdata = $hasdata || $DB->record_exists_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]); $hasdata = $hasdata || $DB->record_exists_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]); @@ -208,6 +231,9 @@ public static function get_contexts_for_userid(int $userid) : contextlist { $contextlist->add_user_context($userid); } + // Add favourite conversations. + \core_favourites\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'core_message', 'message_conversations'); + return $contextlist; } @@ -236,8 +262,18 @@ public static function get_users_in_context(userlist $userlist) { $hasdata = false; $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]); - $hasdata = $hasdata || $DB->record_exists('message_conversation_members', ['userid' => $userid]); - $hasdata = $hasdata || $DB->record_exists('messages', ['useridfrom' => $userid]); + $sql = "SELECT mc.id + FROM {message_conversations} mc + JOIN {message_conversation_members} mcm + ON (mcm.conversationid = mc.id AND mcm.userid = :userid) + WHERE mc.contextid IS NULL"; + $hasdata = $hasdata || $DB->record_exists_sql($sql, ['userid' => $userid]); + $sql = "SELECT mc.id + FROM {message_conversations} mc + JOIN {messages} m + ON (m.conversationid = mc.id AND m.useridfrom = :useridfrom) + WHERE mc.contextid IS NULL"; + $hasdata = $hasdata || $DB->record_exists_sql($sql, ['useridfrom' => $userid]); $hasdata = $hasdata || $DB->record_exists_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]); $hasdata = $hasdata || $DB->record_exists_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]); @@ -247,6 +283,16 @@ public static function get_users_in_context(userlist $userlist) { if ($hasdata) { $userlist->add_user($userid); } + + // Add favourite conversations. + $component = $userlist->get_component(); + if ($component != 'core_message') { + $userlist->set_component('core_message'); + } + \core_favourites\privacy\provider::add_userids_for_context($userlist, 'message_conversations'); + if ($component != 'core_message') { + $userlist->set_component($component); + } } /** @@ -282,8 +328,9 @@ public static function export_user_data(approved_contextlist $contextlist) { // Export the notifications. self::export_user_data_notifications($userid); - // Export the messages, with any related actions. - self::export_user_data_messages($userid); + // Conversations with empty contextid should be exported here because they are not related to any component/itemid. + $context = reset($contexts); + self::export_conversations($userid, '', '', $context); } /** @@ -345,6 +392,397 @@ public static function delete_data_for_users(approved_userlist $userlist) { static::delete_user_data($context->instanceid); } + /** + * Provide a list of contexts which have conversations for the user, in the respective area (component/itemtype combination). + * + * This method is to be called by consumers of the messaging subsystem (plugins), in their get_contexts_for_userid() method, + * to add the contexts for items which may have any conversation, but would normally not be reported as having user data by the + * plugin responsible for them. + * + * @param contextlist $contextlist + * @param int $userid The id of the user in scope. + * @param string $component the frankenstyle component name. + * @param string $itemtype the type of the conversation items. + * @param int $itemid Optional itemid associated with component. + */ + public static function add_contexts_for_conversations(contextlist $contextlist, int $userid, string $component, + string $itemtype, int $itemid = 0) { + // Search for conversations for this user in this area. + $sql = "SELECT mc.contextid + FROM {message_conversations} mc + JOIN {message_conversation_members} mcm + ON (mcm.conversationid = mc.id AND mcm.userid = :userid) + JOIN {context} ctx + ON mc.contextid = ctx.id + WHERE mc.component = :component AND mc.itemtype = :itemtype"; + $params = [ + 'userid' => $userid, + 'component' => $component, + 'itemtype' => $itemtype, + ]; + + if (!empty($itemid)) { + $sql .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } + + $contextlist->add_from_sql($sql, $params); + + // Add favourite conversations. We don't need to filter by itemid because for now they are in the system context. + \core_favourites\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'core_message', 'message_conversations'); + + } + + /** + * Add the list of users who have a conversation in the specified area (component + itemtype + itemid). + * + * @param userlist $userlist The userlist to add the users to. + * @param string $component The component to check. + * @param string $itemtype The type of the conversation items. + * @param int $itemid Optional itemid associated with component. + */ + public static function add_conversations_in_context(userlist $userlist, string $component, string $itemtype, int $itemid = 0) { + $sql = "SELECT mcm.userid + FROM {message_conversation_members} mcm + INNER JOIN {message_conversations} mc + ON mc.id = mcm.conversationid + WHERE mc.contextid = :contextid AND mc.component = :component AND mc.itemtype = :itemtype"; + $params = [ + 'contextid' => $userlist->get_context()->id, + 'component' => $component, + 'itemtype' => $itemtype + ]; + + if (!empty($itemid)) { + $sql .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } + + $userlist->add_from_sql('userid', $sql, $params); + + // Add favourite conversations. + $component = $userlist->get_component(); + if ($component != 'core_message') { + $userlist->set_component('core_message'); + } + \core_favourites\privacy\provider::add_userids_for_context($userlist, 'message_conversations'); + if ($component != 'core_message') { + $userlist->set_component($component); + } + } + + /** + * Store all conversations which match the specified component, itemtype, and itemid. + * + * Conversations without context (for now, the private ones) are stored in '<$context> | Messages | '. + * Conversations with context are stored in '<$context> | Messages | | '. + * + * @param int $userid The user whose information is to be exported. + * @param string $component The component to fetch data from. + * @param string $itemtype The itemtype that the data was exported in within the component. + * @param \context $context The context to export for. + * @param array $subcontext The sub-context in which to export this data. + * @param int $itemid Optional itemid associated with component. + */ + public static function export_conversations(int $userid, string $component, string $itemtype, \context $context, + array $subcontext = [], int $itemid = 0) { + global $DB; + + // Search for conversations for this user in this area. + $sql = "SELECT DISTINCT mc.* + FROM {message_conversations} mc + JOIN {message_conversation_members} mcm + ON (mcm.conversationid = mc.id AND mcm.userid = :userid)"; + $params = [ + 'userid' => $userid + ]; + + // Get the conversations for the defined component and itemtype. + if (!empty($component) && !empty($itemtype)) { + $sql .= " WHERE mc.component = :component AND mc.itemtype = :itemtype"; + $params['component'] = $component; + $params['itemtype'] = $itemtype; + if (!empty($itemid)) { + $sql .= " AND mc.itemid = :itemid"; + $params['itemid'] = $itemid; + } + } else { + // Get all the conversations without any component and itemtype, so with null contextid. + $sql .= " WHERE mc.contextid IS NULL"; + } + + if ($conversations = $DB->get_records_sql($sql, $params)) { + // Export conversation messages. + foreach ($conversations as $conversation) { + self::export_user_data_conversation_messages($userid, $conversation, $context, $subcontext); + } + } + } + + /** + * Deletes all group memberships for a specified context and component. + * + * @param \context $context Details about which context to delete group memberships for. + * @param string $component The component to delete. Empty string means no component. + * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype. + * @param int $itemid Optional itemid associated with component. + */ + public static function delete_conversations_for_all_users(\context $context, string $component, string $itemtype, + int $itemid = 0) { + global $DB; + + if (empty($context)) { + return; + } + + $select = "contextid = :contextid AND component = :component AND itemtype = :itemtype"; + $params = [ + 'contextid' => $context->id, + 'component' => $component, + 'itemtype' => $itemtype + ]; + + if (!empty($itemid)) { + $select .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } + + // Get and remove all the conversations and messages for the specified context and area. + if ($conversationids = $DB->get_records_select('message_conversations', $select, $params, '', 'id')) { + $conversationids = array_keys($conversationids); + $messageids = $DB->get_records_list('messages', 'conversationid', $conversationids); + $messageids = array_keys($messageids); + + // Delete these favourite conversations to all the users. + foreach ($conversationids as $conversationid) { + \core_favourites\privacy\provider::delete_favourites_for_all_users( + $context, + 'core_message', + 'message_conversations', + $conversationid); + } + + // Delete messages and user_actions. + $DB->delete_records_list('message_user_actions', 'messageid', $messageids); + $DB->delete_records_list('messages', 'id', $messageids); + + // Delete members and conversations. + $DB->delete_records_list('message_conversation_members', 'conversationid', $conversationids); + $DB->delete_records_list('message_conversations', 'id', $conversationids); + } + } + + /** + * Deletes all records for a user from a list of approved contexts. + * + * When the component and the itemtype are empty and there is only one user context in the list, all the + * conversations without contextid will be removed. However, if the component and itemtype are defined, + * only the conversations in these area for the contexts in $contextlist wil be deleted. + * + * @param approved_contextlist $contextlist Contains the user ID and a list of contexts to be deleted from. + * @param string $component The component to delete. Empty string means no component. + * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype. + * @param int $itemid Optional itemid associated with component. + */ + public static function delete_conversations_for_user(approved_contextlist $contextlist, string $component, string $itemtype, + int $itemid = 0) { + self::delete_user_data_conversations( + $contextlist->get_user()->id, + $contextlist->get_contextids(), + $component, + $itemtype, + $itemid + ); + } + + /** + * Deletes all records for multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + * @param string $component The component to delete. Empty string means no component. + * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype. + * @param int $itemid Optional itemid associated with component. + */ + public static function delete_conversations_for_users(approved_userlist $userlist, string $component, string $itemtype, + int $itemid = 0) { + global $DB; + + $userids = $userlist->get_userids(); + if (empty($userids)) { + return; + } + + $context = $userlist->get_context(); + $select = "mc.contextid = :contextid AND mc.component = :component AND mc.itemtype = :itemtype"; + $params = [ + 'contextid' => $context->id, + 'component' => $component, + 'itemtype' => $itemtype + ]; + if (!empty($itemid)) { + $select .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } + + // Get conversations in this area where the specified users are a member of. + list($useridsql, $useridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + $sql = "SELECT DISTINCT mcm.conversationid as id + FROM {message_conversation_members} mcm + INNER JOIN {message_conversations} mc + ON mc.id = mcm.conversationid + WHERE mcm.userid $useridsql AND $select"; + $params += $useridparams; + $conversationids = array_keys($DB->get_records_sql($sql, $params)); + if (!empty($conversationids)) { + list($conversationidsql, $conversationidparams) = $DB->get_in_or_equal($conversationids, SQL_PARAMS_NAMED); + + // Get all the messages for these conversations which has some action stored for these users. + $sql = "SELECT DISTINCT m.id + FROM {messages} m + INNER JOIN {message_conversations} mc + ON mc.id = m.conversationid + INNER JOIN {message_user_actions} mua + ON mua.messageid = m.id + WHERE mua.userid $useridsql AND mc.id $conversationidsql"; + $params = $useridparams + $conversationidparams; + $messageids = array_keys($DB->get_records_sql($sql, $params)); + if (!empty($messageids)) { + // Delete all the user_actions for the messages on these conversations where the user has any action. + list($messageidsql, $messageidparams) = $DB->get_in_or_equal($messageids, SQL_PARAMS_NAMED); + $select = "messageid $messageidsql AND userid $useridsql"; + $DB->delete_records_select('message_user_actions', $select, $messageidparams + $useridparams); + } + + // Get all the messages for these conversations sent by these users. + $sql = "SELECT DISTINCT m.id + FROM {messages} m + WHERE m.useridfrom $useridsql AND m.conversationid $conversationidsql"; + // Reuse the $params var because it contains the useridparams and the conversationids. + $messageids = array_keys($DB->get_records_sql($sql, $params)); + if (!empty($messageids)) { + // Delete all the user_actions for the messages sent by any of these users. + $DB->delete_records_list('message_user_actions', 'messageid', $messageids); + + // Delete all the messages sent by any of these users. + $DB->delete_records_list('messages', 'id', $messageids); + } + + // In that case, conversations can't be removed, because they could have more members and messages. + // So, remove only users from the context conversations where they are member of. + $sql = "conversationid $conversationidsql AND userid $useridsql"; + // Reuse the $params var because it contains the useridparams and the conversationids. + $DB->delete_records_select('message_conversation_members', $sql, $params); + + // Delete the favourite conversations. + $userlist = new \core_privacy\local\request\approved_userlist($context, 'core_message', $userids); + \core_favourites\privacy\provider::delete_favourites_for_userlist( + $userlist, + 'message_conversations' + ); + } + } + + /** + * Deletes all records for multiple users within multiple contexts in a component area. + * + * @param int $userid The user identifier to delete information for. + * @param array $contextids The context identifiers to delete information for. Empty array means no context (for + * individual conversations). + * @param string $component The component to delete. Empty string means no component (for individual conversations). + * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype (for individual + * conversations). + * @param int $itemid Optional itemid associated with component. + */ + protected static function delete_user_data_conversations(int $userid, array $contextids, string $component, + string $itemtype, int $itemid = 0) { + global $DB; + + if (empty($contextids) && empty($component) && empty($itemtype) && empty($itemid)) { + // Individual conversations haven't context, component neither itemtype. + $select = "mc.contextid IS NULL"; + $params = []; + } else { + list($contextidsql, $contextidparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); + $select = "mc.contextid $contextidsql AND mc.component = :component AND mc.itemtype = :itemtype"; + $params = [ + 'component' => $component, + 'itemtype' => $itemtype + ]; + $params += $contextidparams; + if (!empty($itemid)) { + $select .= " AND itemid = :itemid"; + $params['itemid'] = $itemid; + } + } + + // Get conversations in these contexts where the specified userid is a member of. + $sql = "SELECT DISTINCT mcm.conversationid as id + FROM {message_conversation_members} mcm + INNER JOIN {message_conversations} mc + ON mc.id = mcm.conversationid + WHERE mcm.userid = :userid AND $select"; + $params['userid'] = $userid; + $conversationids = array_keys($DB->get_records_sql($sql, $params)); + if (!empty($conversationids)) { + list($conversationidsql, $conversationidparams) = $DB->get_in_or_equal($conversationids, SQL_PARAMS_NAMED); + + // Get all the messages for these conversations which has some action stored for the userid. + $sql = "SELECT DISTINCT m.id + FROM {messages} m + INNER JOIN {message_conversations} mc + ON mc.id = m.conversationid + INNER JOIN {message_user_actions} mua + ON mua.messageid = m.id + WHERE mua.userid = :userid AND mc.id $conversationidsql"; + $params = ['userid' => $userid] + $conversationidparams; + $messageids = array_keys($DB->get_records_sql($sql, $params)); + if (!empty($messageids)) { + // Delete all the user_actions for the messages on these conversations where the user has any action. + list($messageidsql, $messageidparams) = $DB->get_in_or_equal($messageids, SQL_PARAMS_NAMED); + $select = "messageid $messageidsql AND userid = :userid"; + $DB->delete_records_select('message_user_actions', $select, $messageidparams + ['userid' => $userid]); + } + + // Get all the messages for these conversations sent by the userid. + $sql = "SELECT DISTINCT m.id + FROM {messages} m + WHERE m.useridfrom = :userid AND m.conversationid $conversationidsql"; + // Reuse the $params var because it contains the userid and the conversationids. + $messageids = array_keys($DB->get_records_sql($sql, $params)); + if (!empty($messageids)) { + // Delete all the user_actions for the messages sent by the userid. + $DB->delete_records_list('message_user_actions', 'messageid', $messageids); + + // Delete all the messages sent by the userid. + $DB->delete_records_list('messages', 'id', $messageids); + } + + // In that case, conversations can't be removed, because they could have more members and messages. + // So, remove only userid from the context conversations where he/she is member of. + $sql = "conversationid $conversationidsql AND userid = :userid"; + // Reuse the $params var because it contains the userid and the conversationids. + $DB->delete_records_select('message_conversation_members', $sql, $params); + + // Delete the favourite conversations. + if (empty($contextids) && empty($component) && empty($itemtype) && empty($itemid)) { + // Favourites for individual conversations are stored into the user context. + $favouritectxids = [\context_user::instance($userid)->id]; + } else { + $favouritectxids = $contextids; + } + $contextlist = new \core_privacy\local\request\approved_contextlist( + \core_user::get_user($userid), + 'core_message', + $favouritectxids + ); + \core_favourites\privacy\provider::delete_favourites_for_user( + $contextlist, + 'core_message', + 'message_conversations' + ); + } + } + /** * Delete all user data for the specified user. * @@ -353,9 +791,10 @@ public static function delete_data_for_users(approved_userlist $userlist) { protected static function delete_user_data(int $userid) { global $DB; - $DB->delete_records('messages', ['useridfrom' => $userid]); - $DB->delete_records('message_user_actions', ['userid' => $userid]); - $DB->delete_records('message_conversation_members', ['userid' => $userid]); + // Delete individual conversations information for this user. + self::delete_user_data_conversations($userid, [], '', ''); + + // Delete contacts, requests, users blocked and notifications. $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]); $DB->delete_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?', [$userid, $userid]); $DB->delete_records_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]); @@ -438,73 +877,102 @@ protected static function export_user_data_blocked_users(int $userid) { } /** - * Export the messaging data. + * Export conversation messages. * - * @param int $userid + * @param int $userid The user identifier. + * @param \stdClass $conversation The conversation to export the messages. + * @param \context $context The context to export for. + * @param array $subcontext The sub-context in which to export this data. */ - protected static function export_user_data_messages(int $userid) { + protected static function export_user_data_conversation_messages(int $userid, \stdClass $conversation, \context $context, + array $subcontext = []) { global $DB; - $context = \context_user::instance($userid); + // Get all the messages for this conversation from start to finish. + $sql = "SELECT m.*, muadelete.timecreated as timedeleted, muaread.timecreated as timeread + FROM {messages} m + LEFT JOIN {message_user_actions} muadelete + ON m.id = muadelete.messageid AND muadelete.action = :deleteaction AND muadelete.userid = :deleteuserid + LEFT JOIN {message_user_actions} muaread + ON m.id = muaread.messageid AND muaread.action = :readaction AND muaread.userid = :readuserid + WHERE conversationid = :conversationid + ORDER BY m.timecreated ASC"; + $messages = $DB->get_recordset_sql($sql, ['deleteaction' => \core_message\api::MESSAGE_ACTION_DELETED, + 'readaction' => \core_message\api::MESSAGE_ACTION_READ, 'conversationid' => $conversation->id, + 'deleteuserid' => $userid, 'readuserid' => $userid]); + $messagedata = []; + foreach ($messages as $message) { + $timeread = !is_null($message->timeread) ? transform::datetime($message->timeread) : '-'; + $issender = $userid == $message->useridfrom; + + $data = [ + 'issender' => transform::yesno($issender), + 'message' => message_format_message_text($message), + 'timecreated' => transform::datetime($message->timecreated), + 'timeread' => $timeread + ]; + if ($conversation->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP && !$issender) { + // Only export sender for group conversations when is not the current user. + $data['sender'] = transform::user($message->useridfrom); + } - $sql = "SELECT DISTINCT mcm.conversationid as id - FROM {message_conversation_members} mcm - WHERE mcm.userid = :userid"; - if ($conversations = $DB->get_records_sql($sql, ['userid' => $userid])) { - // Ok, let's get the other users in the conversations. - $conversationids = array_keys($conversations); - list($conversationidsql, $conversationparams) = $DB->get_in_or_equal($conversationids, SQL_PARAMS_NAMED); - $userfields = \user_picture::fields('u'); - $userssql = "SELECT mcm.conversationid, $userfields - FROM {user} u - INNER JOIN {message_conversation_members} mcm - ON u.id = mcm.userid - WHERE mcm.conversationid $conversationidsql - AND mcm.userid != :userid - AND u.deleted = 0"; - $otherusers = $DB->get_records_sql($userssql, $conversationparams + ['userid' => $userid]); - foreach ($conversations as $conversation) { - $otheruserfullname = get_string('unknownuser', 'core_message'); + if (!is_null($message->timedeleted)) { + $data['timedeleted'] = transform::datetime($message->timedeleted); + } - // It's possible the other user has requested to be deleted, so might not exist - // as a conversation member, or they have just been deleted. - if (isset($otherusers[$conversation->id])) { - $otheruserfullname = fullname($otherusers[$conversation->id]); + $messagedata[] = (object) $data; + } + $messages->close(); + + if (!empty($messagedata)) { + // Get subcontext. + if (empty($conversation->contextid)) { + // Conversations without context are stored in 'Messages | '. + $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation->id]); + $members = array_filter($members, function ($member) use ($userid) { + return $member->userid != $userid; + }); + if ($otheruser = reset($members)) { + $otherusertext = $otheruser->userid; + } else { + $otherusertext = get_string('unknownuser', 'core_message') . '_' . $conversation->id; } - // Get all the messages for this conversation from start to finish. - $sql = "SELECT m.*, muadelete.timecreated as timedeleted, muaread.timecreated as timeread - FROM {messages} m - LEFT JOIN {message_user_actions} muadelete - ON m.id = muadelete.messageid AND muadelete.action = :deleteaction - LEFT JOIN {message_user_actions} muaread - ON m.id = muaread.messageid AND muaread.action = :readaction - WHERE conversationid = :conversationid - ORDER BY m.timecreated ASC"; - $messages = $DB->get_recordset_sql($sql, ['deleteaction' => \core_message\api::MESSAGE_ACTION_DELETED, - 'readaction' => \core_message\api::MESSAGE_ACTION_READ, 'conversationid' => $conversation->id]); - $messagedata = []; - foreach ($messages as $message) { - $timeread = !is_null($message->timeread) ? transform::datetime($message->timeread) : '-'; - $issender = $userid == $message->useridfrom; - - $data = [ - 'sender' => transform::yesno($issender), - 'message' => message_format_message_text($message), - 'timecreated' => transform::datetime($message->timecreated), - 'timeread' => $timeread - ]; - - if (!is_null($message->timedeleted)) { - $data['timedeleted'] = transform::datetime($message->timedeleted); - } - - $messagedata[] = (object) $data; + $subcontext = array_merge( + $subcontext, + [get_string('messages', 'core_message'), $otherusertext] + ); + + // Get the context for the favourite conversation. + $conversationctx = \context_user::instance($userid); + } else { + // Conversations with context are stored in 'Messages | | '. + if (get_string_manager()->string_exists($conversation->itemtype, $conversation->component)) { + $itemtypestring = get_string($conversation->itemtype, $conversation->component); + } else { + // If the itemtype doesn't exist in the component string file, the raw itemtype will be returned. + $itemtypestring = $conversation->itemtype; } - $messages->close(); - writer::with_context($context)->export_data([get_string('messages', 'core_message'), $otheruserfullname], - (object) $messagedata); + $conversationname = get_string('privacy:export:conversationprefix', 'core_message') . $conversation->name; + $subcontext = array_merge( + $subcontext, + [get_string('messages', 'core_message'), $itemtypestring, $conversationname] + ); + + // Get the context for the favourite conversation. + $conversationctx = \context::instance_by_id($conversation->contextid); + } + + // Export the conversation messages. + writer::with_context($context)->export_data($subcontext, (object) $messagedata); + + // Get user's favourites information for the particular conversation. + $conversationfavourite = \core_favourites\privacy\provider::get_favourites_info_for_user($userid, $conversationctx, + 'core_message', 'message_conversations', $conversation->id); + if ($conversationfavourite) { + // If the conversation has been favorited by the user, include it in the export. + writer::with_context($context)->export_related_data($subcontext, 'starred', (object) $conversationfavourite); } } } diff --git a/message/tests/privacy_provider_test.php b/message/tests/privacy_provider_test.php index d069f65c1a282..182c8bb23fd57 100644 --- a/message/tests/privacy_provider_test.php +++ b/message/tests/privacy_provider_test.php @@ -24,8 +24,10 @@ use core_privacy\local\metadata\collection; use core_message\privacy\provider; +use \core_privacy\local\request\contextlist; use \core_privacy\local\request\writer; use \core_privacy\local\request\transform; +use \core_message\tests\helper as testhelper; defined('MOODLE_INTERNAL') || die(); @@ -45,7 +47,7 @@ public function test_get_metadata() { $collection = new collection('core_message'); $newcollection = provider::get_metadata($collection); $itemcollection = $newcollection->get_collection(); - $this->assertCount(8, $itemcollection); + $this->assertCount(9, $itemcollection); $messagestable = array_shift($itemcollection); $this->assertEquals('messages', $messagestable->get_name()); @@ -71,6 +73,10 @@ public function test_get_metadata() { $usersettings = array_shift($itemcollection); $this->assertEquals('core_message_messageprovider_settings', $usersettings->get_name()); + $favouriteconversations = array_shift($itemcollection); + $this->assertEquals('core_favourites', $favouriteconversations->get_name()); + $this->assertEquals('privacy:metadata:core_favourites', $favouriteconversations->get_summary()); + $privacyfields = $messagestable->get_privacy_fields(); $this->assertArrayHasKey('useridfrom', $privacyfields); $this->assertArrayHasKey('conversationid', $privacyfields); @@ -205,37 +211,138 @@ public function test_get_contexts_for_userid_no_data() { } /** - * Test for provider::get_contexts_for_userid() when there is a message between users. + * Test for provider::get_contexts_for_userid() when there is a private message between users. */ - public function test_get_contexts_for_userid_with_message() { + public function test_get_contexts_for_userid_with_private_messages() { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); - // Test nothing is found before message is sent. + // Test nothing is found before group conversations is created or message is sent. $contextlist = provider::get_contexts_for_userid($user1->id); $this->assertCount(0, $contextlist); $contextlist = provider::get_contexts_for_userid($user2->id); $this->assertCount(0, $contextlist); - $this->create_message($user1->id, $user2->id, time() - (9 * DAYSECS)); + // Send some private messages. + $pm1id = $this->create_message($user1->id, $user2->id, time() - (9 * DAYSECS)); - // Test for the sender. + // Test for the sender (user1). $contextlist = provider::get_contexts_for_userid($user1->id); $this->assertCount(1, $contextlist); $contextforuser = $contextlist->current(); $this->assertEquals( - context_user::instance($user1->id)->id, + \context_user::instance($user1->id)->id, $contextforuser->id); - // Test for the receiver. + // Test for the receiver (user2). $contextlist = provider::get_contexts_for_userid($user2->id); $this->assertCount(1, $contextlist); $contextforuser = $contextlist->current(); $this->assertEquals( - context_user::instance($user2->id)->id, + \context_user::instance($user2->id)->id, + $contextforuser->id); + + // Test for user3 (no private messages). + $contextlist = provider::get_contexts_for_userid($user3->id); + $this->assertCount(0, $contextlist); + } + + /** + * Test for provider::get_contexts_for_userid() when there is several messages (private and group). + */ + public function test_get_contexts_for_userid_with_messages() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + + // Test nothing is found before group conversations is created or message is sent. + $contextlist = provider::get_contexts_for_userid($user1->id); + $this->assertCount(0, $contextlist); + $contextlist = provider::get_contexts_for_userid($user2->id); + $this->assertCount(0, $contextlist); + + // Create course. + $course1 = $this->getDataGenerator()->create_course(); + $coursecontext1 = context_course::instance($course1->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + + // Create groups (only one with enablemessaging = 1). + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + + // Get conversation. + $component = 'core_group'; + $itemtype = 'groups'; + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + + // Send some messages to the group conversation. + $now = time(); + $m1id = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1', $now + 1); + $m2id = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 2', $now + 2); + $m3id = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 3', $now + 3); + + // Test for user1 (although is member of the conversation, hasn't any private message). + $contextlist = provider::get_contexts_for_userid($user1->id); + $this->assertCount(0, $contextlist); + + // Test for user2 (although is member of the conversation, hasn't any private message). + $contextlist = provider::get_contexts_for_userid($user2->id); + $this->assertCount(0, $contextlist); + + // Test for user3 (although is member of the conversation, hasn't any private message). + $contextlist = provider::get_contexts_for_userid($user3->id); + $this->assertCount(0, $contextlist); + + // Test for user4 (doesn't belong to the conversation). + $contextlist = provider::get_contexts_for_userid($user4->id); + $this->assertCount(0, $contextlist); + + // Send some private messages. + $pm1id = $this->create_message($user1->id, $user2->id, time() - (9 * DAYSECS)); + + // Test user1 now has the user context because of the private message. + $contextlist = provider::get_contexts_for_userid($user1->id); + $this->assertCount(1, $contextlist); + $contextforuser = $contextlist->current(); + $this->assertEquals( + \context_user::instance($user1->id)->id, + $contextforuser->id); + + // Test user2 now has the user context because of the private message. + $contextlist = provider::get_contexts_for_userid($user2->id); + $this->assertCount(1, $contextlist); + $contextforuser = $contextlist->current(); + $this->assertEquals( + \context_user::instance($user2->id)->id, $contextforuser->id); + + // Test for user3 (although is member of the conversation, hasn't still any private message). + $contextlist = provider::get_contexts_for_userid($user3->id); + $this->assertCount(0, $contextlist); + + // Test for user4 (doesn't belong to the conversation and hasn't any private message). + $contextlist = provider::get_contexts_for_userid($user4->id); + $this->assertCount(0, $contextlist); } /** @@ -489,7 +596,7 @@ public function test_export_for_context_with_blocked_users() { /** * Test for provider::export_user_data(). */ - public function test_export_for_context_with_messages() { + public function test_export_for_context_with_private_messages() { global $DB; $this->resetAfterTest(); @@ -529,7 +636,7 @@ public function test_export_for_context_with_messages() { $this->assertTrue($writer->has_any_data()); // Confirm the messages with user 2 are correct. - $messages = (array) $writer->get_data([get_string('messages', 'core_message'), fullname($user2)]); + $messages = (array) $writer->get_data([get_string('messages', 'core_message'), $user2->id]); $this->assertCount(3, $messages); $dbm1 = $DB->get_record('messages', ['id' => $m1]); @@ -541,25 +648,25 @@ public function test_export_for_context_with_messages() { $m2 = array_shift($messages); $m3 = array_shift($messages); - $this->assertEquals(get_string('yes'), $m1->sender); + $this->assertEquals(get_string('yes'), $m1->issender); $this->assertEquals(message_format_message_text($dbm1), $m1->message); $this->assertEquals(transform::datetime($now - (9 * DAYSECS)), $m1->timecreated); - $this->assertNotEquals('-', $m1->timeread); + $this->assertEquals('-', $m1->timeread); $this->assertArrayNotHasKey('timedeleted', (array) $m1); - $this->assertEquals(get_string('no'), $m2->sender); + $this->assertEquals(get_string('no'), $m2->issender); $this->assertEquals(message_format_message_text($dbm2), $m2->message); $this->assertEquals(transform::datetime($now - (8 * DAYSECS)), $m2->timecreated); $this->assertEquals('-', $m2->timeread); $this->assertArrayHasKey('timedeleted', (array) $m2); - $this->assertEquals(get_string('yes'), $m3->sender); + $this->assertEquals(get_string('yes'), $m3->issender); $this->assertEquals(message_format_message_text($dbm3), $m3->message); $this->assertEquals(transform::datetime($now - (7 * DAYSECS)), $m3->timecreated); $this->assertEquals('-', $m3->timeread); // Confirm the messages with user 3 are correct. - $messages = (array) $writer->get_data([get_string('messages', 'core_message'), fullname($user3)]); + $messages = (array) $writer->get_data([get_string('messages', 'core_message'), $user3->id]); $this->assertCount(3, $messages); $dbm4 = $DB->get_record('messages', ['id' => $m4]); @@ -571,24 +678,126 @@ public function test_export_for_context_with_messages() { $m5 = array_shift($messages); $m6 = array_shift($messages); - $this->assertEquals(get_string('no'), $m4->sender); + $this->assertEquals(get_string('no'), $m4->issender); $this->assertEquals(message_format_message_text($dbm4), $m4->message); $this->assertEquals(transform::datetime($now - (6 * DAYSECS)), $m4->timecreated); $this->assertNotEquals('-', $m4->timeread); $this->assertArrayNotHasKey('timedeleted', (array) $m4); - $this->assertEquals(get_string('yes'), $m5->sender); + $this->assertEquals(get_string('yes'), $m5->issender); $this->assertEquals(message_format_message_text($dbm5), $m5->message); $this->assertEquals(transform::datetime($now - (5 * DAYSECS)), $m5->timecreated); $this->assertEquals('-', $m5->timeread); $this->assertArrayHasKey('timedeleted', (array) $m5); - $this->assertEquals(get_string('no'), $m6->sender); + $this->assertEquals(get_string('no'), $m6->issender); $this->assertEquals(message_format_message_text($dbm6), $m6->message); $this->assertEquals(transform::datetime($now - (4 * DAYSECS)), $m6->timecreated); $this->assertEquals('-', $m6->timeread); } + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context_with_messages() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $now = time(); + $systemcontext = \context_system::instance(); + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user1context = \context_user::instance($user1->id); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + + // Create course groups with group messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); + + // Get conversation. + $component = 'core_group'; + $itemtype = 'groups'; + $conversation = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + + // Send some private messages between user 1 and user 2. + $pm1id = $this->create_message($user1->id, $user2->id, $now); + + $dbpm1 = $DB->get_record('messages', ['id' => $pm1id]); + + // Send some messages to the conversation. + $m1 = testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $now + 1); + $m2 = testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 2', $now + 2); + $m3 = testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 3', $now + 3); + + $dbm1 = $DB->get_record('messages', ['id' => $m1]); + $dbm2 = $DB->get_record('messages', ['id' => $m2]); + $dbm3 = $DB->get_record('messages', ['id' => $m3]); + + // Mark as read and delete some messages. + \core_message\api::mark_message_as_read($user2->id, $dbm1); + \core_message\api::delete_message($user1->id, $m2); + + // Confirm the user1 has no data in any course context because private messages are related to user context. + $this->export_context_data_for_user($user1->id, $coursecontext2, 'core_message'); + + // Check that system context hasn't been exported. + $writer = writer::with_context($systemcontext); + $this->assertFalse($writer->has_any_data()); + + // Check that course1 context hasn't been exported. + $writer = writer::with_context($coursecontext1); + $this->assertFalse($writer->has_any_data()); + + // Check that course2 context has been exported and contains data. + $writer = writer::with_context($coursecontext2); + $this->assertFalse($writer->has_any_data()); + + // Confirm the user1 has only private messages in the user context. + $this->export_context_data_for_user($user1->id, $user1context, 'core_message'); + $writer = writer::with_context($user1context); + $this->assertTrue($writer->has_any_data()); + + // Confirm the messages with user 2 are correct. + $messages = (array) $writer->get_data([get_string('messages', 'core_message'), $user2->id]); + $this->assertCount(1, $messages); + $m1 = reset($messages); + + $this->assertEquals(get_string('yes'), $m1->issender); + $this->assertEquals(message_format_message_text($dbpm1), $m1->message); + $this->assertEquals(transform::datetime($now), $m1->timecreated); + $this->assertEquals('-', $m1->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m1); + + // Confirm the messages with user 3 are correct. + $messages = (array) $writer->get_data([get_string('messages', 'core_message'), fullname($user3)]); + $this->assertCount(0, $messages); + } + /** * Test for provider::export_user_data(). */ @@ -724,8 +933,8 @@ public function test_delete_data_for_all_users_in_context() { // And none of them are from user1. $this->assertEquals(0, $DB->count_records('messages', ['useridfrom' => $user1->id])); - // Confirm there is only 1 user action left - the one that is for user2 reading the message. - $this->assertEquals(1, $DB->count_records('message_user_actions')); + // Confirm there is 0 user action left. + $this->assertEquals(0, $DB->count_records('message_user_actions')); // And it is not for user1. $this->assertEquals(0, $DB->count_records('message_user_actions', ['userid' => $user1->id])); @@ -838,11 +1047,7 @@ public function test_delete_data_for_user() { $message = reset($messages); $this->assertEquals($m2, $message->id); - $this->assertCount(1, $muas); - $mua = reset($muas); - $this->assertEquals($user2->id, $mua->userid); - $this->assertEquals($m1, $mua->messageid); - $this->assertEquals(\core_message\api::MESSAGE_ACTION_READ, $mua->action); + $this->assertCount(0, $muas); $this->assertCount(1, $mcms); $mcm = reset($mcms); @@ -1155,11 +1360,7 @@ public function test_delete_data_for_users() { $message = reset($messages); $this->assertEquals($m2, $message->id); - $this->assertCount(1, $muas); - $mua = reset($muas); - $this->assertEquals($user2->id, $mua->userid); - $this->assertEquals($m1, $mua->messageid); - $this->assertEquals(\core_message\api::MESSAGE_ACTION_READ, $mua->action); + $this->assertCount(0, $muas); $this->assertCount(1, $mcms); $mcm = reset($mcms); @@ -1173,6 +1374,1241 @@ public function test_delete_data_for_users() { $this->assertEquals($user3->id, $notification->useridto); } + /** + * Test for provider::add_contexts_for_conversations(). + */ + public function test_add_contexts_for_conversations() { + $this->resetAfterTest(); + $this->setAdminUser(); + $component = 'core_group'; + $itemtype = 'groups'; + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + + // Test nothing is found before group conversations is created or message is sent. + $contextlist = new contextlist(); + provider::add_contexts_for_conversations($contextlist, $user1->id, $component, $itemtype); + $this->assertCount(0, $contextlist); + provider::add_contexts_for_conversations($contextlist, $user2->id, $component, $itemtype); + $this->assertCount(0, $contextlist); + + // Create courses. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id); + + // Create course groups with messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); + + // Get conversation. + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + + // Send some messages to the group conversation. + $now = time(); + $m1id = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1', $now + 1); + $m2id = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 2', $now + 2); + $m3id = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 3', $now + 3); + + // Test for user1 (is member of the conversation and has sent a message). + $contextlist = new contextlist(); + provider::add_contexts_for_conversations($contextlist, $user1->id, $component, $itemtype); + $this->assertCount(2, $contextlist); + $this->assertContains($coursecontext1->id, $contextlist->get_contextids()); + $this->assertContains($coursecontext2->id, $contextlist->get_contextids()); + + // Test for user2 (is member of the conversation and has sent a message). + $contextlist = new contextlist(); + provider::add_contexts_for_conversations($contextlist, $user2->id, $component, $itemtype); + $this->assertCount(1, $contextlist); + $this->assertEquals($coursecontext1, $contextlist->current()); + + // Test for user3 (is member of the conversation). + $contextlist = new contextlist(); + provider::add_contexts_for_conversations($contextlist, $user3->id, $component, $itemtype); + $this->assertCount(1, $contextlist); + $this->assertEquals($coursecontext1, $contextlist->current()); + + // Test for user4 (doesn't belong to the conversation). + $contextlist = new contextlist(); + provider::add_contexts_for_conversations($contextlist, $user4->id, $component, $itemtype); + $this->assertCount(0, $contextlist); + } + + /** + * Test for provider::add_conversations_in_context(). + */ + public function test_add_conversations_in_context() { + $this->resetAfterTest(); + $this->setAdminUser(); + $component = 'core_group'; + $itemtype = 'groups'; + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + + // Create courses. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Test nothing is found before group conversations is created or message is sent. + $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, 'core_message'); + provider::add_conversations_in_context($userlist1, $component, $itemtype); + $this->assertCount(0, $userlist1); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user4->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id); + + // Create course groups with messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); + + // Get conversation. + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + + // Send some messages to the group conversation. + $now = time(); + $m1id = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1', $now + 1); + $m2id = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 2', $now + 2); + $m3id = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 3', $now + 3); + + // Test for users with any group conversation in course1. + provider::add_conversations_in_context($userlist1, $component, $itemtype); + $this->assertCount(3, $userlist1); + $this->assertEquals( + [$user1->id, $user2->id, $user3->id], + $userlist1->get_userids()); + + // Test for users with any group conversation in course2. + $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, 'core_message'); + provider::add_conversations_in_context($userlist2, $component, $itemtype); + $this->assertCount(1, $userlist2); + $this->assertEquals( + [$user1->id], + $userlist2->get_userids()); + } + + /** + * Test for provider::export_conversations(). + */ + public function test_export_conversations() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $now = time(); + $systemcontext = \context_system::instance(); + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user1context = \context_user::instance($user1->id); + + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + + // Create course groups with group messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); + + // Send some private messages between user 1 and user 2. + $pm1id = $this->create_message($user1->id, $user2->id, $now); + + // Get conversation. + $iconversation1id = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); + $component = 'core_group'; + $itemtype = 'groups'; + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + + // Make favourite some conversations. + \core_message\api::set_favourite_conversation($conversation1->id, $user1->id); + \core_message\api::set_favourite_conversation($iconversation1id, $user2->id); + + // Send some messages to the conversation. + $m1 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1', $now + 1); + $m2 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 2', $now + 2); + $m3 = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 3', $now + 3); + + $dbm1 = $DB->get_record('messages', ['id' => $m1]); + $dbm2 = $DB->get_record('messages', ['id' => $m2]); + $dbm3 = $DB->get_record('messages', ['id' => $m3]); + + // Mark as read and delete some messages. + \core_message\api::mark_message_as_read($user1->id, $dbm3, $now + 5); + \core_message\api::delete_message($user1->id, $m2); + + // Export all the conversations related to the groups in course1 for user1. + provider::export_conversations($user1->id, 'core_group', 'groups', $coursecontext1); + + // Check that system context hasn't been exported. + $writer = writer::with_context($systemcontext); + $this->assertFalse($writer->has_any_data()); + + // Check that course2 context hasn't been exported. + $writer = writer::with_context($coursecontext2); + $this->assertFalse($writer->has_any_data()); + + // Check that course1 context has been exported for user1 and contains data. + $writer = writer::with_context($coursecontext1); + $this->assertTrue($writer->has_any_data()); + + // Confirm the messages for conversation1 are correct. + $messages = (array) $writer->get_data([ + get_string('messages', 'core_message'), + get_string($conversation1->itemtype, $conversation1->component), + get_string('privacy:export:conversationprefix', 'core_message') . $conversation1->name + ]); + $this->assertCount(3, $messages); + + usort($messages, ['static', 'sort_messages']); + $m1 = array_shift($messages); + $m2 = array_shift($messages); + $m3 = array_shift($messages); + + // Check message 1 is correct. + $this->assertEquals(get_string('yes'), $m1->issender); + $this->assertEquals(message_format_message_text($dbm1), $m1->message); + $this->assertEquals(transform::datetime($now + 1), $m1->timecreated); + $this->assertEquals('-', $m1->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m1); + + // Check message 2 is correct. + $this->assertEquals(get_string('yes'), $m2->issender); + $this->assertEquals(message_format_message_text($dbm2), $m2->message); + $this->assertEquals(transform::datetime($now + 2), $m2->timecreated); + $this->assertEquals('-', $m2->timeread); + $this->assertArrayHasKey('timedeleted', (array) $m2); + + // Check message 3 is correct. + $this->assertEquals(get_string('no'), $m3->issender); + $this->assertEquals(message_format_message_text($dbm3), $m3->message); + $this->assertEquals(transform::datetime($now + 3), $m3->timecreated); + $this->assertEquals(transform::datetime($now + 5), $m3->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m3); + + // Confirm the favourite group conversation is correct. + $favourite = (array) $writer->get_related_data([ + get_string('messages', 'core_message'), + get_string($conversation1->itemtype, $conversation1->component), + get_string('privacy:export:conversationprefix', 'core_message') . $conversation1->name + ], 'starred'); + $this->assertCount(4, $favourite); + $this->assertEquals(get_string('yes'), $favourite['starred']); + + // Reset writer before exporting conversations for user2. + writer::reset(); + + // Export all the conversations related to the groups in course1 for user2. + provider::export_conversations($user2->id, 'core_group', 'groups', $coursecontext1); + + // Check that system context hasn't been exported. + $writer = writer::with_context($systemcontext); + $this->assertFalse($writer->has_any_data()); + + // Check that course2 context hasn't been exported. + $writer = writer::with_context($coursecontext2); + $this->assertFalse($writer->has_any_data()); + + // Check that course1 context has been exported for user2 and contains data. + $writer = writer::with_context($coursecontext1); + $this->assertTrue($writer->has_any_data()); + + // Confirm the messages for conversation1 are correct. + $messages = (array) $writer->get_data([ + get_string('messages', 'core_message'), + get_string($conversation1->itemtype, $conversation1->component), + get_string('privacy:export:conversationprefix', 'core_message') . $conversation1->name + ]); + $this->assertCount(3, $messages); + + usort($messages, ['static', 'sort_messages']); + $m1 = array_shift($messages); + $m2 = array_shift($messages); + $m3 = array_shift($messages); + + // Check message 1 is correct. + $this->assertEquals(get_string('no'), $m1->issender); + $this->assertEquals(message_format_message_text($dbm1), $m1->message); + $this->assertEquals(transform::datetime($now + 1), $m1->timecreated); + $this->assertEquals('-', $m1->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m1); + + // Check message 2 is correct. + $this->assertEquals(get_string('no'), $m2->issender); + $this->assertEquals(message_format_message_text($dbm2), $m2->message); + $this->assertEquals(transform::datetime($now + 2), $m2->timecreated); + $this->assertEquals('-', $m2->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m2); + + // Check message 3 is correct. + $this->assertEquals(get_string('yes'), $m3->issender); + $this->assertEquals(message_format_message_text($dbm3), $m3->message); + $this->assertEquals(transform::datetime($now + 3), $m3->timecreated); + $this->assertEquals('-', $m3->timeread); + $this->assertArrayNotHasKey('timedeleted', (array) $m3); + + // Confirm there are no favourite group conversation for user2. + $favourite = (array) $writer->get_related_data([ + get_string('messages', 'core_message'), + get_string($conversation1->itemtype, $conversation1->component), + $conversation1->name + ], 'starred'); + $this->assertCount(0, $favourite); + } + + /** + * Test for provider::delete_conversations_for_all_users(). + */ + public function test_delete_conversations_for_all_users() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $now = time(); + $timeread = $now - DAYSECS; + $component = 'core_group'; + $itemtype = 'groups'; + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + $user5 = $this->getDataGenerator()->create_user(); + $user1context = \context_user::instance($user1->id); + + // Create contacts. + \core_message\api::add_contact($user1->id, $user2->id); + \core_message\api::add_contact($user2->id, $user3->id); + + // Create contact requests. + \core_message\api::create_contact_request($user1->id, $user3->id); + \core_message\api::create_contact_request($user2->id, $user4->id); + + // Block a user. + \core_message\api::block_user($user1->id, $user3->id); + \core_message\api::block_user($user3->id, $user4->id); + + // Create individual messages. + $im1 = $this->create_message($user1->id, $user2->id, $now + (9 * DAYSECS), true); + $im2 = $this->create_message($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $im3 = $this->create_message($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Create notifications. + $n1 = $this->create_notification($user1->id, $user2->id, $now + (9 * DAYSECS), $timeread); + $n2 = $this->create_notification($user2->id, $user1->id, $now + (8 * DAYSECS)); + $n3 = $this->create_notification($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Delete one of the messages. + \core_message\api::delete_message($user1->id, $im2); + + // Create course2. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id); + + // Create course groups with group messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user2->id)); + + // Get conversations. + $iconversation1id = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + $conversation2 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group2a->id, + $coursecontext2->id + ); + + // Make favourite some conversations. + \core_message\api::set_favourite_conversation($iconversation1id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user2->id); + + // Send some messages to the conversation. + $gm1 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1.1', $now + 1); + $gm2 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1.2', $now + 2); + $gm3 = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 1.3', $now + 3); + $gm4 = testhelper::send_fake_message_to_conversation($user1, $conversation2->id, 'Message 2.1', $now + 4); + $gm5 = testhelper::send_fake_message_to_conversation($user2, $conversation2->id, 'Message 2.2', $now + 5); + + $dbgm1 = $DB->get_record('messages', ['id' => $gm1]); + $dbgm2 = $DB->get_record('messages', ['id' => $gm2]); + $dbgm3 = $DB->get_record('messages', ['id' => $gm3]); + $dbgm4 = $DB->get_record('messages', ['id' => $gm4]); + $dbgm5 = $DB->get_record('messages', ['id' => $gm5]); + + // Mark as read one of the conversation messages. + \core_message\api::mark_message_as_read($user1->id, $dbgm3, $now + 5); + + // There should be 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be 8 messages. + $this->assertEquals(8, $DB->count_records('messages')); + + // There should be 4 user actions - 3 for reading the message, 1 for deleting. + $this->assertEquals(4, $DB->count_records('message_user_actions')); + + // There should be 4 conversations - 2 individual + 2 group. + $this->assertEquals(4, $DB->count_records('message_conversations')); + + // There should be 9 conversation members - (2 + 2) individual + (3 + 2) group. + $this->assertEquals(9, $DB->count_records('message_conversation_members')); + + // There should be 5 notifications - 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 3 favourite conversations. + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete conversations for all users in course1. + provider::delete_conversations_for_all_users($coursecontext1, $component, $itemtype); + + // There should be still 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be still 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be still 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be still 5 notifications - 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 5 messages - 3 individual - 2 group (course2). + $this->assertEquals(5, $DB->count_records('messages')); + $messages = array_keys($DB->get_records('messages')); + $this->assertContains($im1, $messages); + $this->assertContains($im2, $messages); + $this->assertContains($im3, $messages); + $this->assertContains($gm4, $messages); + $this->assertContains($gm5, $messages); + + // There should be 3 user actions - 2 for reading the message, 1 for deleting. + $this->assertEquals(3, $DB->count_records('message_user_actions')); + $useractions = $DB->get_records('message_user_actions'); + $useractions = array_map(function($action) { + return $action->messageid; + }, $useractions); + $this->assertNotContains($gm3, $useractions); + + // There should be 3 conversations - 2 individual + 1 group (course2). + $this->assertEquals(3, $DB->count_records('message_conversations')); + $conversations = $DB->get_records('message_conversations'); + $this->assertArrayNotHasKey($conversation1->id, $conversations); + + // There should be 6 conversation members - (2 + 2) individual + 2 group. + $this->assertEquals(6, $DB->count_records('message_conversation_members')); + + // There should be 1 favourite conversation - the individual one. + $this->assertEquals(1, $DB->count_records('favourite')); + } + + /** + * Test for provider::delete_conversations_for_all_users() in the system context. + */ + public function test_delete_conversations_for_all_users_systemcontext() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $now = time(); + $timeread = $now - DAYSECS; + $systemcontext = \context_system::instance(); + $component = 'core_group'; + $itemtype = 'groups'; + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + $user5 = $this->getDataGenerator()->create_user(); + + // Create contacts. + \core_message\api::add_contact($user1->id, $user2->id); + \core_message\api::add_contact($user2->id, $user3->id); + + // Create contact requests. + \core_message\api::create_contact_request($user1->id, $user3->id); + \core_message\api::create_contact_request($user2->id, $user4->id); + + // Block a user. + \core_message\api::block_user($user1->id, $user3->id); + \core_message\api::block_user($user3->id, $user4->id); + + // Create individual messages. + $im1 = $this->create_message($user1->id, $user2->id, $now + (9 * DAYSECS), true); + $im2 = $this->create_message($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $im3 = $this->create_message($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Create notifications. + $n1 = $this->create_notification($user1->id, $user2->id, $now + (9 * DAYSECS), $timeread); + $n2 = $this->create_notification($user2->id, $user1->id, $now + (8 * DAYSECS)); + $n3 = $this->create_notification($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Delete one of the messages. + \core_message\api::delete_message($user1->id, $im2); + + // Create course2. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id); + + // Create course groups with group messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user2->id)); + + // Get conversations. + $iconversation1id = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + $conversation2 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group2a->id, + $coursecontext2->id + ); + + // Make favourite some conversations. + \core_message\api::set_favourite_conversation($iconversation1id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user2->id); + + // Send some messages to the conversation. + $gm1 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1.1', $now + 1); + $gm2 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1.2', $now + 2); + $gm3 = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 1.3', $now + 3); + $gm4 = testhelper::send_fake_message_to_conversation($user1, $conversation2->id, 'Message 2.1', $now + 4); + $gm5 = testhelper::send_fake_message_to_conversation($user2, $conversation2->id, 'Message 2.2', $now + 5); + + $dbgm3 = $DB->get_record('messages', ['id' => $gm3]); + + // Mark as read one of the conversation messages. + \core_message\api::mark_message_as_read($user1->id, $dbgm3, $now + 5); + + // There should be 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be 8 messages. + $this->assertEquals(8, $DB->count_records('messages')); + + // There should be 4 user actions - 3 for reading the message, 1 for deleting. + $this->assertEquals(4, $DB->count_records('message_user_actions')); + + // There should be 4 conversations - 2 individual + 2 group. + $this->assertEquals(4, $DB->count_records('message_conversations')); + + // There should be 9 conversation members - (2 + 2) individual + (3 + 2) group. + $this->assertEquals(9, $DB->count_records('message_conversation_members')); + + // There should be 5 notifications - 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 3 favourite conversations. + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete group conversations for all users in system context. + provider::delete_conversations_for_all_users($systemcontext, $component, $itemtype); + + // No conversations should be removed, because they are in the course context. + $this->assertEquals(2, $DB->count_records('message_contacts')); + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + $this->assertEquals(8, $DB->count_records('messages')); + $this->assertEquals(4, $DB->count_records('message_user_actions')); + $this->assertEquals(4, $DB->count_records('message_conversations')); + $this->assertEquals(9, $DB->count_records('message_conversation_members')); + $this->assertEquals(5, $DB->count_records('notifications')); + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete individual conversations for all users in system context. + provider::delete_conversations_for_all_users($systemcontext, '', ''); + + // No conversations should be removed, because they've been moved to user context. + $this->assertEquals(2, $DB->count_records('message_contacts')); + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + $this->assertEquals(8, $DB->count_records('messages')); + $this->assertEquals(4, $DB->count_records('message_user_actions')); + $this->assertEquals(4, $DB->count_records('message_conversations')); + $this->assertEquals(9, $DB->count_records('message_conversation_members')); + $this->assertEquals(5, $DB->count_records('notifications')); + $this->assertEquals(3, $DB->count_records('favourite')); + } + + /** + * Test for provider::delete_conversations_for_all_users() in the user context. + */ + public function test_delete_conversations_for_all_users_usercontext() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $now = time(); + $timeread = $now - DAYSECS; + $component = 'core_group'; + $itemtype = 'groups'; + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + $user5 = $this->getDataGenerator()->create_user(); + $user1context = \context_user::instance($user1->id); + + // Create contacts. + \core_message\api::add_contact($user1->id, $user2->id); + \core_message\api::add_contact($user2->id, $user3->id); + + // Create contact requests. + \core_message\api::create_contact_request($user1->id, $user3->id); + \core_message\api::create_contact_request($user2->id, $user4->id); + + // Block a user. + \core_message\api::block_user($user1->id, $user3->id); + \core_message\api::block_user($user3->id, $user4->id); + + // Create individual messages. + $im1 = $this->create_message($user1->id, $user2->id, $now + (9 * DAYSECS), true); + $im2 = $this->create_message($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $im3 = $this->create_message($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Create notifications. + $n1 = $this->create_notification($user1->id, $user2->id, $now + (9 * DAYSECS), $timeread); + $n2 = $this->create_notification($user2->id, $user1->id, $now + (8 * DAYSECS)); + $n3 = $this->create_notification($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Delete one of the messages. + \core_message\api::delete_message($user1->id, $im2); + + // Create course2. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id); + + // Create course groups with group messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group2a->id, 'userid' => $user2->id)); + + // Get conversation. + $iconversation1id = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); + $iconversation2id = \core_message\api::get_conversation_between_users([$user2->id, $user3->id]); + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + $conversation2 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group2a->id, + $coursecontext2->id + ); + + // Make favourite some conversations. + \core_message\api::set_favourite_conversation($iconversation1id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user2->id); + + // Send some messages to the conversation. + $gm1 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1.1', $now + 1); + $gm2 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1.2', $now + 2); + $gm3 = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 1.3', $now + 3); + $gm4 = testhelper::send_fake_message_to_conversation($user1, $conversation2->id, 'Message 2.1', $now + 4); + $gm5 = testhelper::send_fake_message_to_conversation($user2, $conversation2->id, 'Message 2.2', $now + 5); + + $dbgm3 = $DB->get_record('messages', ['id' => $gm3]); + + // Mark as read one of the conversation messages. + \core_message\api::mark_message_as_read($user1->id, $dbgm3, $now + 5); + + // There should be 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be 8 messages - 3 individual + 5 group. + $this->assertEquals(8, $DB->count_records('messages')); + + // There should be 4 user actions - 3 for reading the message, 1 for deleting. + $this->assertEquals(4, $DB->count_records('message_user_actions')); + + // There should be 4 conversations - 2 individual + 2 group. + $this->assertEquals(4, $DB->count_records('message_conversations')); + + // There should be 9 conversation members - (2 + 2) individual + (3 + 2) group. + $this->assertEquals(9, $DB->count_records('message_conversation_members')); + + // There should be 5 notifications - 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 3 favourite conversations. + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete group conversations for all users in user context. + provider::delete_conversations_for_all_users($user1context, $component, $itemtype); + + // No conversations should be removed, because they are in the course context. + $this->assertEquals(2, $DB->count_records('message_contacts')); + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + $this->assertEquals(8, $DB->count_records('messages')); + $this->assertEquals(4, $DB->count_records('message_user_actions')); + $this->assertEquals(4, $DB->count_records('message_conversations')); + $this->assertEquals(9, $DB->count_records('message_conversation_members')); + $this->assertEquals(5, $DB->count_records('notifications')); + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete individual conversations for all users in user context. + provider::delete_conversations_for_all_users($user1context, '', ''); + + // No conversations should be removed, because they are in the course context. + $this->assertEquals(2, $DB->count_records('message_contacts')); + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + $this->assertEquals(8, $DB->count_records('messages')); + $this->assertEquals(4, $DB->count_records('message_user_actions')); + $this->assertEquals(4, $DB->count_records('message_conversations')); + $this->assertEquals(9, $DB->count_records('message_conversation_members')); + $this->assertEquals(5, $DB->count_records('notifications')); + $this->assertEquals(3, $DB->count_records('favourite')); + } + + /** + * Test for provider::delete_conversations_for_user(). + */ + public function test_delete_conversations_for_user() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $now = time(); + $timeread = $now - DAYSECS; + $systemcontext = \context_system::instance(); + $component = 'core_group'; + $itemtype = 'groups'; + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + $user5 = $this->getDataGenerator()->create_user(); + $user1context = \context_user::instance($user1->id); + + // Create contacts. + \core_message\api::add_contact($user1->id, $user2->id); + \core_message\api::add_contact($user2->id, $user3->id); + + // Create contact requests. + \core_message\api::create_contact_request($user1->id, $user3->id); + \core_message\api::create_contact_request($user2->id, $user4->id); + + // Block a user. + \core_message\api::block_user($user1->id, $user3->id); + \core_message\api::block_user($user3->id, $user4->id); + + // Create private messages. + $pm1 = $this->create_message($user1->id, $user2->id, $now + (9 * DAYSECS), true); + $pm2 = $this->create_message($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $pm3 = $this->create_message($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Create notifications. + $n1 = $this->create_notification($user1->id, $user2->id, $now + (9 * DAYSECS), $timeread); + $n2 = $this->create_notification($user2->id, $user1->id, $now + (8 * DAYSECS)); + $n3 = $this->create_notification($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Delete one of the messages. + \core_message\api::delete_message($user1->id, $pm2); + + // Create course. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + + // Create course groups with group messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + + // Get conversation. + $iconversation1id = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + + // Make favourite some conversations. + \core_message\api::set_favourite_conversation($iconversation1id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user2->id); + + // Send some messages to the conversation. + $gm1 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1', $now + 1); + $gm2 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 2', $now + 2); + $gm3 = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 3', $now + 3); + + $dbm3 = $DB->get_record('messages', ['id' => $gm3]); + + // Mark as read one of the conversation messages. + \core_message\api::mark_message_as_read($user1->id, $dbm3, $now + 5); + + // There should be 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 6 messages. + $this->assertEquals(6, $DB->count_records('messages')); + + // There should be 4 user actions - 3 for reading the message, one for deleting. + $this->assertEquals(4, $DB->count_records('message_user_actions')); + + // There should be 3 conversations - 2 private + 1 group. + $this->assertEquals(3, $DB->count_records('message_conversations')); + + // There should be 7 conversation members - 2 + 2 private conversations + 3 group conversation. + $this->assertEquals(7, $DB->count_records('message_conversation_members')); + $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation1->id]); + $members = array_map(function($member) { + return $member->userid; + }, $members); + $this->assertContains($user1->id, $members); + + // There should be three favourite conversations. + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete group conversations for user1 in course1 and course2. + $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist($user1, 'core_message', + [$coursecontext1->id, $coursecontext2->id]); + provider::delete_conversations_for_user($approvedcontextlist, $component, $itemtype); + + // There should be still 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be still 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be still 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be still 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 4 messages - 3 private + 1 group sent by user2. + $this->assertEquals(4, $DB->count_records('messages')); + $messages = array_keys($DB->get_records('messages')); + $this->assertContains($pm1, $messages); + $this->assertContains($pm2, $messages); + $this->assertContains($pm3, $messages); + $this->assertContains($gm3, $messages); + + // There should be 3 user actions - 2 for reading the message, one for deleting. + $this->assertEquals(3, $DB->count_records('message_user_actions')); + $useractions = $DB->get_records('message_user_actions'); + $useractions = array_map(function($action) { + return $action->messageid; + }, $useractions); + $this->assertNotContains($gm3, $useractions); + + // There should be still 3 conversations - 2 private + 1 group. + $this->assertEquals(3, $DB->count_records('message_conversations')); + + // There should be 6 conversation members - 2 + 2 private conversations + 2 group conversation. + $this->assertEquals(6, $DB->count_records('message_conversation_members')); + $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation1->id]); + $members = array_map(function($member) { + return $member->userid; + }, $members); + $this->assertNotContains($user1->id, $members); + + // There should be 2 favourite conversations - 2 group. + $this->assertEquals(2, $DB->count_records('favourite')); + $favourites = $DB->get_records('favourite'); + foreach ($favourites as $favourite) { + if ($favourite->userid == $user1->id) { + $this->assertEquals($iconversation1id, $favourite->itemid); + } else if ($favourite->userid == $user2->id) { + $this->assertEquals($conversation1->id, $favourite->itemid); + } + } + } + + + /** + * Test for provider::delete_conversations_for_users(). + */ + public function test_delete_conversations_for_users() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $now = time(); + $timeread = $now - DAYSECS; + $systemcontext = \context_system::instance(); + $component = 'core_group'; + $itemtype = 'groups'; + + // Create users to test with. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + $user5 = $this->getDataGenerator()->create_user(); + $user1context = \context_user::instance($user1->id); + + // Create contacts. + \core_message\api::add_contact($user1->id, $user2->id); + \core_message\api::add_contact($user2->id, $user3->id); + + // Create contact requests. + \core_message\api::create_contact_request($user1->id, $user3->id); + \core_message\api::create_contact_request($user2->id, $user4->id); + + // Block a user. + \core_message\api::block_user($user1->id, $user3->id); + \core_message\api::block_user($user3->id, $user4->id); + + // Create private messages. + $pm1 = $this->create_message($user1->id, $user2->id, $now + (9 * DAYSECS), true); + $pm2 = $this->create_message($user2->id, $user1->id, $now + (8 * DAYSECS), true); + $pm3 = $this->create_message($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Create notifications. + $n1 = $this->create_notification($user1->id, $user2->id, $now + (9 * DAYSECS), $timeread); + $n2 = $this->create_notification($user2->id, $user1->id, $now + (8 * DAYSECS)); + $n3 = $this->create_notification($user2->id, $user3->id, $now + (7 * DAYSECS)); + + // Delete one of the messages. + \core_message\api::delete_message($user1->id, $pm2); + + // Create course. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + + // Enrol users to courses. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id); + $this->getDataGenerator()->enrol_user($user4->id, $course1->id); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id); + + // Create course groups with group messaging enabled. + $group1a = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1)); + $group2a = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'enablemessaging' => 1)); + + // Add users to groups. + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user2->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user3->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $group1a->id, 'userid' => $user4->id)); + + // Get conversation. + $iconversation1id = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]); + $conversation1 = \core_message\api::get_conversation_by_area( + $component, + $itemtype, + $group1a->id, + $coursecontext1->id + ); + + // Make favourite some conversations. + \core_message\api::set_favourite_conversation($iconversation1id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user1->id); + \core_message\api::set_favourite_conversation($conversation1->id, $user3->id); + + // Send some messages to the conversation. + $gm1 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1', $now + 1); + $gm2 = testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 2', $now + 2); + $gm3 = testhelper::send_fake_message_to_conversation($user3, $conversation1->id, 'Message 3', $now + 3); + + $dbm3 = $DB->get_record('messages', ['id' => $gm3]); + + // Mark as read one of the conversation messages. + \core_message\api::mark_message_as_read($user1->id, $dbm3, $now + 5); + + // There should be 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 6 messages. + $this->assertEquals(6, $DB->count_records('messages')); + + // There should be 4 user actions - 3 for reading the message, one for deleting. + $this->assertEquals(4, $DB->count_records('message_user_actions')); + + // There should be 3 conversations - 2 private + 2 group. + $this->assertEquals(4, $DB->count_records('message_conversations')); + + // There should be 8 conversation members - (2 + 2) private + 4 group. + $this->assertEquals(8, $DB->count_records('message_conversation_members')); + $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation1->id]); + $members = array_map(function($member) { + return $member->userid; + }, $members); + $this->assertContains($user1->id, $members); + $this->assertContains($user4->id, $members); + + // There should be 3 favourite conversations. + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete group conversations for user1 and user2 in course2 context. + $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext2, 'core_message', + [$user1->id, $user2->id]); + provider::delete_conversations_for_users($approveduserlist, $component, $itemtype); + + // There should be exactly the same content, because $user1 and $user2 don't belong to any group in course2). + $this->assertEquals(2, $DB->count_records('message_contacts')); + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + $this->assertEquals(5, $DB->count_records('notifications')); + $this->assertEquals(6, $DB->count_records('messages')); + $this->assertEquals(4, $DB->count_records('message_user_actions')); + $this->assertEquals(4, $DB->count_records('message_conversations')); + $this->assertEquals(8, $DB->count_records('message_conversation_members')); + $this->assertEquals(3, $DB->count_records('favourite')); + + // Delete group conversations for user4 in course1 context. + $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'core_message', + [$user4->id]); + provider::delete_conversations_for_users($approveduserlist, $component, $itemtype); + + // There should be the same content except for the members (to remove user4 from the group1 in course1). + $this->assertEquals(2, $DB->count_records('message_contacts')); + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + $this->assertEquals(5, $DB->count_records('notifications')); + $this->assertEquals(6, $DB->count_records('messages')); + $this->assertEquals(4, $DB->count_records('message_user_actions')); + $this->assertEquals(4, $DB->count_records('message_conversations')); + $this->assertEquals(3, $DB->count_records('favourite')); + // There should be 7 conversation members - (2 + 2) private + 3 group. + $this->assertEquals(7, $DB->count_records('message_conversation_members')); + + // Delete group conversations for user1 and user2 in course1 context. + $approveduserlist = new \core_privacy\local\request\approved_userlist($coursecontext1, 'core_message', + [$user1->id, $user2->id]); + provider::delete_conversations_for_users($approveduserlist, $component, $itemtype); + + // There should be still 2 contacts. + $this->assertEquals(2, $DB->count_records('message_contacts')); + + // There should be still 2 contact requests. + $this->assertEquals(2, $DB->count_records('message_contact_requests')); + + // There should be still 2 blocked users. + $this->assertEquals(2, $DB->count_records('message_users_blocked')); + + // There should be still 3 notifications + 2 for the contact request. + $this->assertEquals(5, $DB->count_records('notifications')); + + // There should be 4 messages - 3 private + 1 group sent by user3. + $this->assertEquals(4, $DB->count_records('messages')); + $messages = array_keys($DB->get_records('messages')); + $this->assertContains($pm1, $messages); + $this->assertContains($pm2, $messages); + $this->assertContains($pm3, $messages); + $this->assertContains($gm3, $messages); + + // There should be 3 user actions - 2 for reading the message, one for deleting. + $this->assertEquals(3, $DB->count_records('message_user_actions')); + $useractions = $DB->get_records('message_user_actions'); + $useractions = array_map(function($action) { + return $action->messageid; + }, $useractions); + $this->assertNotContains($gm3, $useractions); + + // There should be still 4 conversations - 2 private + 2 group. + $this->assertEquals(4, $DB->count_records('message_conversations')); + + // There should be 5 conversation members - (2 + 2) private + 1 group. + $this->assertEquals(5, $DB->count_records('message_conversation_members')); + $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation1->id]); + $members = array_map(function($member) { + return $member->userid; + }, $members); + $this->assertNotContains($user1->id, $members); + $this->assertNotContains($user2->id, $members); + + // There should be 2 favourite conversations - user1 individual + user3 group. + $this->assertEquals(2, $DB->count_records('favourite')); + $favourites = $DB->get_records('favourite'); + foreach ($favourites as $favourite) { + if ($favourite->userid == $user1->id) { + $this->assertEquals($iconversation1id, $favourite->itemid); + } else if ($favourite->userid == $user3->id) { + $this->assertEquals($conversation1->id, $favourite->itemid); + } + } + } + /** * Creates a message to be used for testing. * diff --git a/version.php b/version.php index c6eaf3b05464b..82048c50bddd5 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018111900.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018112000.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.