From 2bc753db416f6f58029a655a50bb8aa53c910085 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?= <fred@branchup.tech>
Date: Mon, 23 Apr 2018 16:41:44 +0800
Subject: [PATCH] MDL-62135 tool_log: Implement privacy API

---
 .../tool/log/classes/local/privacy/helper.php | 148 +++++++++++++++
 .../local/privacy/logstore_provider.php       |  78 ++++++++
 .../moodle_database_export_and_delete.php     | 123 ++++++++++++
 admin/tool/log/classes/privacy/provider.php   | 113 +++++++++++
 admin/tool/log/lang/en/tool_log.php           |   8 +-
 admin/tool/log/tests/privacy_test.php         | 176 ++++++++++++++++++
 6 files changed, 645 insertions(+), 1 deletion(-)
 create mode 100644 admin/tool/log/classes/local/privacy/helper.php
 create mode 100644 admin/tool/log/classes/local/privacy/logstore_provider.php
 create mode 100644 admin/tool/log/classes/local/privacy/moodle_database_export_and_delete.php
 create mode 100644 admin/tool/log/classes/privacy/provider.php
 create mode 100644 admin/tool/log/tests/privacy_test.php

diff --git a/admin/tool/log/classes/local/privacy/helper.php b/admin/tool/log/classes/local/privacy/helper.php
new file mode 100644
index 00000000000..4aa17d15ef2
--- /dev/null
+++ b/admin/tool/log/classes/local/privacy/helper.php
@@ -0,0 +1,148 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy helper.
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_log\local\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\request\transform;
+
+/**
+ * Privacy helper class.
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Returns an event from a standard record.
+     *
+     * @see \logstore_standard\log\store::get_log_event()
+     * @param object $data Log data.
+     * @return \core\event\base
+     */
+    protected static function restore_event_from_standard_record($data) {
+        $extra = ['origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid];
+        $data = (array) $data;
+        $id = $data['id'];
+        $data['other'] = unserialize($data['other']);
+        if ($data['other'] === false) {
+            $data['other'] = [];
+        }
+        unset($data['origin']);
+        unset($data['ip']);
+        unset($data['realuserid']);
+        unset($data['id']);
+
+        if (!$event = \core\event\base::restore($data, $extra)) {
+            return null;
+        }
+
+        return $event;
+    }
+
+    /**
+     * Transform a standard log record for a user.
+     *
+     * @param object $record The record.
+     * @param int $userid The user ID.
+     * @return array
+     */
+    public static function transform_standard_log_record_for_userid($record, $userid) {
+
+        // Restore the event to try to get the name, description and other field.
+        $restoredevent = static::restore_event_from_standard_record($record);
+        if ($restoredevent) {
+            $name = $restoredevent->get_name();
+            $description = $restoredevent->get_description();
+            $other = $restoredevent->other;
+
+        } else {
+            $name = $record->eventname;
+            $description = "Unknown event ({$name})";
+            $other = unserialize($record->other);
+        }
+
+        $realuserid = $record->realuserid;
+        $isauthor = $record->userid == $userid;
+        $isrelated = $record->relateduserid == $userid;
+        $isrealuser = $realuserid == $userid;
+        $ismasqueraded = $realuserid !== null && $record->userid != $realuserid;
+        $ismasquerading = $isrealuser && !$isauthor;
+        $isanonymous = $record->anonymous;
+
+        $data = [
+            'name' => $name,
+            'description' => $description,
+            'timecreated' => transform::datetime($record->timecreated),
+            'ip' => $record->ip,
+            'origin' => static::transform_origin($record->origin),
+            'other' => $other ? $other : []
+        ];
+
+        if ($isanonymous) {
+            $data['action_was_done_anonymously'] = transform::yesno($isanonymous);
+        }
+        if ($isauthor || !$isanonymous) {
+            $data['authorid'] = transform::user($record->userid);
+            $data['author_of_the_action_was_you'] = transform::yesno($isauthor);
+        }
+
+        if ($record->relateduserid) {
+            $data['relateduserid'] = transform::user($record->relateduserid);
+            $data['related_user_was_you'] = transform::yesno($isrelated);
+        }
+
+        if ($ismasqueraded) {
+            $data['author_of_the_action_was_masqueraded'] = transform::yesno(true);
+            if ($ismasquerading || !$isanonymous) {
+                $data['masqueradinguserid'] = transform::user($realuserid);
+                $data['masquerading_user_was_you'] = transform::yesno($ismasquerading);
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Transform origin.
+     *
+     * @param string $origin The page request origin.
+     * @return string
+     */
+    public static function transform_origin($origin) {
+        switch ($origin) {
+            case 'cli':
+            case 'restore':
+            case 'web':
+            case 'ws':
+                return get_string('privacy:request:origin:' . $origin, 'tool_log');
+                break;
+        }
+        return $origin;
+    }
+}
diff --git a/admin/tool/log/classes/local/privacy/logstore_provider.php b/admin/tool/log/classes/local/privacy/logstore_provider.php
new file mode 100644
index 00000000000..cecc13006a2
--- /dev/null
+++ b/admin/tool/log/classes/local/privacy/logstore_provider.php
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Logstore provider interface.
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_log\local\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+
+/**
+ * Logstore provider interface.
+ *
+ * Logstore subplugins providers must implement this interface.
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface logstore_provider extends \core_privacy\local\request\plugin\subplugin_provider {
+
+    /**
+     * 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);
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     * @return void
+     */
+    public static function export_user_data(approved_contextlist $contextlist);
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param context $context The specific context to delete data for.
+     * @return void
+     */
+    public static function delete_data_for_all_users_in_context(context $context);
+
+    /**
+     * 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.
+     * @return void
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist);
+
+}
diff --git a/admin/tool/log/classes/local/privacy/moodle_database_export_and_delete.php b/admin/tool/log/classes/local/privacy/moodle_database_export_and_delete.php
new file mode 100644
index 00000000000..da973eadf92
--- /dev/null
+++ b/admin/tool/log/classes/local/privacy/moodle_database_export_and_delete.php
@@ -0,0 +1,123 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Moodle database: export and delete.
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_log\local\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\writer;
+
+/**
+ * Moodle database: export and delete trait.
+ *
+ * This is to be used with logstores which use a database and table with the same columns
+ * as the core plugin 'logstore_standard'.
+ *
+ * This trait expects the following methods to be present in the object:
+ *
+ * - public static function get_database_and_table(): [moodle_database|null, string|null]
+ * - public static function get_export_subcontext(): []
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait moodle_database_export_and_delete {
+
+    /**
+     * 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) {
+        list($db, $table) = static::get_database_and_table();
+        if (!$db || !$table) {
+            return;
+        }
+
+        $userid = $contextlist->get_user()->id;
+        list($insql, $inparams) = $db->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
+
+        $sql = "(userid = :userid1 OR relateduserid = :userid2 OR realuserid = :userid3) AND contextid $insql";
+        $params = array_merge($inparams, [
+            'userid1' => $userid,
+            'userid2' => $userid,
+            'userid3' => $userid,
+        ]);
+
+        $path = static::get_export_subcontext();
+        $flush = function($lastcontextid, $data) use ($path) {
+            $context = context::instance_by_id($lastcontextid);
+            writer::with_context($context)->export_data($path, (object) ['logs' => $data]);
+        };
+
+        $lastcontextid = null;
+        $data = [];
+        $recordset = $db->get_recordset_select($table, $sql, $params, 'contextid, timecreated, id');
+        foreach ($recordset as $record) {
+            if ($lastcontextid && $lastcontextid != $record->contextid) {
+                $flush($lastcontextid, $data);
+                $data = [];
+            }
+            $data[] = helper::transform_standard_log_record_for_userid($record, $userid);
+            $lastcontextid = $record->contextid;
+        }
+        if ($lastcontextid) {
+            $flush($lastcontextid, $data);
+        }
+        $recordset->close();
+    }
+
+    /**
+     * 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) {
+        list($db, $table) = static::get_database_and_table();
+        if (!$db || !$table) {
+            return;
+        }
+        $db->delete_records($table, ['contextid' => $context->id]);
+    }
+
+    /**
+     * 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) {
+        list($db, $table) = static::get_database_and_table();
+        if (!$db || !$table) {
+            return;
+        }
+        list($insql, $inparams) = $db->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
+        $params = array_merge($inparams, ['userid' => $contextlist->get_user()->id]);
+        $db->delete_records_select($table, "userid = :userid AND contextid $insql", $params);
+    }
+
+}
diff --git a/admin/tool/log/classes/privacy/provider.php b/admin/tool/log/classes/privacy/provider.php
new file mode 100644
index 00000000000..87836380dea
--- /dev/null
+++ b/admin/tool/log/classes/privacy/provider.php
@@ -0,0 +1,113 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data provider.
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_log\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use tool_log\log\manager;
+
+/**
+ * Data provider class.
+ *
+ * @package    tool_log
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @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\subsystem\provider {
+
+    /**
+     * 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_plugintype_link('logstore', [], 'privacy:metadata:logstore');
+        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) : \core_privacy\local\request\contextlist {
+        $contextlist = new \core_privacy\local\request\contextlist();
+        static::call_subplugins_method_with_args('add_contexts_for_userid', [$contextlist, $userid]);
+        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) {
+        static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
+    }
+
+    /**
+     * 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) {
+        static::call_subplugins_method_with_args('delete_data_for_all_users_in_context', [$context]);
+    }
+
+    /**
+     * 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) {
+        static::call_subplugins_method_with_args('delete_data_for_user', [$contextlist]);
+    }
+
+    /**
+     * Invoke the subplugins method with arguments.
+     *
+     * @param string $method The method name.
+     * @param array $args The arguments.
+     * @return void
+     */
+    protected static function call_subplugins_method_with_args($method, array $args = []) {
+        $interface = 'tool_log\local\privacy\logstore_provider';
+        $subplugins = manager::get_store_plugins();
+        foreach ($subplugins as $subplugin => $unused) {
+            \core_privacy\manager::component_class_callback($subplugin, $interface, $method, $args);
+        }
+    }
+
+}
diff --git a/admin/tool/log/lang/en/tool_log.php b/admin/tool/log/lang/en/tool_log.php
index c57c424330c..bfda3002e44 100644
--- a/admin/tool/log/lang/en/tool_log.php
+++ b/admin/tool/log/lang/en/tool_log.php
@@ -26,7 +26,13 @@ $string['actlogshdr'] = 'Available log stores';
 $string['configlogplugins'] = 'Please enable all required plugins and arrange them in appropriate order.';
 $string['logging'] = 'Logging';
 $string['managelogging'] = 'Manage log stores';
-$string['reportssupported'] = 'Reports supported';
 $string['pluginname'] = 'Log store manager';
+$string['privacy:metadata:logstore'] = 'The log stores';
+$string['privacy:path:logs'] = 'Logs';
+$string['privacy:request:origin:cli'] = 'Command line tool';
+$string['privacy:request:origin:restore'] = 'Backup being restored';
+$string['privacy:request:origin:web'] = 'Standard web request';
+$string['privacy:request:origin:ws'] = 'Mobile app or web service';
+$string['reportssupported'] = 'Reports supported';
 $string['subplugintype_logstore'] = 'Log store';
 $string['subplugintype_logstore_plural'] = 'Log stores';
diff --git a/admin/tool/log/tests/privacy_test.php b/admin/tool/log/tests/privacy_test.php
new file mode 100644
index 00000000000..9e5be714621
--- /dev/null
+++ b/admin/tool/log/tests/privacy_test.php
@@ -0,0 +1,176 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data provider tests.
+ *
+ * @package    tool_log
+ * @category   test
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @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 tool_log\privacy\provider;
+
+require_once($CFG->dirroot . '/admin/tool/log/store/standard/tests/fixtures/event.php');
+
+/**
+ * Data provider testcase class.
+ *
+ * We're not testing the full functionality, just that the provider passes the requests
+ * down to at least one of its subplugin. Each subplugin should have tests to cover the
+ * different provider methods in depth.
+ *
+ * @package    tool_log
+ * @category   test
+ * @copyright  2018 Frédéric Massart
+ * @author     Frédéric Massart <fred@branchup.tech>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_log_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();
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1ctx = context_course::instance($c1->id);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        $this->setUser($u1);
+        $this->assertEmpty(provider::get_contexts_for_userid($u1->id)->get_contextids(), []);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+        $this->assertEquals($c1ctx->id, provider::get_contexts_for_userid($u1->id)->get_contextids()[0]);
+    }
+
+    public function test_delete_data_for_user() {
+        global $DB;
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1ctx = context_course::instance($c1->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();
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+
+        // Confirm data present.
+        $this->assertTrue($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id]));
+        $this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
+        $this->assertEquals(1, $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(0, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
+        $this->assertEquals(1, $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();
+        $c1ctx = context_course::instance($c1->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();
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+
+        // Confirm data present.
+        $this->assertTrue($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id]));
+        $this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
+        $this->assertEquals(1, $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(0, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
+        $this->assertEquals(0, $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();
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1ctx = context_course::instance($c1->id);
+
+        $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' => 123]]);
+        $e->trigger();
+
+        // Confirm data present for u1.
+        provider::export_user_data(new approved_contextlist($u1, 'tool_log', [$c1ctx->id]));
+        $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(123, $data->logs[0]['other']['i']);
+    }
+
+    /**
+     * 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');
+    }
+}