<?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);
}
}
}
}