diff --git a/admin/tool/log/store/standard/classes/privacy/provider.php b/admin/tool/log/store/standard/classes/privacy/provider.php new file mode 100644 index 00000000000..1994e29f5bc --- /dev/null +++ b/admin/tool/log/store/standard/classes/privacy/provider.php @@ -0,0 +1,109 @@ +. + +/** + * Data provider. + * + * @package logstore_standard + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace logstore_standard\privacy; +defined('MOODLE_INTERNAL') || die(); + +use context; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\contextlist; + +/** + * Data provider class. + * + * @package logstore_standard + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \tool_log\local\privacy\logstore_provider { + + use \tool_log\local\privacy\moodle_database_export_and_delete; + + /** + * Returns metadata. + * + * @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('log', [ + 'eventname' => 'privacy:metadata:log:eventname', + 'userid' => 'privacy:metadata:log:userid', + 'relateduserid' => 'privacy:metadata:log:relateduserid', + 'anonymous' => 'privacy:metadata:log:anonymous', + 'other' => 'privacy:metadata:log:other', + 'timecreated' => 'privacy:metadata:log:timecreated', + 'origin' => 'privacy:metadata:log:origin', + 'ip' => 'privacy:metadata:log:ip', + 'realuserid' => 'privacy:metadata:log:realuserid', + ], 'privacy:metadata:log'); + return $collection; + } + + /** + * Add contexts that contain user information for the specified user. + * + * @param contextlist $contextlist The contextlist to add the contexts to. + * @param int $userid The user to find the contexts for. + * @return void + */ + public static function add_contexts_for_userid(contextlist $contextlist, $userid) { + $sql = " + SELECT ctx.id + FROM {context} ctx + JOIN {logstore_standard_log} l + ON l.contextid = ctx.id + WHERE l.userid = :userid1 + OR l.relateduserid = :userid2 + OR l.realuserid = :userid3"; + $contextlist->add_from_sql($sql, [ + 'userid1' => $userid, + 'userid2' => $userid, + 'userid3' => $userid, + ]); + } + + /** + * Get the database object. + * + * @return array Containing moodle_database, string, or null values. + */ + protected static function get_database_and_table() { + global $DB; + return [$DB, 'logstore_standard_log']; + } + + /** + * Get the path to export the logs to. + * + * @return array + */ + protected static function get_export_subcontext() { + return [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_standard')]; + } +} diff --git a/admin/tool/log/store/standard/lang/en/logstore_standard.php b/admin/tool/log/store/standard/lang/en/logstore_standard.php index 2e2c2a771bc..33c229fbb0e 100644 --- a/admin/tool/log/store/standard/lang/en/logstore_standard.php +++ b/admin/tool/log/store/standard/lang/en/logstore_standard.php @@ -25,4 +25,14 @@ $string['buffersize'] = 'Write buffer size'; $string['pluginname'] = 'Standard log'; $string['pluginname_desc'] = 'A log plugin stores log entries in a Moodle database table.'; +$string['privacy:metadata:log'] = 'A collection of past events'; +$string['privacy:metadata:log:anonymous'] = 'Whether the event was flagged as anonymous'; +$string['privacy:metadata:log:eventname'] = 'The event name'; +$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event'; +$string['privacy:metadata:log:origin'] = 'The origin of the event'; +$string['privacy:metadata:log:other'] = 'Additional information about the event'; +$string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.'; +$string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event'; +$string['privacy:metadata:log:timecreated'] = 'The time at which the event occurred'; +$string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event'; $string['taskcleanup'] = 'Log table cleanup'; diff --git a/admin/tool/log/store/standard/tests/privacy_test.php b/admin/tool/log/store/standard/tests/privacy_test.php new file mode 100644 index 00000000000..5103b3abec4 --- /dev/null +++ b/admin/tool/log/store/standard/tests/privacy_test.php @@ -0,0 +1,373 @@ +. + +/** + * Data provider tests. + * + * @package logstore_standard + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_privacy\tests\provider_testcase; +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 logstore_standard\privacy\provider; + +require_once(__DIR__ . '/fixtures/event.php'); + +/** + * Data provider testcase class. + * + * @package logstore_standard + * @category test + * @copyright 2018 Frédéric Massart + * @author Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class logstore_standard_privacy_testcase extends provider_testcase { + + public function setUp() { + $this->resetAfterTest(); + $this->preventResetByRollback(); // Logging waits till the transaction gets committed. + } + + public function test_get_contexts_for_userid() { + $admin = \core_user::get_user(2); + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + + $c1 = $this->getDataGenerator()->create_course(); + $cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]); + $c2 = $this->getDataGenerator()->create_course(); + $cm2 = $this->getDataGenerator()->create_module('url', ['course' => $c2]); + + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + $cm1ctx = context_module::instance($cm1->cmid); + $cm2ctx = context_module::instance($cm2->cmid); + + $this->enable_logging(); + $manager = get_log_manager(true); + + // User 1 is the author. + $this->setUser($u1); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u1), []); + $e = \logstore_standard\event\unittest_executed::create(['context' => $cm1ctx]); + $e->trigger(); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$cm1ctx]); + + // User 2 is the related user. + $this->setUser(0); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u2), []); + $e = \logstore_standard\event\unittest_executed::create(['context' => $cm2ctx, 'relateduserid' => $u2->id]); + $e->trigger(); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u2), [$cm2ctx]); + + // Admin user is the real user. + $this->assert_contextlist_equals($this->get_contextlist_for_user($admin), []); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u3), []); + $this->setAdminUser(); + \core\session\manager::loginas($u3->id, $sysctx); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]); + $e->trigger(); + $this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx]); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx]); + + // By admin user masquerading u1 related to u3. + $this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$cm1ctx]); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx]); + $this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx]); + $this->setAdminUser(); + \core\session\manager::loginas($u1->id, context_system::instance()); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u3->id]); + $e->trigger(); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$sysctx, $cm1ctx, $c2ctx]); + $this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx, $c2ctx]); + $this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx, $c2ctx]); + } + + public function test_delete_data_for_user() { + global $DB; + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $c1 = $this->getDataGenerator()->create_course(); + $c2 = $this->getDataGenerator()->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + + $this->enable_logging(); + $manager = get_log_manager(true); + + // User 1 is the author. + $this->setUser($u1); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]); + $e->trigger(); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]); + $e->trigger(); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]); + $e->trigger(); + + // User 2 is the author. + $this->setUser($u2); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]); + $e->trigger(); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]); + $e->trigger(); + + // Confirm data present. + $this->assertTrue($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id])); + $this->assertEquals(3, $DB->count_records('logstore_standard_log', ['userid' => $u1->id])); + $this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id])); + + // Delete all the things! + provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_standard', [$c1ctx->id])); + $this->assertFalse($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id])); + $this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u1->id])); + $this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id])); + } + + public function test_delete_data_for_all_users_in_context() { + global $DB; + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $c1 = $this->getDataGenerator()->create_course(); + $c2 = $this->getDataGenerator()->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + + $this->enable_logging(); + $manager = get_log_manager(true); + + // User 1 is the author. + $this->setUser($u1); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]); + $e->trigger(); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]); + $e->trigger(); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]); + $e->trigger(); + + // User 2 is the author. + $this->setUser($u2); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]); + $e->trigger(); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]); + $e->trigger(); + + // Confirm data present. + $this->assertTrue($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id])); + $this->assertEquals(3, $DB->count_records('logstore_standard_log', ['userid' => $u1->id])); + $this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id])); + + // Delete all the things! + provider::delete_data_for_all_users_in_context($c1ctx); + $this->assertFalse($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id])); + $this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u1->id])); + $this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id])); + } + + public function test_export_data_for_user() { + $admin = \core_user::get_user(2); + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + $u4 = $this->getDataGenerator()->create_user(); + $c1 = $this->getDataGenerator()->create_course(); + $cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]); + $c2 = $this->getDataGenerator()->create_course(); + $cm2 = $this->getDataGenerator()->create_module('url', ['course' => $c2]); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + $cm1ctx = context_module::instance($cm1->cmid); + $cm2ctx = context_module::instance($cm2->cmid); + + $path = [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_standard')]; + $this->enable_logging(); + $manager = get_log_manager(true); + + // User 1 is the author. + $this->setUser($u1); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx, 'other' => ['i' => 0]]); + $e->trigger(); + + // User 2 is related. + $this->setUser(0); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx, 'relateduserid' => $u2->id, + 'other' => ['i' => 1]]); + $e->trigger(); + + // Admin user masquerades u3, which is related to u4. + $this->setAdminUser(); + \core\session\manager::loginas($u3->id, $sysctx); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx, 'relateduserid' => $u4->id, + 'other' => ['i' => 2]]); + $e->trigger(); + + // Confirm data present for u1. + provider::export_user_data(new approved_contextlist($u1, 'logstore_standard', [$c2ctx->id, $c1ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']); + $this->assertSame(0, $data->logs[0]['other']['i']); + + // Confirm data present for u2. + writer::reset(); + provider::export_user_data(new approved_contextlist($u2, 'logstore_standard', [$c2ctx->id, $c1ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(false), $data->logs[0]['author_of_the_action_was_you']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']); + $this->assertSame(1, $data->logs[0]['other']['i']); + + // Confirm data present for u3. + writer::reset(); + provider::export_user_data(new approved_contextlist($u3, 'logstore_standard', [$c2ctx->id, $c1ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']); + $this->assertEquals(transform::yesno(false), $data->logs[0]['related_user_was_you']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']); + $this->assertEquals(transform::yesno(false), $data->logs[0]['masquerading_user_was_you']); + $this->assertSame(2, $data->logs[0]['other']['i']); + + // Confirm data present for u4. + writer::reset(); + provider::export_user_data(new approved_contextlist($u4, 'logstore_standard', [$c2ctx->id, $c1ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(false), $data->logs[0]['author_of_the_action_was_you']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']); + $this->assertEquals(transform::yesno(false), $data->logs[0]['masquerading_user_was_you']); + $this->assertSame(2, $data->logs[0]['other']['i']); + + // Add anonymous events. + $this->setUser($u1); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u2->id, + 'anonymous' => true]); + $e->trigger(); + $this->setAdminUser(); + \core\session\manager::loginas($u3->id, $sysctx); + $e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u4->id, + 'anonymous' => true]); + $e->trigger(); + + // Confirm data present for u1. + provider::export_user_data(new approved_contextlist($u1, 'logstore_standard', [$c2ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']); + + // Confirm data present for u2. + writer::reset(); + provider::export_user_data(new approved_contextlist($u2, 'logstore_standard', [$c2ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']); + $this->assertArrayNotHasKey('author_of_the_action_was_you', $data->logs[0]); + $this->assertArrayNotHasKey('authorid', $data->logs[0]); + $this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']); + + // Confirm data present for u3. + writer::reset(); + provider::export_user_data(new approved_contextlist($u3, 'logstore_standard', [$c2ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']); + $this->assertArrayNotHasKey('masquerading_user_was_you', $data->logs[0]); + $this->assertArrayNotHasKey('masqueradinguserid', $data->logs[0]); + + // Confirm data present for u4. + writer::reset(); + provider::export_user_data(new approved_contextlist($u4, 'logstore_standard', [$c2ctx->id])); + $data = writer::with_context($c2ctx)->get_data($path); + $this->assertCount(1, $data->logs); + $this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']); + $this->assertArrayNotHasKey('author_of_the_action_was_you', $data->logs[0]); + $this->assertArrayNotHasKey('authorid', $data->logs[0]); + $this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']); + $this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']); + $this->assertArrayNotHasKey('masquerading_user_was_you', $data->logs[0]); + $this->assertArrayNotHasKey('masqueradinguserid', $data->logs[0]); + } + + /** + * Assert the content of a context list. + * + * @param contextlist $contextlist The collection. + * @param array $expected List of expected contexts or IDs. + * @return void + */ + protected function assert_contextlist_equals($contextlist, array $expected) { + $expectedids = array_map(function($context) { + if (is_object($context)) { + return $context->id; + } + return $context; + }, $expected); + $contextids = array_map('intval', $contextlist->get_contextids()); + sort($contextids); + sort($expectedids); + $this->assertEquals($expectedids, $contextids); + } + + /** + * Enable logging. + * + * @return void + */ + protected function enable_logging() { + set_config('enabled_stores', 'logstore_standard', 'tool_log'); + set_config('buffersize', 0, 'logstore_standard'); + set_config('logguests', 1, 'logstore_standard'); + } + + /** + * Get the contextlist for a user. + * + * @param object $user The user. + * @return contextlist + */ + protected function get_contextlist_for_user($user) { + $contextlist = new contextlist(); + provider::add_contexts_for_userid($contextlist, $user->id); + return $contextlist; + } +}