diff --git a/backup/tests/privacy_provider_test.php b/backup/tests/privacy_provider_test.php index d4a2ef8a7fc..4bec91e9d38 100644 --- a/backup/tests/privacy_provider_test.php +++ b/backup/tests/privacy_provider_test.php @@ -23,6 +23,7 @@ */ use core_backup\privacy\provider; +use core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); @@ -35,37 +36,26 @@ defined('MOODLE_INTERNAL') || die(); class core_backup_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { /** - * @var stdClass The user + * Test getting the context for the user ID related to this plugin. */ - protected $user = null; - - /** - * @var stdClass The course - */ - protected $course = null; - - /** - * Basic setup for these tests. - */ - public function setUp() { + public function test_get_contexts_for_userid() { global $DB; $this->resetAfterTest(); - $this->course = $this->getDataGenerator()->create_course(); - - $this->user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_user(); // Just insert directly into the 'backup_controllers' table. $bcdata = (object) [ 'backupid' => 1, 'operation' => 'restore', 'type' => 'course', - 'itemid' => $this->course->id, + 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, - 'userid' => $this->user->id, + 'userid' => $user->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, @@ -76,21 +66,10 @@ class core_backup_privacy_provider_testcase extends \core_privacy\tests\provider ]; $DB->insert_record('backup_controllers', $bcdata); - // Create another user who will perform a backup operation. - $user = $this->getDataGenerator()->create_user(); - $bcdata->backupid = 2; - $bcdata->userid = $user->id; - $DB->insert_record('backup_controllers', $bcdata); - } - - /** - * Test getting the context for the user ID related to this plugin. - */ - public function test_get_contexts_for_userid() { - $contextlist = provider::get_contexts_for_userid($this->user->id); + $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(1, $contextlist); $contextforuser = $contextlist->current(); - $context = context_course::instance($this->course->id); + $context = context_course::instance($course->id); $this->assertEquals($context->id, $contextforuser->id); } @@ -100,16 +79,62 @@ class core_backup_privacy_provider_testcase extends \core_privacy\tests\provider public function test_export_for_context() { global $DB; - // Create another backup_controllers record. - $bcdata = (object) [ - 'backupid' => 3, - 'operation' => 'backup', + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user1 = $this->getDataGenerator()->create_user(); + + // Just insert directly into the 'backup_controllers' table. + $bcdata1 = (object) [ + 'backupid' => 1, + 'operation' => 'restore', 'type' => 'course', - 'itemid' => $this->course->id, + 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, - 'userid' => $this->user->id, + 'userid' => $user1->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata1); + + // Create another user who will perform a backup operation. + $user2 = $this->getDataGenerator()->create_user(); + $bcdata2 = (object) [ + 'backupid' => 2, + 'operation' => 'restore', + 'type' => 'course', + 'itemid' => $course->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user2->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata2); + + // Create another backup_controllers record. + $bcdata3 = (object) [ + 'backupid' => 3, + 'operation' => 'backup', + 'type' => 'course', + 'itemid' => $course->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user1->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, @@ -118,16 +143,16 @@ class core_backup_privacy_provider_testcase extends \core_privacy\tests\provider 'timemodified' => time() + DAYSECS, 'controller' => '' ]; - $DB->insert_record('backup_controllers', $bcdata); + $DB->insert_record('backup_controllers', $bcdata3); - $coursecontext = context_course::instance($this->course->id); + $coursecontext = context_course::instance($course->id); // Export all of the data for the context. - $this->export_context_data_for_user($this->user->id, $coursecontext, 'core_backup'); + $this->export_context_data_for_user($user1->id, $coursecontext, 'core_backup'); $writer = \core_privacy\local\request\writer::with_context($coursecontext); $this->assertTrue($writer->has_any_data()); - $data = (array) $writer->get_data([get_string('backup'), $this->course->id]); + $data = (array) $writer->get_data([get_string('backup'), $course->id]); $this->assertCount(2, $data); @@ -144,16 +169,62 @@ class core_backup_privacy_provider_testcase extends \core_privacy\tests\provider public function test_delete_data_for_all_users_in_context() { global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user1 = $this->getDataGenerator()->create_user(); + + // Just insert directly into the 'backup_controllers' table. + $bcdata1 = (object) [ + 'backupid' => 1, + 'operation' => 'restore', + 'type' => 'course', + 'itemid' => $course->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user1->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata1); + + // Create another user who will perform a backup operation. + $user2 = $this->getDataGenerator()->create_user(); + $bcdata2 = (object) [ + 'backupid' => 2, + 'operation' => 'restore', + 'type' => 'course', + 'itemid' => $course->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user2->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata2); + // Before deletion, we should have 2 operations. - $count = $DB->count_records('backup_controllers', ['itemid' => $this->course->id]); + $count = $DB->count_records('backup_controllers', ['itemid' => $course->id]); $this->assertEquals(2, $count); // Delete data based on context. - $coursecontext = context_course::instance($this->course->id); + $coursecontext = context_course::instance($course->id); provider::delete_data_for_all_users_in_context($coursecontext); // After deletion, the operations for that course should have been deleted. - $count = $DB->count_records('backup_controllers', ['itemid' => $this->course->id]); + $count = $DB->count_records('backup_controllers', ['itemid' => $course->id]); $this->assertEquals(0, $count); } @@ -163,23 +234,306 @@ class core_backup_privacy_provider_testcase extends \core_privacy\tests\provider public function test_delete_data_for_user() { global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user1 = $this->getDataGenerator()->create_user(); + + // Just insert directly into the 'backup_controllers' table. + $bcdata1 = (object) [ + 'backupid' => 1, + 'operation' => 'restore', + 'type' => 'course', + 'itemid' => $course->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user1->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata1); + + // Create another user who will perform a backup operation. + $user2 = $this->getDataGenerator()->create_user(); + $bcdata2 = (object) [ + 'backupid' => 2, + 'operation' => 'restore', + 'type' => 'course', + 'itemid' => $course->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user2->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata2); + // Before deletion, we should have 2 operations. - $count = $DB->count_records('backup_controllers', ['itemid' => $this->course->id]); + $count = $DB->count_records('backup_controllers', ['itemid' => $course->id]); $this->assertEquals(2, $count); - $coursecontext = context_course::instance($this->course->id); - $contextlist = new \core_privacy\local\request\approved_contextlist($this->user, 'core_backup', + $coursecontext = context_course::instance($course->id); + $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_backup', [context_system::instance()->id, $coursecontext->id]); provider::delete_data_for_user($contextlist); // After deletion, the backup operation for the user should have been deleted. - $count = $DB->count_records('backup_controllers', ['itemid' => $this->course->id, 'userid' => $this->user->id]); + $count = $DB->count_records('backup_controllers', ['itemid' => $course->id, 'userid' => $user1->id]); $this->assertEquals(0, $count); // Confirm we still have the other users record. $bcs = $DB->get_records('backup_controllers'); $this->assertCount(1, $bcs); $lastsubmission = reset($bcs); - $this->assertNotEquals($this->user->id, $lastsubmission->userid); + $this->assertNotEquals($user1->id, $lastsubmission->userid); + } + + /** + * Test that only users with a course and module context are fetched. + */ + public function test_get_users_in_context() { + global $DB; + + $this->resetAfterTest(); + + $component = 'core_backup'; + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]); + + $user = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $coursecontext = context_course::instance($course->id); + $activitycontext = \context_module::instance($activity->cmid); + + // The list of users for course context should return the user. + $userlist = new \core_privacy\local\request\userlist($coursecontext, $component); + provider::get_users_in_context($userlist); + $this->assertCount(0, $userlist); + + // Create a course backup. + // Just insert directly into the 'backup_controllers' table. + $bcdata = (object) [ + 'backupid' => 1, + 'operation' => 'restore', + 'type' => 'course', + 'itemid' => $course->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + + $DB->insert_record('backup_controllers', $bcdata); + + // The list of users for the course context should return user. + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $expected = [$user->id]; + $actual = $userlist->get_userids(); + $this->assertEquals($expected, $actual); + + // Create an activity backup. + // Just insert directly into the 'backup_controllers' table. + $bcdata = (object) [ + 'backupid' => 2, + 'operation' => 'restore', + 'type' => 'activity', + 'itemid' => $activity->cmid, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user2->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + + $DB->insert_record('backup_controllers', $bcdata); + + // The list of users for the course context should return user2. + $userlist = new \core_privacy\local\request\userlist($activitycontext, $component); + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $expected = [$user2->id]; + $actual = $userlist->get_userids(); + $this->assertEquals($expected, $actual); + + // The list of users for system context should not return any users. + $systemcontext = context_system::instance(); + $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); + provider::get_users_in_context($userlist); + $this->assertCount(0, $userlist); + } + + /** + * Test that data for users in approved userlist is deleted. + */ + public function test_delete_data_for_users() { + global $DB; + + $this->resetAfterTest(); + + $component = 'core_backup'; + + // Create course1. + $course1 = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course1->id); + // Create course2. + $course2 = $this->getDataGenerator()->create_course(); + $coursecontext2 = context_course::instance($course2->id); + // Create an activity. + $activity = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]); + $activitycontext = \context_module::instance($activity->cmid); + // Create user1. + $user1 = $this->getDataGenerator()->create_user(); + // Create user2. + $user2 = $this->getDataGenerator()->create_user(); + // Create user2. + $user3 = $this->getDataGenerator()->create_user(); + + // Just insert directly into the 'backup_controllers' table. + $bcdata1 = (object) [ + 'backupid' => 1, + 'operation' => 'restore', + 'type' => 'course', + 'itemid' => $course1->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user1->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata1); + + // Just insert directly into the 'backup_controllers' table. + $bcdata2 = (object) [ + 'backupid' => 2, + 'operation' => 'backup', + 'type' => 'course', + 'itemid' => $course1->id, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user2->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata2); + + // Just insert directly into the 'backup_controllers' table. + $bcdata3 = (object) [ + 'backupid' => 3, + 'operation' => 'restore', + 'type' => 'activity', + 'itemid' => $activity->cmid, + 'format' => 'moodle2', + 'interactive' => 1, + 'purpose' => 10, + 'userid' => $user3->id, + 'status' => 1000, + 'execution' => 1, + 'executiontime' => 0, + 'checksum' => 'checksumyolo', + 'timecreated' => time(), + 'timemodified' => time(), + 'controller' => '' + ]; + $DB->insert_record('backup_controllers', $bcdata3); + + // The list of users for coursecontext should return user1 and user2. + $userlist1 = new \core_privacy\local\request\userlist($coursecontext, $component); + provider::get_users_in_context($userlist1); + $this->assertCount(2, $userlist1); + $expected = [$user1->id, $user2->id]; + $actual = $userlist1->get_userids(); + $this->assertEquals($expected, $actual, '', 0.0, 10, true); + + // The list of users for coursecontext2 should not return users. + $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); + provider::get_users_in_context($userlist2); + $this->assertCount(0, $userlist2); + + // The list of users for activitycontext should return user3. + $userlist3 = new \core_privacy\local\request\userlist($activitycontext, $component); + provider::get_users_in_context($userlist3); + $this->assertCount(1, $userlist3); + $expected = [$user3->id]; + $actual = $userlist3->get_userids(); + $this->assertEquals($expected, $actual); + + // Add user1 to the approved user list. + $approvedlist = new approved_userlist($coursecontext, $component, [$user1->id]); + // Delete user data using delete_data_for_user for usercontext1. + provider::delete_data_for_users($approvedlist); + + // Re-fetch users in coursecontext - The user list should now return only user2. + $userlist1 = new \core_privacy\local\request\userlist($coursecontext, $component); + provider::get_users_in_context($userlist1); + $this->assertCount(1, $userlist1); + $expected = [$user2->id]; + $actual = $userlist1->get_userids(); + $this->assertEquals($expected, $actual); + + // Re-fetch users in activitycontext - The user list should not be empty (user3). + $userlist3 = new \core_privacy\local\request\userlist($activitycontext, $component); + provider::get_users_in_context($userlist3); + $this->assertCount(1, $userlist3); + + // Add user1 to the approved user list. + $approvedlist = new approved_userlist($activitycontext, $component, [$user3->id]); + // Delete user data using delete_data_for_user for usercontext1. + provider::delete_data_for_users($approvedlist); + + // Re-fetch users in activitycontext - The user list should not return any users. + $userlist3 = new \core_privacy\local\request\userlist($activitycontext, $component); + provider::get_users_in_context($userlist3); + $this->assertCount(0, $userlist3); + + // User data should be only removed in the course context and module context. + $systemcontext = context_system::instance(); + // Add userlist2 to the approved user list in the system context. + $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); + // Delete user1 data using delete_data_for_user. + provider::delete_data_for_users($approvedlist); + // Re-fetch users in usercontext2 - The user list should not be empty (user2). + $userlist2 = new \core_privacy\local\request\userlist($coursecontext, $component); + provider::get_users_in_context($userlist2); + $this->assertCount(1, $userlist2); } } diff --git a/backup/util/ui/classes/privacy/provider.php b/backup/util/ui/classes/privacy/provider.php index cdffbc461e3..907baf924fb 100644 --- a/backup/util/ui/classes/privacy/provider.php +++ b/backup/util/ui/classes/privacy/provider.php @@ -29,6 +29,8 @@ 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(); @@ -40,6 +42,7 @@ defined('MOODLE_INTERNAL') || die(); */ class provider implements \core_privacy\local\metadata\provider, + \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\subsystem\provider { /** @@ -92,6 +95,67 @@ class provider implements 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 = [ + 'contextcourse' => CONTEXT_COURSE, + 'contextid' => $context->id, + + ]; + + $sql = "SELECT bc.userid + FROM {backup_controllers} bc + JOIN {context} ctx + ON ctx.instanceid = bc.itemid + AND ctx.contextlevel = :contextcourse + WHERE ctx.id = :contextid + 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 + JOIN {context} ctx + ON ctx.instanceid = c.course + AND ctx.contextlevel = :contextcourse + WHERE ctx.id = :contextid + AND bc.type = :typesection"; + + $sectionparams = ['typesection' => 'section'] + $params; + + $userlist->add_from_sql('userid', $sql, $sectionparams); + } + + if ($context instanceof \context_module) { + $params = [ + 'contextmodule' => CONTEXT_MODULE, + 'contextid' => $context->id, + 'typeactivity' => 'activity' + ]; + + $sql = "SELECT bc.userid + FROM {backup_controllers} bc + JOIN {context} ctx + ON ctx.instanceid = bc.itemid + AND ctx.contextlevel = :contextmodule + WHERE ctx.id = :contextid + AND bc.type = :typeactivity"; + + $userlist->add_from_sql('userid', $sql, $params); + } + } + /** * Export all user data for the specified user, in the specified contexts. * @@ -148,6 +212,45 @@ class provider implements $DB->delete_records('backup_controllers', ['itemid' => $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) { + 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); + + // Delete course backup data. + $select = "itemid = :itemid AND type = :type AND userid {$usersql}"; + $params = ['itemid' => $context->instanceid, 'type' => 'course'] + $userparams; + $DB->delete_records_select('backup_controllers', $select, $params); + + // Delete course sections backup data. + $select = "itemid IN (SELECT id FROM {course_sections} WHERE course = :course) AND userid + {$usersql} AND type = :type"; + $params = ['course' => $context->instanceid, 'type' => 'section'] + $userparams; + $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); + + // 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. *