From f5edb0c63874bcd2d82b56cb9db84d92425d2c2a Mon Sep 17 00:00:00 2001 From: Zig Tan Date: Mon, 9 Apr 2018 14:09:14 +0800 Subject: [PATCH] MDL-61829 block_community: Implement privacy provider --- blocks/community/classes/privacy/provider.php | 181 ++++++++++++ blocks/community/lang/en/block_community.php | 6 + blocks/community/tests/privacy_test.php | 267 ++++++++++++++++++ 3 files changed, 454 insertions(+) create mode 100644 blocks/community/classes/privacy/provider.php create mode 100644 blocks/community/tests/privacy_test.php diff --git a/blocks/community/classes/privacy/provider.php b/blocks/community/classes/privacy/provider.php new file mode 100644 index 00000000000..569906641e5 --- /dev/null +++ b/blocks/community/classes/privacy/provider.php @@ -0,0 +1,181 @@ +. + +/** + * Privacy Subsystem implementation for block_community. + * + * @package block_community + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_community\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\deletion_criteria; +use \core_privacy\local\metadata\collection; + +/** + * Privacy Subsystem implementation for block_community. + * + * @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 information about how block_community stores its data. + * + * @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( + 'block_community', + [ + 'coursename' => 'privacy:metadata:block_community:coursename', + 'coursedescription' => 'privacy:metadata:block_community:coursedescription', + 'courseurl' => 'privacy:metadata:block_community:courseurl', + 'imageurl' => 'privacy:metadata:block_community:imageurl', + 'userid' => 'privacy:metadata:block_community:userid', + ], + 'privacy:metadata:block_community' + ); + + 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 \core_privacy\local\request\contextlist(); + + // The block_community data is associated at the user context level, so retrieve the user's context id. + $sql = "SELECT c.id + FROM {block_community} bc + JOIN {context} c ON c.instanceid = bc.userid AND c.contextlevel = :contextuser + WHERE bc.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 using the User context level. + * + * @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 block_community 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; + + // The block_community data export is organised in: {User Context}/Community Finder/My communities/data.json. + $subcontext = [ + get_string('pluginname', 'block_community'), + get_string('mycommunities', 'block_community') + ]; + + $sql = "SELECT bc.id as id, + bc.coursename as name, + bc.coursedescription as description, + bc.courseurl as url, + bc.imageurl as imageurl + FROM {block_community} bc + WHERE bc.userid = :userid + ORDER BY bc.coursename"; + + $params = [ + 'userid' => $userid + ]; + + $communities = $DB->get_records_sql($sql, $params); + + $data = (object) [ + 'communities' => $communities + ]; + + 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; + + $DB->delete_records('block_community', ['userid' => $userid]); + } + + /** + * Delete all user data for the specified user. + * + * @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 block_community 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; + + $DB->delete_records('block_community', ['userid' => $userid]); + } + +} diff --git a/blocks/community/lang/en/block_community.php b/blocks/community/lang/en/block_community.php index 1d8fa915aea..73e65d4ca46 100644 --- a/blocks/community/lang/en/block_community.php +++ b/blocks/community/lang/en/block_community.php @@ -95,6 +95,12 @@ $string['orderbypublisher'] = 'Publisher'; $string['orderbyratingaverage'] = 'Rating'; $string['outcomes'] = 'Outcomes: {$a}'; $string['pluginname'] = 'Community finder'; +$string['privacy:metadata:block_community'] = 'The Community block stores links to shared community courses users can enrol in.'; +$string['privacy:metadata:block_community:coursename'] = 'The name of the linked community course.'; +$string['privacy:metadata:block_community:coursedescription'] = 'The description of the linked community course.'; +$string['privacy:metadata:block_community:courseurl'] = 'The course URL of the linked community course.'; +$string['privacy:metadata:block_community:imageurl'] = 'The image URL of the linked community course.'; +$string['privacy:metadata:block_community:userid'] = 'The ID of the user who created the linked community course.'; $string['rateandcomment'] = 'Rate and comment'; $string['rating'] = 'Rating'; $string['removecommunitycourse'] = 'Remove community course'; diff --git a/blocks/community/tests/privacy_test.php b/blocks/community/tests/privacy_test.php new file mode 100644 index 00000000000..48f672f8946 --- /dev/null +++ b/blocks/community/tests/privacy_test.php @@ -0,0 +1,267 @@ +. + +/** + * Unit tests for the block_community implementation of the privacy API. + * + * @package block_community + * @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 \block_community\privacy\provider; + +/** + * Unit tests for the block_community implementation of the privacy API. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_community_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_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('block_community'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(1, $itemcollection); + + $table = reset($itemcollection); + $this->assertEquals('block_community', $table->get_name()); + + $privacyfields = $table->get_privacy_fields(); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('coursename', $privacyfields); + $this->assertArrayHasKey('coursedescription', $privacyfields); + $this->assertArrayHasKey('courseurl', $privacyfields); + $this->assertArrayHasKey('imageurl', $privacyfields); + + $this->assertEquals('privacy:metadata:block_community', $table->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + global $DB; + + // Test setup. + $teacher = $this->getDataGenerator()->create_user(); + $this->setUser($teacher); + + // Add two community links for the User. + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name - 1', + 'coursedescription' => 'Dummy Community Course Description - 1', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-1', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name - 2', + 'coursedescription' => 'Dummy Community Course Description - 2', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-2', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($teacher->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($teacher->id, $context->instanceid); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_user_data() { + global $DB; + + // Test setup. + $teacher = $this->getDataGenerator()->create_user(); + $this->setUser($teacher); + + // Add 3 community links for the User. + $nocommunities = 3; + for ($c = 0; $c < $nocommunities; $c++) { + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name - ' . $c, + 'coursedescription' => 'Dummy Community Course Description - ' . $c, + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-' . $c, + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + } + + // Test the created block_community records matches the test number of communities specified. + $communities = $DB->get_records('block_community', ['userid' => $teacher->id]); + $this->assertCount($nocommunities, $communities); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($teacher->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($teacher->id, $context->instanceid); + + $approvedcontextlist = new approved_contextlist($teacher, 'block_community', $contextlist->get_contextids()); + + // Retrieve Calendar Event and Subscriptions data only for this user. + provider::export_user_data($approvedcontextlist); + + // Test the block_community 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. + $teacher = $this->getDataGenerator()->create_user(); + $this->setUser($teacher); + + // Add a community link for the User. + $community = (object)[ + 'userid' => $teacher->id, + 'coursename' => 'Dummy Community Course Name', + 'coursedescription' => 'Dummy Community Course Description', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($teacher->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($teacher->id, $context->instanceid); + + // Test delete all users content by context. + provider::delete_data_for_all_users_in_context($context); + $blockcommunity = $DB->get_records('block_community', ['userid' => $teacher->id]); + $this->assertCount(0, $blockcommunity); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + // Test setup. + $teacher1 = $this->getDataGenerator()->create_user(); + $teacher2 = $this->getDataGenerator()->create_user(); + $this->setUser($teacher1); + + // Add 3 community links for Teacher 1. + $nocommunities = 3; + for ($c = 0; $c < $nocommunities; $c++) { + $community = (object)[ + 'userid' => $teacher1->id, + 'coursename' => 'Dummy Community Course Name - ' . $c, + 'coursedescription' => 'Dummy Community Course Description - ' . $c, + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-' . $c, + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + } + + // Add 1 community link for Teacher 2. + $community = (object)[ + 'userid' => $teacher2->id, + 'coursename' => 'Dummy Community Course Name - Blah', + 'coursedescription' => 'Dummy Community Course Description - Blah', + 'courseurl' => 'https://moodle.org/community_courses/Dummy_Community_Course-Blah', + 'imageurl' => '' + ]; + $DB->insert_record('block_community', $community); + + // Test the created block_community records for Teacher 1 equals test number of communities specified. + $communities = $DB->get_records('block_community', ['userid' => $teacher1->id]); + $this->assertCount($nocommunities, $communities); + + // Test the created block_community records for Teacher 2 equals 1. + $communities = $DB->get_records('block_community', ['userid' => $teacher2->id]); + $this->assertCount(1, $communities); + + // Test the deletion of block_community records for Teacher 1 results in zero records. + $contextlist = provider::get_contexts_for_userid($teacher1->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($teacher1->id, $context->instanceid); + + $approvedcontextlist = new approved_contextlist($teacher1, 'block_community', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + $communities = $DB->get_records('block_community', ['userid' => $teacher1->id]); + $this->assertCount(0, $communities); + + + // Test that Teacher 2's single block_community record still exists. + $contextlist = provider::get_contexts_for_userid($teacher2->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($teacher2->id, $context->instanceid); + + $communities = $DB->get_records('block_community', ['userid' => $teacher2->id]); + $this->assertCount(1, $communities); + } + +}