See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for mod_forum. * * @package mod_forum * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\privacy; use core_grades\component_gradeitem as gradeitem; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\approved_userlist; use \core_privacy\local\request\deletion_criteria; use \core_privacy\local\request\writer; use \core_privacy\local\request\helper as request_helper; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; use tool_dataprivacy\context_instance; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/grade/grading/lib.php'); /** * Implementation of the privacy subsystem plugin provider for the forum activity module. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // This plugin has data. \core_privacy\local\metadata\provider, // This plugin currently implements the original plugin\provider interface. \core_privacy\local\request\plugin\provider, // This plugin is capable of determining which users have data within it. \core_privacy\local\request\core_userlist_provider, // This plugin has some sitewide user preferences to export. \core_privacy\local\request\user_preference_provider { use subcontext_info; /** * Returns meta data about this system. * * @param collection $items The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $items) : collection { // The 'forum' table does not store any specific user data. $items->add_database_table('forum_digests', [ 'forum' => 'privacy:metadata:forum_digests:forum', 'userid' => 'privacy:metadata:forum_digests:userid', 'maildigest' => 'privacy:metadata:forum_digests:maildigest', ], 'privacy:metadata:forum_digests'); // The 'forum_discussions' table stores the metadata about each forum discussion. $items->add_database_table('forum_discussions', [ 'name' => 'privacy:metadata:forum_discussions:name', 'userid' => 'privacy:metadata:forum_discussions:userid', 'assessed' => 'privacy:metadata:forum_discussions:assessed', 'timemodified' => 'privacy:metadata:forum_discussions:timemodified', 'usermodified' => 'privacy:metadata:forum_discussions:usermodified', ], 'privacy:metadata:forum_discussions'); // The 'forum_discussion_subs' table stores information about which discussions a user is subscribed to. $items->add_database_table('forum_discussion_subs', [ 'discussionid' => 'privacy:metadata:forum_discussion_subs:discussionid', 'preference' => 'privacy:metadata:forum_discussion_subs:preference', 'userid' => 'privacy:metadata:forum_discussion_subs:userid', ], 'privacy:metadata:forum_discussion_subs'); // The 'forum_posts' table stores the metadata about each forum discussion. $items->add_database_table('forum_posts', [ 'discussion' => 'privacy:metadata:forum_posts:discussion', 'parent' => 'privacy:metadata:forum_posts:parent', 'created' => 'privacy:metadata:forum_posts:created', 'modified' => 'privacy:metadata:forum_posts:modified', 'subject' => 'privacy:metadata:forum_posts:subject', 'message' => 'privacy:metadata:forum_posts:message', 'userid' => 'privacy:metadata:forum_posts:userid', 'privatereplyto' => 'privacy:metadata:forum_posts:privatereplyto', ], 'privacy:metadata:forum_posts'); // The 'forum_queue' table contains user data, but it is only a temporary cache of other data. // We should not need to export it as it does not allow profiling of a user. // The 'forum_read' table stores data about which forum posts have been read by each user. $items->add_database_table('forum_read', [ 'userid' => 'privacy:metadata:forum_read:userid', 'discussionid' => 'privacy:metadata:forum_read:discussionid', 'postid' => 'privacy:metadata:forum_read:postid', 'firstread' => 'privacy:metadata:forum_read:firstread', 'lastread' => 'privacy:metadata:forum_read:lastread', ], 'privacy:metadata:forum_read'); // The 'forum_subscriptions' table stores information about which forums a user is subscribed to. $items->add_database_table('forum_subscriptions', [ 'userid' => 'privacy:metadata:forum_subscriptions:userid', 'forum' => 'privacy:metadata:forum_subscriptions:forum', ], 'privacy:metadata:forum_subscriptions'); // The 'forum_subscriptions' table stores information about which forums a user is subscribed to. $items->add_database_table('forum_track_prefs', [ 'userid' => 'privacy:metadata:forum_track_prefs:userid', 'forumid' => 'privacy:metadata:forum_track_prefs:forumid', ], 'privacy:metadata:forum_track_prefs'); // The 'forum_queue' table stores temporary data that is not exported/deleted. $items->add_database_table('forum_queue', [ 'userid' => 'privacy:metadata:forum_queue:userid', 'discussionid' => 'privacy:metadata:forum_queue:discussionid', 'postid' => 'privacy:metadata:forum_queue:postid', 'timemodified' => 'privacy:metadata:forum_queue:timemodified' ], 'privacy:metadata:forum_queue'); // The 'forum_grades' table stores grade data. $items->add_database_table('forum_grades', [ 'userid' => 'privacy:metadata:forum_grades:userid', 'forum' => 'privacy:metadata:forum_grades:forum', 'grade' => 'privacy:metadata:forum_grades:grade', ], 'privacy:metadata:forum_grades'); // Forum posts can be tagged and rated. $items->link_subsystem('core_tag', 'privacy:metadata:core_tag'); $items->link_subsystem('core_rating', 'privacy:metadata:core_rating'); // There are several user preferences. $items->add_user_preference('maildigest', 'privacy:metadata:preference:maildigest'); $items->add_user_preference('autosubscribe', 'privacy:metadata:preference:autosubscribe'); $items->add_user_preference('trackforums', 'privacy:metadata:preference:trackforums'); $items->add_user_preference('markasreadonnotification', 'privacy:metadata:preference:markasreadonnotification'); $items->add_user_preference('forum_discussionlistsortorder', 'privacy:metadata:preference:forum_discussionlistsortorder'); return $items; } /** * Get the list of contexts that contain user information for the specified user. * * In the case of forum, that is any forum where the user has made any post, rated any content, or has any preferences. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist { $contextlist = new \core_privacy\local\request\contextlist(); $params = [ 'modname' => 'forum', 'contextlevel' => CONTEXT_MODULE, 'userid' => $userid, ]; // Discussion creators. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id WHERE d.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Post authors. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id WHERE p.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Forum digest records. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_digests} dig ON dig.forum = f.id WHERE dig.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Forum subscriptions. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_subscriptions} sub ON sub.forum = f.id WHERE sub.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Discussion subscriptions. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id WHERE dsub.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Discussion tracking preferences. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_track_prefs} pref ON pref.forumid = f.id WHERE pref.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Discussion read records. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_read} hasread ON hasread.forumid = f.id WHERE hasread.userid = :userid "; $contextlist->add_from_sql($sql, $params); // Rating authors. $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid, true); $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id {$ratingsql->join} WHERE {$ratingsql->userwhere} "; $params += $ratingsql->params; $contextlist->add_from_sql($sql, $params); // Forum grades. $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {forum} f ON f.id = cm.instance JOIN {forum_grades} fg ON fg.forum = f.id WHERE fg.userid = :userid "; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!is_a($context, \context_module::class)) { return; } $params = [ 'instanceid' => $context->instanceid, 'modulename' => 'forum', ]; // Discussion authors. $sql = "SELECT d.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum authors. $sql = "SELECT p.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON d.id = p.discussion WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum post ratings. $sql = "SELECT p.id FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON d.id = p.discussion WHERE cm.id = :instanceid"; \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_forum', 'post', $sql, $params); // Forum Digest settings. $sql = "SELECT dig.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_digests} dig ON dig.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum Subscriptions. $sql = "SELECT sub.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_subscriptions} sub ON sub.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Discussion subscriptions. $sql = "SELECT dsub.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Read Posts. $sql = "SELECT hasread.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_read} hasread ON hasread.forumid = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Tracking Preferences. $sql = "SELECT pref.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_track_prefs} pref ON pref.forumid = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); // Forum grades. $sql = "SELECT fg.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {forum} f ON f.id = cm.instance JOIN {forum_grades} fg ON fg.forum = f.id WHERE cm.id = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); } /** * Store all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $user = \core_user::get_user($userid); switch ($user->maildigest) { case 1: $digestdescription = get_string('emaildigestcomplete'); break; case 2: $digestdescription = get_string('emaildigestsubjects'); break; case 0: default: $digestdescription = get_string('emaildigestoff'); break; } writer::export_user_preference('mod_forum', 'maildigest', $user->maildigest, $digestdescription); switch ($user->autosubscribe) { case 0: $subscribedescription = get_string('autosubscribeno'); break; case 1: default: $subscribedescription = get_string('autosubscribeyes'); break; } writer::export_user_preference('mod_forum', 'autosubscribe', $user->autosubscribe, $subscribedescription); switch ($user->trackforums) { case 0: $trackforumdescription = get_string('trackforumsno'); break; case 1: default: $trackforumdescription = get_string('trackforumsyes'); break; } writer::export_user_preference('mod_forum', 'trackforums', $user->trackforums, $trackforumdescription); $markasreadonnotification = get_user_preferences('markasreadonnotification', null, $user->id); if (null !== $markasreadonnotification) { switch ($markasreadonnotification) { case 0: $markasreadonnotificationdescription = get_string('markasreadonnotificationno', 'mod_forum'); break; case 1: default: $markasreadonnotificationdescription = get_string('markasreadonnotificationyes', 'mod_forum'); break; } writer::export_user_preference('mod_forum', 'markasreadonnotification', $markasreadonnotification, $markasreadonnotificationdescription); } $vaultfactory = \mod_forum\local\container::get_vault_factory(); $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault(); $discussionlistsortorder = get_user_preferences('forum_discussionlistsortorder', $discussionlistvault::SORTORDER_LASTPOST_DESC, $user->id); switch ($discussionlistsortorder) { case $discussionlistvault::SORTORDER_LASTPOST_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbylastpostdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_LASTPOST_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbylastpostasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_CREATED_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbycreateddesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_CREATED_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbycreatedasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_REPLIES_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbyrepliesdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_REPLIES_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbyrepliesasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_DISCUSSION_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbydiscussiondesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_DISCUSSION_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbydiscussionasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_STARTER_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbystarterdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_STARTER_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbystarterasc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_GROUP_DESC: $discussionlistsortorderdescription = get_string('discussionlistsortbygroupdesc', 'mod_forum'); break; case $discussionlistvault::SORTORDER_GROUP_ASC: $discussionlistsortorderdescription = get_string('discussionlistsortbygroupasc', 'mod_forum'); break; } writer::export_user_preference('mod_forum', 'forum_discussionlistsortorder', $discussionlistsortorder, $discussionlistsortorderdescription); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; if (empty($contextlist)) { return; } $user = $contextlist->get_user(); $userid = $user->id; list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); $params = $contextparams; // Digested forums. $sql = "SELECT c.id AS contextid, dig.maildigest AS maildigest FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_digests} dig ON dig.forum = f.id WHERE ( dig.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $digests = $DB->get_records_sql_menu($sql, $params); // Forum subscriptions. $sql = "SELECT c.id AS contextid, sub.userid AS subscribed FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_subscriptions} sub ON sub.forum = f.id WHERE ( sub.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $subscriptions = $DB->get_records_sql_menu($sql, $params); // Tracked forums. $sql = "SELECT c.id AS contextid, pref.userid AS tracked FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_track_prefs} pref ON pref.forumid = f.id WHERE ( pref.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $tracked = $DB->get_records_sql_menu($sql, $params); // Forum grades. $sql = "SELECT c.id AS contextid, fg.grade AS grade, f.grade_forum AS gradetype FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance JOIN {forum_grades} fg ON fg.forum = f.id WHERE ( fg.userid = :userid AND c.id {$contextsql} ) "; $params['userid'] = $userid; $grades = $DB->get_records_sql_menu($sql, $params); $sql = "SELECT c.id AS contextid, f.*, cm.id AS cmid FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid JOIN {forum} f ON f.id = cm.instance WHERE ( c.id {$contextsql} ) "; $params += $contextparams; // Keep a mapping of forumid to contextid. $mappings = []; $forums = $DB->get_recordset_sql($sql, $params); foreach ($forums as $forum) { $mappings[$forum->id] = $forum->contextid; $context = \context::instance_by_id($mappings[$forum->id]); // Store the main forum data. $data = request_helper::get_context_data($context, $user); writer::with_context($context) ->export_data([], $data); request_helper::export_context_files($context, $user); // Store relevant metadata about this forum instance. if (isset($digests[$forum->contextid])) { static::export_digest_data($userid, $forum, $digests[$forum->contextid]); } if (isset($subscriptions[$forum->contextid])) { static::export_subscription_data($userid, $forum, $subscriptions[$forum->contextid]); } if (isset($tracked[$forum->contextid])) { static::export_tracking_data($userid, $forum, $tracked[$forum->contextid]); } if (isset($grades[$forum->contextid])) { static::export_grading_data($userid, $forum, $grades[$forum->contextid]); } } $forums->close(); if (!empty($mappings)) { // Store all discussion data for this forum. static::export_discussion_data($userid, $mappings); // Store all post data for this forum. static::export_all_posts($userid, $mappings); } } /** * Store all information about all discussions that we have detected this user to have access to. * * @param int $userid The userid of the user whose data is to be exported. * @param array $mappings A list of mappings from forumid => contextid. * @return array Which forums had data written for them. */ protected static function export_discussion_data(int $userid, array $mappings) { global $DB; // Find all of the discussions, and discussion subscriptions for this forum. list($foruminsql, $forumparams) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $sql = "SELECT d.*, g.name as groupname, dsub.preference FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id LEFT JOIN {groups} g ON g.id = d.groupid LEFT JOIN {forum_discussion_subs} dsub ON dsub.discussion = d.id AND dsub.userid = :dsubuserid LEFT JOIN {forum_posts} p ON p.discussion = d.id< WHERE f.id ${foruminsql}> WHERE f.id {$foruminsql}AND ( d.userid = :discussionuserid OR p.userid = :postuserid OR dsub.id IS NOT NULL ) "; $params = [ 'postuserid' => $userid, 'discussionuserid' => $userid, 'dsubuserid' => $userid, ]; $params += $forumparams; // Keep track of the forums which have data. $forumswithdata = []; $discussions = $DB->get_recordset_sql($sql, $params); foreach ($discussions as $discussion) { // No need to take timestart into account as the user has some involvement already. // Ignore discussion timeend as it should not block access to user data. $forumswithdata[$discussion->forum] = true; $context = \context::instance_by_id($mappings[$discussion->forum]); // Store related metadata for this discussion. static::export_discussion_subscription_data($userid, $context, $discussion); $discussiondata = (object) [ 'name' => format_string($discussion->name, true), 'pinned' => transform::yesno((bool) $discussion->pinned), 'timemodified' => transform::datetime($discussion->timemodified), 'usermodified' => transform::datetime($discussion->usermodified), 'creator_was_you' => transform::yesno($discussion->userid == $userid), ]; // Store the discussion content. writer::with_context($context) ->export_data(static::get_discussion_area($discussion), $discussiondata); // Forum discussions do not have any files associately directly with them. } $discussions->close(); return $forumswithdata; } /** * Store all information about all posts that we have detected this user to have access to. * * @param int $userid The userid of the user whose data is to be exported. * @param array $mappings A list of mappings from forumid => contextid. * @return array Which forums had data written for them. */ protected static function export_all_posts(int $userid, array $mappings) { global $DB; $commonsql = "SELECT p.discussion AS id, f.id AS forumid, d.name, d.groupid FROM {forum} f JOIN {forum_discussions} d ON d.forum = f.id JOIN {forum_posts} p ON p.discussion = d.id"; // All discussions with posts authored by the user or containing private replies to the user. list($foruminsql1, $forumparams1) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $sql1 = "{$commonsql} WHERE f.id {$foruminsql1} AND (p.userid = :postuserid OR p.privatereplyto = :privatereplyrecipient)"; // All discussions with the posts marked as read by the user. list($foruminsql2, $forumparams2) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $sql2 = "{$commonsql} JOIN {forum_read} fr ON fr.postid = p.id WHERE f.id {$foruminsql2} AND fr.userid = :readuserid"; // All discussions with ratings provided by the user. list($foruminsql3, $forumparams3) = $DB->get_in_or_equal(array_keys($mappings), SQL_PARAMS_NAMED); $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid, true); $sql3 = "{$commonsql} {$ratingsql->join} WHERE f.id {$foruminsql3} AND {$ratingsql->userwhere}"; $sql = "SELECT * FROM ({$sql1} UNION {$sql2} UNION {$sql3}) united GROUP BY id, forumid, name, groupid"; $params = [ 'postuserid' => $userid, 'readuserid' => $userid, 'privatereplyrecipient' => $userid, ]; $params += $forumparams1; $params += $forumparams2; $params += $forumparams3; $params += $ratingsql->params; $discussions = $DB->get_records_sql($sql, $params); foreach ($discussions as $discussion) { $context = \context::instance_by_id($mappings[$discussion->forumid]); static::export_all_posts_in_discussion($userid, $context, $discussion); } } /** * Store all information about all posts that we have detected this user to have access to. * * @param int $userid The userid of the user whose data is to be exported. * @param \context $context The instance of the forum context. * @param \stdClass $discussion The discussion whose data is being exported. */ protected static function export_all_posts_in_discussion(int $userid, \context $context, \stdClass $discussion) { global $DB, $USER; $discussionid = $discussion->id; // Find all of the posts, and post subscriptions for this forum. $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid); $sql = "SELECT p.*, d.forum AS forumid, fr.firstread, fr.lastread, fr.id AS readflag, rat.id AS hasratings FROM {forum_discussions} d JOIN {forum_posts} p ON p.discussion = d.id LEFT JOIN {forum_read} fr ON fr.postid = p.id AND fr.userid = :readuserid {$ratingsql->join} AND {$ratingsql->userwhere} WHERE d.id = :discussionid AND ( p.privatereplyto = 0 OR p.privatereplyto = :privatereplyrecipient OR p.userid = :privatereplyauthor ) "; $params = [ 'discussionid' => $discussionid, 'readuserid' => $userid, 'privatereplyrecipient' => $userid, 'privatereplyauthor' => $userid, ]; $params += $ratingsql->params; // Keep track of the forums which have data. $structure = (object) [ 'children' => [], ]; $posts = $DB->get_records_sql($sql, $params); foreach ($posts as $post) { $post->hasdata = (isset($post->hasdata)) ? $post->hasdata : false; $post->hasdata = $post->hasdata || !empty($post->hasratings); $post->hasdata = $post->hasdata || $post->readflag; $post->hasdata = $post->hasdata || ($post->userid == $USER->id); $post->hasdata = $post->hasdata || ($post->privatereplyto == $USER->id); if (0 == $post->parent) { $structure->children[$post->id] = $post; } else { if (empty($posts[$post->parent]->children)) { $posts[$post->parent]->children = []; } $posts[$post->parent]->children[$post->id] = $post; } // Set all parents. if ($post->hasdata) { $curpost = $post; while ($curpost->parent != 0) { $curpost = $posts[$curpost->parent]; $curpost->hasdata = true; } } } $discussionarea = static::get_discussion_area($discussion); $discussionarea[] = get_string('posts', 'mod_forum'); static::export_posts_in_structure($userid, $context, $discussionarea, $structure); } /** * Export all posts in the provided structure. * * @param int $userid The userid of the user whose data is to be exported. * @param \context $context The instance of the forum context. * @param array $parentarea The subcontext of the parent. * @param \stdClass $structure The post structure and all of its children */ protected static function export_posts_in_structure(int $userid, \context $context, $parentarea, \stdClass $structure) { foreach ($structure->children as $post) { if (!$post->hasdata) { // This tree has no content belonging to the user. Skip it and all children. continue; } $postarea = array_merge($parentarea, static::get_post_area($post)); // Store the post content. static::export_post_data($userid, $context, $postarea, $post); if (isset($post->children)) { // Now export children of this post. static::export_posts_in_structure($userid, $context, $postarea, $post); } } } /** * Export all data in the post. * * @param int $userid The userid of the user whose data is to be exported. * @param \context $context The instance of the forum context. * @param array $postarea The subcontext of the parent. * @param \stdClass $post The post structure and all of its children */ protected static function export_post_data(int $userid, \context $context, $postarea, $post) { // Store related metadata. static::export_read_data($userid, $context, $postarea, $post); $postdata = (object) [ 'subject' => format_string($post->subject, true), 'created' => transform::datetime($post->created), 'modified' => transform::datetime($post->modified), 'author_was_you' => transform::yesno($post->userid == $userid), ]; if (!empty($post->privatereplyto)) { $postdata->privatereply = transform::yesno(true); } $postdata->message = writer::with_context($context) ->rewrite_pluginfile_urls($postarea, 'mod_forum', 'post', $post->id, $post->message); $postdata->message = format_text($postdata->message, $post->messageformat, (object) [ 'para' => false, 'trusted' => $post->messagetrust, 'context' => $context, ]); writer::with_context($context) // Store the post. ->export_data($postarea, $postdata) // Store the associated files. ->export_area_files($postarea, 'mod_forum', 'post', $post->id); if ($post->userid == $userid) { // Store all ratings against this post as the post belongs to the user. All ratings on it are ratings of their content. \core_rating\privacy\provider::export_area_ratings($userid, $context, $postarea, 'mod_forum', 'post', $post->id, false); // Store all tags against this post as the tag belongs to the user. \core_tag\privacy\provider::export_item_tags($userid, $context, $postarea, 'mod_forum', 'forum_posts', $post->id); // Export all user data stored for this post from the plagiarism API. $coursecontext = $context->get_course_context(); \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $postarea, [ 'cmid' => $context->instanceid, 'course' => $coursecontext->instanceid, 'forum' => $post->forumid, 'discussionid' => $post->discussion, 'postid' => $post->id, ]); } // Check for any ratings that the user has made on this post. \core_rating\privacy\provider::export_area_ratings($userid, $context, $postarea, 'mod_forum', 'post', $post->id, $userid, true ); } /** * Store data about daily digest preferences * * @param int $userid The userid of the user whose data is to be exported. * @param \stdClass $forum The forum whose data is being exported. * @param int $maildigest The mail digest setting for this forum. * @return bool Whether any data was stored. */ protected static function export_digest_data(int $userid, \stdClass $forum, int $maildigest) { if (null !== $maildigest) { // The user has a specific maildigest preference for this forum. $a = (object) [ 'forum' => format_string($forum->name, true), ]; switch ($maildigest) { case 0: $a->type = get_string('emaildigestoffshort', 'mod_forum'); break; case 1: $a->type = get_string('emaildigestcompleteshort', 'mod_forum'); break; case 2: $a->type = get_string('emaildigestsubjectsshort', 'mod_forum'); break; } writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata([], 'digestpreference', $maildigest, get_string('privacy:digesttypepreference', 'mod_forum', $a)); return true; } return false; } /** * Store data about whether the user subscribes to forum. * * @param int $userid The userid of the user whose data is to be exported. * @param \stdClass $forum The forum whose data is being exported. * @param int $subscribed if the user is subscribed * @return bool Whether any data was stored. */ protected static function export_subscription_data(int $userid, \stdClass $forum, int $subscribed) { if (null !== $subscribed) { // The user is subscribed to this forum. writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata([], 'subscriptionpreference', 1, get_string('privacy:subscribedtoforum', 'mod_forum')); return true; } return false; } /** * Store data about whether the user subscribes to this particular discussion. * * @param int $userid The userid of the user whose data is to be exported. * @param \context_module $context The instance of the forum context. * @param \stdClass $discussion The discussion whose data is being exported. * @return bool Whether any data was stored. */ protected static function export_discussion_subscription_data(int $userid, \context_module $context, \stdClass $discussion) { $area = static::get_discussion_area($discussion); if (null !== $discussion->preference) { // The user has a specific subscription preference for this discussion. $a = (object) []; switch ($discussion->preference) { case \mod_forum\subscriptions::FORUM_DISCUSSION_UNSUBSCRIBED: $a->preference = get_string('unsubscribed', 'mod_forum'); break; default: $a->preference = get_string('subscribed', 'mod_forum'); break; } writer::with_context($context) ->export_metadata( $area, 'subscriptionpreference', $discussion->preference, get_string('privacy:discussionsubscriptionpreference', 'mod_forum', $a) ); return true; } return true; } /** * Store forum read-tracking data about a particular forum. * * This is whether a forum has read-tracking enabled or not. * * @param int $userid The userid of the user whose data is to be exported. * @param \stdClass $forum The forum whose data is being exported. * @param int $tracke if the user is subscribed * @return bool Whether any data was stored. */ protected static function export_tracking_data(int $userid, \stdClass $forum, int $tracked) { if (null !== $tracked) { // The user has a main preference to track all forums, but has opted out of this one. writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata([], 'trackreadpreference', 0, get_string('privacy:readtrackingdisabled', 'mod_forum')); return true; } return false; } protected static function export_grading_data(int $userid, \stdClass $forum, int $grade) { global $USER; if (null !== $grade) { $context = \context_module::instance($forum->cmid); $exportpath = array_merge([], [get_string('privacy:metadata:forum_grades', 'mod_forum')]); $gradingmanager = get_grading_manager($context, 'mod_forum', 'forum'); $controller = $gradingmanager->get_active_controller(); // Check for advanced grading and retrieve that information. if (isset($controller)) { $gradeduser = \core_user::get_user($userid); // Fetch the gradeitem instance. $gradeitem = gradeitem::instance($controller->get_component(), $context, $controller->get_area()); $grade = $gradeitem->get_grade_for_user($gradeduser, $USER); $controllercontext = $controller->get_context(); \core_grading\privacy\provider::export_item_data($controllercontext, $grade->id, $exportpath); } else { self::export_grade_data($grade, $context, $forum, $exportpath); } // The user has a grade for this forum. writer::with_context(\context_module::instance($forum->cmid)) ->export_metadata($exportpath, 'gradingenabled', 1, get_string('privacy:metadata:forum_grades:grade', 'mod_forum')); return true; } return false; } protected static function export_grade_data(int $grade, \context $context, \stdClass $forum, array $path) { $gradedata = (object)[ 'forum' => $forum->name, 'grade' => $grade, ]; writer::with_context($context) ->export_data($path, $gradedata); } /** * Store read-tracking information about a particular forum post. * * @param int $userid The userid of the user whose data is to be exported. * @param \context_module $context The instance of the forum context. * @param array $postarea The subcontext for this post. * @param \stdClass $post The post whose data is being exported. * @return bool Whether any data was stored. */ protected static function export_read_data(int $userid, \context_module $context, array $postarea, \stdClass $post) { if (null !== $post->firstread) { $a = (object) [ 'firstread' => $post->firstread, 'lastread' => $post->lastread, ]; writer::with_context($context) ->export_metadata( $postarea, 'postread', (object) [ 'firstread' => $post->firstread, 'lastread' => $post->lastread, ], get_string('privacy:postwasread', 'mod_forum', $a) ); return true; } return false; } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; // Check that this is a context_module. if (!$context instanceof \context_module) { return; } // Get the course module. if (!$cm = get_coursemodule_from_id('forum', $context->instanceid)) { return; } $forumid = $cm->instance; $DB->delete_records('forum_track_prefs', ['forumid' => $forumid]); $DB->delete_records('forum_subscriptions', ['forum' => $forumid]); $DB->delete_records('forum_grades', ['forum' => $forumid]); $DB->delete_records('forum_read', ['forumid' => $forumid]); $DB->delete_records('forum_digests', ['forum' => $forumid]); // Delete advanced grading information. $gradingmanager = get_grading_manager($context, 'mod_forum', 'forum'); $controller = $gradingmanager->get_active_controller(); if (isset($controller)) { \core_grading\privacy\provider::delete_instance_data($context); } $DB->delete_records('forum_grades', ['forum' => $forumid]); // Delete all discussion items. $DB->delete_records_select( 'forum_queue', "discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)", [ 'forum' => $forumid, ] ); $DB->delete_records_select( 'forum_posts', "discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)", [ 'forum' => $forumid, ] ); $DB->delete_records('forum_discussion_subs', ['forum' => $forumid]); $DB->delete_records('forum_discussions', ['forum' => $forumid]); // Delete all files from the posts. $fs = get_file_storage(); $fs->delete_area_files($context->id, 'mod_forum', 'post'); $fs->delete_area_files($context->id, 'mod_forum', 'attachment'); // Delete all ratings in the context. \core_rating\privacy\provider::delete_ratings($context, 'mod_forum', 'post'); // Delete all Tags. \core_tag\privacy\provider::delete_item_tags($context, 'mod_forum', 'forum_posts'); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; $user = $contextlist->get_user(); $userid = $user->id; foreach ($contextlist as $context) { // Get the course module. $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); $forum = $DB->get_record('forum', ['id' => $cm->instance]); $DB->delete_records('forum_track_prefs', [ 'forumid' => $forum->id, 'userid' => $userid, ]); $DB->delete_records('forum_subscriptions', [ 'forum' => $forum->id, 'userid' => $userid, ]); $DB->delete_records('forum_read', [ 'forumid' => $forum->id, 'userid' => $userid, ]); $DB->delete_records('forum_digests', [ 'forum' => $forum->id, 'userid' => $userid, ]); // Delete all discussion items. $DB->delete_records_select( 'forum_queue', "userid = :userid AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)", [ 'userid' => $userid, 'forum' => $forum->id, ] ); $DB->delete_records('forum_discussion_subs', [ 'forum' => $forum->id, 'userid' => $userid, ]); // Handle any advanced grading method data first. $grades = $DB->get_records('forum_grades', ['forum' => $forum->id, 'userid' => $user->id]); $gradingmanager = get_grading_manager($context, 'forum_grades', 'forum'); $controller = $gradingmanager->get_active_controller(); foreach ($grades as $grade) { // Delete advanced grading information. if (isset($controller)) { \core_grading\privacy\provider::delete_instance_data($context, $grade->id); } } // Advanced grading methods have been cleared, lets clear our module now. $DB->delete_records('forum_grades', [ 'forum' => $forum->id, 'userid' => $userid, ]); // Do not delete discussion or forum posts. // Instead update them to reflect that the content has been deleted. $postsql = "userid = :userid AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forum)"; $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}"; $postparams = [ 'forum' => $forum->id, 'userid' => $userid, ]; // Update the subject. $DB->set_field_select('forum_posts', 'subject', '', $postsql, $postparams); // Update the message and its format. $DB->set_field_select('forum_posts', 'message', '', $postsql, $postparams); $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $postparams); // Mark the post as deleted. $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $postparams); // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts. // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating // of any post. \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post', "IN ($postidsql)", $postparams); // Delete all Tags. \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts', "IN ($postidsql)", $postparams); // Delete all files from the posts. $fs = get_file_storage(); $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $postparams); $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $postparams); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); $forum = $DB->get_record('forum', ['id' => $cm->instance]); list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $params = array_merge(['forumid' => $forum->id], $userinparams); $DB->delete_records_select('forum_track_prefs', "forumid = :forumid AND userid {$userinsql}", $params); $DB->delete_records_select('forum_subscriptions', "forum = :forumid AND userid {$userinsql}", $params); $DB->delete_records_select('forum_read', "forumid = :forumid AND userid {$userinsql}", $params); $DB->delete_records_select( 'forum_queue', "userid {$userinsql} AND discussionid IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)", $params ); $DB->delete_records_select('forum_discussion_subs', "forum = :forumid AND userid {$userinsql}", $params); // Do not delete discussion or forum posts. // Instead update them to reflect that the content has been deleted. $postsql = "userid {$userinsql} AND discussion IN (SELECT id FROM {forum_discussions} WHERE forum = :forumid)"; $postidsql = "SELECT fp.id FROM {forum_posts} fp WHERE {$postsql}"; // Update the subject. $DB->set_field_select('forum_posts', 'subject', '', $postsql, $params); // Update the subject and its format. $DB->set_field_select('forum_posts', 'message', '', $postsql, $params); $DB->set_field_select('forum_posts', 'messageformat', FORMAT_PLAIN, $postsql, $params); // Mark the post as deleted. $DB->set_field_select('forum_posts', 'deleted', 1, $postsql, $params); // Note: Do _not_ delete ratings of other users. Only delete ratings on the users own posts. // Ratings are aggregate fields and deleting the rating of this post will have an effect on the rating // of any post. \core_rating\privacy\provider::delete_ratings_select($context, 'mod_forum', 'post', "IN ($postidsql)", $params); // Delete all Tags. \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_forum', 'forum_posts', "IN ($postidsql)", $params); // Delete all files from the posts. $fs = get_file_storage(); $fs->delete_area_files_select($context->id, 'mod_forum', 'post', "IN ($postidsql)", $params); $fs->delete_area_files_select($context->id, 'mod_forum', 'attachment', "IN ($postidsql)", $params); list($sql, $params) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $params['forum'] = $forum->id; // Delete advanced grading information. $grades = $DB->get_records_select('forum_grades', "forum = :forum AND userid $sql", $params); $gradeids = array_keys($grades); $gradingmanager = get_grading_manager($context, 'mod_forum', 'forum'); $controller = $gradingmanager->get_active_controller(); if (isset($controller)) { // Careful here, if no gradeids are provided then all data is deleted for the context. if (!empty($gradeids)) { \core_grading\privacy\provider::delete_data_for_instances($context, $gradeids); } } } }