Merge branch 'MDL-62135-master' of git://github.com/FMCorz/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 2 May 2018 09:37:47 +0000 (17:37 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 2 May 2018 09:37:47 +0000 (17:37 +0800)
15 files changed:
admin/tool/log/classes/local/privacy/helper.php [new file with mode: 0644]
admin/tool/log/classes/local/privacy/logstore_provider.php [new file with mode: 0644]
admin/tool/log/classes/local/privacy/moodle_database_export_and_delete.php [new file with mode: 0644]
admin/tool/log/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/log/lang/en/tool_log.php
admin/tool/log/store/database/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/log/store/database/lang/en/logstore_database.php
admin/tool/log/store/database/tests/privacy_test.php [new file with mode: 0644]
admin/tool/log/store/legacy/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/log/store/legacy/lang/en/logstore_legacy.php
admin/tool/log/store/legacy/tests/privacy_test.php [new file with mode: 0644]
admin/tool/log/store/standard/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/log/store/standard/lang/en/logstore_standard.php
admin/tool/log/store/standard/tests/privacy_test.php [new file with mode: 0644]
admin/tool/log/tests/privacy_test.php [new file with mode: 0644]

diff --git a/admin/tool/log/classes/local/privacy/helper.php b/admin/tool/log/classes/local/privacy/helper.php
new file mode 100644 (file)
index 0000000..4aa17d1
--- /dev/null
@@ -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 (file)
index 0000000..cecc130
--- /dev/null
@@ -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 (file)
index 0000000..da973ea
--- /dev/null
@@ -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 (file)
index 0000000..8783638
--- /dev/null
@@ -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);
+        }
+    }
+
+}
index c57c424..bfda300 100644 (file)
@@ -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/store/database/classes/privacy/provider.php b/admin/tool/log/store/database/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..b5e8eac
--- /dev/null
@@ -0,0 +1,114 @@
+<?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    logstore_database
+ * @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 logstore_database\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+
+/**
+ * Data provider class.
+ *
+ * @package    logstore_database
+ * @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,
+    \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_external_location_link('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) {
+        list($db, $table) = static::get_database_and_table();
+        if (!$db || !$table) {
+            return;
+        }
+
+        $sql = 'userid = :userid1 OR relateduserid = :userid2 OR realuserid = :userid3';
+        $params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
+        $contextids = $db->get_fieldset_select($table, 'DISTINCT contextid', $sql, $params);
+        if (empty($contextids)) {
+            return;
+        }
+
+        $sql = implode(' UNION ', array_map(function($id) use ($db) {
+            return 'SELECT ' . $id . $db->sql_null_from_clause();
+        }, $contextids));
+        $contextlist->add_from_sql($sql, []);
+    }
+
+    /**
+     * Get the database object.
+     *
+     * @return array Containing moodle_database, string, or null values.
+     */
+    protected static function get_database_and_table() {
+        $manager = get_log_manager();
+        $store = new \logstore_database\log\store($manager);
+        $db = $store->get_extdb();
+        return $db ? [$db, $store->get_config_value('dbtable')] : [null, null];
+    }
+
+    /**
+     * 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_database')];
+    }
+}
index 0873229..8f49696 100644 (file)
@@ -44,6 +44,16 @@ $string['other'] = 'Other';
 $string['participating'] = 'Participating';
 $string['pluginname'] = 'External database log';
 $string['pluginname_desc'] = 'A log plugin that stores log entries in an external 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['read'] = 'Read';
 $string['tablenotfound'] = 'Specified table was not found';
 $string['teaching'] = 'Teaching';
diff --git a/admin/tool/log/store/database/tests/privacy_test.php b/admin/tool/log/store/database/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..4a1e37e
--- /dev/null
@@ -0,0 +1,416 @@
+<?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    logstore_database
+ * @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 logstore_database\privacy\provider;
+
+require_once(__DIR__ . '/fixtures/event.php');
+
+/**
+ * Data provider testcase class.
+ *
+ * This testcase is almost identical to the logstore_standard testcase, aside from the
+ * initialisation of the relevant logstore obviously.
+ *
+ * @package    logstore_database
+ * @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 logstore_database_privacy_testcase extends provider_testcase {
+
+    public function setUp() {
+        global $CFG;
+        $this->resetAfterTest();
+        $this->preventResetByRollback(); // Logging waits till the transaction gets committed.
+
+        // Fake the settings, we will abuse the standard plugin table here...
+        set_config('dbdriver', $CFG->dblibrary . '/' . $CFG->dbtype, 'logstore_database');
+        set_config('dbhost', $CFG->dbhost, 'logstore_database');
+        set_config('dbuser', $CFG->dbuser, 'logstore_database');
+        set_config('dbpass', $CFG->dbpass, 'logstore_database');
+        set_config('dbname', $CFG->dbname, 'logstore_database');
+        set_config('dbtable', $CFG->prefix . 'logstore_standard_log', 'logstore_database');
+        if (!empty($CFG->dboptions['dbpersist'])) {
+            set_config('dbpersist', 1, 'logstore_database');
+        } else {
+            set_config('dbpersist', 0, 'logstore_database');
+        }
+        if (!empty($CFG->dboptions['dbsocket'])) {
+            set_config('dbsocket', $CFG->dboptions['dbsocket'], 'logstore_database');
+        } else {
+            set_config('dbsocket', '', 'logstore_database');
+        }
+        if (!empty($CFG->dboptions['dbport'])) {
+            set_config('dbport', $CFG->dboptions['dbport'], 'logstore_database');
+        } else {
+            set_config('dbport', '', 'logstore_database');
+        }
+        if (!empty($CFG->dboptions['dbschema'])) {
+            set_config('dbschema', $CFG->dboptions['dbschema'], 'logstore_database');
+        } else {
+            set_config('dbschema', '', 'logstore_database');
+        }
+        if (!empty($CFG->dboptions['dbcollation'])) {
+            set_config('dbcollation', $CFG->dboptions['dbcollation'], 'logstore_database');
+        } else {
+            set_config('dbcollation', '', 'logstore_database');
+        }
+        if (!empty($CFG->dboptions['dbhandlesoptions'])) {
+            set_config('dbhandlesoptions', $CFG->dboptions['dbhandlesoptions'], 'logstore_database');
+        } else {
+            set_config('dbhandlesoptions', false, 'logstore_database');
+        }
+    }
+
+    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_database\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_database\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_database\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_database\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_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx]);
+        $e->trigger();
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+        $e = \logstore_database\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_database', [$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_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx]);
+        $e->trigger();
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
+        $e->trigger();
+        $e = \logstore_database\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_database')];
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        // User 1 is the author.
+        $this->setUser($u1);
+        $e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx, 'other' => ['i' => 0]]);
+        $e->trigger();
+
+        // User 2 is related.
+        $this->setUser(0);
+        $e = \logstore_database\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_database\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_database', [$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_database', [$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_database', [$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_database', [$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_database\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u2->id,
+            'anonymous' => true]);
+        $e->trigger();
+        $this->setAdminUser();
+        \core\session\manager::loginas($u3->id, $sysctx);
+        $e = \logstore_database\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_database', [$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_database', [$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_database', [$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_database', [$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_database', 'tool_log');
+        set_config('buffersize', 0, 'logstore_database');
+        set_config('logguests', 1, 'logstore_database');
+        get_log_manager(true);
+    }
+
+    /**
+     * 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;
+    }
+}
diff --git a/admin/tool/log/store/legacy/classes/privacy/provider.php b/admin/tool/log/store/legacy/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..298d821
--- /dev/null
@@ -0,0 +1,220 @@
+<?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    logstore_legacy
+ * @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 logstore_legacy\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\contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use tool_log\local\privacy\helper;
+
+/**
+ * Data provider class.
+ *
+ * @package    logstore_legacy
+ * @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,
+    \tool_log\local\privacy\logstore_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_external_location_link('log', [
+            'time' => 'privacy:metadata:log:time',
+            'userid' => 'privacy:metadata:log:userid',
+            'ip' => 'privacy:metadata:log:ip',
+            'action' => 'privacy:metadata:log:action',
+            'url' => 'privacy:metadata:log:url',
+            'info' => 'privacy:metadata:log:info',
+        ], '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 {log} l
+                ON (l.cmid = 0 AND l.course = ctx.instanceid AND ctx.contextlevel = :courselevel)
+                OR (l.cmid > 0 AND l.cmid = ctx.instanceid AND ctx.contextlevel = :modulelevel)
+                OR (l.course <= 0 AND ctx.id = :syscontextid)
+             WHERE l.userid = :userid";
+        $params = [
+            'courselevel' => CONTEXT_COURSE,
+            'modulelevel' => CONTEXT_MODULE,
+            'syscontextid' => SYSCONTEXTID,
+            'userid' => $userid,
+        ];
+        $contextlist->add_from_sql($sql, $params);
+    }
+
+    /**
+     * 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;
+
+        $userid = $contextlist->get_user()->id;
+        list($insql, $inparams) = static::get_sql_where_from_contexts($contextlist->get_contexts());
+        if (empty($insql)) {
+            return;
+        }
+        $sql = "userid = :userid AND $insql";
+        $params = array_merge($inparams, ['userid' => $userid]);
+
+        $path = [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_legacy')];
+        $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('log', $sql, $params, 'course, cmid, time, id');
+        foreach ($recordset as $record) {
+            $event = \logstore_legacy\event\legacy_logged::restore_legacy($record);
+            $context = $event->get_context();
+            if ($lastcontextid && $lastcontextid != $context->id) {
+                $flush($lastcontextid, $data);
+                $data = [];
+            }
+
+            $extra = $event->get_logextra();
+            $data[] = [
+                'name' => $event->get_name(),
+                'description' => $event->get_description(),
+                'timecreated' => transform::datetime($event->timecreated),
+                'ip' => $extra['ip'],
+                'origin' => helper::transform_origin($extra['origin']),
+            ];
+
+            $lastcontextid = $context->id;
+        }
+        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) {
+        global $DB;
+        list($sql, $params) = static::get_sql_where_from_contexts([$context]);
+        if (empty($sql)) {
+            return;
+        }
+        $DB->delete_records_select('log', $sql, $params);
+    }
+
+    /**
+     * 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;
+        list($sql, $params) = static::get_sql_where_from_contexts($contextlist->get_contexts());
+        if (empty($sql)) {
+            return;
+        }
+        $userid = $contextlist->get_user()->id;
+        $DB->delete_records_select('log', "$sql AND userid = :userid", array_merge($params, ['userid' => $userid]));
+    }
+
+    /**
+     * Get an SQL where statement from a list of contexts.
+     *
+     * @param array $contexts The contexts.
+     * @return array [$sql, $params]
+     */
+    protected static function get_sql_where_from_contexts(array $contexts) {
+        global $DB;
+
+        $sorted = array_reduce($contexts, function ($carry, $context) {
+            $level = $context->contextlevel;
+            if ($level == CONTEXT_MODULE || $level == CONTEXT_COURSE) {
+                $carry[$level][] = $context->instanceid;
+            } else if ($level == CONTEXT_SYSTEM) {
+                $carry[$level] = $context->id;
+            }
+            return $carry;
+        }, [
+            CONTEXT_COURSE => [],
+            CONTEXT_MODULE => [],
+            CONTEXT_SYSTEM => null,
+        ]);
+
+        $sqls = [];
+        $params = [];
+
+        if (!empty($sorted[CONTEXT_MODULE])) {
+            list($insql, $inparams) = $DB->get_in_or_equal($sorted[CONTEXT_MODULE], SQL_PARAMS_NAMED);
+            $sqls[] = "cmid $insql";
+            $params = array_merge($params, $inparams);
+        }
+
+        if (!empty($sorted[CONTEXT_COURSE])) {
+            list($insql, $inparams) = $DB->get_in_or_equal($sorted[CONTEXT_COURSE], SQL_PARAMS_NAMED);
+
+            $sqls[] = "cmid = 0 AND course $insql";
+            $params = array_merge($params, $inparams);
+        }
+
+        if (!empty($sorted[CONTEXT_SYSTEM])) {
+            $sqls[] = "course <= 0";
+        }
+
+        if (empty($sqls)) {
+            return [null, null];
+        }
+
+        return ['((' . implode(') OR (', $sqls) . '))', $params];
+    }
+}
index 1086cc6..5f35555 100644 (file)
@@ -27,4 +27,11 @@ $string['loglegacy'] = 'Log legacy data';
 $string['loglegacy_help'] = 'This plugin records log data to the legacy log table (mdl_log). This functionality has been replaced by newer, richer and more efficient logging plugins, so you should only run this plugin if you have old custom reports that directly query the old log table. Writing to the legacy logs will increase load, so it is recommended that you disable this plugin for performance reasons when it is not needed.';
 $string['pluginname'] = 'Legacy log';
 $string['pluginname_desc'] = 'A log plugin that stores log entries in the legacy log table.';
