diff --git a/enrol/classes/privacy/provider.php b/enrol/classes/privacy/provider.php new file mode 100644 index 00000000000..46a0c6309ba --- /dev/null +++ b/enrol/classes/privacy/provider.php @@ -0,0 +1,233 @@ +. +/** + * Privacy Subsystem implementation for core_enrol. + * + * @package core_enrol + * @copyright 2018 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_enrol\privacy; +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\context; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +/** + * Privacy Subsystem for core_enrol implementing metadata and plugin providers. + * + * @copyright 2018 Carlos Escobedo + * @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\subsystem\provider { + /** + * Returns meta data about this system. + * + * @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( + 'user_enrolments', + [ + 'status' => 'privacy:metadata:user_enrolments:status', + 'enrolid' => 'privacy:metadata:user_enrolments:enrolid', + 'userid' => 'privacy:metadata:user_enrolments:userid', + 'timestart' => 'privacy:metadata:user_enrolments:timestart', + 'timeend' => 'privacy:metadata:user_enrolments:timeend', + 'modifierid' => 'privacy:metadata:user_enrolments:modifierid', + 'timecreated' => 'privacy:metadata:user_enrolments:timecreated', + 'timemodified' => 'privacy:metadata:user_enrolments:timemodified' + ], + 'privacy:metadata:user_enrolments:tableexplanation' + ); + + 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) : contextlist { + $sql = "SELECT ctx.id + FROM {user_enrolments} ue + JOIN {enrol} e + ON e.id = ue.enrolid + AND ue.userid = :userid + JOIN {context} ctx + ON ctx.instanceid = e.courseid + AND ctx.contextlevel = :contextlevel"; + $params = [ + 'contextlevel' => CONTEXT_COURSE, + 'userid' => $userid + ]; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + /** + * 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; + } + $userid = $contextlist->get_user()->id; + list($insql, $inparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $params = [ + 'contextlevel' => CONTEXT_COURSE, + 'userid' => $userid + ]; + $params += $inparams; + $sql = "SELECT ue.id, + ue.status, + ue.timestart, + ue.timeend, + ue.timecreated, + ue.timemodified, + e.enrol, + ctx.id as contextid + FROM {user_enrolments} ue + JOIN {enrol} e + ON e.id = ue.enrolid + AND ue.userid = :userid + JOIN {context} ctx + ON ctx.instanceid = e.courseid + AND ctx.contextlevel = :contextlevel + WHERE ctx.id $insql + ORDER BY ctx.id, e.enrol"; + $data = []; + $lastcontextid = null; + $lastenrol = null; + $path = [get_string('privacy:metadata:user_enrolments', 'core_enrol')]; + $flush = function($lastcontextid, $lastenrol, $data) use ($path) { + $context = \context::instance_by_id($lastcontextid); + writer::with_context($context)->export_related_data( + $path, + $lastenrol, + (object)$data + ); + }; + $userenrolments = $DB->get_recordset_sql($sql, $params); + foreach ($userenrolments as $userenrolment) { + if (($lastcontextid && $lastcontextid != $userenrolment->contextid) || + ($lastenrol && $lastenrol != $userenrolment->enrol)) { + $flush($lastcontextid, $lastenrol, $data); + $data = []; + } + $data[] = (object) [ + 'status' => $userenrolment->status, + 'timecreated' => transform::datetime($userenrolment->timecreated), + 'timemodified' => transform::datetime($userenrolment->timemodified), + 'timestart' => transform::datetime($userenrolment->timestart), + 'timeend' => transform::datetime($userenrolment->timeend) + ]; + $lastcontextid = $userenrolment->contextid; + $lastenrol = $userenrolment->enrol; + } + if (!empty($data)) { + $flush($lastcontextid, $lastenrol, $data); + } + $userenrolments->close(); + } + /** + * 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; + + if (empty($context)) { + return; + } + // Sanity check that context is at the User context level. + if ($context->contextlevel == CONTEXT_COURSE) { + $sql = "SELECT ue.id + FROM {user_enrolments} ue + JOIN {enrol} e + ON e.id = ue.enrolid + JOIN {context} ctx + ON ctx.instanceid = e.courseid + WHERE ctx.id = :contextid"; + $params = ['contextid' => $context->id]; + $enrolsids = $DB->get_fieldset_sql($sql, $params); + if (!empty($enrolsids)) { + list($insql, $inparams) = $DB->get_in_or_equal($enrolsids, SQL_PARAMS_NAMED); + static::delete_user_data($insql, $inparams); + } + } + } + /** + * 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; + + if (empty($contextlist->count())) { + return; + } + $userid = $contextlist->get_user()->id; + list($insql, $inparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $params = [ + 'contextlevel' => CONTEXT_COURSE, + 'userid' => $userid + ]; + $params += $inparams; + $sql = "SELECT ue.id + FROM {user_enrolments} ue + JOIN {enrol} e + ON e.id = ue.enrolid + AND ue.userid = :userid + JOIN {context} ctx + ON ctx.instanceid = e.courseid + AND ctx.contextlevel = :contextlevel + WHERE ctx.id $insql"; + $enrolsids = $DB->get_fieldset_sql($sql, $params); + if (!empty($enrolsids)) { + list($insql, $inparams) = $DB->get_in_or_equal($enrolsids, SQL_PARAMS_NAMED); + static::delete_user_data($insql, $inparams); + } + } + + /** + * Delete data from $tablename with the IDs returned by $sql query. + * + * @param string $sql SQL query for getting the IDs of the uer enrolments entries to delete. + * @param array $params SQL params for the query. + */ + protected static function delete_user_data(string $sql, array $params) { + global $DB; + + $DB->delete_records_select('user_enrolments', "id $sql", $params); + } + +} diff --git a/enrol/tests/privacy_test.php b/enrol/tests/privacy_test.php new file mode 100644 index 00000000000..8d98fe2b920 --- /dev/null +++ b/enrol/tests/privacy_test.php @@ -0,0 +1,175 @@ +. +/** + * Privacy test for the core_enrol implementation of the privacy API. + * + * @package core_enrol + * @category test + * @copyright 2018 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); +use core_enrol\privacy\provider; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\writer; +use core_privacy\tests\provider_testcase; +use \core_privacy\local\request\transform; +/** + * Privacy test for the core_enrol. + * + * @package core_enrol + * @category test + * @copyright 2018 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_enrol_privacy_testcase extends provider_testcase { + /** + * Check that a course context is returned if there is any user data for this user. + */ + public function test_get_contexts_for_userid() { + $this->resetAfterTest(); + $user1 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $this->assertEmpty(provider::get_contexts_for_userid($user1->id)); + // Enrol user into courses and check contextlist. + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'manual'); + $contextlist = provider::get_contexts_for_userid($user1->id); + // Check that we only get back two context. + $this->assertCount(1, $contextlist); + // Check that the context is returned is the expected. + $coursecontext1 = \context_course::instance($course1->id); + $this->assertEquals($coursecontext1->id, $contextlist->get_contextids()[0]); + } + /** + * Test that user data is exported correctly. + */ + public function test_export_user_data() { + global $DB; + + $this->resetAfterTest(); + $user1 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'self'); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id, null, 'manual'); + $subcontexts = [ + get_string('privacy:metadata:user_enrolments', 'core_enrol') + ]; + $coursecontext1 = \context_course::instance($course1->id); + $coursecontext2 = \context_course::instance($course2->id); + $this->setUser($user1); + $writer = writer::with_context($coursecontext1); + $this->assertFalse($writer->has_any_data()); + $this->export_context_data_for_user($user1->id, $coursecontext1, 'core_enrol'); + $data = $writer->get_related_data($subcontexts); + $this->assertCount(2, (array)$data); + + $sql = "SELECT ue.id, + ue.status, + ue.timestart, + ue.timeend, + ue.timecreated, + ue.timemodified + FROM {user_enrolments} ue + JOIN {enrol} e + ON e.id = ue.enrolid + AND e.courseid = :courseid + WHERE ue.userid = :userid"; + $enrolmentcouse2 = $DB->get_record_sql($sql, array('userid' => $user1->id, 'courseid' => $course2->id)); + writer::reset(); + $writer = writer::with_context($coursecontext2); + $this->export_context_data_for_user($user1->id, $coursecontext2, 'core_enrol'); + $data = $writer->get_related_data($subcontexts, 'manual'); + $this->assertEquals($enrolmentcouse2->status, reset($data)->status); + $this->assertEquals(transform::datetime($enrolmentcouse2->timestart), reset($data)->timestart); + $this->assertEquals(transform::datetime($enrolmentcouse2->timeend), reset($data)->timeend); + $this->assertEquals(transform::datetime($enrolmentcouse2->timecreated), reset($data)->timecreated); + $this->assertEquals(transform::datetime($enrolmentcouse2->timemodified), reset($data)->timemodified); + } + /** + * Test deleting all user data for a specific context. + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(); + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user2->id, $course2->id, null, 'manual'); + // Get all user enrolments. + $userenrolments = $DB->get_records('user_enrolments', array()); + $this->assertCount(5, $userenrolments); + // Get all user enrolments match with course1. + $sql = "SELECT ue.id + FROM {user_enrolments} ue + JOIN {enrol} e + ON e.id = ue.enrolid + AND e.courseid = :courseid"; + $userenrolments = $DB->get_records_sql($sql, array('courseid' => $course1->id)); + $this->assertCount(3, $userenrolments); + // Delete everything for the first course context. + $coursecontext1 = \context_course::instance($course1->id); + provider::delete_data_for_all_users_in_context($coursecontext1); + // Get all user enrolments match with this course contest. + $userenrolments = $DB->get_records_sql($sql, array('courseid' => $course1->id)); + $this->assertCount(0, $userenrolments); + // Get all user enrolments. + $userenrolments = $DB->get_records('user_enrolments', array()); + $this->assertCount(2, $userenrolments); + } + /** + * This should work identical to the above test. + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->enrol_user($user1->id, $course1->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual'); + $this->getDataGenerator()->enrol_user($user1->id, $course2->id, null, 'manual'); + + // Get all user enrolments. + $userenrolments = $DB->get_records('user_enrolments', array()); + $this->assertCount(4, $userenrolments); + // Get all user enrolments match with user1. + $userenrolments = $DB->get_records('user_enrolments', array('userid' => $user1->id)); + $this->assertCount(2, $userenrolments); + // Delete everything for the user1 in the context course 1. + $coursecontext1 = \context_course::instance($course1->id); + $approvedlist = new approved_contextlist($user1, 'core_enrol', [$coursecontext1->id]); + provider::delete_data_for_user($approvedlist); + // Get all user enrolments match with user. + $userenrolments = $DB->get_records('user_enrolments', ['userid' => $user1->id]); + $this->assertCount(1, $userenrolments); + // Get all user enrolments accounts. + $userenrolments = $DB->get_records('user_enrolments', array()); + $this->assertCount(3, $userenrolments); + } +} \ No newline at end of file diff --git a/lang/en/enrol.php b/lang/en/enrol.php index 1d29ca68ae8..7315719014a 100644 --- a/lang/en/enrol.php +++ b/lang/en/enrol.php @@ -149,3 +149,13 @@ $string['extremovedsuspend'] = 'Disable course enrolment'; $string['extremovedsuspendnoroles'] = 'Disable course enrolment and remove roles'; $string['extremovedkeep'] = 'Keep user enrolled'; $string['extremovedunenrol'] = 'Unenrol user from course'; +$string['privacy:metadata:user_enrolments'] = 'Enrolments'; +$string['privacy:metadata:user_enrolments:enrolid'] = 'The instance of the enrol plugin.'; +$string['privacy:metadata:user_enrolments:modifierid'] = 'The ID of the user who last modified the user enrolment.'; +$string['privacy:metadata:user_enrolments:status'] = 'The status of the user enrolment in a course.'; +$string['privacy:metadata:user_enrolments:tableexplanation'] = 'This is where Enrol management stores enrolled users.'; +$string['privacy:metadata:user_enrolments:timecreated'] = 'The date/time of when the user enrolment was created.'; +$string['privacy:metadata:user_enrolments:timeend'] = 'The date/time of when the user enrolment ends.'; +$string['privacy:metadata:user_enrolments:timestart'] = 'The date/time of when the user enrolment starts.'; +$string['privacy:metadata:user_enrolments:timemodified'] = 'The date/time of when the user enrolment was modified.'; +$string['privacy:metadata:user_enrolments:userid'] = 'The ID of the user.';