From ef43b1d4d6c46519eb0e2d779d2a4ff2bf039d74 Mon Sep 17 00:00:00 2001 From: Zig Tan Date: Tue, 17 Apr 2018 10:53:00 +0800 Subject: [PATCH] MDL-61568 core_repository: Implement privacy providers --- lang/en/repository.php | 9 ++ repository/classes/privacy/provider.php | 193 ++++++++++++++++++++++++ repository/tests/privacy_test.php | 190 +++++++++++++++++++++++ 3 files changed, 392 insertions(+) create mode 100644 repository/classes/privacy/provider.php create mode 100644 repository/tests/privacy_test.php diff --git a/lang/en/repository.php b/lang/en/repository.php index c47d3db12d7..6f76a1013d8 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -247,3 +247,12 @@ $string['unzipped'] = 'Unzipped successfully'; $string['wrongcontext'] = 'You cannot access to this context'; $string['xhtmlerror'] = 'You are probably using an XHTML strict header. Certain YUI components don\'t work in this mode; please turn it off.'; $string['ziped'] = 'Compress folder successfully'; +$string['privacy:metadata:repository'] = 'The Repository component stores the repository types within the core subsystem.'; +$string['privacy:metadata:repository_instances'] = 'The Repository plug-ins component stores user repository instances data within the core subsystem.'; +$string['privacy:metadata:repository_instances:name'] = 'The custom name of the repository instance.'; +$string['privacy:metadata:repository_instances:typeid'] = 'The ID type of the repository instance.'; +$string['privacy:metadata:repository_instances:userid'] = 'The ID of the user owning the repository instance.'; +$string['privacy:metadata:repository_instances:username'] = 'The optional username configured for the repository instance.'; +$string['privacy:metadata:repository_instances:password'] = 'The optional password configured for the repository instance.'; +$string['privacy:metadata:repository_instances:timecreated'] = 'The date/time of creation for the repository instance.'; +$string['privacy:metadata:repository_instances:timemodified'] = 'The date/time of modification of the repository instance.'; diff --git a/repository/classes/privacy/provider.php b/repository/classes/privacy/provider.php new file mode 100644 index 00000000000..82d5c2a6603 --- /dev/null +++ b/repository/classes/privacy/provider.php @@ -0,0 +1,193 @@ +. + +/** + * Privacy Subsystem implementation for core_repository. + * + * @package core_repository + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_repository\privacy; + +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; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for core_repository implementing metadata and plugin providers. + * + * @copyright 2018 Zig Tan + * @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\plugin\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( + 'repository_instances', + [ + 'name' => 'privacy:metadata:repository_instances:name', + 'typeid' => 'privacy:metadata:repository_instances:typeid', + 'userid' => 'privacy:metadata:repository_instances:userid', + 'username' => 'privacy:metadata:repository_instances:username', + 'password' => 'privacy:metadata:repository_instances:password', + 'timecreated' => 'privacy:metadata:repository_instances:timecreated', + 'timemodified' => 'privacy:metadata:repository_instances:timemodified', + ], + 'privacy:metadata:repository_instances' + ); + + $collection->add_plugintype_link('repository', [], 'privacy:metadata:repository'); + + 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 { + $contextlist = new contextlist(); + + // The repository_instances data is associated at the user context level, so retrieve the user's context id. + $sql = "SELECT c.id + FROM {repository_instances} ri + JOIN {context} c ON c.instanceid = ri.userid AND c.contextlevel = :contextuser + WHERE ri.userid = :userid + GROUP BY c.id"; + + $params = [ + 'contextuser' => CONTEXT_USER, + 'userid' => $userid + ]; + + $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 the user has repository_instances data, then only the User context should be present so get the first context. + $contexts = $contextlist->get_contexts(); + if (count($contexts) == 0) { + return; + } + $context = reset($contexts); + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + $sql = "SELECT DISTINCT + ri.id as id, + r.type as type, + ri.name as name, + ri.timecreated as timecreated, + ri.timemodified as timemodified + FROM {repository_instances} ri + JOIN {repository} r ON r.id = ri.typeid + WHERE ri.userid = :userid"; + + $params = [ + 'userid' => $userid + ]; + + $repositoryinstances = $DB->get_records_sql($sql, $params); + + foreach ($repositoryinstances as $repositoryinstance) { + // The repository_instances data export is organised in: {User Context}/Repository plug-ins/{Plugin Name}/data.json. + $subcontext = [ + get_string('plugin', 'core_repository'), + get_string('pluginname', 'repository_' . $repositoryinstance->type) + ]; + + $data = (object) [ + 'type' => $repositoryinstance->type, + 'name' => $repositoryinstance->name, + 'timecreated' => transform::datetime($repositoryinstance->timecreated), + 'timemodified' => transform::datetime($repositoryinstance->timemodified) + ]; + + writer::with_context($context)->export_data($subcontext, $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) { + global $DB; + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + // Delete the repository_instances records created for the userid. + $DB->delete_records('repository_instances', ['userid' => $userid]); + } + + /** + * 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 the user has repository_instances data, then only the User context should be present so get the first context. + $contexts = $contextlist->get_contexts(); + if (count($contexts) == 0) { + return; + } + $context = reset($contexts); + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + // Delete the repository_instances records created for the userid. + $DB->delete_records('repository_instances', ['userid' => $userid]); + } + +} diff --git a/repository/tests/privacy_test.php b/repository/tests/privacy_test.php new file mode 100644 index 00000000000..75a9adce63c --- /dev/null +++ b/repository/tests/privacy_test.php @@ -0,0 +1,190 @@ +. +/** + * Unit tests for the core_repository implementation of the privacy API. + * + * @package core_repository + * @category test + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \core_repository\privacy\provider; +/** + * Unit tests for the core_repository implementation of the privacy API. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_repository_privacy_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Overriding setUp() function to always reset after tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Test the User's retrieved contextlist is empty because no repository_instances have added for the User yet. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(0, $contexts); + + // Create 3 repository_instances records for the User. + $this->setup_test_scenario_data($user->id, 3); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($user->id, $context->instanceid); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_user_data() { + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Create 3 repository_instances records for the User. + $this->setup_test_scenario_data($user->id, 3); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($user->id, $context->instanceid); + + // Retrieve repository_instances data only for this user. + $approvedcontextlist = new approved_contextlist($user, 'core_repository', $contextlist->get_contextids()); + provider::export_user_data($approvedcontextlist); + + // Test the repository_instances data is exported at the User context level. + $user = $approvedcontextlist->get_user(); + $contextuser = context_user::instance($user->id); + $writer = writer::with_context($contextuser); + $this->assertTrue($writer->has_any_data()); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Create 3 repository_instances records for the User. + $this->setup_test_scenario_data($user->id, 3); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + + // Delete all the User's records in mdl_repository_instances table by the specified User context. + provider::delete_data_for_all_users_in_context($context); + + // Test the cohort roles records in mdl_repository_instances table is equals zero. + $repositoryinstances = $DB->get_records('repository_instances', ['userid' => $user->id]); + $this->assertCount(0, $repositoryinstances); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Create 3 repository_instances records for the User. + $this->setup_test_scenario_data($user->id, 3); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + + // Delete all the User's records in mdl_repository_instances table by the specified User approved context list. + $approvedcontextlist = new approved_contextlist($user, 'repository_instances', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + + // Test the cohort roles records in mdl_repository_instances table is equals zero. + $repositoryinstances = $DB->get_records('repository_instances', ['userid' => $user->id]); + $this->assertCount(0, $repositoryinstances); + } + + /** + * Helper function to setup repository_instances records for testing a specific user. + * + * @param int $userid The Id of the User used for testing. + * @param int $noscenarios The number of repository_instance records to create for the User. + * @throws dml_exception + */ + private function setup_test_scenario_data($userid, $noscenarios) { + global $DB; + + for ($i = 0; $i < $noscenarios; $i++) { + $repositoryinstance = (object)[ + 'typeid' => ($i + 1), + 'name' => 'My Test Repo', + 'userid' => $userid, + 'contextid' => 1, + 'username' => 'some username', + 'password' => 'some password', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + 'readonly' => 0 + ]; + $DB->insert_record('repository_instances', $repositoryinstance); + } + } + +}