+$string['privacy:metadata:log'] = 'A collection of past events';
+$string['privacy:metadata:log:action'] = 'A description of the action';
+$string['privacy:metadata:log:info'] = 'Additional information';
+$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
+$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
+$string['privacy:metadata:log:url'] = 'The URL related to the event';
+$string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
 $string['taskcleanup'] = 'Legacy log table cleanup';
diff --git a/admin/tool/log/store/legacy/tests/privacy_test.php b/admin/tool/log/store/legacy/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..a60960a
--- /dev/null
@@ -0,0 +1,344 @@
+<?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    logstore_legacy
+ * @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 logstore_legacy\privacy\provider;
+use logstore_legacy\event\unittest_executed;
+
+require_once(__DIR__ . '/fixtures/event.php');
+
+/**
+ * Data provider testcase class.
+ *
+ * @package    logstore_legacy
+ * @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 logstore_legacy_privacy_testcase extends provider_testcase {
+
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    public function test_get_contexts_for_userid() {
+        $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]);
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $cm1ctx = context_module::instance($cm1->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 = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 1]]);
+        $e->trigger();
+        $this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$cm1ctx]);
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $this->assert_contextlist_equals($this->get_contextlist_for_user($u2), []);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 2]]);
+        $e->trigger();
+        $this->assert_contextlist_equals($this->get_contextlist_for_user($u2), [$cm1ctx]);
+
+        // User 3 is the author.
+        $this->setUser($u3);
+        $this->assert_contextlist_equals($this->get_contextlist_for_user($u3), []);
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 3]]);
+        $e->trigger();
+        $this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx]);
+    }
+
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+        $cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]);
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+        $cm1ctx = context_module::instance($cm1->cmid);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        // User 1 is the author.
+        $this->setUser($u1);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 1]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 2]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 3]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 4]]);
+        $e->trigger();
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 5]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 6]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 7]]);
+        $e->trigger();
+
+        // Assert what we have.
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => 0]));
+        $this->assertEquals(4, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete other context.
+        provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_legacy', [$c2ctx->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => 0]));
+        $this->assertEquals(4, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete system.
+        provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_legacy', [$sysctx->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => 0]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete course.
+        provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_legacy', [$c1ctx->id]));
+        $this->assertTrue($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => 0]));
+        $this->assertEquals(2, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete course.
+        provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_legacy', [$cm1ctx->id]));
+        $this->assertFalse($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['userid' => $u1->id, 'cmid' => 0, 'course' => 0]));
+        $this->assertEquals(0, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(3, $DB->count_records('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();
+        $u3 = $this->getDataGenerator()->create_user();
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+        $cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]);
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+        $cm1ctx = context_module::instance($cm1->cmid);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+
+        // User 1 is the author.
+        $this->setUser($u1);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 1]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 2]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 3]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 4]]);
+        $e->trigger();
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 5]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 6]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 7]]);
+        $e->trigger();
+
+        // Assert what we have.
+        $this->assertTrue($DB->record_exists('log', ['cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['cmid' => 0, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['cmid' => 0, 'course' => 0]));
+        $this->assertEquals(4, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete other context.
+        provider::delete_data_for_all_users_in_context($c2ctx);
+        $this->assertTrue($DB->record_exists('log', ['cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['cmid' => 0, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['cmid' => 0, 'course' => 0]));
+        $this->assertEquals(4, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete system.
+        provider::delete_data_for_all_users_in_context($sysctx);
+        $this->assertTrue($DB->record_exists('log', ['cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertTrue($DB->record_exists('log', ['cmid' => 0, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['cmid' => 0, 'course' => 0]));
+        $this->assertEquals(3, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(2, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete course.
+        provider::delete_data_for_all_users_in_context($c1ctx);
+        $this->assertTrue($DB->record_exists('log', ['cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['cmid' => 0, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['cmid' => 0, 'course' => 0]));
+        $this->assertEquals(2, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(1, $DB->count_records('log', ['userid' => $u2->id]));
+
+        // Delete course.
+        provider::delete_data_for_all_users_in_context($cm1ctx);
+        $this->assertFalse($DB->record_exists('log', ['cmid' => $cm1->cmid, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['cmid' => 0, 'course' => $c1->id]));
+        $this->assertFalse($DB->record_exists('log', ['cmid' => 0, 'course' => 0]));
+        $this->assertEquals(0, $DB->count_records('log', ['userid' => $u1->id]));
+        $this->assertEquals(0, $DB->count_records('log', ['userid' => $u2->id]));
+    }
+
+    public function test_export_data_for_user() {
+        global $DB;
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $u2 = $this->getDataGenerator()->create_user();
+        $u3 = $this->getDataGenerator()->create_user();
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+        $cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]);
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+        $cm1ctx = context_module::instance($cm1->cmid);
+
+        $this->enable_logging();
+        $manager = get_log_manager(true);
+        $path = [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_legacy')];
+
+        // User 1 is the author.
+        $this->setUser($u1);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 1]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 2]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 3]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 4]]);
+        $e->trigger();
+
+        // User 2 is the author.
+        $this->setUser($u2);
+        $e = unittest_executed::create(['context' => $cm1ctx, 'other' => ['sample' => 5]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $c1ctx, 'other' => ['sample' => 6]]);
+        $e->trigger();
+        $e = unittest_executed::create(['context' => $sysctx, 'other' => ['sample' => 7]]);
+        $e->trigger();
+
+        // Test export.
+        provider::export_user_data(new approved_contextlist($u1, 'logstore_legacy', [$cm1ctx->id]));
+        $data = writer::with_context($c1ctx)->get_data($path);
+        $this->assertEmpty($data);
+        $data = writer::with_context($cm1ctx)->get_data($path);
+        $this->assertCount(2, $data->logs);
+
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u1, 'logstore_legacy', [$c1ctx->id]));
+        $data = writer::with_context($cm1ctx)->get_data($path);
+        $this->assertEmpty($data);
+        $data = writer::with_context($c1ctx)->get_data($path);
+        $this->assertCount(1, $data->logs);
+
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u1, 'logstore_legacy', [$sysctx->id]));
+        $data = writer::with_context($sysctx)->get_data($path);
+        $this->assertCount(1, $data->logs);
+    }
+
+    /**
+     * 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_legacy', 'tool_log');
+        set_config('loglegacy', 1, 'logstore_legacy');
+        get_log_manager(true);
+    }
+
+    /**
+     * 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;
+    }
+}
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 (file)
index 0000000..1994e29
--- /dev/null
@@ -0,0 +1,109 @@
+<?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    logstore_standard
+ * @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 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 <fred@branchup.tech>
+ * @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')];
+    }
+}
index 2e2c2a7..33c229f 100644 (file)
 $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 (file)
index 0000000..5103b3a
--- /dev/null
@@ -0,0 +1,373 @@
+<?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    logstore_standard
+ * @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 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 <fred@branchup.tech>
+ * @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;
+    }
+}
diff --git a/admin/tool/log/tests/privacy_test.php b/admin/tool/log/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..9e5be71
--- /dev/null
@@ -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');
+    }
+}