Merge branch 'MDL-63632-master' of git://github.com/andrewnicols/moodle
[moodle.git] / mod / forum / classes / privacy / provider.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  * Privacy Subsystem implementation for mod_forum.
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 namespace mod_forum\privacy;
27 use \core_privacy\local\request\approved_contextlist;
28 use \core_privacy\local\request\deletion_criteria;
29 use \core_privacy\local\request\writer;
30 use \core_privacy\local\request\helper as request_helper;
31 use \core_privacy\local\metadata\collection;
32 use \core_privacy\local\request\transform;
34 defined('MOODLE_INTERNAL') || die();
36 /**
37  * Implementation of the privacy subsystem plugin provider for the forum activity module.
38  *
39  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 class provider implements
43     // This plugin has data.
44     \core_privacy\local\metadata\provider,
46     // This plugin currently implements the original plugin\provider interface.
47     \core_privacy\local\request\plugin\provider,
49     // This plugin has some sitewide user preferences to export.
50     \core_privacy\local\request\user_preference_provider
51 {
53     use subcontext_info;
55     /**
56      * Returns meta data about this system.
57      *
58      * @param   collection     $items The initialised collection to add items to.
59      * @return  collection     A listing of user data stored through this system.
60      */
61     public static function get_metadata(collection $items) : collection {
62         // The 'forum' table does not store any specific user data.
63         $items->add_database_table('forum_digests', [
64             'forum' => 'privacy:metadata:forum_digests:forum',
65             'userid' => 'privacy:metadata:forum_digests:userid',
66             'maildigest' => 'privacy:metadata:forum_digests:maildigest',
67         ], 'privacy:metadata:forum_digests');
69         // The 'forum_discussions' table stores the metadata about each forum discussion.
70         $items->add_database_table('forum_discussions', [
71             'name' => 'privacy:metadata:forum_discussions:name',
72             'userid' => 'privacy:metadata:forum_discussions:userid',
73             'assessed' => 'privacy:metadata:forum_discussions:assessed',
74             'timemodified' => 'privacy:metadata:forum_discussions:timemodified',
75             'usermodified' => 'privacy:metadata:forum_discussions:usermodified',
76         ], 'privacy:metadata:forum_discussions');
78         // The 'forum_discussion_subs' table stores information about which discussions a user is subscribed to.
79         $items->add_database_table('forum_discussion_subs', [
80             'discussionid' => 'privacy:metadata:forum_discussion_subs:discussionid',
81             'preference' => 'privacy:metadata:forum_discussion_subs:preference',
82             'userid' => 'privacy:metadata:forum_discussion_subs:userid',
83         ], 'privacy:metadata:forum_discussion_subs');
85         // The 'forum_posts' table stores the metadata about each forum discussion.
86         $items->add_database_table('forum_posts', [
87             'discussion' => 'privacy:metadata:forum_posts:discussion',
88             'parent' => 'privacy:metadata:forum_posts:parent',
89             'created' => 'privacy:metadata:forum_posts:created',
90             'modified' => 'privacy:metadata:forum_posts:modified',
91             'subject' => 'privacy:metadata:forum_posts:subject',
92             'message' => 'privacy:metadata:forum_posts:message',
93             'userid' => 'privacy:metadata:forum_posts:userid',
94         ], 'privacy:metadata:forum_posts');
96         // The 'forum_queue' table contains user data, but it is only a temporary cache of other data.
97         // We should not need to export it as it does not allow profiling of a user.
99         // The 'forum_read' table stores data about which forum posts have been read by each user.
100         $items->add_database_table('forum_read', [
101             'userid' => 'privacy:metadata:forum_read:userid',
102             'discussionid' => 'privacy:metadata:forum_read:discussionid',
103             'postid' => 'privacy:metadata:forum_read:postid',
104             'firstread' => 'privacy:metadata:forum_read:firstread',
105             'lastread' => 'privacy:metadata:forum_read:lastread',
106         ], 'privacy:metadata:forum_read');
108         // The 'forum_subscriptions' table stores information about which forums a user is subscribed to.
109         $items->add_database_table('forum_subscriptions', [
110             'userid' => 'privacy:metadata:forum_subscriptions:userid',
111             'forum' => 'privacy:metadata:forum_subscriptions:forum',
112         ], 'privacy:metadata:forum_subscriptions');
114         // The 'forum_subscriptions' table stores information about which forums a user is subscribed to.
115         $items->add_database_table('forum_track_prefs', [
116             'userid' => 'privacy:metadata:forum_track_prefs:userid',
117             'forumid' => 'privacy:metadata:forum_track_prefs:forumid',
118         ], 'privacy:metadata:forum_track_prefs');
120         // The 'forum_queue' table stores temporary data that is not exported/deleted.
121         $items->add_database_table('forum_queue', [
122             'userid' => 'privacy:metadata:forum_queue:userid',
123             'discussionid' => 'privacy:metadata:forum_queue:discussionid',
124             'postid' => 'privacy:metadata:forum_queue:postid',
125             'timemodified' => 'privacy:metadata:forum_queue:timemodified'
126         ], 'privacy:metadata:forum_queue');
128         // Forum posts can be tagged and rated.
129         $items->link_subsystem('core_tag', 'privacy:metadata:core_tag');
130         $items->link_subsystem('core_rating', 'privacy:metadata:core_rating');
132         // There are several user preferences.
133         $items->add_user_preference('maildigest', 'privacy:metadata:preference:maildigest');
134         $items->add_user_preference('autosubscribe', 'privacy:metadata:preference:autosubscribe');
135         $items->add_user_preference('trackforums', 'privacy:metadata:preference:trackforums');
136         $items->add_user_preference('markasreadonnotification', 'privacy:metadata:preference:markasreadonnotification');
138         return $items;
139     }
141     /**
142      * Get the list of contexts that contain user information for the specified user.
143      *
144      * In the case of forum, that is any forum where the user has made any post, rated any content, or has any preferences.
145      *
146      * @param   int         $userid     The user to search.
147      * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
148      */
149     public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
150         $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
151         // Fetch all forum discussions, and forum posts.
152         $sql = "SELECT c.id
153                   FROM {context} c
154                   JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
155                   JOIN {modules} m ON m.id = cm.module AND m.name = :modname
156                   JOIN {forum} f ON f.id = cm.instance
157              LEFT JOIN {forum_discussions} d ON d.forum = f.id
158              LEFT JOIN {forum_posts} p ON p.discussion = d.id
159              LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
160              LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
161              LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
162              LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id AND hasread.userid = :hasreaduserid
163              LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id AND dsub.userid = :dsubuserid
164              {$ratingsql->join}
165                  WHERE (
166                     p.userid        = :postuserid OR
167                     d.userid        = :discussionuserid OR
168                     dig.id IS NOT NULL OR
169                     sub.id IS NOT NULL OR
170                     pref.id IS NOT NULL OR
171                     hasread.id IS NOT NULL OR
172                     dsub.id IS NOT NULL OR
173                     {$ratingsql->userwhere}
174                 )
175         ";
176         $params = [
177             'modname'           => 'forum',
178             'contextlevel'      => CONTEXT_MODULE,
179             'postuserid'        => $userid,
180             'discussionuserid'  => $userid,
181             'digestuserid'      => $userid,
182             'subuserid'         => $userid,
183             'prefuserid'        => $userid,
184             'hasreaduserid'     => $userid,
185             'dsubuserid'        => $userid,
186         ];
187         $params += $ratingsql->params;
189         $contextlist = new \core_privacy\local\request\contextlist();
190         $contextlist->add_from_sql($sql, $params);
192         return $contextlist;
193     }
195     /**
196      * Store all user preferences for the plugin.
197      *
198      * @param   int         $userid The userid of the user whose data is to be exported.
199      */
200     public static function export_user_preferences(int $userid) {
201         $user = \core_user::get_user($userid);
203         switch ($user->maildigest) {
204             case 1:
205                 $digestdescription = get_string('emaildigestcomplete');
206                 break;
207             case 2:
208                 $digestdescription = get_string('emaildigestsubjects');
209                 break;
210             case 0:
211             default:
212                 $digestdescription = get_string('emaildigestoff');
213                 break;
214         }
215         writer::export_user_preference('mod_forum', 'maildigest', $user->maildigest, $digestdescription);
217         switch ($user->autosubscribe) {
218             case 0:
219                 $subscribedescription = get_string('autosubscribeno');
220                 break;
221             case 1:
222             default:
223                 $subscribedescription = get_string('autosubscribeyes');
224                 break;
225         }
226         writer::export_user_preference('mod_forum', 'autosubscribe', $user->autosubscribe, $subscribedescription);
228         switch ($user->trackforums) {
229             case 0:
230                 $trackforumdescription = get_string('trackforumsno');
231                 break;
232             case 1:
233             default:
234                 $trackforumdescription = get_string('trackforumsyes');
235                 break;
236         }
237         writer::export_user_preference('mod_forum', 'trackforums', $user->trackforums, $trackforumdescription);
239         $markasreadonnotification = get_user_preferences('markasreadonnotification', null, $user->id);
240         if (null !== $markasreadonnotification) {
241             switch ($markasreadonnotification) {
242                 case 0:
243                     $markasreadonnotificationdescription = get_string('markasreadonnotificationno', 'mod_forum');
244                     break;
245                 case 1:
246                 default:
247                     $markasreadonnotificationdescription = get_string('markasreadonnotificationyes', 'mod_forum');
248                     break;
249             }
250             writer::export_user_preference('mod_forum', 'markasreadonnotification', $markasreadonnotification,
251                     $markasreadonnotificationdescription);
252         }
253     }
255     /**
256      * Export all user data for the specified user, in the specified contexts.
257      *
258      * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
259      */
260     public static function export_user_data(approved_contextlist $contextlist) {
261         global $DB;
263         if (empty($contextlist)) {
264             return;
265         }
267         $user = $contextlist->get_user();
268         $userid = $user->id;
270         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
272         $sql = "SELECT
273                     c.id AS contextid,
274                     f.*,
275                     cm.id AS cmid,
276                     dig.maildigest,
277                     sub.userid AS subscribed,
278                     pref.userid AS tracked
279                   FROM {context} c
280                   JOIN {course_modules} cm ON cm.id = c.instanceid
281                   JOIN {forum} f ON f.id = cm.instance
282              LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
283              LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
284              LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
285                  WHERE (
286                     c.id {$contextsql}
287                 )
288         ";
290         $params = [
291             'digestuserid'  => $userid,
292             'subuserid'     => $userid,
293             'prefuserid'    => $userid,
294         ];
295         $params += $contextparams;
297         // Keep a mapping of forumid to contextid.
298         $mappings = [];
300         $forums = $DB->get_recordset_sql($sql, $params);
301         foreach ($forums as $forum) {
302             $mappings[$forum->id] = $forum->contextid;
304             $context = \context::instance_by_id($mappings[$forum->id]);
306             // Store the main forum data.
307             $data = request_helper::get_context_data($context, $user);
308             writer::with_context($context)
309                 ->export_data([], $data);
310             request_helper::export_context_files($context, $user);
312             // Store relevant metadata about this forum instance.
313             static::export_digest_data($userid, $forum);
314             static::export_subscription_data($userid, $forum);
315             static::export_tracking_data($userid, $forum);
316         }
317         $forums->close();
319         if (!empty($mappings)) {
320             // Store all discussion data for this forum.
321             static::export_discussion_data($userid, $mappings);
323             // Store all post data for this forum.
324             static::export_all_posts($userid, $mappings);
325         }
326     }
328     /**
329      * Store all information about all discussions that we have detected this user to have access to.
330      *
331      * @param   int         $userid The userid of the user whose data is to be exported.
332      * @param   array       $mappings A list of mappings from forumid => contextid.
333      * @return  array       Which forums had data written for them.
334      */
335     protected static function export_discussion_data(int $userid, array $mappings) {
336         global $DB;
338         // Find all of the discussions, and discussion subscriptions for this forum.
339         list($foruminsql, $forumparams) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED);
340         $sql = "SELECT
341                     d.*,
342                     g.name as groupname,
343                     dsub.preference
344                   FROM {forum} f
345                   JOIN {forum_discussions} d ON d.forum = f.id
346              LEFT JOIN {groups} g ON g.id = d.groupid
347              LEFT JOIN {forum_discussion_subs} dsub ON dsub.discussion = d.id AND dsub.userid = :dsubuserid
348              LEFT JOIN {forum_posts} p ON p.discussion = d.id
349                  WHERE f.id ${foruminsql}
350                    AND (
351                         d.userid    = :discussionuserid OR
352                         p.userid    = :postuserid OR
353                         dsub.id IS NOT NULL
354                    )
355         ";
357         $params = [
358             'postuserid'        => $userid,
359             'discussionuserid'  => $userid,
360             'dsubuserid'        => $userid,
361         ];
362         $params += $forumparams;
364         // Keep track of the forums which have data.
365         $forumswithdata = [];
367         $discussions = $DB->get_recordset_sql($sql, $params);
368         foreach ($discussions as $discussion) {
369             // No need to take timestart into account as the user has some involvement already.
370             // Ignore discussion timeend as it should not block access to user data.
371             $forumswithdata[$discussion->forum] = true;
372             $context = \context::instance_by_id($mappings[$discussion->forum]);
374             // Store related metadata for this discussion.
375             static::export_discussion_subscription_data($userid, $context, $discussion);
377             $discussiondata = (object) [
378                 'name' => format_string($discussion->name, true),
379                 'pinned' => transform::yesno((bool) $discussion->pinned),
380                 'timemodified' => transform::datetime($discussion->timemodified),
381                 'usermodified' => transform::datetime($discussion->usermodified),
382                 'creator_was_you' => transform::yesno($discussion->userid == $userid),
383             ];
385             // Store the discussion content.
386             writer::with_context($context)
387                 ->export_data(static::get_discussion_area($discussion), $discussiondata);
389             // Forum discussions do not have any files associately directly with them.
390         }
392         $discussions->close();
394         return $forumswithdata;
395     }
397     /**
398      * Store all information about all posts that we have detected this user to have access to.
399      *
400      * @param   int         $userid The userid of the user whose data is to be exported.
401      * @param   array       $mappings A list of mappings from forumid => contextid.
402      * @return  array       Which forums had data written for them.
403      */
404     protected static function export_all_posts(int $userid, array $mappings) {
405         global $DB;
407         // Find all of the posts, and post subscriptions for this forum.
408         list($foruminsql, $forumparams) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED);
409         $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
410         $sql = "SELECT
411                     p.discussion AS id,
412                     f.id AS forumid,
413                     d.name,
414                     d.groupid
415                   FROM {forum} f
416                   JOIN {forum_discussions} d ON d.forum = f.id
417                   JOIN {forum_posts} p ON p.discussion = d.id
418              LEFT JOIN {forum_read} fr ON fr.postid = p.id AND fr.userid = :readuserid
419             {$ratingsql->join}
420                  WHERE f.id ${foruminsql} AND
421                 (
422                     p.userid = :postuserid OR
423                     fr.id IS NOT NULL OR
424                     {$ratingsql->userwhere}
425                 )
426               GROUP BY f.id, p.discussion, d.name, d.groupid
427         ";
429         $params = [
430             'postuserid'    => $userid,
431             'readuserid'    => $userid,
432         ];
433         $params += $forumparams;
434         $params += $ratingsql->params;
436         $discussions = $DB->get_records_sql($sql, $params);
437         foreach ($discussions as $discussion) {
438             $context = \context::instance_by_id($mappings[$discussion->forumid]);
439             static::export_all_posts_in_discussion($userid, $context, $discussion);
440         }
441     }
443     /**
444      * Store all information about all posts that we have detected this user to have access to.
445      *
446      * @param   int         $userid The userid of the user whose data is to be exported.
447      * @param   \context    $context The instance of the forum context.
448      * @param   \stdClass   $discussion The discussion whose data is being exported.
449      */
450     protected static function export_all_posts_in_discussion(int $userid, \context $context, \stdClass $discussion) {
451         global $DB, $USER;
453         $discussionid = $discussion->id;
455         // Find all of the posts, and post subscriptions for this forum.
456         $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
457         $sql = "SELECT
458                     p.*,
459                     d.forum AS forumid,
460                     fr.firstread,
461                     fr.lastread,
462                     fr.id AS readflag,
463                     rat.id AS hasratings
464                     FROM {forum_discussions} d
465                     JOIN {forum_posts} p ON p.discussion = d.id
466                LEFT JOIN {forum_read} fr ON fr.postid = p.id AND fr.userid = :readuserid
467             {$ratingsql->join} AND {$ratingsql->userwhere}
468                    WHERE d.id = :discussionid
469         ";
471         $params = [
472             'discussionid'  => $discussionid,
473             'readuserid'    => $userid,
474         ];
475         $params += $ratingsql->params;
477         // Keep track of the forums which have data.
478         $structure = (object) [
479             'children' => [],
480         ];
482         $posts = $DB->get_records_sql($sql, $params);
483         foreach ($posts as $post) {
484             $post->hasdata = (isset($post->hasdata)) ? $post->hasdata : false;
485             $post->hasdata = $post->hasdata || !empty($post->hasratings);
486             $post->hasdata = $post->hasdata || $post->readflag;
487             $post->hasdata = $post->hasdata || ($post->userid == $USER->id);
489             if (0 == $post->parent) {
490                 $structure->children[$post->id] = $post;
491             } else {
492                 if (empty($posts[$post->parent]->children)) {
493                     $posts[$post->parent]->children = [];
494                 }
495                 $posts[$post->parent]->children[$post->id] = $post;
496             }
498             // Set all parents.
499             if ($post->hasdata) {
500                 $curpost = $post;
501                 while ($curpost->parent != 0) {
502                     $curpost = $posts[$curpost->parent];
503                     $curpost->hasdata = true;
504                 }
505             }
506         }
508         $discussionarea = static::get_discussion_area($discussion);
509         $discussionarea[] = get_string('posts', 'mod_forum');
510         static::export_posts_in_structure($userid, $context, $discussionarea, $structure);
511     }
513     /**
514      * Export all posts in the provided structure.
515      *
516      * @param   int         $userid The userid of the user whose data is to be exported.
517      * @param   \context    $context The instance of the forum context.
518      * @param   array       $parentarea The subcontext of the parent.
519      * @param   \stdClass   $structure The post structure and all of its children
520      */
521     protected static function export_posts_in_structure(int $userid, \context $context, $parentarea, \stdClass $structure) {
522         foreach ($structure->children as $post) {
523             if (!$post->hasdata) {
524                 // This tree has no content belonging to the user. Skip it and all children.
525                 continue;
526             }
528             $postarea = array_merge($parentarea, static::get_post_area($post));
530             // Store the post content.
531             static::export_post_data($userid, $context, $postarea, $post);
533             if (isset($post->children)) {
534                 // Now export children of this post.
535                 static::export_posts_in_structure($userid, $context, $postarea, $post);
536             }
537         }
538     }
540     /**
541      * Export all data in the post.
542      *
543      * @param   int         $userid The userid of the user whose data is to be exported.
544      * @param   \context    $context The instance of the forum context.
545      * @param   array       $postarea The subcontext of the parent.
546      * @param   \stdClass   $post The post structure and all of its children
547      */
548     protected static function export_post_data(int $userid, \context $context, $postarea, $post) {
549         // Store related metadata.
550         static::export_read_data($userid, $context, $postarea, $post);
552         $postdata = (object) [
553             'subject' => format_string($post->subject, true),
554             'created' => transform::datetime($post->created),
555             'modified' => transform::datetime($post->modified),
556             'author_was_you' => transform::yesno($post->userid == $userid),
557         ];
559         $postdata->message = writer::with_context($context)
560             ->rewrite_pluginfile_urls($postarea, 'mod_forum', 'post', $post->id, $post->message);
562         $postdata->message = format_text($postdata->message, $post->messageformat, (object) [
563             'para'    => false,
564             'trusted' => $post->messagetrust,
565             'context' => $context,
566         ]);
568         writer::with_context($context)
569             // Store the post.
570             ->export_data($postarea, $postdata)
572             // Store the associated files.
573             ->export_area_files($postarea, 'mod_forum', 'post', $post->id);
575         if ($post->userid == $userid) {
576             // Store all ratings against this post as the post belongs to the user. All ratings on it are ratings of their content.
577             \core_rating\privacy\provider::export_area_ratings($userid, $context, $postarea, 'mod_forum', 'post', $post->id, false);
579             // Store all tags against this post as the tag belongs to the user.
580             \core_tag\privacy\provider::export_item_tags($userid, $context, $postarea, 'mod_forum', 'forum_posts', $post->id);
582             // Export all user data stored for this post from the plagiarism API.
583             $coursecontext = $context->get_course_context();
584             \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $postarea, [
585                     'cmid' => $context->instanceid,
586                     'course' => $coursecontext->instanceid,
587                     'forum' => $post->forumid,
588                     'discussionid' => $post->discussion,
589                     'postid' => $post->id,
590                 ]);
591         }
593         // Check for any ratings that the user has made on this post.
594         \core_rating\privacy\provider::export_area_ratings($userid,
595                 $context,
596                 $postarea,
597                 'mod_forum',
598                 'post',
599                 $post->id,
600                 $userid,
601                 true
602             );
603     }
605     /**
606      * Store data about daily digest preferences
607      *
608      * @param   int         $userid The userid of the user whose data is to be exported.
609      * @param   \stdClass   $forum The forum whose data is being exported.
610      * @return  bool        Whether any data was stored.
611      */
612     protected static function export_digest_data(int $userid, \stdClass $forum) {
613         if (null !== $forum->maildigest) {
614             // The user has a specific maildigest preference for this forum.
615             $a = (object) [
616                 'forum' => format_string($forum->name, true),
617             ];
619             switch ($forum->maildigest) {
620                 case 0:
621                     $a->type = get_string('emaildigestoffshort', 'mod_forum');
622                     break;
623                 case 1:
624                     $a->type = get_string('emaildigestcompleteshort', 'mod_forum');
625                     break;
626                 case 2:
627                     $a->type = get_string('emaildigestsubjectsshort', 'mod_forum');
628                     break;
629             }
631             writer::with_context(\context_module::instance($forum->cmid))
632                 ->export_metadata([], 'digestpreference', $forum->maildigest,
633                     get_string('privacy:digesttypepreference', 'mod_forum', $a));
635             return true;
636         }
638         return false;
639     }
641     /**
642      * Store data about whether the user subscribes to forum.
643      *
644      * @param   int         $userid The userid of the user whose data is to be exported.
645      * @param   \stdClass   $forum The forum whose data is being exported.
646      * @return  bool        Whether any data was stored.
647      */
648     protected static function export_subscription_data(int $userid, \stdClass $forum) {
649         if (null !== $forum->subscribed) {
650             // The user is subscribed to this forum.
651             writer::with_context(\context_module::instance($forum->cmid))
652                 ->export_metadata([], 'subscriptionpreference', 1, get_string('privacy:subscribedtoforum', 'mod_forum'));
654             return true;
655         }
657         return false;
658     }
660     /**
661      * Store data about whether the user subscribes to this particular discussion.
662      *
663      * @param   int         $userid The userid of the user whose data is to be exported.
664      * @param   \context_module $context The instance of the forum context.
665      * @param   \stdClass   $discussion The discussion whose data is being exported.
666      * @return  bool        Whether any data was stored.
667      */
668     protected static function export_discussion_subscription_data(int $userid, \context_module $context, \stdClass $discussion) {
669         $area = static::get_discussion_area($discussion);
670         if (null !== $discussion->preference) {
671             // The user has a specific subscription preference for this discussion.
672             $a = (object) [];
674             switch ($discussion->preference) {
675                 case \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED:
676                     $a->preference = get_string('unsubscribed', 'mod_forum');
677                     break;
678                 default:
679                     $a->preference = get_string('subscribed', 'mod_forum');
680                     break;
681             }
683             writer::with_context($context)
684                 ->export_metadata(
685                     $area,
686                     'subscriptionpreference',
687                     $discussion->preference,
688                     get_string('privacy:discussionsubscriptionpreference', 'mod_forum', $a)
689                 );
691             return true;
692         }
694         return true;
695     }
697     /**
698      * Store forum read-tracking data about a particular forum.
699      *
700      * This is whether a forum has read-tracking enabled or not.
701      *
702      * @param   int         $userid The userid of the user whose data is to be exported.
703      * @param   \stdClass   $forum The forum whose data is being exported.
704      * @return  bool        Whether any data was stored.
705      */
706     protected static function export_tracking_data(int $userid, \stdClass $forum) {
707         if (null !== $forum->tracked) {
708             // The user has a main preference to track all forums, but has opted out of this one.
709             writer::with_context(\context_module::instance($forum->cmid))
710                 ->export_metadata([], 'trackreadpreference', 0, get_string('privacy:readtrackingdisabled', 'mod_forum'));
712             return true;
713         }
715         return false;
716     }
718     /**
719      * Store read-tracking information about a particular forum post.
720      *
721      * @param   int         $userid The userid of the user whose data is to be exported.
722      * @param   \context_module $context The instance of the forum context.
723      * @param   array       $postarea The subcontext for this post.
724      * @param   \stdClass   $post The post whose data is being exported.
725      * @return  bool        Whether any data was stored.
726      */
727     protected static function export_read_data(int $userid, \context_module $context, array $postarea, \stdClass $post) {
728         if (null !== $post->firstread) {
729             $a = (object) [
730                 'firstread' => $post->firstread,
731                 'lastread'  => $post->lastread,
732             ];
734             writer::with_context($context)
735                 ->export_metadata(
736                     $postarea,
737                     'postread',
738                     (object) [
739                         'firstread' => $post->firstread,
740                         'lastread' => $post->lastread,
741                     ],
742                     get_string('privacy:postwasread', 'mod_forum', $a)
743                 );
745             return true;
746         }
748         return false;
749     }
751     /**
752      * Delete all data for all users in the specified context.
753      *
754      * @param   context                 $context   The specific context to delete data for.
755      */
756     public static function delete_data_for_all_users_in_context(\context $context) {
757         global $DB;
759         // Check that this is a context_module.
760         if (!$context instanceof \context_module) {
761             return;
762         }
764         // Get the course module.
765         if (!$cm = get_coursemodule_from_id('forum', $context->instanceid)) {
766             return;
767         }
769         $forumid = $cm->instance;
771         $DB->delete_records('forum_track_prefs', ['forumid' => $forumid]);
772         $DB->delete_records('forum_subscriptions', ['forum' => $forumid]);
773         $DB->delete_records('forum_read', ['forumid' => $forumid]);
774         $DB->delete_records('forum_digests', ['forum' => $forumid]);
776         // Delete all discussion items.
777         $DB->delete_records_select(
778             'forum_queue',
779             "discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
780             [
781                 'forum' => $forumid,
782             ]
783         );
785         $DB->delete_records_select(
786             'forum_posts',
787             "discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
788             [
789                 'forum' => $forumid,
790             ]
791         );
793         $DB->delete_records('forum_discussion_subs', ['forum' => $forumid]);
794         $DB->delete_records('forum_discussions', ['forum' => $forumid]);
796         // Delete all files from the posts.
797         $fs = get_file_storage();
798         $fs->delete_area_files($context->id, 'mod_forum', 'post');
799         $fs->delete_area_files($context->id, 'mod_forum', 'attachment');
801         // Delete all ratings in the context.
802         \core_rating\privacy\provider::delete_ratings($context, 'mod_forum', 'post');
804         // Delete all Tags.
805         \core_tag\privacy\provider::delete_item_tags($context, 'mod_forum', 'forum_posts');
806     }
808     /**
809      * Delete all user data for the specified user, in the specified contexts.
810      *
811      * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
812      */
813     public static function delete_data_for_user(approved_contextlist $contextlist) {
814         global $DB;
815         $user = $contextlist->get_user();
816         $userid = $user->id;
817         foreach ($contextlist as $context) {
818             // Get the course module.
819             $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
820             $forum = $DB->get_record('forum', ['id' => $cm->instance]);
822             $DB->delete_records('forum_track_prefs', [
823                 'forumid' => $forum->id,
824                 'userid' => $userid,
825             ]);
826             $DB->delete_records('forum_subscriptions', [
827                 'forum' => $forum->id,
828                 'userid' => $userid,
829             ]);
830             $DB->delete_records('forum_read', [
831                 'forumid' => $forum->id,
832                 'userid' => $userid,
833             ]);
835             $DB->delete_records('forum_digests', [
836                 'forum' => $forum->id,
837                 'userid' => $userid,
838             ]);
840             // Delete all discussion items.
841             $DB->delete_records_select(
842                 'forum_queue',
843                 "userid = :userid AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
844                 [
845                     'userid' => $userid,
846                     'forum' => $forum->id,
847                 ]
848             );
850             $DB->delete_records('forum_discussion_subs', [
851                 'forum' => $forum->id,
852                 'userid' => $userid,
853             ]);
855             // Do not delete discussion or forum posts.
856             // Instead update them to reflect that the content has been deleted.
857             $postsql = "userid = :userid AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)";
858             $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}";
859             $postparams = [
860                 'forum' => $forum->id,
861                 'userid' => $userid,
862             ];
864             // Update the subject.
865             $DB->set_field_select('forum_posts', 'subject', '', $postsql, $postparams);
867             // Update the message and its format.
868             $DB->set_field_select('forum_posts', 'message', '', $postsql, $postparams);
869             $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $postparams);
871             // Mark the post as deleted.
872             $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $postparams);
874             // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts.
875             // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating
876             // of any post.
877             \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post',
878                     "IN ($postidsql)", $postparams);
880             // Delete all Tags.
881             \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts',
882                     "IN ($postidsql)", $postparams);
884             // Delete all files from the posts.
885             $fs = get_file_storage();
886             $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $postparams);
887             $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $postparams);
888         }
889     }