MDL-63495 mod_forum: Add intial support for removal of multiple context users
[moodle.git] / mod / forum / tests / privacy_provider_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Tests for the forum implementation of the Privacy Provider API.
19  *
20  * @package    mod_forum
21  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 global $CFG;
29 require_once(__DIR__ . '/helper.php');
30 require_once($CFG->dirroot . '/rating/lib.php');
32 use \mod_forum\privacy\provider;
34 /**
35  * Tests for the forum implementation of the Privacy Provider API.
36  *
37  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
42     // Include the privacy subcontext_info trait.
43     // This includes the subcontext builders.
44     use \mod_forum\privacy\subcontext_info;
46     // Include the mod_forum test helpers.
47     // This includes functions to create forums, users, discussions, and posts.
48     use helper;
50     // Include the privacy helper trait for the ratings API.
51     use \core_rating\phpunit\privacy_helper;
53     // Include the privacy helper trait for the tag API.
54     use \core_tag\tests\privacy_helper;
56     /**
57      * Test setUp.
58      */
59     public function setUp() {
60         $this->resetAfterTest(true);
61     }
63     /**
64      * Helper to assert that the forum data is correct.
65      *
66      * @param   object  $expected The expected data in the forum.
67      * @param   object  $actual The actual data in the forum.
68      */
69     protected function assert_forum_data($expected, $actual) {
70         // Exact matches.
71         $this->assertEquals(format_string($expected->name, true), $actual->name);
72     }
74     /**
75      * Helper to assert that the discussion data is correct.
76      *
77      * @param   object  $expected The expected data in the discussion.
78      * @param   object  $actual The actual data in the discussion.
79      */
80     protected function assert_discussion_data($expected, $actual) {
81         // Exact matches.
82         $this->assertEquals(format_string($expected->name, true), $actual->name);
83         $this->assertEquals(
84             \core_privacy\local\request\transform::yesno($expected->pinned),
85             $actual->pinned
86         );
88         $this->assertEquals(
89             \core_privacy\local\request\transform::datetime($expected->timemodified),
90             $actual->timemodified
91         );
93         $this->assertEquals(
94             \core_privacy\local\request\transform::datetime($expected->usermodified),
95             $actual->usermodified
96         );
97     }
99     /**
100      * Helper to assert that the post data is correct.
101      *
102      * @param   object  $expected The expected data in the post.
103      * @param   object  $actual The actual data in the post.
104      * @param   \core_privacy\local\request\writer  $writer The writer used
105      */
106     protected function assert_post_data($expected, $actual, $writer) {
107         // Exact matches.
108         $this->assertEquals(format_string($expected->subject, true), $actual->subject);
110         // The message should have been passed through the rewriter.
111         // Note: The testable rewrite_pluginfile_urls function in the ignores all items except the text.
112         $this->assertEquals(
113             $writer->rewrite_pluginfile_urls([], '', '', '', $expected->message),
114             $actual->message
115         );
117         $this->assertEquals(
118             \core_privacy\local\request\transform::datetime($expected->created),
119             $actual->created
120         );
122         $this->assertEquals(
123             \core_privacy\local\request\transform::datetime($expected->modified),
124             $actual->modified
125         );
126     }
128     /**
129      * Test that a user who is enrolled in a course, but who has never
130      * posted and has no other metadata stored will not have any link to
131      * that context.
132      */
133     public function test_user_has_never_posted() {
134         // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
135         $course = $this->getDataGenerator()->create_course();
136         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
137         $course = $this->getDataGenerator()->create_course();
138         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
139         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
140         list($user, $otheruser) = $this->helper_create_users($course, 2);
141         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
142         $cm = get_coursemodule_from_instance('forum', $forum->id);
143         $context = \context_module::instance($cm->id);
145         // Test that no contexts were retrieved.
146         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
147         $contexts = $contextlist->get_contextids();
148         $this->assertCount(0, $contexts);
150         // Attempting to export data for this context should return nothing either.
151         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
153         $writer = \core_privacy\local\request\writer::with_context($context);
155         // The provider should always export data for any context explicitly asked of it, but there should be no
156         // metadata, files, or discussions.
157         $this->assertEmpty($writer->get_data([get_string('discussions', 'mod_forum')]));
158         $this->assertEmpty($writer->get_all_metadata([]));
159         $this->assertEmpty($writer->get_files([]));
160     }
162     /**
163      * Test that a user who is enrolled in a course, and who has never
164      * posted and has subscribed to the forum will have relevant
165      * information returned.
166      */
167     public function test_user_has_never_posted_subscribed_to_forum() {
168         // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
169         $course = $this->getDataGenerator()->create_course();
170         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
171         $course = $this->getDataGenerator()->create_course();
172         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
173         $course = $this->getDataGenerator()->create_course();
174         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
175         list($user, $otheruser) = $this->helper_create_users($course, 2);
176         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
177         $cm = get_coursemodule_from_instance('forum', $forum->id);
178         $context = \context_module::instance($cm->id);
180         // Subscribe the user to the forum.
181         \mod_forum\subscriptions::subscribe_user($user->id, $forum);
183         // Retrieve all contexts - only this context should be returned.
184         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
185         $this->assertCount(1, $contextlist);
186         $this->assertEquals($context, $contextlist->current());
188         // Export all of the data for the context.
189         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
190         $writer = \core_privacy\local\request\writer::with_context($context);
191         $this->assertTrue($writer->has_any_data());
193         $subcontext = $this->get_subcontext($forum);
194         // There should be one item of metadata.
195         $this->assertCount(1, $writer->get_all_metadata($subcontext));
197         // It should be the subscriptionpreference whose value is 1.
198         $this->assertEquals(1, $writer->get_metadata($subcontext, 'subscriptionpreference'));
200         // There should be data about the forum itself.
201         $this->assertNotEmpty($writer->get_data($subcontext));
202     }
204     /**
205      * Test that a user who is enrolled in a course, and who has never
206      * posted and has subscribed to the discussion will have relevant
207      * information returned.
208      */
209     public function test_user_has_never_posted_subscribed_to_discussion() {
210         // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
211         $course = $this->getDataGenerator()->create_course();
212         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
213         $course = $this->getDataGenerator()->create_course();
214         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
215         $course = $this->getDataGenerator()->create_course();
216         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
217         list($user, $otheruser) = $this->helper_create_users($course, 2);
218         // Post twice - only the second discussion should be included.
219         $this->helper_post_to_forum($forum, $otheruser);
220         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
221         $cm = get_coursemodule_from_instance('forum', $forum->id);
222         $context = \context_module::instance($cm->id);
224         // Subscribe the user to the discussion.
225         \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion);
227         // Retrieve all contexts - only this context should be returned.
228         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
229         $this->assertCount(1, $contextlist);
230         $this->assertEquals($context, $contextlist->current());
232         // Export all of the data for the context.
233         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
234         $writer = \core_privacy\local\request\writer::with_context($context);
235         $this->assertTrue($writer->has_any_data());
237         // There should be nothing in the forum. The user is not subscribed there.
238         $forumsubcontext = $this->get_subcontext($forum);
239         $this->assertCount(0, $writer->get_all_metadata($forumsubcontext));
240         $this->assert_forum_data($forum, $writer->get_data($forumsubcontext));
242         // There should be metadata in the discussion.
243         $discsubcontext = $this->get_subcontext($forum, $discussion);
244         $this->assertCount(1, $writer->get_all_metadata($discsubcontext));
246         // It should be the subscriptionpreference whose value is an Integer.
247         // (It's a timestamp, but it doesn't matter).
248         $metadata = $writer->get_metadata($discsubcontext, 'subscriptionpreference');
249         $this->assertGreaterThan(1, $metadata);
251         // For context we output the discussion content.
252         $data = $writer->get_data($discsubcontext);
253         $this->assertInstanceOf('stdClass', $data);
254         $this->assert_discussion_data($discussion, $data);
256         // Post content is not exported unless the user participated.
257         $postsubcontext = $this->get_subcontext($forum, $discussion, $post);
258         $this->assertCount(0, $writer->get_data($postsubcontext));
259     }
261     /**
262      * Test that a user who has posted their own discussion will have all
263      * content returned.
264      */
265     public function test_user_has_posted_own_discussion() {
266         $course = $this->getDataGenerator()->create_course();
267         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
268         $course = $this->getDataGenerator()->create_course();
269         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
270         $course = $this->getDataGenerator()->create_course();
271         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
272         list($user, $otheruser) = $this->helper_create_users($course, 2);
274         // Post twice - only the second discussion should be included.
275         list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
276         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
277         $cm = get_coursemodule_from_instance('forum', $forum->id);
278         $context = \context_module::instance($cm->id);
280         // Retrieve all contexts - only this context should be returned.
281         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
282         $this->assertCount(1, $contextlist);
283         $this->assertEquals($context, $contextlist->current());
285         // Export all of the data for the context.
286         $this->setUser($user);
287         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
288         $writer = \core_privacy\local\request\writer::with_context($context);
289         $this->assertTrue($writer->has_any_data());
291         // The other discussion should not have been returned as we did not post in it.
292         $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
294         $this->assert_discussion_data($discussion, $writer->get_data($this->get_subcontext($forum, $discussion)));
295         $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
296     }
298     /**
299      * Test that a user who has posted a reply to another users discussion
300      * will have all content returned.
301      */
302     public function test_user_has_posted_reply() {
303         global $DB;
305         // Create several courses and forums. We only insert data into the final one.
306         $course = $this->getDataGenerator()->create_course();
307         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
308         $course = $this->getDataGenerator()->create_course();
309         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
311         $course = $this->getDataGenerator()->create_course();
312         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
313         list($user, $otheruser) = $this->helper_create_users($course, 2);
314         // Post twice - only the second discussion should be included.
315         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
316         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
317         $cm = get_coursemodule_from_instance('forum', $forum->id);
318         $context = \context_module::instance($cm->id);
320         // Post a reply to the other person's post.
321         $reply = $this->helper_reply_to_post($post, $user);
323         // Testing as user $user.
324         $this->setUser($user);
326         // Retrieve all contexts - only this context should be returned.
327         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
328         $this->assertCount(1, $contextlist);
329         $this->assertEquals($context, $contextlist->current());
331         // Export all of the data for the context.
332         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
333         $writer = \core_privacy\local\request\writer::with_context($context);
334         $this->assertTrue($writer->has_any_data());
336         // Refresh the discussions.
337         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
338         $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
340         // The other discussion should not have been returned as we did not post in it.
341         $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
343         // Our discussion should have been returned as we did post in it.
344         $data = $writer->get_data($this->get_subcontext($forum, $discussion));
345         $this->assertNotEmpty($data);
346         $this->assert_discussion_data($discussion, $data);
348         // The reply will be included.
349         $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
350     }
352     /**
353      * Test that the rating of another users content will have only the
354      * rater's information returned.
355      */
356     public function test_user_has_rated_others() {
357         $course = $this->getDataGenerator()->create_course();
358         $forum = $this->getDataGenerator()->create_module('forum', [
359             'course' => $course->id,
360             'scale' => 100,
361         ]);
362         list($user, $otheruser) = $this->helper_create_users($course, 2);
363         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
364         $cm = get_coursemodule_from_instance('forum', $forum->id);
365         $context = \context_module::instance($cm->id);
367         // Rate the other users content.
368         $rm = new rating_manager();
369         $ratingoptions = new stdClass;
370         $ratingoptions->context = $context;
371         $ratingoptions->component = 'mod_forum';
372         $ratingoptions->ratingarea = 'post';
373         $ratingoptions->itemid  = $post->id;
374         $ratingoptions->scaleid = $forum->scale;
375         $ratingoptions->userid  = $user->id;
377         $rating = new \rating($ratingoptions);
378         $rating->update_rating(75);
380         // Run as the user under test.
381         $this->setUser($user);
383         // Retrieve all contexts - only this context should be returned.
384         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
385         $this->assertCount(1, $contextlist);
386         $this->assertEquals($context, $contextlist->current());
388         // Export all of the data for the context.
389         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
390         $writer = \core_privacy\local\request\writer::with_context($context);
391         $this->assertTrue($writer->has_any_data());
393         // The discussion should not have been returned as we did not post in it.
394         $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
396         $this->assert_all_own_ratings_on_context(
397             $user->id,
398             $context,
399             $this->get_subcontext($forum, $discussion, $post),
400             'mod_forum',
401             'post',
402             $post->id
403         );
405         // The original post will not be included.
406         $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
407     }
409     /**
410      * Test that ratings of a users own content will all be returned.
411      */
412     public function test_user_has_been_rated() {
413         $course = $this->getDataGenerator()->create_course();
414         $forum = $this->getDataGenerator()->create_module('forum', [
415             'course' => $course->id,
416             'scale' => 100,
417         ]);
418         list($user, $otheruser, $anotheruser) = $this->helper_create_users($course, 3);
419         list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
420         $cm = get_coursemodule_from_instance('forum', $forum->id);
421         $context = \context_module::instance($cm->id);
423         // Other users rate my content.
424         $rm = new rating_manager();
425         $ratingoptions = new stdClass;
426         $ratingoptions->context = $context;
427         $ratingoptions->component = 'mod_forum';
428         $ratingoptions->ratingarea = 'post';
429         $ratingoptions->itemid  = $post->id;
430         $ratingoptions->scaleid = $forum->scale;
432         $ratingoptions->userid  = $otheruser->id;
433         $rating = new \rating($ratingoptions);
434         $rating->update_rating(75);
436         $ratingoptions->userid  = $anotheruser->id;
437         $rating = new \rating($ratingoptions);
438         $rating->update_rating(75);
440         // Run as the user under test.
441         $this->setUser($user);
443         // Retrieve all contexts - only this context should be returned.
444         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
445         $this->assertCount(1, $contextlist);
446         $this->assertEquals($context, $contextlist->current());
448         // Export all of the data for the context.
449         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
450         $writer = \core_privacy\local\request\writer::with_context($context);
451         $this->assertTrue($writer->has_any_data());
453         $this->assert_all_ratings_on_context(
454             $context,
455             $this->get_subcontext($forum, $discussion, $post),
456             'mod_forum',
457             'post',
458             $post->id
459         );
460     }
462     /**
463      * Test that per-user daily digest settings are included correctly.
464      */
465     public function test_user_forum_digest() {
466         $course = $this->getDataGenerator()->create_course();
468         $forum0 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
469         $cm0 = get_coursemodule_from_instance('forum', $forum0->id);
470         $context0 = \context_module::instance($cm0->id);
472         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
473         $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
474         $context1 = \context_module::instance($cm1->id);
476         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
477         $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
478         $context2 = \context_module::instance($cm2->id);
480         $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
481         $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
482         $context3 = \context_module::instance($cm3->id);
484         list($user) = $this->helper_create_users($course, 1);
486         // Set a digest value for each forum.
487         forum_set_user_maildigest($forum0, 0, $user);
488         forum_set_user_maildigest($forum1, 1, $user);
489         forum_set_user_maildigest($forum2, 2, $user);
491         // Run as the user under test.
492         $this->setUser($user);
494         // Retrieve all contexts - three contexts should be returned - the fourth should not be included.
495         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
496         $this->assertCount(3, $contextlist);
498         $contextids = [
499                 $context0->id,
500                 $context1->id,
501                 $context2->id,
502             ];
503         sort($contextids);
504         $contextlistids = $contextlist->get_contextids();
505         sort($contextlistids);
506         $this->assertEquals($contextids, $contextlistids);
508         // Check export data for each context.
509         $this->export_context_data_for_user($user->id, $context0, 'mod_forum');
510         $this->assertEquals(0, \core_privacy\local\request\writer::with_context($context0)->get_metadata([], 'digestpreference'));
512         $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
513         $this->assertEquals(1, \core_privacy\local\request\writer::with_context($context1)->get_metadata([], 'digestpreference'));
515         $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
516         $this->assertEquals(2, \core_privacy\local\request\writer::with_context($context2)->get_metadata([], 'digestpreference'));
517     }
519     /**
520      * Test that the per-user, per-forum user tracking data is exported.
521      */
522     public function test_user_tracking_data() {
523         $course = $this->getDataGenerator()->create_course();
525         $forumoff = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
526         $cmoff = get_coursemodule_from_instance('forum', $forumoff->id);
527         $contextoff = \context_module::instance($cmoff->id);
529         $forumon = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
530         $cmon = get_coursemodule_from_instance('forum', $forumon->id);
531         $contexton = \context_module::instance($cmon->id);
533         list($user) = $this->helper_create_users($course, 1);
535         // Set user tracking data.
536         forum_tp_stop_tracking($forumoff->id, $user->id);
537         forum_tp_start_tracking($forumon->id, $user->id);
539         // Run as the user under test.
540         $this->setUser($user);
542         // Retrieve all contexts - only the forum tracking reads should be included.
543         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
544         $this->assertCount(1, $contextlist);
545         $this->assertEquals($contextoff, $contextlist->current());
547         // Check export data for each context.
548         $this->export_context_data_for_user($user->id, $contextoff, 'mod_forum');
549         $this->assertEquals(0,
550                 \core_privacy\local\request\writer::with_context($contextoff)->get_metadata([], 'trackreadpreference'));
551     }
553     /**
554      * Test that the posts which a user has read are returned correctly.
555      */
556     public function test_user_read_posts() {
557         global $DB;
559         $course = $this->getDataGenerator()->create_course();
561         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
562         $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
563         $context1 = \context_module::instance($cm1->id);
565         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
566         $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
567         $context2 = \context_module::instance($cm2->id);
569         $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
570         $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
571         $context3 = \context_module::instance($cm3->id);
573         $forum4 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
574         $cm4 = get_coursemodule_from_instance('forum', $forum4->id);
575         $context4 = \context_module::instance($cm4->id);
577         list($author, $user) = $this->helper_create_users($course, 2);
579         list($f1d1, $f1p1) = $this->helper_post_to_forum($forum1, $author);
580         $f1p1reply = $this->helper_post_to_discussion($forum1, $f1d1, $author);
581         $f1d1 = $DB->get_record('forum_discussions', ['id' => $f1d1->id]);
582         list($f1d2, $f1p2) = $this->helper_post_to_forum($forum1, $author);
584         list($f2d1, $f2p1) = $this->helper_post_to_forum($forum2, $author);
585         $f2p1reply = $this->helper_post_to_discussion($forum2, $f2d1, $author);
586         $f2d1 = $DB->get_record('forum_discussions', ['id' => $f2d1->id]);
587         list($f2d2, $f2p2) = $this->helper_post_to_forum($forum2, $author);
589         list($f3d1, $f3p1) = $this->helper_post_to_forum($forum3, $author);
590         $f3p1reply = $this->helper_post_to_discussion($forum3, $f3d1, $author);
591         $f3d1 = $DB->get_record('forum_discussions', ['id' => $f3d1->id]);
592         list($f3d2, $f3p2) = $this->helper_post_to_forum($forum3, $author);
594         list($f4d1, $f4p1) = $this->helper_post_to_forum($forum4, $author);
595         $f4p1reply = $this->helper_post_to_discussion($forum4, $f4d1, $author);
596         $f4d1 = $DB->get_record('forum_discussions', ['id' => $f4d1->id]);
597         list($f4d2, $f4p2) = $this->helper_post_to_forum($forum4, $author);
599         // Insert read info.
600         // User has read post1, but not the reply or second post in forum1.
601         forum_tp_add_read_record($user->id, $f1p1->id);
603         // User has read post1 and its reply, but not the second post in forum2.
604         forum_tp_add_read_record($user->id, $f2p1->id);
605         forum_tp_add_read_record($user->id, $f2p1reply->id);
607         // User has read post2 in forum3.
608         forum_tp_add_read_record($user->id, $f3p2->id);
610         // Nothing has been read in forum4.
612         // Run as the user under test.
613         $this->setUser($user);
615         // Retrieve all contexts - should be three - forum4 has no data.
616         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
617         $this->assertCount(3, $contextlist);
619         $contextids = [
620                 $context1->id,
621                 $context2->id,
622                 $context3->id,
623             ];
624         sort($contextids);
625         $contextlistids = $contextlist->get_contextids();
626         sort($contextlistids);
627         $this->assertEquals($contextids, $contextlistids);
629         // Forum 1.
630         $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
631         $writer = \core_privacy\local\request\writer::with_context($context1);
633         // User has read f1p1.
634         $readdata = $writer->get_metadata(
635                 $this->get_subcontext($forum1, $f1d1, $f1p1),
636                 'postread'
637             );
638         $this->assertNotEmpty($readdata);
639         $this->assertTrue(isset($readdata->firstread));
640         $this->assertTrue(isset($readdata->lastread));
642         // User has not f1p1reply.
643         $readdata = $writer->get_metadata(
644                 $this->get_subcontext($forum1, $f1d1, $f1p1reply),
645                 'postread'
646             );
647         $this->assertEmpty($readdata);
649         // User has not f1p2.
650         $readdata = $writer->get_metadata(
651                 $this->get_subcontext($forum1, $f1d2, $f1p2),
652                 'postread'
653             );
654         $this->assertEmpty($readdata);
656         // Forum 2.
657         $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
658         $writer = \core_privacy\local\request\writer::with_context($context2);
660         // User has read f2p1.
661         $readdata = $writer->get_metadata(
662                 $this->get_subcontext($forum2, $f2d1, $f2p1),
663                 'postread'
664             );
665         $this->assertNotEmpty($readdata);
666         $this->assertTrue(isset($readdata->firstread));
667         $this->assertTrue(isset($readdata->lastread));
669         // User has read f2p1reply.
670         $readdata = $writer->get_metadata(
671                 $this->get_subcontext($forum2, $f2d1, $f2p1reply),
672                 'postread'
673             );
674         $this->assertNotEmpty($readdata);
675         $this->assertTrue(isset($readdata->firstread));
676         $this->assertTrue(isset($readdata->lastread));
678         // User has not read f2p2.
679         $readdata = $writer->get_metadata(
680                 $this->get_subcontext($forum2, $f2d2, $f2p2),
681                 'postread'
682             );
683         $this->assertEmpty($readdata);
685         // Forum 3.
686         $this->export_context_data_for_user($user->id, $context3, 'mod_forum');
687         $writer = \core_privacy\local\request\writer::with_context($context3);
689         // User has not read f3p1.
690         $readdata = $writer->get_metadata(
691                 $this->get_subcontext($forum3, $f3d1, $f3p1),
692                 'postread'
693             );
694         $this->assertEmpty($readdata);
696         // User has not read f3p1reply.
697         $readdata = $writer->get_metadata(
698                 $this->get_subcontext($forum3, $f3d1, $f3p1reply),
699                 'postread'
700             );
701         $this->assertEmpty($readdata);
703         // User has read f3p2.
704         $readdata = $writer->get_metadata(
705                 $this->get_subcontext($forum3, $f3d2, $f3p2),
706                 'postread'
707             );
708         $this->assertNotEmpty($readdata);
709         $this->assertTrue(isset($readdata->firstread));
710         $this->assertTrue(isset($readdata->lastread));
711     }
713     /**
714      * Test that posts with attachments have their attachments correctly exported.
715      */
716     public function test_post_attachment_inclusion() {
717         global $DB;
719         $fs = get_file_storage();
720         $course = $this->getDataGenerator()->create_course();
721         list($author, $otheruser) = $this->helper_create_users($course, 2);
723         $forum = $this->getDataGenerator()->create_module('forum', [
724             'course' => $course->id,
725             'scale' => 100,
726         ]);
727         $cm = get_coursemodule_from_instance('forum', $forum->id);
728         $context = \context_module::instance($cm->id);
730         // Create a new discussion + post in the forum.
731         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
732         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
734         // Add a number of replies.
735         $reply = $this->helper_reply_to_post($post, $author);
736         $reply = $this->helper_reply_to_post($post, $author);
737         $reply = $this->helper_reply_to_post($reply, $author);
738         $posts[$reply->id] = $reply;
740         // Add a fake inline image to the original post.
741         $createdfile = $fs->create_file_from_string([
742                 'contextid' => $context->id,
743                 'component' => 'mod_forum',
744                 'filearea'  => 'post',
745                 'itemid'    => $post->id,
746                 'filepath'  => '/',
747                 'filename'  => 'example.jpg',
748             ],
749         'image contents (not really)');
751         // Tag the post and the final reply.
752         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
753         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
755         // Create a second discussion + post in the forum without tags.
756         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
757         $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
759         // Add a number of replies.
760         $reply = $this->helper_reply_to_post($otherpost, $author);
761         $reply = $this->helper_reply_to_post($otherpost, $author);
763         // Run as the user under test.
764         $this->setUser($author);
766         // Retrieve all contexts - should be one.
767         $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
768         $this->assertCount(1, $contextlist);
770         $this->export_context_data_for_user($author->id, $context, 'mod_forum');
771         $writer = \core_privacy\local\request\writer::with_context($context);
773         // The inline file should be on the first forum post.
774         $subcontext = $this->get_subcontext($forum, $discussion, $post);
775         $foundfiles = $writer->get_files($subcontext);
776         $this->assertCount(1, $foundfiles);
777         $this->assertEquals($createdfile, reset($foundfiles));
778     }
780     /**
781      * Test that posts which include tags have those tags exported.
782      */
783     public function test_post_tags() {
784         global $DB;
786         $course = $this->getDataGenerator()->create_course();
787         list($author, $otheruser) = $this->helper_create_users($course, 2);
789         $forum = $this->getDataGenerator()->create_module('forum', [
790             'course' => $course->id,
791             'scale' => 100,
792         ]);
793         $cm = get_coursemodule_from_instance('forum', $forum->id);
794         $context = \context_module::instance($cm->id);
796         // Create a new discussion + post in the forum.
797         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
798         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
800         // Add a number of replies.
801         $reply = $this->helper_reply_to_post($post, $author);
802         $reply = $this->helper_reply_to_post($post, $author);
803         $reply = $this->helper_reply_to_post($reply, $author);
804         $posts[$reply->id] = $reply;
806         // Tag the post and the final reply.
807         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
808         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
810         // Create a second discussion + post in the forum without tags.
811         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
812         $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
814         // Add a number of replies.
815         $reply = $this->helper_reply_to_post($otherpost, $author);
816         $reply = $this->helper_reply_to_post($otherpost, $author);
818         // Run as the user under test.
819         $this->setUser($author);
821         // Retrieve all contexts - should be two.
822         $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
823         $this->assertCount(1, $contextlist);
825         $this->export_all_data_for_user($author->id, 'mod_forum');
826         $writer = \core_privacy\local\request\writer::with_context($context);
828         $this->assert_all_tags_match_on_context(
829             $author->id,
830             $context,
831             $this->get_subcontext($forum, $discussion, $post),
832             'mod_forum',
833             'forum_posts',
834             $post->id
835         );
836     }
838     /**
839      * Ensure that all user data is deleted from a context.
840      */
841     public function test_all_users_deleted_from_context() {
842         global $DB;
844         $fs = get_file_storage();
845         $course = $this->getDataGenerator()->create_course();
846         $users = $this->helper_create_users($course, 5);
848         $forums = [];
849         $contexts = [];
850         for ($i = 0; $i < 2; $i++) {
851             $forum = $this->getDataGenerator()->create_module('forum', [
852                 'course' => $course->id,
853                 'scale' => 100,
854             ]);
855             $cm = get_coursemodule_from_instance('forum', $forum->id);
856             $context = \context_module::instance($cm->id);
857             $forums[$forum->id] = $forum;
858             $contexts[$forum->id] = $context;
859         }
861         $discussions = [];
862         $posts = [];
863         foreach ($users as $user) {
864             foreach ($forums as $forum) {
865                 $context = $contexts[$forum->id];
867                 // Create a new discussion + post in the forum.
868                 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
869                 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
870                 $discussions[$discussion->id] = $discussion;
872                 // Add a number of replies.
873                 $posts[$post->id] = $post;
874                 $reply = $this->helper_reply_to_post($post, $user);
875                 $posts[$reply->id] = $reply;
876                 $reply = $this->helper_reply_to_post($post, $user);
877                 $posts[$reply->id] = $reply;
878                 $reply = $this->helper_reply_to_post($reply, $user);
879                 $posts[$reply->id] = $reply;
881                 // Add a fake inline image to the original post.
882                 $fs->create_file_from_string([
883                         'contextid' => $context->id,
884                         'component' => 'mod_forum',
885                         'filearea'  => 'post',
886                         'itemid'    => $post->id,
887                         'filepath'  => '/',
888                         'filename'  => 'example.jpg',
889                     ], 'image contents (not really)');
890                 // And an attachment.
891                 $fs->create_file_from_string([
892                         'contextid' => $context->id,
893                         'component' => 'mod_forum',
894                         'filearea'  => 'attachment',
895                         'itemid'    => $post->id,
896                         'filepath'  => '/',
897                         'filename'  => 'example.jpg',
898                     ], 'image contents (not really)');
899             }
900         }
902         // Mark all posts as read by user.
903         $user = reset($users);
904         $ratedposts = [];
905         foreach ($posts as $post) {
906             $discussion = $discussions[$post->discussion];
907             $forum = $forums[$discussion->forum];
908             $context = $contexts[$forum->id];
910             // Mark the post as being read by user.
911             forum_tp_add_read_record($user->id, $post->id);
913             // Tag the post.
914             \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
916             // Rate the other users content.
917             if ($post->userid != $user->id) {
918                 $ratedposts[$post->id] = $post;
919                 $rm = new rating_manager();
920                 $ratingoptions = (object) [
921                     'context' => $context,
922                     'component' => 'mod_forum',
923                     'ratingarea' => 'post',
924                     'itemid' => $post->id,
925                     'scaleid' => $forum->scale,
926                     'userid' => $user->id,
927                 ];
929                 $rating = new \rating($ratingoptions);
930                 $rating->update_rating(75);
931             }
932         }
934         // Run as the user under test.
935         $this->setUser($user);
937         // Retrieve all contexts - should be two.
938         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
939         $this->assertCount(2, $contextlist);
941         // These are the contexts we expect.
942         $contextids = array_map(function($context) {
943             return $context->id;
944         }, $contexts);
945         sort($contextids);
947         $contextlistids = $contextlist->get_contextids();
948         sort($contextlistids);
949         $this->assertEquals($contextids, $contextlistids);
951         // Delete for the first forum.
952         $forum = reset($forums);
953         $context = $contexts[$forum->id];
954         provider::delete_data_for_all_users_in_context($context);
956         // Determine what should have been deleted.
957         $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
958             return $discussion->forum == $forum->id;
959         });
961         $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
962             return isset($discussionsinforum[$post->discussion]);
963         });
965         // All forum discussions and posts should have been deleted in this forum.
966         $this->assertCount(0, $DB->get_records('forum_discussions', ['forum' => $forum->id]));
968         list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
969         $this->assertCount(0, $DB->get_records_select('forum_posts', "discussion {$insql}", $inparams));
971         // All uploaded files relating to this context should have been deleted (post content).
972         foreach ($postsinforum as $post) {
973             $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
974             $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id));
975         }
977         // All ratings should have been deleted.
978         $rm = new rating_manager();
979         foreach ($postsinforum as $post) {
980             $ratings = $rm->get_all_ratings_for_item((object) [
981                 'context' => $context,
982                 'component' => 'mod_forum',
983                 'ratingarea' => 'post',
984                 'itemid' => $post->id,
985             ]);
986             $this->assertEmpty($ratings);
987         }
989         // All tags should have been deleted.
990         $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
991         foreach ($posttags as $tags) {
992             $this->assertEmpty($tags);
993         }
995         // Check the other forum too. It should remain intact.
996         $forum = next($forums);
997         $context = $contexts[$forum->id];
999         // Grab the list of discussions and posts in the forum.
1000         $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
1001             return $discussion->forum == $forum->id;
1002         });
1004         $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
1005             return isset($discussionsinforum[$post->discussion]);
1006         });
1008         // Forum discussions and posts should not have been deleted in this forum.
1009         $this->assertGreaterThan(0, $DB->count_records('forum_discussions', ['forum' => $forum->id]));
1011         list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
1012         $this->assertGreaterThan(0, $DB->count_records_select('forum_posts', "discussion {$insql}", $inparams));
1014         // Uploaded files relating to this context should remain.
1015         foreach ($postsinforum as $post) {
1016             if ($post->parent == 0) {
1017                 $this->assertNotEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
1018             }
1019         }
1021         // Ratings should not have been deleted.
1022         $rm = new rating_manager();
1023         foreach ($postsinforum as $post) {
1024             if (!isset($ratedposts[$post->id])) {
1025                 continue;
1026             }
1027             $ratings = $rm->get_all_ratings_for_item((object) [
1028                 'context' => $context,
1029                 'component' => 'mod_forum',
1030                 'ratingarea' => 'post',
1031                 'itemid' => $post->id,
1032             ]);
1033             $this->assertNotEmpty($ratings);
1034         }
1036         // All tags should remain.
1037         $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
1038         foreach ($posttags as $tags) {
1039             $this->assertNotEmpty($tags);
1040         }
1041     }
1043     /**
1044      * Ensure that all user data is deleted for a specific context.
1045      */
1046     public function test_delete_data_for_user() {
1047         global $DB;
1049         $fs = get_file_storage();
1050         $course = $this->getDataGenerator()->create_course();
1051         $users = $this->helper_create_users($course, 5);
1053         $forums = [];
1054         $contexts = [];
1055         for ($i = 0; $i < 2; $i++) {
1056             $forum = $this->getDataGenerator()->create_module('forum', [
1057                 'course' => $course->id,
1058                 'scale' => 100,
1059             ]);
1060             $cm = get_coursemodule_from_instance('forum', $forum->id);
1061             $context = \context_module::instance($cm->id);
1062             $forums[$forum->id] = $forum;
1063             $contexts[$forum->id] = $context;
1064         }
1066         $discussions = [];
1067         $posts = [];
1068         $postsbyforum = [];
1069         foreach ($users as $user) {
1070             $postsbyforum[$user->id] = [];
1071             foreach ($forums as $forum) {
1072                 $context = $contexts[$forum->id];
1074                 // Create a new discussion + post in the forum.
1075                 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1076                 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1077                 $discussions[$discussion->id] = $discussion;
1078                 $postsbyforum[$user->id][$context->id] = [];
1080                 // Add a number of replies.
1081                 $posts[$post->id] = $post;
1082                 $thisforumposts[$post->id] = $post;
1083                 $postsbyforum[$user->id][$context->id][$post->id] = $post;
1085                 $reply = $this->helper_reply_to_post($post, $user);
1086                 $posts[$reply->id] = $reply;
1087                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1089                 $reply = $this->helper_reply_to_post($post, $user);
1090                 $posts[$reply->id] = $reply;
1091                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1093                 $reply = $this->helper_reply_to_post($reply, $user);
1094                 $posts[$reply->id] = $reply;
1095                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1097                 // Add a fake inline image to the original post.
1098                 $fs->create_file_from_string([
1099                         'contextid' => $context->id,
1100                         'component' => 'mod_forum',
1101                         'filearea'  => 'post',
1102                         'itemid'    => $post->id,
1103                         'filepath'  => '/',
1104                         'filename'  => 'example.jpg',
1105                     ], 'image contents (not really)');
1106                 // And a fake attachment.
1107                 $fs->create_file_from_string([
1108                         'contextid' => $context->id,
1109                         'component' => 'mod_forum',
1110                         'filearea'  => 'attachment',
1111                         'itemid'    => $post->id,
1112                         'filepath'  => '/',
1113                         'filename'  => 'example.jpg',
1114                     ], 'image contents (not really)');
1115             }
1116         }
1118         // Mark all posts as read by user1.
1119         $user1 = reset($users);
1120         foreach ($posts as $post) {
1121             $discussion = $discussions[$post->discussion];
1122             $forum = $forums[$discussion->forum];
1123             $context = $contexts[$forum->id];
1125             // Mark the post as being read by user1.
1126             forum_tp_add_read_record($user1->id, $post->id);
1127         }
1129         // Rate and tag all posts.
1130         $ratedposts = [];
1131         foreach ($users as $user) {
1132             foreach ($posts as $post) {
1133                 $discussion = $discussions[$post->discussion];
1134                 $forum = $forums[$discussion->forum];
1135                 $context = $contexts[$forum->id];
1137                 // Tag the post.
1138                 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1140                 // Rate the other users content.
1141                 if ($post->userid != $user->id) {
1142                     $ratedposts[$post->id] = $post;
1143                     $rm = new rating_manager();
1144                     $ratingoptions = (object) [
1145                         'context' => $context,
1146                         'component' => 'mod_forum',
1147                         'ratingarea' => 'post',
1148                         'itemid' => $post->id,
1149                         'scaleid' => $forum->scale,
1150                         'userid' => $user->id,
1151                     ];
1153                     $rating = new \rating($ratingoptions);
1154                     $rating->update_rating(75);
1155                 }
1156             }
1157         }
1159         // Delete for one of the forums for the first user.
1160         $firstcontext = reset($contexts);
1162         $deletedpostids = [];
1163         $otherpostids = [];
1164         foreach ($postsbyforum as $user => $contexts) {
1165             foreach ($contexts as $thiscontextid => $theseposts) {
1166                 $thesepostids = array_map(function($post) {
1167                     return $post->id;
1168                 }, $theseposts);
1170                 if ($user == $user1->id && $thiscontextid == $firstcontext->id) {
1171                     // This post is in the deleted context and by the target user.
1172                     $deletedpostids = array_merge($deletedpostids, $thesepostids);
1173                 } else {
1174                     // This post is by another user, or in a non-target context.
1175                     $otherpostids = array_merge($otherpostids, $thesepostids);
1176                 }
1177             }
1178         }
1179         list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED);
1180         list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED);
1182         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
1183             \core_user::get_user($user1->id),
1184             'mod_forum',
1185             [$firstcontext->id]
1186         );
1187         provider::delete_data_for_user($approvedcontextlist);
1189         // All posts should remain.
1190         $this->assertCount(40, $DB->get_records('forum_posts'));
1192         // There should be 8 posts belonging to user1.
1193         $this->assertCount(8, $DB->get_records('forum_posts', [
1194                 'userid' => $user1->id,
1195             ]));
1197         // Four of those posts should have been marked as deleted.
1198         // That means that the deleted flag is set, and both the subject and message are empty.
1199         $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
1200                     . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
1201                     . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
1202                 , [
1203                     'userid' => $user1->id,
1204                     'deleted' => 1,
1205                     'subject' => '',
1206                     'message' => '',
1207                 ]));
1209         // Only user1's posts should have been marked this way.
1210         $this->assertCount(4, $DB->get_records('forum_posts', [
1211                 'deleted' => 1,
1212             ]));
1213         $this->assertCount(4, $DB->get_records_select('forum_posts',
1214             $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
1215                 'subject' => '',
1216             ]));
1217         $this->assertCount(4, $DB->get_records_select('forum_posts',
1218             $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
1219                 'message' => '',
1220             ]));
1222         // Only the posts in the first discussion should have been marked this way.
1223         $this->assertCount(4, $DB->get_records_select('forum_posts',
1224             "deleted = :deleted AND id {$postinsql}",
1225                 array_merge($postinparams, [
1226                     'deleted' => 1,
1227                 ])
1228             ));
1230         // Ratings should have been removed from the affected posts.
1231         $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
1233         // Ratings should remain on posts in the other context, and posts not belonging to the affected user.
1234         $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
1236         // Ratings should remain where the user has rated another person's post.
1237         $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
1239         // Tags for the affected posts should be removed.
1240         $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
1242         // Tags should remain for the other posts by this user, and all posts by other users.
1243         $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
1245         // Files for the affected posts should be removed.
1246         // 5 users * 2 forums * 1 file in each forum
1247         // Original total: 10
1248         // One post with file removed.
1249         $this->assertCount(0, $DB->get_records_select('files', "itemid {$postinsql}", $postinparams));
1251         // Files for the other posts should remain.
1252         $this->assertCount(18, $DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));
1253     }
1255     /**
1256      * Ensure that user data for specific users is deleted from a specified context.
1257      */
1258     public function test_delete_data_for_users() {
1259         global $DB;
1261         $fs = get_file_storage();
1262         $course = $this->getDataGenerator()->create_course();
1263         $users = $this->helper_create_users($course, 5);
1265         $forums = [];
1266         $contexts = [];
1267         for ($i = 0; $i < 2; $i++) {
1268             $forum = $this->getDataGenerator()->create_module('forum', [
1269                 'course' => $course->id,
1270                 'scale' => 100,
1271             ]);
1272             $cm = get_coursemodule_from_instance('forum', $forum->id);
1273             $context = \context_module::instance($cm->id);
1274             $forums[$forum->id] = $forum;
1275             $contexts[$forum->id] = $context;
1276         }
1278         $discussions = [];
1279         $posts = [];
1280         $postsbyforum = [];
1281         foreach ($users as $user) {
1282             $postsbyforum[$user->id] = [];
1283             foreach ($forums as $forum) {
1284                 $context = $contexts[$forum->id];
1286                 // Create a new discussion + post in the forum.
1287                 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1288                 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1289                 $discussions[$discussion->id] = $discussion;
1290                 $postsbyforum[$user->id][$context->id] = [];
1292                 // Add a number of replies.
1293                 $posts[$post->id] = $post;
1294                 $thisforumposts[$post->id] = $post;
1295                 $postsbyforum[$user->id][$context->id][$post->id] = $post;
1297                 $reply = $this->helper_reply_to_post($post, $user);
1298                 $posts[$reply->id] = $reply;
1299                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1301                 $reply = $this->helper_reply_to_post($post, $user);
1302                 $posts[$reply->id] = $reply;
1303                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1305                 $reply = $this->helper_reply_to_post($reply, $user);
1306                 $posts[$reply->id] = $reply;
1307                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1309                 // Add a fake inline image to the original post.
1310                 $fs->create_file_from_string([
1311                         'contextid' => $context->id,
1312                         'component' => 'mod_forum',
1313                         'filearea'  => 'post',
1314                         'itemid'    => $post->id,
1315                         'filepath'  => '/',
1316                         'filename'  => 'example.jpg',
1317                     ], 'image contents (not really)');
1318                 // And a fake attachment.
1319                 $fs->create_file_from_string([
1320                         'contextid' => $context->id,
1321                         'component' => 'mod_forum',
1322                         'filearea'  => 'attachment',
1323                         'itemid'    => $post->id,
1324                         'filepath'  => '/',
1325                         'filename'  => 'example.jpg',
1326                     ], 'image contents (not really)');
1327             }
1328         }
1330         // Mark all posts as read by user1.
1331         $user1 = reset($users);
1332         foreach ($posts as $post) {
1333             $discussion = $discussions[$post->discussion];
1334             $forum = $forums[$discussion->forum];
1335             $context = $contexts[$forum->id];
1337             // Mark the post as being read by user1.
1338             forum_tp_add_read_record($user1->id, $post->id);
1339         }
1341         // Rate and tag all posts.
1342         $ratedposts = [];
1343         foreach ($users as $user) {
1344             foreach ($posts as $post) {
1345                 $discussion = $discussions[$post->discussion];
1346                 $forum = $forums[$discussion->forum];
1347                 $context = $contexts[$forum->id];
1349                 // Tag the post.
1350                 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1352                 // Rate the other users content.
1353                 if ($post->userid != $user->id) {
1354                     $ratedposts[$post->id] = $post;
1355                     $rm = new rating_manager();
1356                     $ratingoptions = (object) [
1357                         'context' => $context,
1358                         'component' => 'mod_forum',
1359                         'ratingarea' => 'post',
1360                         'itemid' => $post->id,
1361                         'scaleid' => $forum->scale,
1362                         'userid' => $user->id,
1363                     ];
1365                     $rating = new \rating($ratingoptions);
1366                     $rating->update_rating(75);
1367                 }
1368             }
1369         }
1371         // Delete for one of the forums for the first user.
1372         $firstcontext = reset($contexts);
1374         $deletedpostids = [];
1375         $otherpostids = [];
1376         foreach ($postsbyforum as $user => $contexts) {
1377             foreach ($contexts as $thiscontextid => $theseposts) {
1378                 $thesepostids = array_map(function($post) {
1379                     return $post->id;
1380                 }, $theseposts);
1382                 if ($user == $user1->id && $thiscontextid == $firstcontext->id) {
1383                     // This post is in the deleted context and by the target user.
1384                     $deletedpostids = array_merge($deletedpostids, $thesepostids);
1385                 } else {
1386                     // This post is by another user, or in a non-target context.
1387                     $otherpostids = array_merge($otherpostids, $thesepostids);
1388                 }
1389             }
1390         }
1391         list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED);
1392         list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED);
1394         $approveduserlist = new \core_privacy\local\request\approved_userlist($firstcontext, 'mod_forum', [$user1->id]);
1395         provider::delete_data_for_users($approveduserlist);
1397         // All posts should remain.
1398         $this->assertCount(40, $DB->get_records('forum_posts'));
1400         // There should be 8 posts belonging to user1.
1401         $this->assertCount(8, $DB->get_records('forum_posts', [
1402                 'userid' => $user1->id,
1403             ]));
1405         // Four of those posts should have been marked as deleted.
1406         // That means that the deleted flag is set, and both the subject and message are empty.
1407         $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
1408                     . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
1409                     . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
1410                 , [
1411                     'userid' => $user1->id,
1412                     'deleted' => 1,
1413                     'subject' => '',
1414                     'message' => '',
1415                 ]));
1417         // Only user1's posts should have been marked this way.
1418         $this->assertCount(4, $DB->get_records('forum_posts', [
1419                 'deleted' => 1,
1420             ]));
1421         $this->assertCount(4, $DB->get_records_select('forum_posts',
1422             $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
1423                 'subject' => '',
1424             ]));
1425         $this->assertCount(4, $DB->get_records_select('forum_posts',
1426             $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
1427                 'message' => '',
1428             ]));
1430         // Only the posts in the first discussion should have been marked this way.
1431         $this->assertCount(4, $DB->get_records_select('forum_posts',
1432             "deleted = :deleted AND id {$postinsql}",
1433                 array_merge($postinparams, [
1434                     'deleted' => 1,
1435                 ])
1436             ));
1438         // Ratings should have been removed from the affected posts.
1439         $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
1441         // Ratings should remain on posts in the other context, and posts not belonging to the affected user.
1442         $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
1444         // Ratings should remain where the user has rated another person's post.
1445         $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
1447         // Tags for the affected posts should be removed.
1448         $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
1450         // Tags should remain for the other posts by this user, and all posts by other users.
1451         $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
1453         // Files for the affected posts should be removed.
1454         // 5 users * 2 forums * 1 file in each forum
1455         // Original total: 10
1456         // One post with file removed.
1457         $this->assertCount(0, $DB->get_records_select('files', "itemid {$postinsql}", $postinparams));
1459         // Files for the other posts should remain.
1460         $this->assertCount(18,
1461                 $DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));
1462     }
1464     /**
1465      * Ensure that the discussion author is listed as a user in the context.
1466      */
1467     public function test_get_users_in_context_post_author() {
1468         global $DB;
1469         $component = 'mod_forum';
1471         $course = $this->getDataGenerator()->create_course();
1473         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1474         $cm = get_coursemodule_from_instance('forum', $forum->id);
1475         $context = \context_module::instance($cm->id);
1477         list($author, $user) = $this->helper_create_users($course, 2);
1479         list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1481         $userlist = new \core_privacy\local\request\userlist($context, $component);
1482         \mod_forum\privacy\provider::get_users_in_context($userlist);
1484         // There should only be one user in the list.
1485         $this->assertCount(1, $userlist);
1486         $this->assertEquals([$author->id], $userlist->get_userids());
1487     }
1489     /**
1490      * Ensure that all post authors are included as a user in the context.
1491      */
1492     public function test_get_users_in_context_post_authors() {
1493         global $DB;
1494         $component = 'mod_forum';
1496         $course = $this->getDataGenerator()->create_course();
1498         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1499         $cm = get_coursemodule_from_instance('forum', $forum->id);
1500         $context = \context_module::instance($cm->id);
1502         list($author, $user, $other) = $this->helper_create_users($course, 3);
1504         list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1505         $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $user);
1506         $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]);
1508         $userlist = new \core_privacy\local\request\userlist($context, $component);
1509         \mod_forum\privacy\provider::get_users_in_context($userlist);
1511         // Two users - author and replier.
1512         $this->assertCount(2, $userlist);
1514         $expected = [$author->id, $user->id];
1515         sort($expected);
1517         $actual = $userlist->get_userids();
1518         sort($actual);
1520         $this->assertEquals($expected, $actual);
1521     }
1523     /**
1524      * Ensure that all post raters are included as a user in the context.
1525      */
1526     public function test_get_users_in_context_post_ratings() {
1527         global $DB;
1528         $component = 'mod_forum';
1530         $course = $this->getDataGenerator()->create_course();
1532         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1533         $cm = get_coursemodule_from_instance('forum', $forum->id);
1534         $context = \context_module::instance($cm->id);
1536         list($author, $user, $other) = $this->helper_create_users($course, 3);
1538         list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1540         // Rate the other users content.
1541         $rm = new rating_manager();
1542         $ratingoptions = (object) [
1543             'context' => $context,
1544             'component' => 'mod_forum',
1545             'ratingarea' => 'post',
1546             'itemid' => $fp1->id,
1547             'scaleid' => $forum->scale,
1548             'userid' => $user->id,
1549         ];
1551         $rating = new \rating($ratingoptions);
1552         $rating->update_rating(75);
1554         $fp1reply = $this->helper_post_to_discussion($forum, $fd1, $author);
1555         $fd1 = $DB->get_record('forum_discussions', ['id' => $fd1->id]);
1557         $userlist = new \core_privacy\local\request\userlist($context, $component);
1558         \mod_forum\privacy\provider::get_users_in_context($userlist);
1560         // Two users - author and rater.
1561         $this->assertCount(2, $userlist);
1563         $expected = [$author->id, $user->id];
1564         sort($expected);
1566         $actual = $userlist->get_userids();
1567         sort($actual);
1569         $this->assertEquals($expected, $actual);
1570     }
1572     /**
1573      * Ensure that all users with a digest preference are included as a user in the context.
1574      */
1575     public function test_get_users_in_context_digest_preference() {
1576         global $DB;
1577         $component = 'mod_forum';
1579         $course = $this->getDataGenerator()->create_course();
1581         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1582         $cm = get_coursemodule_from_instance('forum', $forum->id);
1583         $context = \context_module::instance($cm->id);
1585         $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1586         $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1587         $othercontext = \context_module::instance($othercm->id);
1589         list($user, $otheruser) = $this->helper_create_users($course, 2);
1591         // Add digest subscriptions.
1592         forum_set_user_maildigest($forum, 0, $user);
1593         forum_set_user_maildigest($otherforum, 0, $otheruser);
1595         $userlist = new \core_privacy\local\request\userlist($context, $component);
1596         \mod_forum\privacy\provider::get_users_in_context($userlist);
1598         // One user - the one with a digest preference.
1599         $this->assertCount(1, $userlist);
1601         $expected = [$user->id];
1602         sort($expected);
1604         $actual = $userlist->get_userids();
1605         sort($actual);
1607         $this->assertEquals($expected, $actual);
1608     }
1610     /**
1611      * Ensure that all users with a forum subscription preference included as a user in the context.
1612      */
1613     public function test_get_users_in_context_with_subscription() {
1614         global $DB;
1615         $component = 'mod_forum';
1617         $course = $this->getDataGenerator()->create_course();
1619         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1620         $cm = get_coursemodule_from_instance('forum', $forum->id);
1621         $context = \context_module::instance($cm->id);
1623         $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1624         $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1625         $othercontext = \context_module::instance($othercm->id);
1627         list($user, $otheruser) = $this->helper_create_users($course, 2);
1629         // Subscribe the user to the forum.
1630         \mod_forum\subscriptions::subscribe_user($user->id, $forum);
1632         $userlist = new \core_privacy\local\request\userlist($context, $component);
1633         \mod_forum\privacy\provider::get_users_in_context($userlist);
1635         // One user - the one with a digest preference.
1636         $this->assertCount(1, $userlist);
1638         $expected = [$user->id];
1639         sort($expected);
1641         $actual = $userlist->get_userids();
1642         sort($actual);
1644         $this->assertEquals($expected, $actual);
1645     }
1647     /**
1648      * Ensure that all users with a per-discussion subscription preference included as a user in the context.
1649      */
1650     public function test_get_users_in_context_with_discussion_subscription() {
1651         global $DB;
1652         $component = 'mod_forum';
1654         $course = $this->getDataGenerator()->create_course();
1656         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1657         $cm = get_coursemodule_from_instance('forum', $forum->id);
1658         $context = \context_module::instance($cm->id);
1660         $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1661         $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1662         $othercontext = \context_module::instance($othercm->id);
1664         list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
1666         // Post in both of the forums.
1667         list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1668         list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author);
1670         // Subscribe the user to the discussions.
1671         \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $fd1);
1672         \mod_forum\subscriptions::subscribe_user_to_discussion($otheruser->id, $ofd1);
1674         $userlist = new \core_privacy\local\request\userlist($context, $component);
1675         \mod_forum\privacy\provider::get_users_in_context($userlist);
1677         // Two users - the author, and the one who subscribed.
1678         $this->assertCount(2, $userlist);
1680         $expected = [$author->id, $user->id];
1681         sort($expected);
1683         $actual = $userlist->get_userids();
1684         sort($actual);
1686         $this->assertEquals($expected, $actual);
1687     }
1689     /**
1690      * Ensure that all users with read tracking are included as a user in the context.
1691      */
1692     public function test_get_users_in_context_with_read_post_tracking() {
1693         global $DB;
1694         $component = 'mod_forum';
1696         $course = $this->getDataGenerator()->create_course();
1698         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1699         $cm = get_coursemodule_from_instance('forum', $forum->id);
1700         $context = \context_module::instance($cm->id);
1702         $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1703         $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1704         $othercontext = \context_module::instance($othercm->id);
1706         list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
1708         // Post in both of the forums.
1709         list($fd1, $fp1) = $this->helper_post_to_forum($forum, $author);
1710         list($ofd1, $ofp1) = $this->helper_post_to_forum($otherforum, $author);
1712         // Add read information for those users.
1713         forum_tp_add_read_record($user->id, $fp1->id);
1714         forum_tp_add_read_record($otheruser->id, $ofp1->id);
1716         $userlist = new \core_privacy\local\request\userlist($context, $component);
1717         \mod_forum\privacy\provider::get_users_in_context($userlist);
1719         // Two user - the author, and the one who has read the post.
1720         $this->assertCount(2, $userlist);
1722         $expected = [$author->id, $user->id];
1723         sort($expected);
1725         $actual = $userlist->get_userids();
1726         sort($actual);
1728         $this->assertEquals($expected, $actual);
1729     }
1731     /**
1732      * Ensure that all users with tracking preferences are included as a user in the context.
1733      */
1734     public function test_get_users_in_context_with_tracking_preferences() {
1735         global $DB;
1736         $component = 'mod_forum';
1738         $course = $this->getDataGenerator()->create_course();
1740         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1741         $cm = get_coursemodule_from_instance('forum', $forum->id);
1742         $context = \context_module::instance($cm->id);
1744         $otherforum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1745         $othercm = get_coursemodule_from_instance('forum', $otherforum->id);
1746         $othercontext = \context_module::instance($othercm->id);
1748         list($author, $user, $otheruser) = $this->helper_create_users($course, 3);
1750         // Forum tracking is opt-out.
1751         // Stop tracking the read posts.
1752         forum_tp_stop_tracking($forum->id, $user->id);
1753         forum_tp_stop_tracking($otherforum->id, $otheruser->id);
1755         $userlist = new \core_privacy\local\request\userlist($context, $component);
1756         \mod_forum\privacy\provider::get_users_in_context($userlist);
1758         // One user - the one who is tracking that forum.
1759         $this->assertCount(1, $userlist);
1761         $expected = [$user->id];
1762         sort($expected);
1764         $actual = $userlist->get_userids();
1765         sort($actual);
1767         $this->assertEquals($expected, $actual);
1768     }