Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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/>./*** Data provider.** @package core_badges* @copyright 2018 Frédéric Massart* @author Frédéric Massart <fred@branchup.tech>* @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 <fred@branchup.tech>* @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');$collection->add_database_table('badge_backpack_oauth2', ['userid' => 'privacy:metadata:backpackoauth2:userid','usermodified' => 'privacy:metadata:backpackoauth2:usermodified','token' => 'privacy:metadata:backpackoauth2:token','issuerid' => 'privacy:metadata:backpackoauth2:issuerid','scope' => 'privacy:metadata:backpackoauth2:scope',], 'privacy:metadata:backpackoauth2');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.idFROM {badge} bJOIN {context} ctxON (b.type = :typecourse AND b.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel)OR (b.type = :typesite AND ctx.id = :syscontextid)WHERE b.usermodified = :userid1OR 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.idFROM {badge_manual_award} bmaJOIN {context} ctxON ctx.instanceid = bma.recipientidAND ctx.contextlevel = :userlevelWHERE 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.idFROM {context} ctxLEFT JOIN {badge_manual_award} bmaON bma.recipientid = ctx.instanceidLEFT JOIN {badge_issued} biON bi.userid = ctx.instanceidLEFT JOIN {badge_criteria_met} bcmON bcm.userid = ctx.instanceidLEFT JOIN {badge_backpack} bbON bb.userid = ctx.instanceidWHERE ctx.contextlevel = :userlevelAND ctx.instanceid = :useridAND (bma.id IS NOT NULLOR bi.id IS NOT NULLOR bcm.id IS NOT NULLOR 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.usercreatedFROM {badge} bWHERE 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, recipientidFROM {badge_manual_award}WHERE recipientid = :instanceid";$userlist->add_from_sql('issuerid', $sql, $params);$userlist->add_from_sql('recipientid', $sql, $params);$sql = "SELECT useridFROM {badge_issued}WHERE userid = :instanceid";$userlist->add_from_sql('userid', $sql, $params);$sql = "SELECT useridFROM {badge_criteria_met}WHERE userid = :instanceid";$userlist->add_from_sql('userid', $sql, $params);$sql = "SELECT useridFROM {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 normalisedcourseidFROM {badge} bWHERE (b.usermodified = :userid1 OR b.usercreated = :userid2)AND $sqlwhereORDER 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,$ctxfieldsFROM {badge_manual_award} bmaJOIN {badge} bON b.id = bma.badgeidJOIN {role} rON r.id = bma.issuerroleJOIN {context} ctxON (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 $insqlAND bma.issuerid = :useridORDER 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,$ctxfieldsFROM {badge} bLEFT JOIN {badge_issued} biON bi.badgeid = b.idAND bi.userid = :userid1LEFT JOIN {badge_related} brbON ( b.id = brb.badgeid OR b.id = brb.relatedbadgeid )LEFT JOIN {badge_alignment} baON ( b.id = ba.badgeid )LEFT JOIN {badge_endorsement} beON be.badgeid = b.idLEFT JOIN {badge_manual_award} bmaON bma.badgeid = b.idAND bma.recipientid = :userid2LEFT JOIN {badge_criteria} bcON bc.badgeid = b.idLEFT JOIN {badge_criteria_met} bcmON bcm.critid = bc.idAND bcm.userid = :userid3LEFT JOIN {course} cON c.id = b.courseidAND b.type = :typecourseLEFT JOIN {context} ctxON ctx.instanceid = c.idAND ctx.contextlevel = :courselevelWHERE bi.id IS NOT NULLOR bma.id IS NOT NULLOR bcm.id IS NOT NULLORDER 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);}}}