From 25c8fd74acb2c8cd8728af10c2a52507c7cbe4ec Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Sat, 31 Mar 2018 10:08:54 +0800 Subject: [PATCH] MDL-61808 tool_monitor: Implemented privacy classes. This plugin is now compatible with the new privacy system. --- .../tool/monitor/classes/privacy/provider.php | 211 +++++++++++++ admin/tool/monitor/classes/rule.php | 7 +- admin/tool/monitor/lang/en/tool_monitor.php | 22 ++ admin/tool/monitor/tests/privacy_test.php | 289 ++++++++++++++++++ 4 files changed, 526 insertions(+), 3 deletions(-) create mode 100644 admin/tool/monitor/classes/privacy/provider.php create mode 100644 admin/tool/monitor/tests/privacy_test.php diff --git a/admin/tool/monitor/classes/privacy/provider.php b/admin/tool/monitor/classes/privacy/provider.php new file mode 100644 index 00000000000..9b481d3dbea --- /dev/null +++ b/admin/tool/monitor/classes/privacy/provider.php @@ -0,0 +1,211 @@ +. +/** + * Privacy class for requesting user data. + * + * @package tool_monitor + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_monitor\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\transform; +use \core_privacy\local\request\writer; +use \tool_monitor\subscription_manager; +use \tool_monitor\rule_manager; + +/** + * Privacy provider for tool_monitor + * + * @package tool_monitor + * @copyright 2018 Adrian Greeve + * @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 { + + /** + * Get information about the user data stored by this plugin. + * + * @param collection $collection An object for storing metadata. + * @return collection The metadata. + */ + public static function get_metadata(collection $collection) : collection { + $toolmonitorrules = [ + 'description' => 'privacy:metadata:description', + 'name' => 'privacy:metadata:name', + 'userid' => 'privacy:metadata:userid', + 'plugin' => 'privacy:metadata:plugin', + 'eventname' => 'privacy:metadata:eventname', + 'template' => 'privacy:metadata:template', + 'frequency' => 'privacy:metadata:frequency', + 'timewindow' => 'privacy:metadata:timewindow', + 'timemodified' => 'privacy:metadata:timemodifiedrule', + 'timecreated' => 'privacy:metadata:timecreatedrule' + ]; + $toolmonitorsubscriptions = [ + 'userid' => 'privacy:metadata:useridsub', + 'timecreated' => 'privacy:metadata:timecreatedsub', + 'lastnotificationsent' => 'privacy:metadata:lastnotificationsent', + 'inactivedate' => 'privacy:metadata:inactivedate' + ]; + // Tool monitor history doesn't look like it is used at all. + $toolmonitorhistory = [ + 'userid' => 'privacy:metadata:useridhistory', + 'timesent' => 'privacy:metadata:timesent' + ]; + $collection->add_database_table('tool_monitor_rules', $toolmonitorrules, 'privacy:metadata:rulessummary'); + $collection->add_database_table('tool_monitor_subscriptions', $toolmonitorsubscriptions, + 'privacy:metadata:subscriptionssummary'); + $collection->add_database_table('tool_monitor_history', $toolmonitorhistory, 'privacy:metadata:historysummary'); + $collection->link_subsystem('core_message', 'privacy:metadata:messagesummary'); + return $collection; + } + + /** + * Return all contexts for this userid. In this situation the user context. + * + * @param int $userid The user ID. + * @return contextlist The list of context IDs. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $params = ['useridrules' => $userid, 'useridsubscriptions' => $userid, 'contextuserrule' => CONTEXT_USER, + 'contextusersub' => CONTEXT_USER]; + $sql = "SELECT DISTINCT ctx.id + FROM {context} ctx + LEFT JOIN {tool_monitor_rules} mr ON ctx.instanceid = mr.userid AND ctx.contextlevel = :contextuserrule + LEFT JOIN {tool_monitor_subscriptions} ms ON ctx.instanceid = ms.userid AND ctx.contextlevel = :contextusersub + WHERE (ms.userid = :useridrules OR mr.userid = :useridsubscriptions)"; + + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + return $contextlist; + } + + /** + * Export all event monitor information for the list of contexts and this user. + * + * @param approved_contextlist $contextlist The list of approved contexts for a user. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + // Export rules. + $context = \context_user::instance($contextlist->get_user()->id); + $rules = $DB->get_records('tool_monitor_rules', ['userid' => $contextlist->get_user()->id]); + if ($rules) { + static::export_monitor_rules($rules, $context); + } + // Export subscriptions. + $subscriptions = subscription_manager::get_user_subscriptions(0, 0, $contextlist->get_user()->id); + if ($subscriptions) { + static::export_monitor_subscriptions($subscriptions, $context); + } + } + + /** + * Delete all user data for this context. + * + * @param \context $context The context to delete data for. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + // Only delete data for user contexts. + if ($context->contextlevel == CONTEXT_USER) { + static::delete_user_data($context->instanceid); + } + } + + /** + * Delete all user data for this user only. + * + * @param approved_contextlist $contextlist The list of approved contexts for a user. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + static::delete_user_data($contextlist->get_user()->id); + } + + /** + * This does the deletion of user data for the event monitor. + * + * @param int $userid The user ID + */ + protected static function delete_user_data(int $userid) { + global $DB; + // Delete this user's subscriptions first. + subscription_manager::delete_user_subscriptions($userid); + // Because we only use user contexts the instance ID is the user ID. + // Get the rules and check if this user has the capability to delete them. + $rules = $DB->get_records('tool_monitor_rules', ['userid' => $userid]); + foreach ($rules as $ruledata) { + $rule = rule_manager::get_rule($ruledata); + // If no-one is suscribed to the rule then it is safe to delete. + if ($rule->can_manage_rule($userid) && subscription_manager::count_rule_subscriptions($rule->id) == 0) { + $rule->delete_rule(); + } + } + } + + /** + * This formats and then exports the monitor rules. + * + * @param array $rules The monitor rules. + * @param context_user $context The user context + */ + protected static function export_monitor_rules(array $rules, \context_user $context) { + foreach ($rules as $rule) { + $rule = rule_manager::get_rule($rule); + $ruledata = new \stdClass(); + $ruledata->name = $rule->name; + $ruledata->eventname = $rule->get_event_name(); + $ruledata->description = $rule->get_description($context); + $ruledata->plugin = $rule->get_plugin_name(); + $ruledata->template = $rule->template; + $ruledata->frequency = $rule->get_filters_description(); + $ruledata->course = $rule->get_course_name($context); + $ruledata->timecreated = transform::datetime($rule->timecreated); + $ruledata->timemodified = transform::datetime($rule->timemodified); + writer::with_context($context)->export_data([get_string('privacy:createdrules', 'tool_monitor'), + $rule->name . '_' . $rule->id], $ruledata); + } + } + + /** + * This formats and then exports the event monitor subscriptions. + * + * @param array $subscriptions Subscriptions + * @param \context_user $context The user context + */ + protected static function export_monitor_subscriptions(array $subscriptions, \context_user $context) { + foreach ($subscriptions as $subscription) { + $subscriptiondata = new \stdClass(); + $subscriptiondata->instancename = $subscription->get_instance_name(); + $subscriptiondata->eventname = $subscription->get_event_name(); + $subscriptiondata->frequency = $subscription->get_filters_description(); + $subscriptiondata->name = $subscription->get_name($context); + $subscriptiondata->description = $subscription->get_description($context); + $subscriptiondata->pluginname = $subscription->get_plugin_name(); + $subscriptiondata->course = $subscription->get_course_name($context); + $subscriptiondata->timecreated = transform::datetime($subscription->timecreated); + $subscriptiondata->lastnotificationsent = transform::datetime($subscription->lastnotificationsent); + writer::with_context($context)->export_data([get_string('privacy:subscriptions', 'tool_monitor'), + $subscriptiondata->name . '_' . $subscription->id, $subscriptiondata->course, $subscriptiondata->instancename], + $subscriptiondata); + } + } +} diff --git a/admin/tool/monitor/classes/rule.php b/admin/tool/monitor/classes/rule.php index 3c5e6d263c5..4cc3920688e 100644 --- a/admin/tool/monitor/classes/rule.php +++ b/admin/tool/monitor/classes/rule.php @@ -51,14 +51,15 @@ class rule { } /** - * Can the current user manage this rule? + * Can the user manage this rule? Defaults to $USER. * + * @param int $userid Check against this userid. * @return bool true if the current user can manage this rule, else false. */ - public function can_manage_rule() { + public function can_manage_rule($userid = null) { $courseid = $this->courseid; $context = empty($courseid) ? \context_system::instance() : \context_course::instance($this->courseid); - return has_capability('tool/monitor:managerules', $context); + return has_capability('tool/monitor:managerules', $context, $userid); } /** diff --git a/admin/tool/monitor/lang/en/tool_monitor.php b/admin/tool/monitor/lang/en/tool_monitor.php index ed41095a9db..81d20bb635f 100644 --- a/admin/tool/monitor/lang/en/tool_monitor.php +++ b/admin/tool/monitor/lang/en/tool_monitor.php @@ -79,6 +79,28 @@ $string['monitor:managetool'] = 'Enable/disable event monitoring'; $string['monitor:subscribe'] = 'Subscribe to event monitor rules'; $string['norules'] = 'There are no event monitoring rules.'; $string['pluginname'] = 'Event monitor'; +$string['privacy:createdrules'] = 'Event monitor rules I created'; +$string['privacy:metadata:description'] = 'Description of the rule'; +$string['privacy:metadata:eventname'] = 'Fully qualified name of the event'; +$string['privacy:metadata:frequency'] = 'Frequency of notifications'; +$string['privacy:metadata:historysummary'] = 'Stores the history of the message notifications sent'; +$string['privacy:metadata:inactivedate'] = 'Period of time, in days, after which an inactive subscription will be removed completely'; +$string['privacy:metadata:lastnotificationsent'] = 'When a notification was last sent for this subscription.'; +$string['privacy:metadata:messagesummary'] = 'Notifications are sent to the message system.'; +$string['privacy:metadata:name'] = 'Name of the rule'; +$string['privacy:metadata:plugin'] = 'Frankenstlye name of the plugin'; +$string['privacy:metadata:rulessummary'] = 'This stores monitor rules.'; +$string['privacy:metadata:subscriptionssummary'] = 'Stores user subscriptions to various rules'; +$string['privacy:metadata:template'] = 'Message template'; +$string['privacy:metadata:timecreatedrule'] = 'When this rule was created'; +$string['privacy:metadata:timecreatedsub'] = 'When this subscription was created'; +$string['privacy:metadata:timemodifiedrule'] = 'When this rule was last modified'; +$string['privacy:metadata:timesent'] = 'When the message was sent'; +$string['privacy:metadata:timewindow'] = 'Time window in seconds'; +$string['privacy:metadata:userid'] = 'Id of user who created the rule'; +$string['privacy:metadata:useridhistory'] = 'User to whom this notification was sent'; +$string['privacy:metadata:useridsub'] = 'User id of the subscriber'; +$string['privacy:subscriptions'] = 'My event monitor subscriptions'; $string['processevents'] = 'Process events'; $string['rulename'] = 'Rule name'; $string['ruleareyousure'] = 'Are you sure you want to delete the rule "{$a}"?'; diff --git a/admin/tool/monitor/tests/privacy_test.php b/admin/tool/monitor/tests/privacy_test.php new file mode 100644 index 00000000000..5c5f11c5c99 --- /dev/null +++ b/admin/tool/monitor/tests/privacy_test.php @@ -0,0 +1,289 @@ +. + +/** + * Privacy test for the event monitor + * + * @package tool_monitor + * @category test + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \tool_monitor\privacy\provider; +use \core_privacy\local\request\approved_contextlist; + +/** + * Privacy test for the event monitor + * + * @package tool_monitor + * @category test + * @copyright 2018 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_monitor_privacy_testcase extends advanced_testcase { + + /** + * Set up method. + */ + public function setUp() { + $this->resetAfterTest(); + // Enable monitor. + set_config('enablemonitor', 1, 'tool_monitor'); + } + + /** + * Assign a capability to $USER + * The function creates a student $USER if $USER->id is empty + * + * @param string $capability capability name + * @param int $contextid + * @param int $roleid + * @return int the role id - mainly returned for creation, so calling function can reuse it + */ + public static function assign_user_capability($capability, $contextid, $roleid = null) { + global $USER; + + // Create a new student $USER if $USER doesn't exist. + if (empty($USER->id)) { + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + } + + if (empty($roleid)) { + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + } + + assign_capability($capability, CAP_ALLOW, $roleid, $contextid); + + role_assign($roleid, $USER->id, $contextid); + + accesslib_clear_all_caches_for_unit_testing(); + + return $roleid; + } + + /** + * Test that a collection with data is returned when calling this function. + */ + public function test_get_metadata() { + $collection = new \core_privacy\local\metadata\collection('tool_monitor'); + $collection = provider::get_metadata($collection); + $this->assertNotEmpty($collection); + } + + /** + * Check that a user context is returned if there is any user data for this user. + */ + public function test_get_contexts_for_userid() { + $user = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($user->id); + $usercontext2 = \context_user::instance($user2->id); + $this->assertEmpty(provider::get_contexts_for_userid($user->id)); + $this->assertEmpty(provider::get_contexts_for_userid($user2->id)); + + $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor'); + + // Create a rule with this user. + $this->setUser($user); + $rule = $monitorgenerator->create_rule(); + $contextlist = provider::get_contexts_for_userid($user->id); + + // Check that we only get back one context. + $this->assertCount(1, $contextlist); + + // Check that a context is returned for just creating a rule. + $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]); + + $this->setUser($user2); + + $record = new stdClass(); + $record->courseid = 0; + $record->userid = $user2->id; + $record->ruleid = $rule->id; + + $subscription = $monitorgenerator->create_subscription($record); + $contextlist = provider::get_contexts_for_userid($user2->id); + + // Check that we only get back one context. + $this->assertCount(1, $contextlist); + + // Check that a context is returned for just subscribing to a rule. + $this->assertEquals($usercontext2->id, $contextlist->get_contextids()[0]); + } + + /** + * Test that user data is exported correctly. + */ + public function test_export_user_data() { + $user = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($user->id); + $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor'); + + $this->setUser($user); + $rulerecord = (object)['name' => 'privacy rule']; + $rule = $monitorgenerator->create_rule($rulerecord); + + $secondrulerecord = (object)['name' => 'privacy rule2']; + $rule2 = $monitorgenerator->create_rule($secondrulerecord); + + $subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id]; + $subscription = $monitorgenerator->create_subscription($subscription); + + $writer = \core_privacy\local\request\writer::with_context($usercontext); + $this->assertFalse($writer->has_any_data()); + + $approvedlist = new approved_contextlist($user, 'tool_monitor', [$usercontext->id]); + provider::export_user_data($approvedlist); + + // Check that the rules created by this user are exported. + $this->assertEquals($rulerecord->name, $writer->get_data([get_string('privacy:createdrules', 'tool_monitor'), + $rulerecord->name . '_' . $rule->id])->name); + $this->assertEquals($secondrulerecord->name, $writer->get_data([get_string('privacy:createdrules', 'tool_monitor'), + $secondrulerecord->name . '_' . $rule2->id])->name); + + // Check that the subscriptions for this user are also exported. + $this->assertEquals($rulerecord->name, $writer->get_data([get_string('privacy:subscriptions', 'tool_monitor'), + $rulerecord->name . '_' . $subscription->id, 'Site' , 'All events'])->name); + } + + /** + * Test deleting all user data for a specific context. + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $user = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($user->id); + $usercontext2 = \context_user::instance($user2->id); + $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor'); + + $this->setUser($user); + // Need to give user one the ability to manage rules. + $this->assign_user_capability('tool/monitor:managerules', \context_system::instance()); + + $rulerecord = (object)['name' => 'privacy rule']; + $rule = $monitorgenerator->create_rule($rulerecord); + + $secondrulerecord = (object)['name' => 'privacy rule2']; + $rule2 = $monitorgenerator->create_rule($secondrulerecord); + + $subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id]; + $subscription = $monitorgenerator->create_subscription($subscription); + + // Have user 2 subscribe to the second rule created by user 1. + $subscription2 = (object)['ruleid' => $rule2->id, 'userid' => $user2->id]; + $subscription2 = $monitorgenerator->create_subscription($subscription2); + + $this->setUser($user2); + $thirdrulerecord = (object)['name' => 'privacy rule for second user']; + $rule3 = $monitorgenerator->create_rule($thirdrulerecord); + + $subscription3 = (object)['ruleid' => $rule3->id, 'userid' => $user2->id]; + $subscription3 = $monitorgenerator->create_subscription($subscription3); + + // Try a different context first. + provider::delete_data_for_all_users_in_context(context_system::instance()); + + // Get all of the monitor rules. + $dbrules = $DB->get_records('tool_monitor_rules'); + + // All of the rules should still be present. + $this->assertCount(3, $dbrules); + $this->assertEquals($user->id, $dbrules[$rule->id]->userid); + $this->assertEquals($user->id, $dbrules[$rule2->id]->userid); + $this->assertEquals($user2->id, $dbrules[$rule3->id]->userid); + + // Delete everything for the first user context. + provider::delete_data_for_all_users_in_context($usercontext); + + // Get all of the monitor rules. + $dbrules = $DB->get_records('tool_monitor_rules'); + + // Only the rules for user 1 that does not have any more subscriptions should be deleted (the first rule). + $this->assertCount(2, $dbrules); + $this->assertEquals($user->id, $dbrules[$rule2->id]->userid); + $this->assertEquals($user2->id, $dbrules[$rule3->id]->userid); + + // Get all of the monitor subscriptions. + $dbsubs = $DB->get_records('tool_monitor_subscriptions'); + // There should be two subscriptions left, both for user 2. + $this->assertCount(2, $dbsubs); + $this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid); + $this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid); + } + + /** + * This should work identical to the above test. + */ + public function test_delete_data_for_user() { + global $DB; + + $user = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $usercontext = \context_user::instance($user->id); + $usercontext2 = \context_user::instance($user2->id); + $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor'); + + $this->setUser($user); + // Need to give user one the ability to manage rules. + $this->assign_user_capability('tool/monitor:managerules', \context_system::instance()); + + $rulerecord = (object)['name' => 'privacy rule']; + $rule = $monitorgenerator->create_rule($rulerecord); + + $secondrulerecord = (object)['name' => 'privacy rule2']; + $rule2 = $monitorgenerator->create_rule($secondrulerecord); + + $subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id]; + $subscription = $monitorgenerator->create_subscription($subscription); + + // Have user 2 subscribe to the second rule created by user 1. + $subscription2 = (object)['ruleid' => $rule2->id, 'userid' => $user2->id]; + $subscription2 = $monitorgenerator->create_subscription($subscription2); + + $this->setUser($user2); + $thirdrulerecord = (object)['name' => 'privacy rule for second user']; + $rule3 = $monitorgenerator->create_rule($thirdrulerecord); + + $subscription3 = (object)['ruleid' => $rule3->id, 'userid' => $user2->id]; + $subscription3 = $monitorgenerator->create_subscription($subscription3); + + $approvedlist = new approved_contextlist($user, 'tool_monitor', [$usercontext->id]); + + // Delete everything for the first user. + provider::delete_data_for_user($approvedlist); + + // Get all of the monitor rules. + $dbrules = $DB->get_records('tool_monitor_rules'); + + // Only the rules for user 1 that does not have any more subscriptions should be deleted (the first rule). + $this->assertCount(2, $dbrules); + $this->assertEquals($user->id, $dbrules[$rule2->id]->userid); + $this->assertEquals($user2->id, $dbrules[$rule3->id]->userid); + + // Get all of the monitor subscriptions. + $dbsubs = $DB->get_records('tool_monitor_subscriptions'); + // There should be two subscriptions left, both for user 2. + $this->assertCount(2, $dbsubs); + $this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid); + $this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid); + } +}