MDL-63495 mod_forum: Add intial support for removal of multiple context users
[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\userlist;
28 use \core_privacy\local\request\approved_contextlist;
29 use \core_privacy\local\request\approved_userlist;
30 use \core_privacy\local\request\deletion_criteria;
31 use \core_privacy\local\request\writer;
32 use \core_privacy\local\request\helper as request_helper;
33 use \core_privacy\local\metadata\collection;
34 use \core_privacy\local\request\transform;
36 defined('MOODLE_INTERNAL') || die();
38 /**
39  * Implementation of the privacy subsystem plugin provider for the forum activity module.
40  *
41  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class provider implements
45     // This plugin has data.
46     \core_privacy\local\metadata\provider,
48     // This plugin currently implements the original plugin\provider interface.
49     \core_privacy\local\request\plugin\provider,
51     // This plugin is capable of determining which users have data within it.
52     \core_privacy\local\request\core_userlist_provider,
54     // This plugin has some sitewide user preferences to export.
55     \core_privacy\local\request\user_preference_provider
56 {
58     use subcontext_info;
60     /**
61      * Returns meta data about this system.
62      *
63      * @param   collection     $items The initialised collection to add items to.
64      * @return  collection     A listing of user data stored through this system.
65      */
66     public static function get_metadata(collection $items) : collection {
67         // The 'forum' table does not store any specific user data.
68         $items->add_database_table('forum_digests', [
69             'forum' => 'privacy:metadata:forum_digests:forum',
70             'userid' => 'privacy:metadata:forum_digests:userid',
71             'maildigest' => 'privacy:metadata:forum_digests:maildigest',
72         ], 'privacy:metadata:forum_digests');
74         // The 'forum_discussions' table stores the metadata about each forum discussion.
75         $items->add_database_table('forum_discussions', [
76             'name' => 'privacy:metadata:forum_discussions:name',
77             'userid' => 'privacy:metadata:forum_discussions:userid',
78             'assessed' => 'privacy:metadata:forum_discussions:assessed',
79             'timemodified' => 'privacy:metadata:forum_discussions:timemodified',
80             'usermodified' => 'privacy:metadata:forum_discussions:usermodified',
81         ], 'privacy:metadata:forum_discussions');
83         // The 'forum_discussion_subs' table stores information about which discussions a user is subscribed to.
84         $items->add_database_table('forum_discussion_subs', [
85             'discussionid' => 'privacy:metadata:forum_discussion_subs:discussionid',
86             'preference' => 'privacy:metadata:forum_discussion_subs:preference',
87             'userid' => 'privacy:metadata:forum_discussion_subs:userid',
88         ], 'privacy:metadata:forum_discussion_subs');
90         // The 'forum_posts' table stores the metadata about each forum discussion.
91         $items->add_database_table('forum_posts', [
92             'discussion' => 'privacy:metadata:forum_posts:discussion',
93             'parent' => 'privacy:metadata:forum_posts:parent',
94             'created' => 'privacy:metadata:forum_posts:created',
95             'modified' => 'privacy:metadata:forum_posts:modified',
96             'subject' => 'privacy:metadata:forum_posts:subject',
97             'message' => 'privacy:metadata:forum_posts:message',
98             'userid' => 'privacy:metadata:forum_posts:userid',
99         ], 'privacy:metadata:forum_posts');
101         // The 'forum_queue' table contains user data, but it is only a temporary cache of other data.
102         // We should not need to export it as it does not allow profiling of a user.
104         // The 'forum_read' table stores data about which forum posts have been read by each user.
105         $items->add_database_table('forum_read', [
106             'userid' => 'privacy:metadata:forum_read:userid',
107             'discussionid' => 'privacy:metadata:forum_read:discussionid',
108             'postid' => 'privacy:metadata:forum_read:postid',
109             'firstread' => 'privacy:metadata:forum_read:firstread',
110             'lastread' => 'privacy:metadata:forum_read:lastread',
111         ], 'privacy:metadata:forum_read');
113         // The 'forum_subscriptions' table stores information about which forums a user is subscribed to.
114         $items->add_database_table('forum_subscriptions', [
115             'userid' => 'privacy:metadata:forum_subscriptions:userid',
116             'forum' => 'privacy:metadata:forum_subscriptions:forum',
117         ], 'privacy:metadata:forum_subscriptions');
119         // The 'forum_subscriptions' table stores information about which forums a user is subscribed to.
120         $items->add_database_table('forum_track_prefs', [
121             'userid' => 'privacy:metadata:forum_track_prefs:userid',
122             'forumid' => 'privacy:metadata:forum_track_prefs:forumid',
123         ], 'privacy:metadata:forum_track_prefs');
125         // The 'forum_queue' table stores temporary data that is not exported/deleted.
126         $items->add_database_table('forum_queue', [
127             'userid' => 'privacy:metadata:forum_queue:userid',
128             'discussionid' => 'privacy:metadata:forum_queue:discussionid',
129             'postid' => 'privacy:metadata:forum_queue:postid',
130             'timemodified' => 'privacy:metadata:forum_queue:timemodified'
131         ], 'privacy:metadata:forum_queue');
133         // Forum posts can be tagged and rated.
134         $items->link_subsystem('core_tag', 'privacy:metadata:core_tag');
135         $items->link_subsystem('core_rating', 'privacy:metadata:core_rating');
137         // There are several user preferences.
138         $items->add_user_preference('maildigest', 'privacy:metadata:preference:maildigest');
139         $items->add_user_preference('autosubscribe', 'privacy:metadata:preference:autosubscribe');
140         $items->add_user_preference('trackforums', 'privacy:metadata:preference:trackforums');
141         $items->add_user_preference('markasreadonnotification', 'privacy:metadata:preference:markasreadonnotification');
143         return $items;
144     }
146     /**
147      * Get the list of contexts that contain user information for the specified user.
148      *
149      * In the case of forum, that is any forum where the user has made any post, rated any content, or has any preferences.
150      *
151      * @param   int         $userid     The user to search.
152      * @return  contextlist $contextlist  The contextlist containing the list of contexts used in this plugin.
153      */
154     public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
155         $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
156         // Fetch all forum discussions, and forum posts.
157         $sql = "SELECT c.id
158                   FROM {context} c
159                   JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
160                   JOIN {modules} m ON m.id = cm.module AND m.name = :modname
161                   JOIN {forum} f ON f.id = cm.instance
162              LEFT JOIN {forum_discussions} d ON d.forum = f.id
163              LEFT JOIN {forum_posts} p ON p.discussion = d.id
164              LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
165              LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
166              LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
167              LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id AND hasread.userid = :hasreaduserid
168              LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id AND dsub.userid = :dsubuserid
169              {$ratingsql->join}
170                  WHERE (
171                     p.userid        = :postuserid OR
172                     d.userid        = :discussionuserid OR
173                     dig.id IS NOT NULL OR
174                     sub.id IS NOT NULL OR
175                     pref.id IS NOT NULL OR
176                     hasread.id IS NOT NULL OR
177                     dsub.id IS NOT NULL OR
178                     {$ratingsql->userwhere}
179                 )
180         ";
181         $params = [
182             'modname'           => 'forum',
183             'contextlevel'      => CONTEXT_MODULE,
184             'postuserid'        => $userid,
185             'discussionuserid'  => $userid,
186             'digestuserid'      => $userid,
187             'subuserid'         => $userid,
188             'prefuserid'        => $userid,
189             'hasreaduserid'     => $userid,
190             'dsubuserid'        => $userid,
191         ];
192         $params += $ratingsql->params;
194         $contextlist = new \core_privacy\local\request\contextlist();
195         $contextlist->add_from_sql($sql, $params);
197         return $contextlist;
198     }
200     /**
201      * Get the list of users within a specific context.
202      *
203      * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
204      */
205     public static function get_users_in_context(userlist $userlist) {
206         $context = $userlist->get_context();
208         if (!is_a($context, \context_module::class)) {
209             return;
210         }
212         $params = [
213             'instanceid'    => $context->instanceid,
214             'modulename'    => 'forum',
215         ];
217         // Discussion authors.
218         $sql = "SELECT d.userid
219                   FROM {course_modules} cm
220                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
221                   JOIN {forum} f ON f.id = cm.instance
222                   JOIN {forum_discussions} d ON d.forum = f.id
223                  WHERE cm.id = :instanceid";
224         $userlist->add_from_sql('userid', $sql, $params);
226         // Forum authors.
227         $sql = "SELECT p.userid
228                   FROM {course_modules} cm
229                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
230                   JOIN {forum} f ON f.id = cm.instance
231                   JOIN {forum_discussions} d ON d.forum = f.id
232                   JOIN {forum_posts} p ON d.id = p.discussion
233                  WHERE cm.id = :instanceid";
234         $userlist->add_from_sql('userid', $sql, $params);
236         // Forum post ratings.
237         $sql = "SELECT p.id
238                   FROM {course_modules} cm
239                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
240                   JOIN {forum} f ON f.id = cm.instance
241                   JOIN {forum_discussions} d ON d.forum = f.id
242                   JOIN {forum_posts} p ON d.id = p.discussion
243                  WHERE cm.id = :instanceid";
244         \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_forum', 'post', $sql, $params);
246         // Forum Digest settings.
247         $sql = "SELECT dig.userid
248                   FROM {course_modules} cm
249                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
250                   JOIN {forum} f ON f.id = cm.instance
251                   JOIN {forum_digests} dig ON dig.forum = f.id
252                  WHERE cm.id = :instanceid";
253         $userlist->add_from_sql('userid', $sql, $params);
255         // Forum Subscriptions.
256         $sql = "SELECT sub.userid
257                   FROM {course_modules} cm
258                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
259                   JOIN {forum} f ON f.id = cm.instance
260                   JOIN {forum_subscriptions} sub ON sub.forum = f.id
261                  WHERE cm.id = :instanceid";
262         $userlist->add_from_sql('userid', $sql, $params);
264         // Discussion subscriptions.
265         $sql = "SELECT dsub.userid
266                   FROM {course_modules} cm
267                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
268                   JOIN {forum} f ON f.id = cm.instance
269                   JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id
270                  WHERE cm.id = :instanceid";
271         $userlist->add_from_sql('userid', $sql, $params);
273         // Read Posts.
274         $sql = "SELECT hasread.userid
275                   FROM {course_modules} cm
276                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
277                   JOIN {forum} f ON f.id = cm.instance
278                   JOIN {forum_read} hasread ON hasread.forumid = f.id
279                  WHERE cm.id = :instanceid";
280         $userlist->add_from_sql('userid', $sql, $params);
282         // Tracking Preferences.
283         $sql = "SELECT pref.userid
284                   FROM {course_modules} cm
285                   JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
286                   JOIN {forum} f ON f.id = cm.instance
287                   JOIN {forum_track_prefs} pref ON pref.forumid = f.id
288                  WHERE cm.id = :instanceid";
289         $userlist->add_from_sql('userid', $sql, $params);
290     }
292     /**
293      * Store all user preferences for the plugin.
294      *
295      * @param   int         $userid The userid of the user whose data is to be exported.
296      */
297     public static function export_user_preferences(int $userid) {
298         $user = \core_user::get_user($userid);
300         switch ($user->maildigest) {
301             case 1:
302                 $digestdescription = get_string('emaildigestcomplete');
303                 break;
304             case 2:
305                 $digestdescription = get_string('emaildigestsubjects');
306                 break;
307             case 0:
308             default:
309                 $digestdescription = get_string('emaildigestoff');
310                 break;
311         }
312         writer::export_user_preference('mod_forum', 'maildigest', $user->maildigest, $digestdescription);
314         switch ($user->autosubscribe) {
315             case 0:
316                 $subscribedescription = get_string('autosubscribeno');
317                 break;
318             case 1:
319             default:
320                 $subscribedescription = get_string('autosubscribeyes');
321                 break;
322         }
323         writer::export_user_preference('mod_forum', 'autosubscribe', $user->autosubscribe, $subscribedescription);
325         switch ($user->trackforums) {
326             case 0:
327                 $trackforumdescription = get_string('trackforumsno');
328                 break;
329             case 1:
330             default:
331                 $trackforumdescription = get_string('trackforumsyes');
332                 break;
333         }
334         writer::export_user_preference('mod_forum', 'trackforums', $user->trackforums, $trackforumdescription);
336         $markasreadonnotification = get_user_preferences('markasreadonnotification', null, $user->id);
337         if (null !== $markasreadonnotification) {
338             switch ($markasreadonnotification) {
339                 case 0:
340                     $markasreadonnotificationdescription = get_string('markasreadonnotificationno', 'mod_forum');
341                     break;
342                 case 1:
343                 default:
344                     $markasreadonnotificationdescription = get_string('markasreadonnotificationyes', 'mod_forum');
345                     break;
346             }
347             writer::export_user_preference('mod_forum', 'markasreadonnotification', $markasreadonnotification,
348                     $markasreadonnotificationdescription);
349         }
350     }
352     /**
353      * Export all user data for the specified user, in the specified contexts.
354      *
355      * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
356      */
357     public static function export_user_data(approved_contextlist $contextlist) {
358         global $DB;
360         if (empty($contextlist)) {
361             return;
362         }
364         $user = $contextlist->get_user();
365         $userid = $user->id;
367         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
369         $sql = "SELECT
370                     c.id AS contextid,
371                     f.*,
372                     cm.id AS cmid,
373                     dig.maildigest,
374                     sub.userid AS subscribed,
375                     pref.userid AS tracked
376                   FROM {context} c
377                   JOIN {course_modules} cm ON cm.id = c.instanceid
378                   JOIN {forum} f ON f.id = cm.instance
379              LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
380              LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
381              LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
382                  WHERE (
383                     c.id {$contextsql}
384                 )
385         ";
387         $params = [
388             'digestuserid'  => $userid,
389             'subuserid'     => $userid,
390             'prefuserid'    => $userid,
391         ];
392         $params += $contextparams;
394         // Keep a mapping of forumid to contextid.
395         $mappings = [];
397         $forums = $DB->get_recordset_sql($sql, $params);
398         foreach ($forums as $forum) {
399             $mappings[$forum->id] = $forum->contextid;
401             $context = \context::instance_by_id($mappings[$forum->id]);
403             // Store the main forum data.
404             $data = request_helper::get_context_data($context, $user);
405             writer::with_context($context)
406                 ->export_data([], $data);
407             request_helper::export_context_files($context, $user);
409             // Store relevant metadata about this forum instance.
410             static::export_digest_data($userid, $forum);
411             static::export_subscription_data($userid, $forum);
412             static::export_tracking_data($userid, $forum);
413         }
414         $forums->close();
416         if (!empty($mappings)) {
417             // Store all discussion data for this forum.
418             static::export_discussion_data($userid, $mappings);
420             // Store all post data for this forum.
421             static::export_all_posts($userid, $mappings);
422         }
423     }
425     /**
426      * Store all information about all discussions that we have detected this user to have access to.
427      *
428      * @param   int         $userid The userid of the user whose data is to be exported.
429      * @param   array       $mappings A list of mappings from forumid => contextid.
430      * @return  array       Which forums had data written for them.
431      */
432     protected static function export_discussion_data(int $userid, array $mappings) {
433         global $DB;
435         // Find all of the discussions, and discussion subscriptions for this forum.
436         list($foruminsql, $forumparams) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED);
437         $sql = "SELECT
438                     d.*,
439                     g.name as groupname,
440                     dsub.preference
441                   FROM {forum} f
442                   JOIN {forum_discussions} d ON d.forum = f.id
443              LEFT JOIN {groups} g ON g.id = d.groupid
444              LEFT JOIN {forum_discussion_subs} dsub ON dsub.discussion = d.id AND dsub.userid = :dsubuserid
445              LEFT JOIN {forum_posts} p ON p.discussion = d.id
446                  WHERE f.id ${foruminsql}
447                    AND (
448                         d.userid    = :discussionuserid OR
449                         p.userid    = :postuserid OR
450                         dsub.id IS NOT NULL
451                    )
452         ";
454         $params = [
455             'postuserid'        => $userid,
456             'discussionuserid'  => $userid,
457             'dsubuserid'        => $userid,
458         ];
459         $params += $forumparams;
461         // Keep track of the forums which have data.
462         $forumswithdata = [];
464         $discussions = $DB->get_recordset_sql($sql, $params);
465         foreach ($discussions as $discussion) {
466             // No need to take timestart into account as the user has some involvement already.
467             // Ignore discussion timeend as it should not block access to user data.
468             $forumswithdata[$discussion->forum] = true;
469             $context = \context::instance_by_id($mappings[$discussion->forum]);
471             // Store related metadata for this discussion.
472             static::export_discussion_subscription_data($userid, $context, $discussion);
474             $discussiondata = (object) [
475                 'name' => format_string($discussion->name, true),
476                 'pinned' => transform::yesno((bool) $discussion->pinned),
477                 'timemodified' => transform::datetime($discussion->timemodified),
478                 'usermodified' => transform::datetime($discussion->usermodified),
479                 'creator_was_you' => transform::yesno($discussion->userid == $userid),
480             ];
482             // Store the discussion content.
483             writer::with_context($context)
484                 ->export_data(static::get_discussion_area($discussion), $discussiondata);
486             // Forum discussions do not have any files associately directly with them.
487         }
489         $discussions->close();
491         return $forumswithdata;
492     }
494     /**
495      * Store all information about all posts that we have detected this user to have access to.
496      *
497      * @param   int         $userid The userid of the user whose data is to be exported.
498      * @param   array       $mappings A list of mappings from forumid => contextid.
499      * @return  array       Which forums had data written for them.
500      */
501     protected static function export_all_posts(int $userid, array $mappings) {
502         global $DB;
504         // Find all of the posts, and post subscriptions for this forum.
505         list($foruminsql, $forumparams) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED);
506         $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
507         $sql = "SELECT
508                     p.discussion AS id,
509                     f.id AS forumid,
510                     d.name,
511                     d.groupid
512                   FROM {forum} f
513                   JOIN {forum_discussions} d ON d.forum = f.id
514                   JOIN {forum_posts} p ON p.discussion = d.id
515              LEFT JOIN {forum_read} fr ON fr.postid = p.id AND fr.userid = :readuserid
516             {$ratingsql->join}
517                  WHERE f.id ${foruminsql} AND
518                 (
519                     p.userid = :postuserid OR
520                     fr.id IS NOT NULL OR
521                     {$ratingsql->userwhere}
522                 )
523               GROUP BY f.id, p.discussion, d.name, d.groupid
524         ";
526         $params = [
527             'postuserid'    => $userid,
528             'readuserid'    => $userid,
529         ];
530         $params += $forumparams;
531         $params += $ratingsql->params;
533         $discussions = $DB->get_records_sql($sql, $params);
534         foreach ($discussions as $discussion) {
535             $context = \context::instance_by_id($mappings[$discussion->forumid]);
536             static::export_all_posts_in_discussion($userid, $context, $discussion);
537         }
538     }
540     /**
541      * Store all information about all posts that we have detected this user to have access to.
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   \stdClass   $discussion The discussion whose data is being exported.
546      */
547     protected static function export_all_posts_in_discussion(int $userid, \context $context, \stdClass $discussion) {
548         global $DB, $USER;
550         $discussionid = $discussion->id;
552         // Find all of the posts, and post subscriptions for this forum.
553         $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
554         $sql = "SELECT
555                     p.*,
556                     d.forum AS forumid,
557                     fr.firstread,
558                     fr.lastread,
559                     fr.id AS readflag,
560                     rat.id AS hasratings
561                     FROM {forum_discussions} d
562                     JOIN {forum_posts} p ON p.discussion = d.id
563                LEFT JOIN {forum_read} fr ON fr.postid = p.id AND fr.userid = :readuserid
564             {$ratingsql->join} AND {$ratingsql->userwhere}
565                    WHERE d.id = :discussionid
566         ";
568         $params = [
569             'discussionid'  => $discussionid,
570             'readuserid'    => $userid,
571         ];
572         $params += $ratingsql->params;
574         // Keep track of the forums which have data.
575         $structure = (object) [
576             'children' => [],
577         ];
579         $posts = $DB->get_records_sql($sql, $params);
580         foreach ($posts as $post) {
581             $post->hasdata = (isset($post->hasdata)) ? $post->hasdata : false;
582             $post->hasdata = $post->hasdata || !empty($post->hasratings);
583             $post->hasdata = $post->hasdata || $post->readflag;
584             $post->hasdata = $post->hasdata || ($post->userid == $USER->id);
586             if (0 == $post->parent) {
587                 $structure->children[$post->id] = $post;
588             } else {
589                 if (empty($posts[$post->parent]->children)) {
590                     $posts[$post->parent]->children = [];
591                 }
592                 $posts[$post->parent]->children[$post->id] = $post;
593             }
595             // Set all parents.
596             if ($post->hasdata) {
597                 $curpost = $post;
598                 while ($curpost->parent != 0) {
599                     $curpost = $posts[$curpost->parent];
600                     $curpost->hasdata = true;
601                 }
602             }
603         }
605         $discussionarea = static::get_discussion_area($discussion);
606         $discussionarea[] = get_string('posts', 'mod_forum');
607         static::export_posts_in_structure($userid, $context, $discussionarea, $structure);
608     }
610     /**
611      * Export all posts in the provided structure.
612      *
613      * @param   int         $userid The userid of the user whose data is to be exported.
614      * @param   \context    $context The instance of the forum context.
615      * @param   array       $parentarea The subcontext of the parent.
616      * @param   \stdClass   $structure The post structure and all of its children
617      */
618     protected static function export_posts_in_structure(int $userid, \context $context, $parentarea, \stdClass $structure) {
619         foreach ($structure->children as $post) {
620             if (!$post->hasdata) {
621                 // This tree has no content belonging to the user. Skip it and all children.
622                 continue;
623             }
625             $postarea = array_merge($parentarea, static::get_post_area($post));
627             // Store the post content.
628             static::export_post_data($userid, $context, $postarea, $post);
630             if (isset($post->children)) {
631                 // Now export children of this post.
632                 static::export_posts_in_structure($userid, $context, $postarea, $post);
633             }
634         }
635     }
637     /**
638      * Export all data in the post.
639      *
640      * @param   int         $userid The userid of the user whose data is to be exported.
641      * @param   \context    $context The instance of the forum context.
642      * @param   array       $postarea The subcontext of the parent.
643      * @param   \stdClass   $post The post structure and all of its children
644      */
645     protected static function export_post_data(int $userid, \context $context, $postarea, $post) {
646         // Store related metadata.
647         static::export_read_data($userid, $context, $postarea, $post);
649         $postdata = (object) [
650             'subject' => format_string($post->subject, true),
651             'created' => transform::datetime($post->created),
652             'modified' => transform::datetime($post->modified),
653             'author_was_you' => transform::yesno($post->userid == $userid),
654         ];
656         $postdata->message = writer::with_context($context)
657             ->rewrite_pluginfile_urls($postarea, 'mod_forum', 'post', $post->id, $post->message);
659         $postdata->message = format_text($postdata->message, $post->messageformat, (object) [
660             'para'    => false,
661             'trusted' => $post->messagetrust,
662             'context' => $context,
663         ]);
665         writer::with_context($context)
666             // Store the post.
667             ->export_data($postarea, $postdata)
669             // Store the associated files.
670             ->export_area_files($postarea, 'mod_forum', 'post', $post->id);
672         if ($post->userid == $userid) {
673             // Store all ratings against this post as the post belongs to the user. All ratings on it are ratings of their content.
674             \core_rating\privacy\provider::export_area_ratings($userid, $context, $postarea, 'mod_forum', 'post', $post->id, false);
676             // Store all tags against this post as the tag belongs to the user.
677             \core_tag\privacy\provider::export_item_tags($userid, $context, $postarea, 'mod_forum', 'forum_posts', $post->id);
679             // Export all user data stored for this post from the plagiarism API.
680             $coursecontext = $context->get_course_context();
681             \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $postarea, [
682                     'cmid' => $context->instanceid,
683                     'course' => $coursecontext->instanceid,
684                     'forum' => $post->forumid,
685                     'discussionid' => $post->discussion,
686                     'postid' => $post->id,
687                 ]);
688         }
690         // Check for any ratings that the user has made on this post.
691         \core_rating\privacy\provider::export_area_ratings($userid,
692                 $context,
693                 $postarea,
694                 'mod_forum',
695                 'post',
696                 $post->id,
697                 $userid,
698                 true
699             );
700     }
702     /**
703      * Store data about daily digest preferences
704      *
705      * @param   int         $userid The userid of the user whose data is to be exported.
706      * @param   \stdClass   $forum The forum whose data is being exported.
707      * @return  bool        Whether any data was stored.
708      */
709     protected static function export_digest_data(int $userid, \stdClass $forum) {
710         if (null !== $forum->maildigest) {
711             // The user has a specific maildigest preference for this forum.
712             $a = (object) [
713                 'forum' => format_string($forum->name, true),
714             ];
716             switch ($forum->maildigest) {
717                 case 0:
718                     $a->type = get_string('emaildigestoffshort', 'mod_forum');
719                     break;
720                 case 1:
721                     $a->type = get_string('emaildigestcompleteshort', 'mod_forum');
722                     break;
723                 case 2:
724                     $a->type = get_string('emaildigestsubjectsshort', 'mod_forum');
725                     break;
726             }
728             writer::with_context(\context_module::instance($forum->cmid))
729                 ->export_metadata([], 'digestpreference', $forum->maildigest,
730                     get_string('privacy:digesttypepreference', 'mod_forum', $a));
732             return true;
733         }
735         return false;
736     }
738     /**
739      * Store data about whether the user subscribes to forum.
740      *
741      * @param   int         $userid The userid of the user whose data is to be exported.
742      * @param   \stdClass   $forum The forum whose data is being exported.
743      * @return  bool        Whether any data was stored.
744      */
745     protected static function export_subscription_data(int $userid, \stdClass $forum) {
746         if (null !== $forum->subscribed) {
747             // The user is subscribed to this forum.
748             writer::with_context(\context_module::instance($forum->cmid))
749                 ->export_metadata([], 'subscriptionpreference', 1, get_string('privacy:subscribedtoforum', 'mod_forum'));
751             return true;
752         }
754         return false;
755     }
757     /**
758      * Store data about whether the user subscribes to this particular discussion.
759      *
760      * @param   int         $userid The userid of the user whose data is to be exported.
761      * @param   \context_module $context The instance of the forum context.
762      * @param   \stdClass   $discussion The discussion whose data is being exported.
763      * @return  bool        Whether any data was stored.
764      */
765     protected static function export_discussion_subscription_data(int $userid, \context_module $context, \stdClass $discussion) {
766         $area = static::get_discussion_area($discussion);
767         if (null !== $discussion->preference) {
768             // The user has a specific subscription preference for this discussion.
769             $a = (object) [];
771             switch ($discussion->preference) {
772                 case \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED:
773                     $a->preference = get_string('unsubscribed', 'mod_forum');
774                     break;
775                 default:
776                     $a->preference = get_string('subscribed', 'mod_forum');
777                     break;
778             }
780             writer::with_context($context)
781                 ->export_metadata(
782                     $area,
783                     'subscriptionpreference',
784                     $discussion->preference,
785                     get_string('privacy:discussionsubscriptionpreference', 'mod_forum', $a)
786                 );
788             return true;
789         }
791         return true;
792     }
794     /**
795      * Store forum read-tracking data about a particular forum.
796      *
797      * This is whether a forum has read-tracking enabled or not.
798      *
799      * @param   int         $userid The userid of the user whose data is to be exported.
800      * @param   \stdClass   $forum The forum whose data is being exported.
801      * @return  bool        Whether any data was stored.
802      */
803     protected static function export_tracking_data(int $userid, \stdClass $forum) {
804         if (null !== $forum->tracked) {
805             // The user has a main preference to track all forums, but has opted out of this one.
806             writer::with_context(\context_module::instance($forum->cmid))
807                 ->export_metadata([], 'trackreadpreference', 0, get_string('privacy:readtrackingdisabled', 'mod_forum'));
809             return true;
810         }
812         return false;
813     }
815     /**
816      * Store read-tracking information about a particular forum post.
817      *
818      * @param   int         $userid The userid of the user whose data is to be exported.
819      * @param   \context_module $context The instance of the forum context.
820      * @param   array       $postarea The subcontext for this post.
821      * @param   \stdClass   $post The post whose data is being exported.
822      * @return  bool        Whether any data was stored.
823      */
824     protected static function export_read_data(int $userid, \context_module $context, array $postarea, \stdClass $post) {
825         if (null !== $post->firstread) {
826             $a = (object) [
827                 'firstread' => $post->firstread,
828                 'lastread'  => $post->lastread,
829             ];
831             writer::with_context($context)
832                 ->export_metadata(
833                     $postarea,
834                     'postread',
835                     (object) [
836                         'firstread' => $post->firstread,
837                         'lastread' => $post->lastread,
838                     ],
839                     get_string('privacy:postwasread', 'mod_forum', $a)
840                 );
842             return true;
843         }
845         return false;
846     }
848     /**
849      * Delete all data for all users in the specified context.
850      *
851      * @param   context                 $context   The specific context to delete data for.
852      */
853     public static function delete_data_for_all_users_in_context(\context $context) {
854         global $DB;
856         // Check that this is a context_module.
857         if (!$context instanceof \context_module) {
858             return;
859         }
861         // Get the course module.
862         if (!$cm = get_coursemodule_from_id('forum', $context->instanceid)) {
863             return;
864         }
866         $forumid = $cm->instance;
868         $DB->delete_records('forum_track_prefs', ['forumid' => $forumid]);
869         $DB->delete_records('forum_subscriptions', ['forum' => $forumid]);
870         $DB->delete_records('forum_read', ['forumid' => $forumid]);
872         // Delete all discussion items.
873         $DB->delete_records_select(
874             'forum_queue',
875             "discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
876             [
877                 'forum' => $forumid,
878             ]
879         );
881         $DB->delete_records_select(
882             'forum_posts',
883             "discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
884             [
885                 'forum' => $forumid,
886             ]
887         );
889         $DB->delete_records('forum_discussion_subs', ['forum' => $forumid]);
890         $DB->delete_records('forum_discussions', ['forum' => $forumid]);
892         // Delete all files from the posts.
893         $fs = get_file_storage();
894         $fs->delete_area_files($context->id, 'mod_forum', 'post');
895         $fs->delete_area_files($context->id, 'mod_forum', 'attachment');
897         // Delete all ratings in the context.
898         \core_rating\privacy\provider::delete_ratings($context, 'mod_forum', 'post');
900         // Delete all Tags.
901         \core_tag\privacy\provider::delete_item_tags($context, 'mod_forum', 'forum_posts');
902     }
904     /**
905      * Delete all user data for the specified user, in the specified contexts.
906      *
907      * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
908      */
909     public static function delete_data_for_user(approved_contextlist $contextlist) {
910         global $DB;
911         $user = $contextlist->get_user();
912         $userid = $user->id;
913         foreach ($contextlist as $context) {
914             // Get the course module.
915             $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
916             $forum = $DB->get_record('forum', ['id' => $cm->instance]);
918             $DB->delete_records('forum_track_prefs', [
919                 'forumid' => $forum->id,
920                 'userid' => $userid,
921             ]);
922             $DB->delete_records('forum_subscriptions', [
923                 'forum' => $forum->id,
924                 'userid' => $userid,
925             ]);
926             $DB->delete_records('forum_read', [
927                 'forumid' => $forum->id,
928                 'userid' => $userid,
929             ]);
931             // Delete all discussion items.
932             $DB->delete_records_select(
933                 'forum_queue',
934                 "userid = :userid AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)",
935                 [
936                     'userid' => $userid,
937                     'forum' => $forum->id,
938                 ]
939             );
941             $DB->delete_records('forum_discussion_subs', [
942                 'forum' => $forum->id,
943                 'userid' => $userid,
944             ]);
946             $uniquediscussions = $DB->get_recordset('forum_discussions', [
947                     'forum' => $forum->id,
948                     'userid' => $userid,
949                 ]);
951             foreach ($uniquediscussions as $discussion) {
952                 // Do not delete discussion or forum posts.
953                 // Instead update them to reflect that the content has been deleted.
954                 $postsql = "userid = :userid AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)";
955                 $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}";
956                 $postparams = [
957                     'forum' => $forum->id,
958                     'userid' => $userid,
959                 ];
961                 // Update the subject.
962                 $DB->set_field_select('forum_posts', 'subject', '', $postsql, $postparams);
964                 // Update the subject and its format.
965                 $DB->set_field_select('forum_posts', 'message', '', $postsql, $postparams);
966                 $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $postparams);
968                 // Mark the post as deleted.
969                 $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $postparams);
971                 // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts.
972                 // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating
973                 // of any post.
974                 \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post',
975                         "IN ($postidsql)", $postparams);
977                 // Delete all Tags.
978                 \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts',
979                         "IN ($postidsql)", $postparams);
981                 // Delete all files from the posts.
982                 $fs = get_file_storage();
983                 $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $postparams);
984                 $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $postparams);
985             }
987             $uniquediscussions->close();
988         }
989     }
991     /**
992      * Delete multiple users within a single context.
993      *
994      * @param   approved_userlist       $userlist The approved context and user information to delete information for.
995      */
996     public static function delete_data_for_users(approved_userlist $userlist) {
997         global $DB;
999         $context = $userlist->get_context();
1000         $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
1001         $forum = $DB->get_record('forum', ['id' => $cm->instance]);
1003         list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
1004         $params = array_merge(['forumid' => $forum->id], $userinparams);
1006         $DB->delete_records_select('forum_track_prefs', "forumid = :forumid AND userid {$userinsql}", $params);
1007         $DB->delete_records_select('forum_subscriptions', "forum = :forumid AND userid {$userinsql}", $params);
1008         $DB->delete_records_select('forum_read', "forumid = :forumid AND userid {$userinsql}", $params);
1009         $DB->delete_records_select(
1010             'forum_queue',
1011             "userid {$userinsql} AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)",
1012             $params
1013         );
1014         $DB->delete_records_select('forum_discussion_subs', "forum = :forumid AND userid {$userinsql}", $params);
1016         // Do not delete discussion or forum posts.
1017         // Instead update them to reflect that the content has been deleted.
1018         $postsql = "userid {$userinsql} AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)";
1019         $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}";
1021         // Update the subject.
1022         $DB->set_field_select('forum_posts', 'subject', '', $postsql, $params);
1024         // Update the subject and its format.
1025         $DB->set_field_select('forum_posts', 'message', '', $postsql, $params);
1026         $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $params);
1028         // Mark the post as deleted.
1029         $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $params);
1031         // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts.
1032         // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating
1033         // of any post.
1034         \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post', "IN ($postidsql)", $params);
1036         // Delete all Tags.
1037         \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts', "IN ($postidsql)", $params);
1039         // Delete all files from the posts.
1040         $fs = get_file_storage();
1041         $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $params);
1042         $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $params);
1043     }