diff --git a/mod/forum/classes/privacy/provider.php b/mod/forum/classes/privacy/provider.php index c26c307ae92..c5a59ae97a0 100644 --- a/mod/forum/classes/privacy/provider.php +++ b/mod/forum/classes/privacy/provider.php @@ -24,7 +24,9 @@ namespace mod_forum\privacy; +use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\approved_userlist; use \core_privacy\local\request\deletion_criteria; use \core_privacy\local\request\writer; use \core_privacy\local\request\helper as request_helper; @@ -46,6 +48,9 @@ class provider implements // This plugin currently implements the original plugin\provider interface. \core_privacy\local\request\plugin\provider, + // This plugin is capable of determining which users have data within it. + \core_privacy\local\request\core_userlist_provider, + // This plugin has some sitewide user preferences to export. \core_privacy\local\request\user_preference_provider { @@ -144,7 +149,7 @@ class provider implements * In the case of forum, that is any forum where the user has made any post, rated any content, or has any preferences. * * @param int $userid The user to search. - * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist { $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid); @@ -192,6 +197,98 @@ class provider implements return $contextlist; } + /** + * Get the list of users within a specific context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if (!is_a($context, \context_module::class)) { + return; + } + + $params = [ + 'instanceid' => $context->instanceid, + 'modulename' => 'forum', + ]; + + // Discussion authors. + $sql = "SELECT d.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_discussions} d ON d.forum = f.id + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + + // Forum authors. + $sql = "SELECT p.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_discussions} d ON d.forum = f.id + JOIN {forum_posts} p ON d.id = p.discussion + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + + // Forum post ratings. + $sql = "SELECT p.id + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_discussions} d ON d.forum = f.id + JOIN {forum_posts} p ON d.id = p.discussion + WHERE cm.id = :instanceid"; + \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_forum', 'post', $sql, $params); + + // Forum Digest settings. + $sql = "SELECT dig.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_digests} dig ON dig.forum = f.id + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + + // Forum Subscriptions. + $sql = "SELECT sub.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_subscriptions} sub ON sub.forum = f.id + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + + // Discussion subscriptions. + $sql = "SELECT dsub.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + + // Read Posts. + $sql = "SELECT hasread.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_read} hasread ON hasread.forumid = f.id + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + + // Tracking Preferences. + $sql = "SELECT pref.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {forum} f ON f.id = cm.instance + JOIN {forum_track_prefs} pref ON pref.forumid = f.id + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + } + /** * Store all user preferences for the plugin. * @@ -890,4 +987,58 @@ class provider implements $uniquediscussions->close(); } } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); + $forum = $DB->get_record('forum', ['id' => $cm->instance]); + + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); + $params = array_merge(['forumid' => $forum->id], $userinparams); + + $DB->delete_records_select('forum_track_prefs', "forumid = :forumid AND userid {$userinsql}", $params); + $DB->delete_records_select('forum_subscriptions', "forum = :forumid AND userid {$userinsql}", $params); + $DB->delete_records_select('forum_read', "forumid = :forumid AND userid {$userinsql}", $params); + $DB->delete_records_select( + 'forum_queue', + "userid {$userinsql} AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)", + $params + ); + $DB->delete_records_select('forum_discussion_subs', "forum = :forumid AND userid {$userinsql}", $params); + + // Do not delete discussion or forum posts. + // Instead update them to reflect that the content has been deleted. + $postsql = "userid {$userinsql} AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)"; + $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}"; + + // Update the subject. + $DB->set_field_select('forum_posts', 'subject', '', $postsql, $params); + + // Update the subject and its format. + $DB->set_field_select('forum_posts', 'message', '', $postsql, $params); + $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $params); + + // Mark the post as deleted. + $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $params); + + // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts. + // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating + // of any post. + \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post', "IN ($postidsql)", $params); + + // Delete all Tags. + \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts', "IN ($postidsql)", $params); + + // Delete all files from the posts. + $fs = get_file_storage(); + $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $params); + $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $params); + } } diff --git a/mod/forum/tests/privacy_provider_test.php b/mod/forum/tests/privacy_provider_test.php index d9a0add3356..6cfaed08b09 100644 --- a/mod/forum/tests/privacy_provider_test.php +++ b/mod/forum/tests/privacy_provider_test.php @@ -1251,4 +1251,519 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t // Files for the other posts should remain. $this->assertCount(18, $DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams)); } + + /** + * Ensure that user data for specific users is deleted from a specified context. + */ + public function test_delete_data_for_users() { + global $DB; + + $fs = get_file_storage(); + $course = $this->getDataGenerator()->create_course(); + $users = $this->helper_create_users($course, 5); + + $forums = []; + $contexts = []; + for ($i = 0; $i < 2; $i++) { + $forum = $this->getDataGenerator()->create_module('forum', [ + 'course' => $course->id, + 'scale' => 100, + ]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + $forums[$forum->id] = $forum; + $contexts[$forum->id] = $context; + } + + $discussions = []; + $posts = []; + $postsbyforum = []; + foreach ($users as $user) { + $postsbyforum[$user->id] = []; + foreach ($forums as $forum) { + $context = $contexts[$forum->id]; + + // Create a new discussion + post in the forum. + list($discussion, $post) = $this->helper_post_to_forum($forum, $user); + $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]); + $discussions[$discussion->id] = $discussion; + $postsbyforum[$user->id][$context->id] = []; + + // Add a number of replies. + $posts[$post->id] = $post; + $thisforumposts[$post->id] = $post; + $postsbyforum[$user->id][$context->id][$post->id] = $post; + + $reply = $this->helper_reply_to_post($post, $user); + $posts[$reply->id] = $reply; + $postsbyforum[$user->id][$context->id][$reply->id] = $reply; + + $reply = $this->helper_reply_to_post($post, $user); + $posts[$reply->id] = $reply; + $postsbyforum[$user->id][$context->id][$reply->id] = $reply; + + $reply = $this->helper_reply_to_post($reply, $user); + $posts[$reply->id] = $reply; + $postsbyforum[$user->id][$context->id][$reply->id] = $reply; + + // Add a fake inline image to the original post. + $fs->create_file_from_string([ + 'contextid' => $context->id, + 'component' => 'mod_forum', + 'filearea' => 'post', + 'itemid' => $post->id, + 'filepath' => '/', + 'filename' => 'example.jpg', + ], 'image contents (not really)'); + // And a fake attachment. + $fs->create_file_from_string([ + 'contextid' => $context->id, + 'component' => 'mod_forum', + 'filearea' => 'attachment', + 'itemid' => $post->id, + 'filepath' => '/', + 'filename' => 'example.jpg', + ], 'image contents (not really)'); + } + } + + // Mark all posts as read by user1. + $user1 = reset($users); + foreach ($posts as $post) { + $discussion = $discussions[$post->discussion]; + $forum = $forums[$discussion->forum]; + $context = $contexts[$forum->id]; + + // Mark the post as being read by user1. + forum_tp_add_read_record($user1->id, $post->id); + } + + // Rate and tag all posts. + $ratedposts = []; + foreach ($users as $user) { + foreach ($posts as $post) { + $discussion = $discussions[$post->discussion]; + $forum = $forums[$discussion->forum]; + $context = $contexts[$forum->id]; + + // Tag the post. + \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']); + + // Rate the other users content. + if ($post->userid != $user->id) { + $ratedposts[$post->id] = $post; + $rm = new rating_manager(); + $ratingoptions = (object) [ + 'context' => $context, + 'component' => 'mod_forum', + 'ratingarea' => 'post', + 'itemid' => $post->id, + 'scaleid' => $forum->scale, + 'userid' => $user->id, + ]; + + $rating = new \rating($ratingoptions); + $rating->update_rating(75); + } + } + } + + // Delete for one of the forums for the first user. + $firstcontext = reset($contexts); + + $deletedpostids = []; + $otherpostids = []; + foreach ($postsbyforum as $user => $contexts) { + foreach ($contexts as $thiscontextid => $theseposts) { + $thesepostids = array_map(function($post) { + return $post->id; + }, $theseposts); + + if ($user == $user1->id && $thiscontextid == $firstcontext->id) { + // This post is in the deleted context and by the target user. + $deletedpostids = array_merge($deletedpostids, $thesepostids); + } else { + // This post is by another user, or in a non-target context. + $otherpostids = array_merge($otherpostids, $thesepostids); + } + } + } + list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED); + list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED); + + $approveduserlist = new \core_privacy\local\request\approved_userlist($firstcontext, 'mod_forum', [$user1->id]); + provider::delete_data_for_users($approveduserlist); + + // All posts should remain. + $this->assertCount(40, $DB->get_records('forum_posts')); + + // There should be 8 posts belonging to user1. + $this->assertCount(8, $DB->get_records('forum_posts', [ + 'userid' => $user1->id, + ])); + + // Four of those posts should have been marked as deleted. + // That means that the deleted flag is set, and both the subject and message are empty. + $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted" + . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject') + . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message') + , [ + 'userid' => $user1->id, + 'deleted' => 1, + 'subject' => '', + 'message' => '', + ])); + + // Only user1's posts should have been marked this way. + $this->assertCount(4, $DB->get_records('forum_posts', [ + 'deleted' => 1, + ])); + $this->assertCount(4, $DB->get_records_select('forum_posts', + $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [ + 'subject' => '', + ])); + $this->assertCount(4, $DB->get_records_select('forum_posts', + $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [ + 'message' => '', + ])); + + // Only the posts in the first discussion should have been marked this way. + $this->assertCount(4, $DB->get_records_select('forum_posts', + "deleted = :deleted AND id {$postinsql}", + array_merge($postinparams, [ + 'deleted' => 1, + ]) + )); + + // Ratings should have been removed from the affected posts. + $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams)); + + // Ratings should remain on posts in the other context, and posts not belonging to the affected user. + $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams)); + + // Ratings should remain where the user has rated another person's post. + $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id])); + + // Tags for the affected posts should be removed. + $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams)); + + // Tags should remain for the other posts by this user, and all posts by other users. + $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams)); + + // Files for the affected posts should be removed. + // 5 users * 2 forums * 1 file in each forum + // Original total: 10 + // One post with file removed. + $this->assertCount(0, $DB->get_records_select('files', "itemid {$postinsql}", $postinparams)); + + // Files for the other posts should remain. + $this->assertCount(18, + $DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams)); + } + + /** + * Ensure that the discussion author is listed as a user in the context. + */ + public function test_get_users_in_context_post_author() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + list($author, $user) = $this->helper_create_users($course, 2); + + list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // There should only be one user in the list. + $this->assertCount(1, $userlist); + $this->assertEquals([$author->id], $userlist->get_userids()); + } + + /** + * Ensure that all post authors are included as a user in the context. + */ + public function test_get_users_in_context_post_authors() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + list($author, $user, $other) = $this->helper_create_users($course, 3); + + list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); + $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $user); + $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // Two users - author and replier. + $this->assertCount(2, $userlist); + + $expected = [$author->id, $user->id]; + sort($expected); + + $actual = $userlist->get_userids(); + sort($actual); + + $this->assertEquals($expected, $actual); + } + + /** + * Ensure that all post raters are included as a user in the context. + */ + public function test_get_users_in_context_post_ratings() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + list($author, $user, $other) = $this->helper_create_users($course, 3); + + list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); + + // Rate the other users content. + $rm = new rating_manager(); + $ratingoptions = (object) [ + 'context' => $context, + 'component' => 'mod_forum', + 'ratingarea' => 'post', + 'itemid' => $fp1->id, + 'scaleid' => $forum->scale, + 'userid' => $user->id, + ]; + + $rating = new \rating($ratingoptions); + $rating->update_rating(75); + + $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $author); + $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // Two users - author and rater. + $this->assertCount(2, $userlist); + + $expected = [$author->id, $user->id]; + sort($expected); + + $actual = $userlist->get_userids(); + sort($actual); + + $this->assertEquals($expected, $actual); + } + + /** + * Ensure that all users with a digest preference are included as a user in the context. + */ + public function test_get_users_in_context_digest_preference() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $othercm = get_coursemodule_from_instance('forum', $otherforum->id); + $othercontext = \context_module::instance($othercm->id); + + list($user, $otheruser) = $this->helper_create_users($course, 2); + + // Add digest subscriptions. + forum_set_user_maildigest($forum, 0, $user); + forum_set_user_maildigest($otherforum, 0, $otheruser); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // One user - the one with a digest preference. + $this->assertCount(1, $userlist); + + $expected = [$user->id]; + sort($expected); + + $actual = $userlist->get_userids(); + sort($actual); + + $this->assertEquals($expected, $actual); + } + + /** + * Ensure that all users with a forum subscription preference included as a user in the context. + */ + public function test_get_users_in_context_with_subscription() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $othercm = get_coursemodule_from_instance('forum', $otherforum->id); + $othercontext = \context_module::instance($othercm->id); + + list($user, $otheruser) = $this->helper_create_users($course, 2); + + // Subscribe the user to the forum. + \mod_forum\subscriptions::subscribe_user($user->id, $forum); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // One user - the one with a digest preference. + $this->assertCount(1, $userlist); + + $expected = [$user->id]; + sort($expected); + + $actual = $userlist->get_userids(); + sort($actual); + + $this->assertEquals($expected, $actual); + } + + /** + * Ensure that all users with a per-discussion subscription preference included as a user in the context. + */ + public function test_get_users_in_context_with_discussion_subscription() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $othercm = get_coursemodule_from_instance('forum', $otherforum->id); + $othercontext = \context_module::instance($othercm->id); + + list($author, $user, $otheruser) = $this->helper_create_users($course, 3); + + // Post in both of the forums. + list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); + list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author); + + // Subscribe the user to the discussions. + \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $fd1); + \mod_forum\subscriptions::subscribe_user_to_discussion($otheruser->id, $ofd1); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // Two users - the author, and the one who subscribed. + $this->assertCount(2, $userlist); + + $expected = [$author->id, $user->id]; + sort($expected); + + $actual = $userlist->get_userids(); + sort($actual); + + $this->assertEquals($expected, $actual); + } + + /** + * Ensure that all users with read tracking are included as a user in the context. + */ + public function test_get_users_in_context_with_read_post_tracking() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $othercm = get_coursemodule_from_instance('forum', $otherforum->id); + $othercontext = \context_module::instance($othercm->id); + + list($author, $user, $otheruser) = $this->helper_create_users($course, 3); + + // Post in both of the forums. + list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author); + list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author); + + // Add read information for those users. + forum_tp_add_read_record($user->id, $fp1->id); + forum_tp_add_read_record($otheruser->id, $ofp1->id); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // Two user - the author, and the one who has read the post. + $this->assertCount(2, $userlist); + + $expected = [$author->id, $user->id]; + sort($expected); + + $actual = $userlist->get_userids(); + sort($actual); + + $this->assertEquals($expected, $actual); + } + + /** + * Ensure that all users with tracking preferences are included as a user in the context. + */ + public function test_get_users_in_context_with_tracking_preferences() { + global $DB; + $component = 'mod_forum'; + + $course = $this->getDataGenerator()->create_course(); + + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = \context_module::instance($cm->id); + + $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $othercm = get_coursemodule_from_instance('forum', $otherforum->id); + $othercontext = \context_module::instance($othercm->id); + + list($author, $user, $otheruser) = $this->helper_create_users($course, 3); + + // Forum tracking is opt-out. + // Stop tracking the read posts. + forum_tp_stop_tracking($forum->id, $user->id); + forum_tp_stop_tracking($otherforum->id, $otheruser->id); + + $userlist = new \core_privacy\local\request\userlist($context, $component); + \mod_forum\privacy\provider::get_users_in_context($userlist); + + // One user - the one who is tracking that forum. + $this->assertCount(1, $userlist); + + $expected = [$user->id]; + sort($expected); + + $actual = $userlist->get_userids(); + sort($actual); + + $this->assertEquals($expected, $actual); + } }