. /** * Data provider. * * @package core_badges * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_badges\privacy; defined('MOODLE_INTERNAL') || die(); use badge; use context; use context_course; use context_helper; use context_system; use context_user; use core_text; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_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; require_once($CFG->libdir . '/badgeslib.php'); /** * Data provider class. * * @package core_badges * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @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 { /** * Returns metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { $collection->add_database_table('badge', [ 'usercreated' => 'privacy:metadata:badge:usercreated', 'usermodified' => 'privacy:metadata:badge:usermodified', 'timecreated' => 'privacy:metadata:badge:timecreated', 'timemodified' => 'privacy:metadata:badge:timemodified', ], 'privacy:metadata:badge'); $collection->add_database_table('badge_issued', [ 'userid' => 'privacy:metadata:issued:userid', 'dateissued' => 'privacy:metadata:issued:dateissued', 'dateexpire' => 'privacy:metadata:issued:dateexpire', ], 'privacy:metadata:issued'); $collection->add_database_table('badge_criteria_met', [ 'userid' => 'privacy:metadata:criteriamet:userid', 'datemet' => 'privacy:metadata:criteriamet:datemet', ], 'privacy:metadata:criteriamet'); $collection->add_database_table('badge_manual_award', [ 'recipientid' => 'privacy:metadata:manualaward:recipientid', 'issuerid' => 'privacy:metadata:manualaward:issuerid', 'issuerrole' => 'privacy:metadata:manualaward:issuerrole', 'datemet' => 'privacy:metadata:manualaward:datemet', ], 'privacy:metadata:manualaward'); $collection->add_database_table('badge_backpack', [ 'userid' => 'privacy:metadata:backpack:userid', 'email' => 'privacy:metadata:backpack:email', 'externalbackpackid' => 'privacy:metadata:backpack:externalbackpackid', 'backpackuid' => 'privacy:metadata:backpack:backpackuid', // The columns autosync and password are not used. ], 'privacy:metadata:backpack'); $collection->add_external_location_link('backpacks', [ 'name' => 'privacy:metadata:external:backpacks:badge', 'description' => 'privacy:metadata:external:backpacks:description', 'image' => 'privacy:metadata:external:backpacks:image', 'url' => 'privacy:metadata:external:backpacks:url', 'issuer' => 'privacy:metadata:external:backpacks:issuer', ], 'privacy:metadata:external:backpacks'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @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(); // Find the modifications we made on badges (course & system). $sql = " SELECT ctx.id FROM {badge} b JOIN {context} ctx ON (b.type = :typecourse AND b.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel) OR (b.type = :typesite AND ctx.id = :syscontextid) WHERE b.usermodified = :userid1 OR b.usercreated = :userid2"; $params = [ 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID, 'typecourse' => BADGE_TYPE_COURSE, 'typesite' => BADGE_TYPE_SITE, 'userid1' => $userid, 'userid2' => $userid, ]; $contextlist->add_from_sql($sql, $params); // Find where we've manually awarded a badge (recipient user context). $sql = " SELECT ctx.id FROM {badge_manual_award} bma JOIN {context} ctx ON ctx.instanceid = bma.recipientid AND ctx.contextlevel = :userlevel WHERE bma.issuerid = :userid"; $params = [ 'userlevel' => CONTEXT_USER, 'userid' => $userid, ]; $contextlist->add_from_sql($sql, $params); // Now find where there is real user data (user context). $sql = " SELECT ctx.id FROM {context} ctx LEFT JOIN {badge_manual_award} bma ON bma.recipientid = ctx.instanceid LEFT JOIN {badge_issued} bi ON bi.userid = ctx.instanceid LEFT JOIN {badge_criteria_met} bcm ON bcm.userid = ctx.instanceid LEFT JOIN {badge_backpack} bb ON bb.userid = ctx.instanceid WHERE ctx.contextlevel = :userlevel AND ctx.instanceid = :userid AND (bma.id IS NOT NULL OR bi.id IS NOT NULL OR bcm.id IS NOT NULL OR bb.id IS NOT NULL)"; $params = [ 'userlevel' => CONTEXT_USER, '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(); $allowedcontexts = [ CONTEXT_COURSE, CONTEXT_SYSTEM, CONTEXT_USER ]; if (!in_array($context->contextlevel, $allowedcontexts)) { return; } if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_SYSTEM) { // Find the modifications we made on badges (course & system). if ($context->contextlevel == CONTEXT_COURSE) { $extrawhere = 'AND b.courseid = :courseid'; $params = [ 'badgetype' => BADGE_TYPE_COURSE, 'courseid' => $context->instanceid ]; } else { $extrawhere = ''; $params = ['badgetype' => BADGE_TYPE_SITE]; } $sql = "SELECT b.usermodified, b.usercreated FROM {badge} b WHERE b.type = :badgetype $extrawhere"; $userlist->add_from_sql('usermodified', $sql, $params); $userlist->add_from_sql('usercreated', $sql, $params); } if ($context->contextlevel == CONTEXT_USER) { // Find where we've manually awarded a badge (recipient user context). $params = [ 'instanceid' => $context->instanceid ]; $sql = "SELECT issuerid, recipientid FROM {badge_manual_award} WHERE recipientid = :instanceid"; $userlist->add_from_sql('issuerid', $sql, $params); $userlist->add_from_sql('recipientid', $sql, $params); $sql = "SELECT userid FROM {badge_issued} WHERE userid = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT userid FROM {badge_criteria_met} WHERE userid = :instanceid"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT userid FROM {badge_backpack} WHERE userid = :instanceid"; $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; $userid = $contextlist->get_user()->id; $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) { $level = $context->contextlevel; if ($level == CONTEXT_USER || $level == CONTEXT_COURSE) { $carry[$level][] = $context->instanceid; } else if ($level == CONTEXT_SYSTEM) { $carry[$level] = SYSCONTEXTID; } return $carry; }, [ CONTEXT_COURSE => [], CONTEXT_USER => [], CONTEXT_SYSTEM => null, ]); $path = [get_string('badges', 'core_badges')]; $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); // Export the badges we've created or modified. if (!empty($contexts[CONTEXT_SYSTEM]) || !empty($contexts[CONTEXT_COURSE])) { $sqls = []; $params = []; if (!empty($contexts[CONTEXT_SYSTEM])) { $sqls[] = "b.type = :typesite"; $params['typesite'] = BADGE_TYPE_SITE; } if (!empty($contexts[CONTEXT_COURSE])) { list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_COURSE], SQL_PARAMS_NAMED); $sqls[] = "(b.type = :typecourse AND b.courseid $insql)"; $params = array_merge($params, ['typecourse' => BADGE_TYPE_COURSE], $inparams); } $sqlwhere = '(' . implode(' OR ', $sqls) . ')'; $sql = " SELECT b.*, COALESCE(b.courseid, 0) AS normalisedcourseid FROM {badge} b WHERE (b.usermodified = :userid1 OR b.usercreated = :userid2) AND $sqlwhere ORDER BY b.courseid, b.id"; $params = array_merge($params, ['userid1' => $userid, 'userid2' => $userid]); $recordset = $DB->get_recordset_sql($sql, $params); static::recordset_loop_and_export($recordset, 'normalisedcourseid', [], function($carry, $record) use ($userid) { $carry[] = [ 'name' => $record->name, 'created_on' => transform::datetime($record->timecreated), 'created_by_you' => transform::yesno($record->usercreated == $userid), 'modified_on' => transform::datetime($record->timemodified), 'modified_by_you' => transform::yesno($record->usermodified == $userid), ]; return $carry; }, function($courseid, $data) use ($path) { $context = $courseid ? context_course::instance($courseid) : context_system::instance(); writer::with_context($context)->export_data($path, (object) ['badges' => $data]); }); } // Export the badges we've manually awarded. if (!empty($contexts[CONTEXT_USER])) { list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_USER], SQL_PARAMS_NAMED); $sql = " SELECT bma.id, bma.recipientid, bma.datemet, b.name, b.courseid, r.id AS roleid, r.name AS rolename, r.shortname AS roleshortname, r.archetype AS rolearchetype, $ctxfields FROM {badge_manual_award} bma JOIN {badge} b ON b.id = bma.badgeid JOIN {role} r ON r.id = bma.issuerrole JOIN {context} ctx ON (COALESCE(b.courseid, 0) > 0 AND ctx.instanceid = b.courseid AND ctx.contextlevel = :courselevel) OR (COALESCE(b.courseid, 0) = 0 AND ctx.id = :syscontextid) WHERE bma.recipientid $insql AND bma.issuerid = :userid ORDER BY bma.recipientid, bma.id"; $params = array_merge($inparams, [ 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID, 'userid' => $userid ]); $recordset = $DB->get_recordset_sql($sql, $params); static::recordset_loop_and_export($recordset, 'recipientid', [], function($carry, $record) use ($userid) { // The only reason we fetch the context and role is to format the name of the role, which could be // different to the standard name if the badge was created in a course. context_helper::preload_from_record($record); $context = $record->courseid ? context_course::instance($record->courseid) : context_system::instance(); $role = (object) [ 'id' => $record->roleid, 'name' => $record->rolename, 'shortname' => $record->roleshortname, 'archetype' => $record->rolearchetype, // Mock those two fields as they do not matter. 'sortorder' => 0, 'description' => '' ]; $carry[] = [ 'name' => $record->name, 'issued_by_you' => transform::yesno(true), 'issued_on' => transform::datetime($record->datemet), 'issuer_role' => role_get_name($role, $context), ]; return $carry; }, function($userid, $data) use ($path) { $context = context_user::instance($userid); writer::with_context($context)->export_related_data($path, 'manual_awards', (object) ['badges' => $data]); }); } // Export our data. if (in_array($userid, $contexts[CONTEXT_USER])) { // Export the badges. $uniqueid = $DB->sql_concat_join("'-'", ['b.id', 'COALESCE(bc.id, 0)', 'COALESCE(bi.id, 0)', 'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)', 'COALESCE(brb.id, 0)', 'COALESCE(ba.id, 0)']); $sql = " SELECT $uniqueid AS uniqueid, b.id, bi.id AS biid, bi.dateissued, bi.dateexpire, bi.uniquehash, bma.id AS bmaid, bma.datemet, bma.issuerid, bcm.id AS bcmid, c.fullname AS coursename, be.id AS beid, be.issuername AS beissuername, be.issuerurl AS beissuerurl, be.issueremail AS beissueremail, be.claimid AS beclaimid, be.claimcomment AS beclaimcomment, be.dateissued AS bedateissued, brb.id as rbid, brb.badgeid as rbbadgeid, brb.relatedbadgeid as rbrelatedbadgeid, ba.id as baid, ba.targetname as batargetname, ba.targeturl as batargeturl, ba.targetdescription as batargetdescription, ba.targetframework as batargetframework, ba.targetcode as batargetcode, $ctxfields FROM {badge} b LEFT JOIN {badge_issued} bi ON bi.badgeid = b.id AND bi.userid = :userid1 LEFT JOIN {badge_related} brb ON ( b.id = brb.badgeid OR b.id = brb.relatedbadgeid ) LEFT JOIN {badge_alignment} ba ON ( b.id = ba.badgeid ) LEFT JOIN {badge_endorsement} be ON be.badgeid = b.id LEFT JOIN {badge_manual_award} bma ON bma.badgeid = b.id AND bma.recipientid = :userid2 LEFT JOIN {badge_criteria} bc ON bc.badgeid = b.id LEFT JOIN {badge_criteria_met} bcm ON bcm.critid = bc.id AND bcm.userid = :userid3 LEFT JOIN {course} c ON c.id = b.courseid AND b.type = :typecourse LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :courselevel WHERE bi.id IS NOT NULL OR bma.id IS NOT NULL OR bcm.id IS NOT NULL ORDER BY b.id"; $params = [ 'userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'courselevel' => CONTEXT_COURSE, 'typecourse' => BADGE_TYPE_COURSE, ]; $recordset = $DB->get_recordset_sql($sql, $params); static::recordset_loop_and_export($recordset, 'id', null, function($carry, $record) use ($userid) { $badge = new badge($record->id); // Export details of the badge. if ($carry === null) { $carry = [ 'name' => $badge->name, 'version' => $badge->version, 'language' => $badge->language, 'imageauthorname' => $badge->imageauthorname, 'imageauthoremail' => $badge->imageauthoremail, 'imageauthorurl' => $badge->imageauthorurl, 'imagecaption' => $badge->imagecaption, 'issued' => null, 'manual_award' => null, 'criteria_met' => [], 'endorsement' => null, ]; if ($badge->type == BADGE_TYPE_COURSE) { context_helper::preload_from_record($record); $carry['course'] = format_string($record->coursename, true, ['context' => $badge->get_context()]); } if (!empty($record->beid)) { $carry['endorsement'] = [ 'issuername' => $record->beissuername, 'issuerurl' => $record->beissuerurl, 'issueremail' => $record->beissueremail, 'claimid' => $record->beclaimid, 'claimcomment' => $record->beclaimcomment, 'dateissued' => $record->bedateissued ? transform::datetime($record->bedateissued) : null ]; } if (!empty($record->biid)) { $carry['issued'] = [ 'issued_on' => transform::datetime($record->dateissued), 'expires_on' => $record->dateexpire ? transform::datetime($record->dateexpire) : null, 'unique_hash' => $record->uniquehash, ]; } if (!empty($record->bmaid)) { $carry['manual_award'] = [ 'awarded_on' => transform::datetime($record->datemet), 'issuer' => transform::user($record->issuerid) ]; } } if (!empty($record->rbid)) { if (empty($carry['related_badge'])) { $carry['related_badge'] = []; } $rbid = $record->rbbadgeid; if ($rbid == $record->id) { $rbid = $record->rbrelatedbadgeid; } $exists = false; foreach ($carry['related_badge'] as $related) { if ($related['badgeid'] == $rbid) { $exists = true; break; } } if (!$exists) { $relatedbadge = new badge($rbid); $carry['related_badge'][] = [ 'badgeid' => $rbid, 'badgename' => $relatedbadge->name ]; } } if (!empty($record->baid)) { if (empty($carry['alignment'])) { $carry['alignment'] = []; } $exists = false; $newalignment = [ 'targetname' => $record->batargetname, 'targeturl' => $record->batargeturl, 'targetdescription' => $record->batargetdescription, 'targetframework' => $record->batargetframework, 'targetcode' => $record->batargetcode, ]; foreach ($carry['alignment'] as $alignment) { if ($alignment == $newalignment) { $exists = true; break; } } if (!$exists) { $carry['alignment'][] = $newalignment; } } // Export the details of the criteria met. // We only do that once, when we find that a least one criteria was met. // This is heavily based on the logic present in core_badges_renderer::render_issued_badge. if (!empty($record->bcmid) && empty($carry['criteria_met'])) { $agg = $badge->get_aggregation_methods(); $evidenceids = array_map(function($record) { return $record->critid; }, $badge->get_criteria_completions($userid)); $criteria = $badge->criteria; unset($criteria[BADGE_CRITERIA_TYPE_OVERALL]); $items = []; foreach ($criteria as $type => $c) { if (in_array($c->id, $evidenceids)) { $details = $c->get_details(true); if (count($c->params) == 1) { $items[] = get_string('criteria_descr_single_' . $type , 'core_badges') . ' ' . $details; } else { $items[] = get_string('criteria_descr_' . $type , 'core_badges', core_text::strtoupper($agg[$badge->get_aggregation_method($type)])) . ' ' . $details; } } } $carry['criteria_met'] = $items; } return $carry; }, function($badgeid, $data) use ($path, $userid) { $path = array_merge($path, ["{$data['name']} ({$badgeid})"]); $writer = writer::with_context(context_user::instance($userid)); $writer->export_data($path, (object) $data); $writer->export_area_files($path, 'badges', 'userbadge', $badgeid); }); // Export the backpacks. $data = []; $recordset = $DB->get_recordset_select('badge_backpack', 'userid = :userid', ['userid' => $userid]); foreach ($recordset as $record) { $data[] = [ 'email' => $record->email, 'externalbackpackid' => $record->externalbackpackid, 'uid' => $record->backpackuid ]; } $recordset->close(); if (!empty($data)) { writer::with_context(context_user::instance($userid))->export_related_data($path, 'backpacks', (object) ['backpacks' => $data]); } } } /** * 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) { // We cannot delete the course or system data as it is needed by the system. if ($context->contextlevel != CONTEXT_USER) { return; } // Delete all the user data. static::delete_user_data($context->instanceid); } /** * 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) { $context = $userlist->get_context(); if (!in_array($context->instanceid, $userlist->get_userids())) { return; } if ($context->contextlevel == CONTEXT_USER) { // We can only delete our own data in the user context, nothing in course or system. static::delete_user_data($context->instanceid); } } /** * 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) { $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) { // We can only delete our own data in the user context, nothing in course or system. static::delete_user_data($userid); break; } } } /** * Delete all the data for a user. * * @param int $userid The user ID. * @return void */ protected static function delete_user_data($userid) { global $DB; // Delete the stuff. $DB->delete_records('badge_manual_award', ['recipientid' => $userid]); $DB->delete_records('badge_criteria_met', ['userid' => $userid]); $DB->delete_records('badge_issued', ['userid' => $userid]); // Delete the backpacks and related stuff. $backpackids = $DB->get_fieldset_select('badge_backpack', 'id', 'userid = :userid', ['userid' => $userid]); if (!empty($backpackids)) { list($insql, $inparams) = $DB->get_in_or_equal($backpackids, SQL_PARAMS_NAMED); $DB->delete_records_select('badge_external', "backpackid $insql", $inparams); $DB->delete_records_select('badge_backpack', "id $insql", $inparams); } } /** * 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 !== null && $record->{$splitkey} != $lastid) { $export($lastid, $data); $data = $initial; } $data = $reducer($data, $record); $lastid = $record->{$splitkey}; } $recordset->close(); if ($lastid !== null) { $export($lastid, $data); } } }