Rev 1 | Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Tests for the forum implementation of the Privacy Provider API.** @package mod_forum* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/namespace mod_forum\privacy;defined('MOODLE_INTERNAL') || die();global $CFG;require_once(__DIR__ . '/../generator_trait.php');require_once($CFG->dirroot . '/rating/lib.php');use mod_forum\privacy\provider;/*** Tests for the forum implementation of the Privacy Provider API.** @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class provider_test extends \core_privacy\tests\provider_testcase {// Include the privacy subcontext_info trait.// This includes the subcontext builders.use \mod_forum\privacy\subcontext_info;// Include the mod_forum test helpers.// This includes functions to create forums, users, discussions, and posts.use \mod_forum_tests_generator_trait;// Include the privacy helper trait for the ratings API.use \core_rating\phpunit\privacy_helper;// Include the privacy helper trait for the tag API.use \core_tag\tests\privacy_helper;/*** Test setUp.*/public function setUp(): void {$this->resetAfterTest(true);}/*** Helper to assert that the forum data is correct.** @param object $expected The expected data in the forum.* @param object $actual The actual data in the forum.*/protected function assert_forum_data($expected, $actual) {// Exact matches.$this->assertEquals(format_string($expected->name, true), $actual->name);}/*** Helper to assert that the discussion data is correct.** @param object $expected The expected data in the discussion.* @param object $actual The actual data in the discussion.*/protected function assert_discussion_data($expected, $actual) {// Exact matches.$this->assertEquals(format_string($expected->name, true), $actual->name);$this->assertEquals(\core_privacy\local\request\transform::yesno($expected->pinned),$actual->pinned);$this->assertEquals(\core_privacy\local\request\transform::datetime($expected->timemodified),$actual->timemodified);$this->assertEquals(\core_privacy\local\request\transform::datetime($expected->usermodified),$actual->usermodified);}/*** Helper to assert that the post data is correct.** @param object $expected The expected data in the post.* @param object $actual The actual data in the post.* @param \core_privacy\local\request\writer $writer The writer used*/protected function assert_post_data($expected, $actual, $writer) {// Exact matches.$this->assertEquals(format_string($expected->subject, true), $actual->subject);// The message should have been passed through the rewriter.// Note: The testable rewrite_pluginfile_urls function in the ignores all items except the text.$this->assertEquals($writer->rewrite_pluginfile_urls([], '', '', '', $expected->message),$actual->message);$this->assertEquals(\core_privacy\local\request\transform::datetime($expected->created),$actual->created);$this->assertEquals(\core_privacy\local\request\transform::datetime($expected->modified),$actual->modified);}/*** Test that a user who is enrolled in a course, but who has never* posted and has no other metadata stored will not have any link to* that context.*/public function test_user_has_never_posted(): void {// Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);list($user, $otheruser) = $this->helper_create_users($course, 2);list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);$cm = get_coursemodule_from_instance('forum', $forum->id);$context = \context_module::instance($cm->id);// Test that no contexts were retrieved.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$contexts = $contextlist->get_contextids();$this->assertCount(0, $contexts);// Attempting to export data for this context should return nothing either.$this->export_context_data_for_user($user->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);// The provider should always export data for any context explicitly asked of it, but there should be no// metadata, files, or discussions.$this->assertEmpty($writer->get_data([get_string('discussions', 'mod_forum')]));$this->assertEmpty($writer->get_all_metadata([]));$this->assertEmpty($writer->get_files([]));}/*** Test that a user who is enrolled in a course, and who has never* posted and has subscribed to the forum will have relevant* information returned.*/public function test_user_has_never_posted_subscribed_to_forum(): void {global $DB;// Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);list($user, $otheruser) = $this->helper_create_users($course, 2);list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);$cm = get_coursemodule_from_instance('forum', $forum->id);$context = \context_module::instance($cm->id);// Subscribe the user to the forum.\mod_forum\subscriptions::subscribe_user($user->id, $forum);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->export_context_data_for_user($user->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());$subcontext = $this->get_subcontext($forum);// There should be one item of metadata.$this->assertCount(1, $writer->get_all_metadata($subcontext));// It should be the subscriptionpreference whose value is 1.$this->assertEquals(1, $writer->get_metadata($subcontext, 'subscriptionpreference'));// There should be data about the forum itself.$this->assertNotEmpty($writer->get_data($subcontext));// Delete the data now.// Only the post by the user under test will be removed.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$context->id]);$this->assertCount(1, $DB->get_records('forum_subscriptions', ['userid' => $user->id]));provider::delete_data_for_user($approvedcontextlist);$this->assertCount(0, $DB->get_records('forum_subscriptions', ['userid' => $user->id]));}/*** Test that a user who is enrolled in a course, and who has never* posted and has subscribed to the discussion will have relevant* information returned.*/public function test_user_has_never_posted_subscribed_to_discussion(): void {global $DB;// Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);list($user, $otheruser) = $this->helper_create_users($course, 2);// Post twice - only the second discussion should be included.$this->helper_post_to_forum($forum, $otheruser);list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);$cm = get_coursemodule_from_instance('forum', $forum->id);$context = \context_module::instance($cm->id);// Subscribe the user to the discussion.\mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->export_context_data_for_user($user->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());// There should be nothing in the forum. The user is not subscribed there.$forumsubcontext = $this->get_subcontext($forum);$this->assertCount(0, $writer->get_all_metadata($forumsubcontext));$this->assert_forum_data($forum, $writer->get_data($forumsubcontext));// There should be metadata in the discussion.$discsubcontext = $this->get_subcontext($forum, $discussion);$this->assertCount(1, $writer->get_all_metadata($discsubcontext));// It should be the subscriptionpreference whose value is an Integer.// (It's a timestamp, but it doesn't matter).$metadata = $writer->get_metadata($discsubcontext, 'subscriptionpreference');$this->assertGreaterThan(1, $metadata);// For context we output the discussion content.$data = $writer->get_data($discsubcontext);$this->assertInstanceOf('stdClass', $data);$this->assert_discussion_data($discussion, $data);// Post content is not exported unless the user participated.$postsubcontext = $this->get_subcontext($forum, $discussion, $post);$this->assertCount(0, $writer->get_data($postsubcontext));// Delete the data now.// Only the post by the user under test will be removed.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$context->id]);$this->assertCount(1, $DB->get_records('forum_discussion_subs', ['userid' => $user->id]));provider::delete_data_for_user($approvedcontextlist);$this->assertCount(0, $DB->get_records('forum_discussion_subs', ['userid' => $user->id]));}/*** Test that a user who has posted their own discussion will have all* content returned.*/public function test_user_has_posted_own_discussion(): void {$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);list($user, $otheruser) = $this->helper_create_users($course, 2);// Post twice - only the second discussion should be included.list($discussion, $post) = $this->helper_post_to_forum($forum, $user);list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);$cm = get_coursemodule_from_instance('forum', $forum->id);$context = \context_module::instance($cm->id);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->setUser($user);$this->export_context_data_for_user($user->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());// The other discussion should not have been returned as we did not post in it.$this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));$this->assert_discussion_data($discussion, $writer->get_data($this->get_subcontext($forum, $discussion)));$this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);}/*** Test that a user who has posted a reply to another users discussion will have all content returned, and* appropriate content removed.*/public function test_user_has_posted_reply(): void {global $DB;// Create several courses and forums. We only insert data into the final one.$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$course = $this->getDataGenerator()->create_course();$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);list($user, $otheruser) = $this->helper_create_users($course, 2);// Post twice - only the second discussion should be included.list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);$cm = get_coursemodule_from_instance('forum', $forum->id);$context = \context_module::instance($cm->id);// Post a reply to the other person's post.$reply = $this->helper_reply_to_post($post, $user);// Testing as user $user.$this->setUser($user);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->export_context_data_for_user($user->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());// Refresh the discussions.$discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);$otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);// The other discussion should not have been returned as we did not post in it.$this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));// Our discussion should have been returned as we did post in it.$data = $writer->get_data($this->get_subcontext($forum, $discussion));$this->assertNotEmpty($data);$this->assert_discussion_data($discussion, $data);// The reply will be included.$this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);// Delete the data now.// Only the post by the user under test will be removed.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$context->id]);provider::delete_data_for_user($approvedcontextlist);$reply = $DB->get_record('forum_posts', ['id' => $reply->id]);$this->assertEmpty($reply->subject);$this->assertEmpty($reply->message);$this->assertEquals(1, $reply->deleted);$post = $DB->get_record('forum_posts', ['id' => $post->id]);$this->assertNotEmpty($post->subject);$this->assertNotEmpty($post->message);$this->assertEquals(0, $post->deleted);}/*** Test private reply in a range of scenarios.*/public function test_user_private_reply(): void {global $DB;$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);[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');[$discussion, $post] = $this->helper_post_to_forum($forum, $student);$reply = $this->helper_reply_to_post($post, $teacher, ['privatereplyto' => $student->id,]);// Testing as user $student.$this->setUser($student);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($student->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->export_context_data_for_user($student->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());// The initial post and reply will be included.$this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);$this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);// Testing as user $teacher.\core_privacy\local\request\writer::reset();$this->setUser($teacher);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($teacher->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->export_context_data_for_user($teacher->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());// The reply will be included.$this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);$this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);// Testing as user $otherteacher.// The user was not involved in any of the conversation.\core_privacy\local\request\writer::reset();$this->setUser($otherteacher);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($otherteacher->id, 'mod_forum');$this->assertCount(0, $contextlist);// Export all of the data for the context.$this->export_context_data_for_user($otherteacher->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);// The user has none of the discussion.$this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));// Testing as user $otherstudent.// The user was not involved in any of the conversation.\core_privacy\local\request\writer::reset();$this->setUser($otherstudent);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($otherstudent->id, 'mod_forum');$this->assertCount(0, $contextlist);// Export all of the data for the context.$this->export_context_data_for_user($otherstudent->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);// The user has none of the discussion.$this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));}/*** Test that the rating of another users content will have only the* rater's information returned.*/public function test_user_has_rated_others(): void {global $DB;$course = $this->getDataGenerator()->create_course();$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id,'scale' => 100,]);list($user, $otheruser) = $this->helper_create_users($course, 2);list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);$cm = get_coursemodule_from_instance('forum', $forum->id);$context = \context_module::instance($cm->id);// Rate the other users content.$rm = new \rating_manager();$ratingoptions = new \stdClass;$ratingoptions->context = $context;$ratingoptions->component = 'mod_forum';$ratingoptions->ratingarea = 'post';$ratingoptions->itemid = $post->id;$ratingoptions->scaleid = $forum->scale;$ratingoptions->userid = $user->id;$rating = new \rating($ratingoptions);$rating->update_rating(75);// Run as the user under test.$this->setUser($user);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->export_context_data_for_user($user->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());// The discussion should not have been returned as we did not post in it.$this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));$this->assert_all_own_ratings_on_context($user->id,$context,$this->get_subcontext($forum, $discussion, $post),'mod_forum','post',$post->id);// The original post will not be included.$this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);// Delete the data of the user who rated the other user.// The rating should not be deleted as it the rating is considered grading data.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$context->id]);provider::delete_data_for_user($approvedcontextlist);// Ratings should remain as they are of another user's content.$this->assertCount(1, $DB->get_records('rating', ['itemid' => $post->id]));}/*** Test that ratings of a users own content will all be returned.*/public function test_user_has_been_rated(): void {global $DB;$course = $this->getDataGenerator()->create_course();$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id,'scale' => 100,]);list($user, $otheruser, $anotheruser) = $this->helper_create_users($course, 3);list($discussion, $post) = $this->helper_post_to_forum($forum, $user);$cm = get_coursemodule_from_instance('forum', $forum->id);$context = \context_module::instance($cm->id);// Other users rate my content.$rm = new \rating_manager();$ratingoptions = new \stdClass;$ratingoptions->context = $context;$ratingoptions->component = 'mod_forum';$ratingoptions->ratingarea = 'post';$ratingoptions->itemid = $post->id;$ratingoptions->scaleid = $forum->scale;$ratingoptions->userid = $otheruser->id;$rating = new \rating($ratingoptions);$rating->update_rating(75);$ratingoptions->userid = $anotheruser->id;$rating = new \rating($ratingoptions);$rating->update_rating(75);// Run as the user under test.$this->setUser($user);// Retrieve all contexts - only this context should be returned.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($context, $contextlist->current());// Export all of the data for the context.$this->export_context_data_for_user($user->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assertTrue($writer->has_any_data());$this->assert_all_ratings_on_context($context,$this->get_subcontext($forum, $discussion, $post),'mod_forum','post',$post->id);// Delete the data of the user who was rated.// The rating should now be deleted.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$context->id]);provider::delete_data_for_user($approvedcontextlist);// Ratings should remain as they are of another user's content.$this->assertCount(0, $DB->get_records('rating', ['itemid' => $post->id]));}/*** Test that per-user daily digest settings are included correctly.*/public function test_user_forum_digest(): void {global $DB;$course = $this->getDataGenerator()->create_course();$forum0 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm0 = get_coursemodule_from_instance('forum', $forum0->id);$context0 = \context_module::instance($cm0->id);$forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm1 = get_coursemodule_from_instance('forum', $forum1->id);$context1 = \context_module::instance($cm1->id);$forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm2 = get_coursemodule_from_instance('forum', $forum2->id);$context2 = \context_module::instance($cm2->id);$forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm3 = get_coursemodule_from_instance('forum', $forum3->id);$context3 = \context_module::instance($cm3->id);list($user) = $this->helper_create_users($course, 1);// Set a digest value for each forum.forum_set_user_maildigest($forum0, 0, $user);forum_set_user_maildigest($forum1, 1, $user);forum_set_user_maildigest($forum2, 2, $user);// Run as the user under test.$this->setUser($user);// Retrieve all contexts - three contexts should be returned - the fourth should not be included.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(3, $contextlist);$contextids = [$context0->id,$context1->id,$context2->id,];sort($contextids);$contextlistids = $contextlist->get_contextids();sort($contextlistids);$this->assertEquals($contextids, $contextlistids);// Check export data for each context.$this->export_context_data_for_user($user->id, $context0, 'mod_forum');$this->assertEquals(0, \core_privacy\local\request\writer::with_context($context0)->get_metadata([], 'digestpreference'));$this->export_context_data_for_user($user->id, $context1, 'mod_forum');$this->assertEquals(1, \core_privacy\local\request\writer::with_context($context1)->get_metadata([], 'digestpreference'));$this->export_context_data_for_user($user->id, $context2, 'mod_forum');$this->assertEquals(2, \core_privacy\local\request\writer::with_context($context2)->get_metadata([], 'digestpreference'));// Delete the data for one of the users in one of the forums.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$context1->id]);$this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id]));$this->assertEquals(1, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id]));$this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id]));provider::delete_data_for_user($approvedcontextlist);$this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id]));$this->assertFalse($DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id]));$this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id]));}/*** Test that the per-user, per-forum user tracking data is exported.*/public function test_user_tracking_data(): void {global $DB;$course = $this->getDataGenerator()->create_course();$forumoff = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cmoff = get_coursemodule_from_instance('forum', $forumoff->id);$contextoff = \context_module::instance($cmoff->id);$forumon = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cmon = get_coursemodule_from_instance('forum', $forumon->id);$contexton = \context_module::instance($cmon->id);list($user) = $this->helper_create_users($course, 1);// Set user tracking data.forum_tp_stop_tracking($forumoff->id, $user->id);forum_tp_start_tracking($forumon->id, $user->id);// Run as the user under test.$this->setUser($user);// Retrieve all contexts - only the forum tracking reads should be included.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->assertEquals($contextoff, $contextlist->current());// Check export data for each context.$this->export_context_data_for_user($user->id, $contextoff, 'mod_forum');$this->assertEquals(0,\core_privacy\local\request\writer::with_context($contextoff)->get_metadata([], 'trackreadpreference'));// Delete the data for one of the users in the 'on' forum.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$contexton->id]);$this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));$this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));provider::delete_data_for_user($approvedcontextlist);$this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));$this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));// Delete the data for one of the users in the 'off' forum.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$contextoff->id]);provider::delete_data_for_user($approvedcontextlist);$this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));$this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));}/*** Test that the posts which a user has read are returned correctly.*/public function test_user_read_posts(): void {global $DB;$course = $this->getDataGenerator()->create_course();$forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm1 = get_coursemodule_from_instance('forum', $forum1->id);$context1 = \context_module::instance($cm1->id);$forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm2 = get_coursemodule_from_instance('forum', $forum2->id);$context2 = \context_module::instance($cm2->id);$forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm3 = get_coursemodule_from_instance('forum', $forum3->id);$context3 = \context_module::instance($cm3->id);$forum4 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);$cm4 = get_coursemodule_from_instance('forum', $forum4->id);$context4 = \context_module::instance($cm4->id);list($author, $user) = $this->helper_create_users($course, 2);list($f1d1, $f1p1) = $this->helper_post_to_forum($forum1, $author);$f1p1reply = $this->helper_post_to_discussion($forum1, $f1d1, $author);$f1d1 = $DB->get_record('forum_discussions', ['id' => $f1d1->id]);list($f1d2, $f1p2) = $this->helper_post_to_forum($forum1, $author);list($f2d1, $f2p1) = $this->helper_post_to_forum($forum2, $author);$f2p1reply = $this->helper_post_to_discussion($forum2, $f2d1, $author);$f2d1 = $DB->get_record('forum_discussions', ['id' => $f2d1->id]);list($f2d2, $f2p2) = $this->helper_post_to_forum($forum2, $author);list($f3d1, $f3p1) = $this->helper_post_to_forum($forum3, $author);$f3p1reply = $this->helper_post_to_discussion($forum3, $f3d1, $author);$f3d1 = $DB->get_record('forum_discussions', ['id' => $f3d1->id]);list($f3d2, $f3p2) = $this->helper_post_to_forum($forum3, $author);list($f4d1, $f4p1) = $this->helper_post_to_forum($forum4, $author);$f4p1reply = $this->helper_post_to_discussion($forum4, $f4d1, $author);$f4d1 = $DB->get_record('forum_discussions', ['id' => $f4d1->id]);list($f4d2, $f4p2) = $this->helper_post_to_forum($forum4, $author);// Insert read info.// User has read post1, but not the reply or second post in forum1.forum_tp_add_read_record($user->id, $f1p1->id);// User has read post1 and its reply, but not the second post in forum2.forum_tp_add_read_record($user->id, $f2p1->id);forum_tp_add_read_record($user->id, $f2p1reply->id);// User has read post2 in forum3.forum_tp_add_read_record($user->id, $f3p2->id);// Nothing has been read in forum4.// Run as the user under test.$this->setUser($user);// Retrieve all contexts - should be three - forum4 has no data.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(3, $contextlist);$contextids = [$context1->id,$context2->id,$context3->id,];sort($contextids);$contextlistids = $contextlist->get_contextids();sort($contextlistids);$this->assertEquals($contextids, $contextlistids);// Forum 1.$this->export_context_data_for_user($user->id, $context1, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context1);// User has read f1p1.$readdata = $writer->get_metadata($this->get_subcontext($forum1, $f1d1, $f1p1),'postread');$this->assertNotEmpty($readdata);$this->assertTrue(isset($readdata->firstread));$this->assertTrue(isset($readdata->lastread));// User has not f1p1reply.$readdata = $writer->get_metadata($this->get_subcontext($forum1, $f1d1, $f1p1reply),'postread');$this->assertEmpty($readdata);// User has not f1p2.$readdata = $writer->get_metadata($this->get_subcontext($forum1, $f1d2, $f1p2),'postread');$this->assertEmpty($readdata);// Forum 2.$this->export_context_data_for_user($user->id, $context2, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context2);// User has read f2p1.$readdata = $writer->get_metadata($this->get_subcontext($forum2, $f2d1, $f2p1),'postread');$this->assertNotEmpty($readdata);$this->assertTrue(isset($readdata->firstread));$this->assertTrue(isset($readdata->lastread));// User has read f2p1reply.$readdata = $writer->get_metadata($this->get_subcontext($forum2, $f2d1, $f2p1reply),'postread');$this->assertNotEmpty($readdata);$this->assertTrue(isset($readdata->firstread));$this->assertTrue(isset($readdata->lastread));// User has not read f2p2.$readdata = $writer->get_metadata($this->get_subcontext($forum2, $f2d2, $f2p2),'postread');$this->assertEmpty($readdata);// Forum 3.$this->export_context_data_for_user($user->id, $context3, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context3);// User has not read f3p1.$readdata = $writer->get_metadata($this->get_subcontext($forum3, $f3d1, $f3p1),'postread');$this->assertEmpty($readdata);// User has not read f3p1reply.$readdata = $writer->get_metadata($this->get_subcontext($forum3, $f3d1, $f3p1reply),'postread');$this->assertEmpty($readdata);// User has read f3p2.$readdata = $writer->get_metadata($this->get_subcontext($forum3, $f3d2, $f3p2),'postread');$this->assertNotEmpty($readdata);$this->assertTrue(isset($readdata->firstread));$this->assertTrue(isset($readdata->lastread));// Delete all data for one of the users in one of the forums.$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user->id),'mod_forum',[$context3->id]);$this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id]));$this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id]));$this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id]));provider::delete_data_for_user($approvedcontextlist);$this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id]));$this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id]));$this->assertFalse($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id]));}/*** Test that posts with attachments have their attachments correctly exported.*/public function test_post_attachment_inclusion(): void {global $DB;$fs = get_file_storage();$course = $this->getDataGenerator()->create_course();list($author, $otheruser) = $this->helper_create_users($course, 2);$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);// Create a new discussion + post in the forum.list($discussion, $post) = $this->helper_post_to_forum($forum, $author);$discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);// Add a number of replies.$reply = $this->helper_reply_to_post($post, $author);$reply = $this->helper_reply_to_post($post, $author);$reply = $this->helper_reply_to_post($reply, $author);$posts[$reply->id] = $reply;// Add a fake inline image to the original post.$createdfile = $fs->create_file_from_string(['contextid' => $context->id,'component' => 'mod_forum','filearea' => 'post','itemid' => $post->id,'filepath' => '/','filename' => 'example.jpg',],'image contents (not really)');// Tag the post and the final reply.\core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);\core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);// Create a second discussion + post in the forum without tags.list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);$otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);// Add a number of replies.$reply = $this->helper_reply_to_post($otherpost, $author);$reply = $this->helper_reply_to_post($otherpost, $author);// Run as the user under test.$this->setUser($author);// Retrieve all contexts - should be one.$contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->export_context_data_for_user($author->id, $context, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);// The inline file should be on the first forum post.$subcontext = $this->get_subcontext($forum, $discussion, $post);$foundfiles = $writer->get_files($subcontext);$this->assertCount(1, $foundfiles);$this->assertEquals($createdfile, reset($foundfiles));}/*** Test that posts which include tags have those tags exported.*/public function test_post_tags(): void {global $DB;$course = $this->getDataGenerator()->create_course();list($author, $otheruser) = $this->helper_create_users($course, 2);$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);// Create a new discussion + post in the forum.list($discussion, $post) = $this->helper_post_to_forum($forum, $author);$discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);// Add a number of replies.$reply = $this->helper_reply_to_post($post, $author);$reply = $this->helper_reply_to_post($post, $author);$reply = $this->helper_reply_to_post($reply, $author);$posts[$reply->id] = $reply;// Tag the post and the final reply.\core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);\core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);// Create a second discussion + post in the forum without tags.list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);$otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);// Add a number of replies.$reply = $this->helper_reply_to_post($otherpost, $author);$reply = $this->helper_reply_to_post($otherpost, $author);// Run as the user under test.$this->setUser($author);// Retrieve all contexts - should be two.$contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');$this->assertCount(1, $contextlist);$this->export_all_data_for_user($author->id, 'mod_forum');$writer = \core_privacy\local\request\writer::with_context($context);$this->assert_all_tags_match_on_context($author->id,$context,$this->get_subcontext($forum, $discussion, $post),'mod_forum','forum_posts',$post->id);}/*** Ensure that all user data is deleted from a context.*/public function test_all_users_deleted_from_context(): void {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 = [];foreach ($users as $user) {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;// Add a number of replies.$posts[$post->id] = $post;$reply = $this->helper_reply_to_post($post, $user);$posts[$reply->id] = $reply;$reply = $this->helper_reply_to_post($post, $user);$posts[$reply->id] = $reply;$reply = $this->helper_reply_to_post($reply, $user);$posts[$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 an 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 user.$user = reset($users);$ratedposts = [];foreach ($posts as $post) {$discussion = $discussions[$post->discussion];$forum = $forums[$discussion->forum];$context = $contexts[$forum->id];// Mark the post as being read by user.forum_tp_add_read_record($user->id, $post->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);}}// Run as the user under test.$this->setUser($user);// Retrieve all contexts - should be two.$contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');$this->assertCount(2, $contextlist);// These are the contexts we expect.$contextids = array_map(function($context) {return $context->id;}, $contexts);sort($contextids);$contextlistids = $contextlist->get_contextids();sort($contextlistids);$this->assertEquals($contextids, $contextlistids);// Delete for the first forum.$forum = reset($forums);$context = $contexts[$forum->id];provider::delete_data_for_all_users_in_context($context);// Determine what should have been deleted.$discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {return $discussion->forum == $forum->id;});$postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {return isset($discussionsinforum[$post->discussion]);});// All forum discussions and posts should have been deleted in this forum.$this->assertCount(0, $DB->get_records('forum_discussions', ['forum' => $forum->id]));list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));$this->assertCount(0, $DB->get_records_select('forum_posts', "discussion {$insql}", $inparams));// All uploaded files relating to this context should have been deleted (post content).foreach ($postsinforum as $post) {$this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));$this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id));}// All ratings should have been deleted.$rm = new \rating_manager();foreach ($postsinforum as $post) {$ratings = $rm->get_all_ratings_for_item((object) ['context' => $context,'component' => 'mod_forum','ratingarea' => 'post','itemid' => $post->id,]);$this->assertEmpty($ratings);}// All tags should have been deleted.$posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));foreach ($posttags as $tags) {$this->assertEmpty($tags);}// Check the other forum too. It should remain intact.$forum = next($forums);$context = $contexts[$forum->id];// Grab the list of discussions and posts in the forum.$discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {return $discussion->forum == $forum->id;});$postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {return isset($discussionsinforum[$post->discussion]);});// Forum discussions and posts should not have been deleted in this forum.$this->assertGreaterThan(0, $DB->count_records('forum_discussions', ['forum' => $forum->id]));list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));$this->assertGreaterThan(0, $DB->count_records_select('forum_posts', "discussion {$insql}", $inparams));// Uploaded files relating to this context should remain.foreach ($postsinforum as $post) {if ($post->parent == 0) {$this->assertNotEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));}}// Ratings should not have been deleted.$rm = new \rating_manager();foreach ($postsinforum as $post) {if (!isset($ratedposts[$post->id])) {continue;}$ratings = $rm->get_all_ratings_for_item((object) ['context' => $context,'component' => 'mod_forum','ratingarea' => 'post','itemid' => $post->id,]);$this->assertNotEmpty($ratings);}// All tags should remain.$posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));foreach ($posttags as $tags) {$this->assertNotEmpty($tags);}}/*** Ensure that all user data is deleted for a specific context.*/public function test_delete_data_for_user(): void {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);$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(\core_user::get_user($user1->id),'mod_forum',[$firstcontext->id]);provider::delete_data_for_user($approvedcontextlist);// 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.$componentsql = "component = 'mod_forum' AND ";$this->assertCount(0, $DB->get_records_select('files',"{$componentsql} itemid {$postinsql}", $postinparams));// Files for the other posts should remain.$this->assertCount(18, $DB->get_records_select('files',"{$componentsql} 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(): void {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.$componentsql = "component = 'mod_forum' AND ";$this->assertCount(0, $DB->get_records_select('files',"{$componentsql} itemid {$postinsql}", $postinparams));// Files for the other posts should remain.$this->assertCount(18,$DB->get_records_select('files',"{$componentsql} 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(): void {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(): void {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(): void {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(): void {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(): void {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(): void {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(): void {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(): void {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);}/*** Test exporting plugin user preferences*/public function test_export_user_preferences(): void {$this->setAdminUser();// Create a user with some forum preferences.$user = $this->getDataGenerator()->create_user(['maildigest' => 2,'autosubscribe' => 1,'trackforums' => 0,]);set_user_preference('markasreadonnotification', 0, $user);set_user_preference('forum_discussionlistsortorder', \mod_forum\local\vaults\discussion_list::SORTORDER_STARTER_ASC,$user);// Export test users preferences.provider::export_user_preferences($user->id);$writer = \core_privacy\local\request\writer::with_context(\context_system::instance());$this->assertTrue($writer->has_any_data());$preferences = (array) $writer->get_user_preferences('mod_forum');$this->assertEquals((object) ['value' => 2,'description' => get_string('emaildigestsubjects'),], $preferences['maildigest']);$this->assertEquals((object) ['value' => 1,'description' => get_string('autosubscribeyes'),], $preferences['autosubscribe']);$this->assertEquals((object) ['value' => 0,'description' => get_string('trackforumsno'),], $preferences['trackforums']);$this->assertEquals((object) ['value' => 0,'description' => get_string('markasreadonnotificationno', 'mod_forum'),], $preferences['markasreadonnotification']);$this->assertEquals((object) ['value' => \mod_forum\local\vaults\discussion_list::SORTORDER_STARTER_ASC,'description' => get_string('discussionlistsortbystarterasc', 'mod_forum'),], $preferences['forum_discussionlistsortorder']);}}