You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

692 lines
29 KiB

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