. /** * Privacy Subsystem implementation for core_backup. * * @package core_backup * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); /** * Privacy Subsystem implementation for core_backup. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\subsystem\provider { /** * Return the fields which contain personal data. * * @param collection $items a reference to the collection to use to store the metadata. * @return collection the updated collection of metadata items. */ public static function get_metadata(collection $items) : collection { $items->link_external_location( 'Backup', [ 'detailsofarchive' => 'privacy:metadata:backup:detailsofarchive' ], 'privacy:metadata:backup:externalpurpose' ); $items->add_database_table( 'backup_controllers', [ 'operation' => 'privacy:metadata:backup_controllers:operation', 'type' => 'privacy:metadata:backup_controllers:type', 'itemid' => 'privacy:metadata:backup_controllers:itemid', 'timecreated' => 'privacy:metadata:backup_controllers:timecreated', 'timemodified' => 'privacy:metadata:backup_controllers:timemodified' ], 'privacy:metadata:backup_controllers' ); return $items; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid) : contextlist { $contextlist = new contextlist(); $sql = "SELECT ctx.id FROM {backup_controllers} bc JOIN {context} ctx ON ctx.instanceid = bc.itemid AND ctx.contextlevel = :contextlevel AND bc.type = :type WHERE bc.userid = :userid"; $params = [ 'contextlevel' => CONTEXT_COURSE, 'userid' => $userid, 'type' => 'course', ]; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {backup_controllers} bc JOIN {course_sections} c ON bc.itemid = c.id AND bc.type = :type JOIN {context} ctx ON ctx.instanceid = c.course AND ctx.contextlevel = :contextlevel WHERE bc.userid = :userid"; $params = [ 'contextlevel' => CONTEXT_COURSE, 'userid' => $userid, 'type' => 'section', ]; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {backup_controllers} bc JOIN {context} ctx ON ctx.instanceid = bc.itemid AND ctx.contextlevel = :contextlevel AND bc.type = :type WHERE bc.userid = :userid"; $params = [ 'contextlevel' => CONTEXT_MODULE, 'userid' => $userid, 'type' => 'activity', ]; $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 ($context instanceof \context_course) { $params = ['courseid' => $context->instanceid]; $sql = "SELECT bc.userid FROM {backup_controllers} bc WHERE bc.itemid = :courseid AND bc.type = :typecourse"; $courseparams = ['typecourse' => 'course'] + $params; $userlist->add_from_sql('userid', $sql, $courseparams); $sql = "SELECT bc.userid FROM {backup_controllers} bc JOIN {course_sections} c ON bc.itemid = c.id WHERE c.course = :courseid AND bc.type = :typesection"; $sectionparams = ['typesection' => 'section'] + $params; $userlist->add_from_sql('userid', $sql, $sectionparams); } if ($context instanceof \context_module) { $params = [ 'cmid' => $context->instanceid, 'typeactivity' => 'activity' ]; $sql = "SELECT bc.userid FROM {backup_controllers} bc WHERE bc.itemid = :cmid AND bc.type = :typeactivity"; $userlist->add_from_sql('userid', $sql, $params); } } /** * 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->count())) { return; } $user = $contextlist->get_user(); list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); $sql = "SELECT bc.* FROM {backup_controllers} bc JOIN {context} ctx ON ctx.instanceid = bc.itemid AND ctx.contextlevel = :contextlevel WHERE ctx.id {$contextsql} AND bc.userid = :userid ORDER BY bc.timecreated ASC"; $params = ['contextlevel' => CONTEXT_COURSE, 'userid' => $user->id] + $contextparams; $backupcontrollers = $DB->get_recordset_sql($sql, $params); self::recordset_loop_and_export($backupcontrollers, 'itemid', [], function($carry, $record) { $carry[] = [ 'operation' => $record->operation, 'type' => $record->type, 'itemid' => $record->itemid, 'timecreated' => transform::datetime($record->timecreated), 'timemodified' => transform::datetime($record->timemodified), ]; return $carry; }, function($courseid, $data) { $context = \context_course::instance($courseid); $finaldata = (object) $data; writer::with_context($context)->export_data([get_string('backup'), $courseid], $finaldata); }); } /** * Delete all user data which matches the specified context. * Only dealing with the specific context - not it's child contexts. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if ($context instanceof \context_course) { $sectionsql = "itemid IN (SELECT id FROM {course_sections} WHERE course = ?) AND type = ?"; $DB->delete_records_select('backup_controllers', $sectionsql, [$context->instanceid, \backup::TYPE_1SECTION]); $DB->delete_records('backup_controllers', ['itemid' => $context->instanceid, 'type' => \backup::TYPE_1COURSE]); } if ($context instanceof \context_module) { $DB->delete_records('backup_controllers', ['itemid' => $context->instanceid, 'type' => \backup::TYPE_1ACTIVITY]); } return; } /** * Delete multiple users within a single context. * Only dealing with the specific context - not it's child contexts. * * @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; if (empty($userlist->get_userids())) { return; } $context = $userlist->get_context(); if ($context instanceof \context_course) { list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $select = "itemid = :itemid AND userid {$usersql} AND type = :type"; $params = $userparams; $params['itemid'] = $context->instanceid; $params['type'] = \backup::TYPE_1COURSE; $DB->delete_records_select('backup_controllers', $select, $params); $params = $userparams; $params['course'] = $context->instanceid; $params['type'] = \backup::TYPE_1SECTION; $sectionsql = "itemid IN (SELECT id FROM {course_sections} WHERE course = :course)"; $select = $sectionsql . " AND userid {$usersql} AND type = :type"; $DB->delete_records_select('backup_controllers', $select, $params); } if ($context instanceof \context_module) { list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $select = "itemid = :itemid AND userid {$usersql} AND type = :type"; $params = $userparams; $params['itemid'] = $context->instanceid; $params['type'] = \backup::TYPE_1ACTIVITY; // Delete activity backup data. $select = "itemid = :itemid AND type = :type AND userid {$usersql}"; $params = ['itemid' => $context->instanceid, 'type' => 'activity'] + $userparams; $DB->delete_records_select('backup_controllers', $select, $params); } } /** * Delete all user data for the specified user, in the specified contexts. * Only dealing with the specific context - not it's child 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; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context instanceof \context_course) { $select = "itemid = :itemid AND userid = :userid AND type = :type"; $params = [ 'userid' => $userid, 'itemid' => $context->instanceid, 'type' => \backup::TYPE_1COURSE ]; $DB->delete_records_select('backup_controllers', $select, $params); $params = [ 'userid' => $userid, 'course' => $context->instanceid, 'type' => \backup::TYPE_1SECTION ]; $sectionsql = "itemid IN (SELECT id FROM {course_sections} WHERE course = :course)"; $select = $sectionsql . " AND userid = :userid AND type = :type"; $DB->delete_records_select('backup_controllers', $select, $params); } if ($context instanceof \context_module) { list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $select = "itemid = :itemid AND userid = :userid AND type = :type"; $params = [ 'itemid' => $context->instanceid, 'userid' => $userid, 'type' => \backup::TYPE_1ACTIVITY ]; $DB->delete_records_select('backup_controllers', $select, $params); } } } /** * Loop and export from a recordset. * * @param \moodle_recordset $recordset The recordset. * @param string $splitkey The record key to determine when to export. * @param mixed $initial The initial data to reduce from. * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. * @return void */ protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, callable $reducer, callable $export) { $data = $initial; $lastid = null; foreach ($recordset as $record) { if ($lastid && $record->{$splitkey} != $lastid) { $export($lastid, $data); $data = $initial; } $data = $reducer($data, $record); $lastid = $record->{$splitkey}; } $recordset->close(); if (!empty($lastid)) { $export($lastid, $data); } } }