From 2207b0fa9f8c44f8dfcb8a32f6e45c8fa232b415 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Mon, 23 Apr 2018 14:34:36 +0800 Subject: [PATCH] MDL-62068 core_tag: implement privacy API --- blog/tests/privacy_test.php | 6 +- tag/classes/privacy/provider.php | 110 +++++++++++++++++++++++++++---- tag/tests/privacy_test.php | 93 +++++++++++++++++++++++++- 3 files changed, 191 insertions(+), 18 deletions(-) diff --git a/blog/tests/privacy_test.php b/blog/tests/privacy_test.php index 6ab6c686b04fc..a71faa572c0fd 100644 --- a/blog/tests/privacy_test.php +++ b/blog/tests/privacy_test.php @@ -586,11 +586,7 @@ public function test_export_data_for_user() { $commentpath = array_merge($path, [get_string('commentsubcontext', 'core_comment')]); if ($e->id == $e1->id) { $tagdata = $writer->get_related_data($path, 'tags'); - $this->assertCount(2, $tagdata); - $tag = array_shift($tagdata); - $this->assertEquals('Beer', $tag->rawname); - $tag = array_shift($tagdata); - $this->assertEquals('Golf', $tag->rawname); + $this->assertEquals(['Beer', 'Golf'], $tagdata, '', 0, 10, true); $comments = $writer->get_data($commentpath); $this->assertCount(2, $comments->comments); diff --git a/tag/classes/privacy/provider.php b/tag/classes/privacy/provider.php index 3b399b1f9622f..744dae94e52b3 100644 --- a/tag/classes/privacy/provider.php +++ b/tag/classes/privacy/provider.php @@ -27,6 +27,10 @@ defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; +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; /** * Privacy Subsystem implementation for core_tag. @@ -39,7 +43,10 @@ class provider implements \core_privacy\local\metadata\provider, // The tag subsystem provides data to other components. - \core_privacy\local\request\subsystem\plugin_provider { + \core_privacy\local\request\subsystem\plugin_provider, + + // The tag subsystem may have data that belongs to this user. + \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. @@ -110,17 +117,9 @@ public static function export_item_tags( ) { global $DB; - // Do not include the mdl_tag userid data because of bug with re-using existing tags by other users. + // Ignore mdl_tag.userid here because it only reflects the user who originally created the tag. $sql = "SELECT - t.id, - t.tagcollid, - t.name, - t.rawname, - t.isstandard, - t.description, - t.descriptionformat, - t.flag, - t.timemodified + t.rawname FROM {tag} t INNER JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.component = :component @@ -141,7 +140,7 @@ public static function export_item_tags( 'userid' => $userid, ]; - if ($tags = $DB->get_records_sql($sql, $params)) { + if ($tags = $DB->get_fieldset_sql($sql, $params)) { $writer = \core_privacy\local\request\writer::with_context($context) ->export_related_data($subcontext, 'tags', $tags); } @@ -194,4 +193,91 @@ public static function delete_item_tags_select(\context $context, $component, $i 'contextid = :contextid AND component = :component AND itemtype = :itemtype AND itemid ' . $itemidstest, $params); } + + /** + * 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(); + $contextlist->add_from_sql("SELECT c.id + FROM {context} c + JOIN {tag} t ON t.userid = :userid + WHERE contextlevel = :contextlevel", + ['userid' => $userid, 'contextlevel' => CONTEXT_SYSTEM]); + 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; + $context = \context_system::instance(); + if (!$contextlist->count() || !in_array($context->id, $contextlist->get_contextids())) { + return; + } + + $user = $contextlist->get_user(); + $sql = "SELECT id, userid, tagcollid, name, rawname, isstandard, description, descriptionformat, flag, timemodified + FROM {tag} WHERE userid = ?"; + $rs = $DB->get_recordset_sql($sql, [$user->id]); + foreach ($rs as $record) { + $subcontext = [get_string('tags', 'tag'), $record->id]; + $tag = (object)[ + 'id' => $record->id, + 'userid' => transform::user($record->userid), + 'name' => $record->name, + 'rawname' => $record->rawname, + 'isstandard' => transform::yesno($record->isstandard), + 'description' => writer::with_context($context)->rewrite_pluginfile_urls($subcontext, + 'tag', 'description', $record->id, strval($record->description)), + 'descriptionformat' => $record->descriptionformat, + 'flag' => $record->flag, + 'timemodified' => transform::datetime($record->timemodified), + + ]; + writer::with_context($context)->export_data($subcontext, $tag); + writer::with_context($context)->export_area_files($subcontext, 'tag', 'description', $record->id); + } + $rs->close(); + } + + /** + * Delete all data for all users in the specified context. + * + * We do not delete tag instances in this method - this should be done by the components that define tagareas. + * We only delete tags themselves in case of system 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; + // Tags can only be defined in system context. + if ($context->id == \context_system::instance()->id) { + $DB->delete_records('tag_instance'); + $DB->delete_records('tag', []); + } + } + + /** + * 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; + $context = \context_system::instance(); + if (!$contextlist->count() || !in_array($context->id, $contextlist->get_contextids())) { + return; + } + + // Do not delete tags themselves in case they are used by somebody else. + // If the user is the only one using the tag, it will be automatically deleted anyway during the next cron cleanup. + $DB->set_field_select('tag', 'userid', 0, 'userid = ?', [$contextlist->get_user()->id]); + } } diff --git a/tag/tests/privacy_test.php b/tag/tests/privacy_test.php index 05ed60741c6e3..ccbda47ae9524 100644 --- a/tag/tests/privacy_test.php +++ b/tag/tests/privacy_test.php @@ -83,7 +83,7 @@ public function test_export_tags() { // Check the exported tag's rawname is found in the initial dummy tags. foreach ($exportedtags as $exportedtag) { - $this->assertContains($exportedtag->rawname, $dummytags); + $this->assertContains($exportedtag, $dummytags); } } @@ -151,4 +151,95 @@ public function test_delete_item_tags_select() { $expectedtagcount -= 2; $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance')); } + + protected function set_up_tags() { + global $CFG; + require_once($CFG->dirroot.'/user/editlib.php'); + + $this->resetAfterTest(true); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->setUser($user1); + useredit_update_interests($user1, ['Birdwatching', 'Computers']); + + $this->setUser($user2); + useredit_update_interests($user2, ['computers']); + + $this->setAdminUser(); + + $tag = core_tag_tag::get_by_name(0, 'computers', '*'); + $tag->update(['description' => '']); + get_file_storage()->create_file_from_string([ + 'contextid' => context_system::instance()->id, + 'component' => 'tag', + 'filearea' => 'description', + 'itemid' => $tag->id, + 'filepath' => '/', + 'filename' => 'computer.jpg' + ], "jpg:image"); + + return [$user1, $user2]; + } + + public function test_export_item_tags() { + list($user1, $user2) = $this->set_up_tags(); + $this->assertEquals([context_system::instance()->id], + provider::get_contexts_for_userid($user1->id)->get_contextids()); + $this->assertEmpty(provider::get_contexts_for_userid($user2->id)->get_contextids()); + } + + public function test_delete_data_for_user() { + global $DB; + list($user1, $user2) = $this->set_up_tags(); + $context = context_system::instance(); + $this->assertEquals(2, $DB->count_records('tag', [])); + $this->assertEquals(0, $DB->count_records('tag', ['userid' => 0])); + provider::delete_data_for_user(new \core_privacy\local\request\approved_contextlist($user2, 'core_tag', [$context->id])); + $this->assertEquals(2, $DB->count_records('tag', [])); + $this->assertEquals(0, $DB->count_records('tag', ['userid' => 0])); + provider::delete_data_for_user(new \core_privacy\local\request\approved_contextlist($user1, 'core_tag', [$context->id])); + $this->assertEquals(2, $DB->count_records('tag', [])); + $this->assertEquals(2, $DB->count_records('tag', ['userid' => 0])); + } + + public function test_delete_data_for_all_users_in_context() { + global $DB; + $course = $this->getDataGenerator()->create_course(); + list($user1, $user2) = $this->set_up_tags(); + $this->assertEquals(2, $DB->count_records('tag', [])); + $this->assertEquals(3, $DB->count_records('tag_instance', [])); + provider::delete_data_for_all_users_in_context(context_course::instance($course->id)); + $this->assertEquals(2, $DB->count_records('tag', [])); + $this->assertEquals(3, $DB->count_records('tag_instance', [])); + provider::delete_data_for_all_users_in_context(context_system::instance()); + $this->assertEquals(0, $DB->count_records('tag', [])); + $this->assertEquals(0, $DB->count_records('tag_instance', [])); + } + + public function test_export_data_for_user() { + global $DB; + list($user1, $user2) = $this->set_up_tags(); + $context = context_system::instance(); + provider::export_user_data(new \core_privacy\local\request\approved_contextlist($user2, 'core_tag', [$context->id])); + $this->assertFalse(writer::with_context($context)->has_any_data()); + + $tagids = array_values(array_map(function($tag) { + return $tag->id; + }, core_tag_tag::get_by_name_bulk(core_tag_collection::get_default(), ['Birdwatching', 'Computers']))); + + provider::export_user_data(new \core_privacy\local\request\approved_contextlist($user1, 'core_tag', [$context->id])); + $writer = writer::with_context($context); + + $data = $writer->get_data(['Tags', $tagids[0]]); + $files = $writer->get_files(['Tags', $tagids[0]]); + $this->assertEquals('Birdwatching', $data->rawname); + $this->assertEmpty($files); + + $data = $writer->get_data(['Tags', $tagids[1]]); + $files = $writer->get_files(['Tags', $tagids[1]]); + $this->assertEquals('Computers', $data->rawname); + $this->assertEquals(['computer.jpg'], array_keys($files)); + } }