Merge branch 'MDL-62202-master' of git://github.com/sarjona/moodle
authorJun Pataleta <jun@moodle.com>
Thu, 3 May 2018 05:31:53 +0000 (13:31 +0800)
committerJun Pataleta <jun@moodle.com>
Thu, 3 May 2018 05:31:53 +0000 (13:31 +0800)
92 files changed:
admin/registration/index.php
admin/registration/renderer.php [deleted file]
admin/renderer.php
admin/settings/development.php
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]
admin/tool/messageinbound/classes/manager.php
admin/tool/messageinbound/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/messageinbound/classes/task/cleanup_task.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/messageinbound/tests/manager_test.php [new file with mode: 0644]
admin/tool/messageinbound/tests/privacy_test.php [new file with mode: 0644]
admin/tool/profiling/settings.php
auth/classes/privacy/provider.php [new file with mode: 0644]
auth/tests/privacy_test.php [new file with mode: 0644]
availability/classes/privacy/provider.php [new file with mode: 0644]
cache/classes/privacy/provider.php [new file with mode: 0644]
cache/locks/file/classes/privacy/provider.php [new file with mode: 0644]
cache/locks/file/lang/en/cachelock_file.php
cache/stores/apcu/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/apcu/lang/en/cachestore_apcu.php
cache/stores/file/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/file/lang/en/cachestore_file.php
cache/stores/memcache/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/memcache/lang/en/cachestore_memcache.php
cache/stores/memcached/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/memcached/lang/en/cachestore_memcached.php
cache/stores/mongodb/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/mongodb/lang/en/cachestore_mongodb.php
cache/stores/redis/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/redis/lang/en/cachestore_redis.php
cache/stores/session/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/session/lang/en/cachestore_session.php
cache/stores/static/classes/privacy/provider.php [new file with mode: 0644]
cache/stores/static/lang/en/cachestore_static.php
course/publish/backup.php
course/publish/index.php
course/publish/metadata.php
course/publish/renderer.php [deleted file]
course/renderer.php
grade/classes/privacy/provider.php [new file with mode: 0644]
grade/tests/privacy_test.php [new file with mode: 0644]
lang/en/auth.php
lang/en/availability.php
lang/en/cache.php
lang/en/grades.php
lang/en/webservice.php
lib/upgrade.txt
lib/xhprof/xhprof_moodle.php
message/templates/message_area_message.mustache
mod/lti/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/lang/en/lti.php
mod/lti/service/gradebookservices/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php
mod/lti/service/gradebookservices/tests/privacy_provider_test.php [new file with mode: 0644]
mod/lti/service/memberships/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/service/memberships/lang/en/ltiservice_memberships.php
mod/lti/service/memberships/tests/privacy_provider_test.php [new file with mode: 0644]
mod/lti/service/profile/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/service/profile/lang/en/ltiservice_profile.php
mod/lti/service/toolproxy/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/service/toolproxy/lang/en/ltiservice_toolproxy.php
mod/lti/service/toolsettings/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php
mod/lti/tests/privacy_provider_test.php [new file with mode: 0644]
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/locallib.php
mod/quiz/tests/locallib_test.php
mod/quiz/tests/structure_test.php
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/modules.scss
theme/boost/templates/core/custom_menu_item.mustache
theme/boost/templates/core/filemanager_modal_generallayout.mustache
theme/boost/tests/behat/behat_theme_boost_behat_repository_upload.php
webservice/classes/privacy/provider.php [new file with mode: 0644]
webservice/tests/privacy_test.php [new file with mode: 0644]

index dee1ba0..57d65a3 100644 (file)
@@ -110,7 +110,7 @@ if (\core\hub\registration::is_registered()) {
     echo $OUTPUT->heading(get_string('registerwithmoodleorg', 'admin'));
 }
 
-$renderer = $PAGE->get_renderer('core', 'register');
+$renderer = $PAGE->get_renderer('core', 'admin');
 echo $renderer->moodleorg_registration_message();
 
 $siteregistrationform->display();
diff --git a/admin/registration/renderer.php b/admin/registration/renderer.php
deleted file mode 100644 (file)
index b7414b4..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-///////////////////////////////////////////////////////////////////////////
-//                                                                       //
-// This file is part of Moodle - http://moodle.org/                      //
-// Moodle - Modular Object-Oriented Dynamic Learning Environment         //
-//                                                                       //
-// 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/>.       //
-//                                                                       //
-///////////////////////////////////////////////////////////////////////////
-
-/**
- * Registration renderer.
- * @package   moodle
- * @subpackage registration
- * @copyright 2010 Moodle Pty Ltd (http://moodle.com)
- * @author    Jerome Mouneyrac
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class core_register_renderer extends plugin_renderer_base {
-
-    /**
-     * Display message about the benefits of registering on Moodle.org
-     *
-     * @return string
-     */
-    public function moodleorg_registration_message() {
-        return format_text(get_string('registermoodlenet', 'admin'), FORMAT_HTML, ['noclean' => true]);
-    }
-}
index dbd77f6..f22c6a2 100644 (file)
@@ -2084,4 +2084,13 @@ class core_admin_renderer extends plugin_renderer_base {
             return $this->warning(get_string('legacylogginginuse'));
         }
     }
+
+    /**
+     * Display message about the benefits of registering on Moodle.org
+     *
+     * @return string
+     */
+    public function moodleorg_registration_message() {
+        return format_text(get_string('registermoodlenet', 'admin'), FORMAT_HTML, ['noclean' => true]);
+    }
 }
index 41d779f..0d42707 100644 (file)
@@ -30,7 +30,9 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $ADMIN->add('development', $temp);
 
     // "Profiling" settingpage (conditionally if the 'xhprof' extension is available only).
-    $xhprofenabled = extension_loaded('xhprof') || extension_loaded('tideways');
+    $xhprofenabled = extension_loaded('tideways_xhprof');
+    $xhprofenabled = $xhprofenabled || extension_loaded('tideways');
+    $xhprofenabled = $xhprofenabled || extension_loaded('xhprof');
     $temp = new admin_settingpage('profiling', new lang_string('profiling', 'admin'), 'moodle/site:config', !$xhprofenabled);
     // Main profiling switch.
     $temp->add(new admin_setting_configcheckbox('profilingenabled', new lang_string('profilingenabled', 'admin'), new lang_string('profilingenabled_help', 'admin'), false));
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..af91b8b
--- /dev/null
@@ -0,0 +1,110 @@
+<?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::class;
+        \core_privacy\manager::plugintype_class_callback('logstore', $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');
+    }
+}
index c8ef59f..6596aad 100644 (file)
@@ -310,6 +310,16 @@ class manager {
         return true;
     }
 
+    /**
+     * Remove older verification failures.
+     *
+     * @return void
+     */
+    public function tidy_old_verification_failures() {
+        global $DB;
+        $DB->delete_records_select('messageinbound_messagelist', 'timecreated < :time', ['time' => time() - DAYSECS]);
+    }
+
     /**
      * Process a message and pass it through the Inbound Message handling systems.
      *
diff --git a/admin/tool/messageinbound/classes/privacy/provider.php b/admin/tool/messageinbound/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..53be916
--- /dev/null
@@ -0,0 +1,173 @@
+<?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_messageinbound
+ * @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_messageinbound\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use context_user;
+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;
+
+/**
+ * Data provider class.
+ *
+ * @package    tool_messageinbound
+ * @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\plugin\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_database_table('messageinbound_messagelist', [
+            'messageid' => 'privacy:metadata:messagelist:messageid',
+            'userid' => 'privacy:metadata:messagelist:userid',
+            'address' => 'privacy:metadata:messagelist:address',
+            'timecreated' => 'privacy:metadata:messagelist:timecreated',
+        ], 'privacy:metadata:messagelist');
+
+        // Arguably the keys are handled by \core\message\inbound\address_manager and thus could/should be handled by core.
+        $collection->add_subsystem_link('core_userkey', [], 'privacy:metadata:coreuserkey');
+
+        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();
+
+        // Always add the user context so we're sure we're not dodging user keys, besides it's not costly to do so.
+        $contextlist->add_user_context($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) {
+        global $DB;
+        if (!static::approved_contextlist_contains_my_context($contextlist)) {
+            // We only care about the user's user context.
+            return;
+        }
+
+        $userid = $contextlist->get_user()->id;
+        $context = context_user::instance($userid);
+        $path = [get_string('messageinbound', 'tool_messageinbound')];
+
+        // Export user keys.
+        \core_userkey\privacy\provider::export_userkeys($context, $path, 'messageinbound_handler');
+
+        // Export the message list.
+        $data = [];
+        $recordset = $DB->get_recordset('messageinbound_messagelist', ['userid' => $userid], 'timecreated, id');
+        foreach ($recordset as $record) {
+            $data[] = [
+                'received_at' => $record->address,
+                'timecreated' => transform::datetime($record->timecreated),
+            ];
+        }
+        $recordset->close();
+        writer::with_context($context)->export_data($path, (object) ['messages_pending_validation' => $data]);
+    }
+
+    /**
+     * 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;
+        if ($context->contextlevel != CONTEXT_USER) {
+            return;
+        }
+
+        static::delete_user_data($context->instanceid);
+    }
+
+    /**
+     * 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;
+        if (!static::approved_contextlist_contains_my_context($contextlist)) {
+            // We only care about the user's user context.
+            return;
+        }
+
+        static::delete_user_data($contextlist->get_user()->id);
+    }
+
+    /**
+     * Delete a user's data.
+     *
+     * @param int $userid The user ID.
+     * @return void
+     */
+    protected static function delete_user_data($userid) {
+        global $DB;
+        $DB->delete_records_select('messageinbound_messagelist', 'userid = :userid', ['userid' => $userid]);
+        \core_userkey\privacy\provider::delete_userkeys('messageinbound_handler', $userid);
+    }
+
+    /**
+     * Return whether the contextlist contains our own context.
+     *
+     * @param approved_contextlist $contextlist The contextlist
+     * @return bool
+     */
+    protected static function approved_contextlist_contains_my_context(approved_contextlist $contextlist) {
+        $userid = $contextlist->get_user()->id;
+        foreach ($contextlist->get_contexts() as $context) {
+            if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
index e8b1c7a..fc2c01d 100644 (file)
@@ -49,6 +49,7 @@ class cleanup_task extends \core\task\scheduled_task {
      */
     public function execute() {
         $manager = new \tool_messageinbound\manager();
-        return $manager->tidy_old_messages();
+        $manager->tidy_old_messages();
+        $manager->tidy_old_verification_failures();
     }
 }
index b074060..c847b03 100644 (file)
@@ -94,6 +94,12 @@ $string['onehour'] = 'One hour';
 $string['oneweek'] = 'One week';
 $string['oneyear'] = 'One year';
 $string['pluginname'] = 'Inbound message configuration';
+$string['privacy:metadata:coreuserkey'] = 'User\'s keys to validate the email received';
+$string['privacy:metadata:messagelist'] = 'A list of message identifiers which failed validation and requires further authorisation';
+$string['privacy:metadata:messagelist:address'] = 'The address at which the email was sent';
+$string['privacy:metadata:messagelist:messageid'] = 'The message ID';
+$string['privacy:metadata:messagelist:timecreated'] = 'The time at which the record was made';
+$string['privacy:metadata:messagelist:userid'] = 'The ID of user who need to approve the message';
 $string['replysubjectprefix'] = 'Re:';
 $string['requirevalidation'] = 'Validate sender address';
 $string['name'] = 'Name';
diff --git a/admin/tool/messageinbound/tests/manager_test.php b/admin/tool/messageinbound/tests/manager_test.php
new file mode 100644 (file)
index 0000000..b88a742
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Manager tests.
+ *
+ * @package    tool_messageinbound
+ * @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\approved_contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use tool_messageinbound\privacy\provider;
+
+/**
+ * Manager testcase class.
+ *
+ * @package    tool_messageinbound
+ * @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_messageinbound_manager_testcase extends provider_testcase {
+
+    public function setUp() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // Pretend the system is enabled.
+        $CFG->messageinbound_enabled = true;
+        $CFG->messageinbound_mailbox = 'mailbox';
+        $CFG->messageinbound_domain = 'example.com';
+    }
+
+    public function test_tidy_old_verification_failures() {
+        global $DB;
+
+        $now = time();
+        $stale = $now - DAYSECS - 1;    // Make a second older because PHP Unit is too damn fast!!
+
+        $this->create_messagelist(['timecreated' => $now]);
+        $this->create_messagelist(['timecreated' => $now - HOURSECS]);
+        $this->create_messagelist(['timecreated' => $stale]);
+        $this->create_messagelist(['timecreated' => $stale - HOURSECS]);
+        $this->create_messagelist(['timecreated' => $stale - YEARSECS]);
+
+        $this->assertEquals(5, $DB->count_records('messageinbound_messagelist', []));
+        $this->assertEquals(3, $DB->count_records_select('messageinbound_messagelist', 'timecreated < :t', ['t' => $stale + 1]));
+
+        $manager = new \tool_messageinbound\manager();
+        $manager->tidy_old_verification_failures();
+
+        $this->assertEquals(2, $DB->count_records('messageinbound_messagelist', []));
+        $this->assertEquals(0, $DB->count_records_select('messageinbound_messagelist', 'timecreated < :t', ['t' => $stale + 1]));
+    }
+
+    /**
+     * Create a message to validate.
+     *
+     * @param array $params The params.
+     * @return stdClass
+     */
+    protected function create_messagelist(array $params) {
+        global $DB, $USER;
+        $record = (object) array_merge([
+            'messageid' => 'abc',
+            'userid' => $USER->id,
+            'address' => 'text@example.com',
+            'timecreated' => time(),
+        ], $params);
+        $record->id = $DB->insert_record('messageinbound_messagelist', $record);
+        return $record;
+    }
+
+}
diff --git a/admin/tool/messageinbound/tests/privacy_test.php b/admin/tool/messageinbound/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..5be172c
--- /dev/null
@@ -0,0 +1,201 @@
+<?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_messageinbound
+ * @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\approved_contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use tool_messageinbound\privacy\provider;
+
+/**
+ * Data provider testcase class.
+ *
+ * @package    tool_messageinbound
+ * @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_messageinbound_privacy_testcase extends provider_testcase {
+
+    public function setUp() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // Pretend the system is enabled.
+        $CFG->messageinbound_enabled = true;
+        $CFG->messageinbound_mailbox = 'mailbox';
+        $CFG->messageinbound_domain = 'example.com';
+    }
+
+    public function test_get_contexts_for_userid() {
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $contexts = provider::get_contexts_for_userid($u1->id)->get_contexts();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u1ctx->id, $contexts[0]->id);
+
+        $contexts = provider::get_contexts_for_userid($u2->id)->get_contexts();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u2ctx->id, $contexts[0]->id);
+    }
+
+    public function test_delete_data_for_user() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $addressmanager = new \core\message\inbound\address_manager();
+        $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
+        $addressmanager->set_data(123);
+
+        // Create a user key for both users.
+        $addressmanager->generate($u1->id);
+        $addressmanager->generate($u2->id);
+
+        // Create a messagelist for both users.
+        $this->create_messagelist(['userid' => $u1->id]);
+        $this->create_messagelist(['userid' => $u2->id]);
+
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
+
+        // Passing another user's context does not do anything.
+        provider::delete_data_for_user(new approved_contextlist($u1, 'tool_messageinbound', [$u2ctx->id]));
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
+
+        // Deleting user 1.
+        provider::delete_data_for_user(new approved_contextlist($u1, 'tool_messageinbound', [$u1ctx->id]));
+        $this->assertFalse($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
+        $this->assertFalse($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
+    }
+
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $addressmanager = new \core\message\inbound\address_manager();
+        $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
+        $addressmanager->set_data(123);
+
+        // Create a user key for both users.
+        $addressmanager->generate($u1->id);
+        $addressmanager->generate($u2->id);
+
+        // Create a messagelist for both users.
+        $this->create_messagelist(['userid' => $u1->id]);
+        $this->create_messagelist(['userid' => $u2->id]);
+
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
+
+        // Deleting user 1.
+        provider::delete_data_for_all_users_in_context($u1ctx);
+        $this->assertFalse($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
+        $this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
+        $this->assertFalse($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
+    }
+
+    public function test_export_data_for_user() {
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $addressmanager = new \core\message\inbound\address_manager();
+        $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
+        $addressmanager->set_data(123);
+
+        // Create a user key for both users.
+        $addressmanager->generate($u1->id);
+        $addressmanager->generate($u2->id);
+
+        // Create a messagelist for both users.
+        $this->create_messagelist(['userid' => $u1->id, 'address' => 'u1@example1.com']);
+        $this->create_messagelist(['userid' => $u1->id, 'address' => 'u1@example2.com']);
+        $this->create_messagelist(['userid' => $u2->id, 'address' => 'u2@example1.com']);
+
+        // Export for user.
+        $this->setUser($u1);
+        provider::export_user_data(new approved_contextlist($u1, 'tool_messageinbound', [$u1ctx->id, $u2ctx->id]));
+        $data = writer::with_context($u2ctx)->get_data([get_string('messageinbound', 'tool_messageinbound')]);
+        $this->assertEmpty($data);
+        $data = writer::with_context($u1ctx)->get_data([get_string('messageinbound', 'tool_messageinbound')]);
+        $this->assertCount(2, $data->messages_pending_validation);
+        $this->assertEquals('u1@example1.com', $data->messages_pending_validation[0]['received_at']);
+        $this->assertEquals('u1@example2.com', $data->messages_pending_validation[1]['received_at']);
+
+        $data = writer::with_context($u2ctx)->get_related_data([get_string('messageinbound', 'tool_messageinbound')], 'userkeys');
+        $this->assertEmpty($data);
+        $data = writer::with_context($u1ctx)->get_related_data([get_string('messageinbound', 'tool_messageinbound')], 'userkeys');
+        $this->assertCount(1, $data->keys);
+        $this->assertEquals('messageinbound_handler', $data->keys[0]->script);
+    }
+
+    /**
+     * Create a message to validate.
+     *
+     * @param array $params The params.
+     * @return stdClass
+     */
+    protected function create_messagelist(array $params) {
+        global $DB, $USER;
+        $record = (object) array_merge([
+            'messageid' => 'abc',
+            'userid' => $USER->id,
+            'address' => 'text@example.com',
+            'timecreated' => time(),
+        ], $params);
+        $record->id = $DB->insert_record('messageinbound_messagelist', $record);
+        return $record;
+    }
+
+}
index c04d32b..2c9840b 100644 (file)
 
 defined('MOODLE_INTERNAL') || die;
 
-// profiling tool, added to development
-if ((extension_loaded('xhprof') || extension_loaded('tideways')) && (!empty($CFG->profilingenabled) || !empty($CFG->earlyprofilingenabled))) {
+// Profiling tool, added to development.
+$hasextension = extension_loaded('tideways_xhprof');
+$hasextension = $hasextension || extension_loaded('tideways');
+$hasextension = $hasextension || extension_loaded('xhprof');
+$isenabled = !empty($CFG->profilingenabled) || !empty($CFG->earlyprofilingenabled);
+if ($hasextension && $isenabled) {
     $ADMIN->add('development', new admin_externalpage('toolprofiling', get_string('pluginname', 'tool_profiling'),
             "$CFG->wwwroot/$CFG->admin/tool/profiling/index.php", 'moodle/site:config'));
 }
diff --git a/auth/classes/privacy/provider.php b/auth/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..1c61c57
--- /dev/null
@@ -0,0 +1,103 @@
+<?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    core_auth
+ * @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 core_auth\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+
+/**
+ * Data provider class.
+ *
+ * @package    core_auth
+ * @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\user_preference_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_user_preference('auth_forcepasswordchange', 'privacy:metadata:userpref:forcepasswordchange');
+        $collection->add_user_preference('create_password', 'privacy:metadata:userpref:createpassword');
+        $collection->add_user_preference('login_failed_count', 'privacy:metadata:userpref:loginfailedcount');
+        $collection->add_user_preference('login_failed_count_since_success',
+            'privacy:metadata:userpref:loginfailedcountsincesuccess');
+        $collection->add_user_preference('login_failed_last', 'privacy:metadata:userpref:loginfailedlast');
+        $collection->add_user_preference('login_lockout', 'privacy:metadata:userpref:loginlockout');
+        $collection->add_user_preference('login_lockout_ignored', 'privacy:metadata:userpref:loginlockoutignored');
+        $collection->add_user_preference('login_lockout_secret', 'privacy:metadata:userpref:loginlockoutsecret');
+
+        return $collection;
+    }
+
+    /**
+     * Export all user preferences for the plugin.
+     *
+     * @param int $userid The userid of the user whose data is to be exported.
+     */
+    public static function export_user_preferences(int $userid) {
+
+        $yesno = function($v) {
+            return transform::yesno($v);
+        };
+        $datetime = function($v) {
+            return $v ? transform::datetime($v) : null;
+        };
+
+        $prefs = [
+            ['auth_forcepasswordchange', 'forcepasswordchange', $yesno],
+            ['create_password', 'createpassword', $yesno],
+            ['login_failed_count', 'loginfailedcount', null],
+            ['login_failed_count_since_success', 'loginfailedcountsincesuccess', null],
+            ['login_failed_last', 'loginfailedlast', $datetime],
+            ['login_lockout', 'loginlockout', $datetime],
+            ['login_lockout_ignored', 'loginlockoutignored', $yesno],
+            ['login_lockout_secret', 'loginlockoutsecret', null],
+        ];
+
+        foreach ($prefs as $prefdata) {
+            list($prefname, $langkey, $transformer) = $prefdata;
+            $value = get_user_preferences($prefname, null, $userid);
+            if ($value === null) {
+                continue;
+            }
+            writer::export_user_preference('core_auth', $prefname, $transformer ? $transformer($value) : $value,
+                get_string("privacy:metadata:userpref:{$langkey}", 'core_auth'));
+        }
+    }
+
+}
diff --git a/auth/tests/privacy_test.php b/auth/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..16701f2
--- /dev/null
@@ -0,0 +1,103 @@
+<?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    core_auth
+ * @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\transform;
+use core_privacy\local\request\writer;
+use core_auth\privacy\provider;
+
+/**
+ * Data provider testcase class.
+ *
+ * @package    core_auth
+ * @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 core_auth_privacy_testcase extends provider_testcase {
+
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    public function test_export_user_preferences() {
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $sysctx = context_system::instance();
+        $now = time();
+
+        // Check nothing is there.
+        writer::reset();
+        provider::export_user_preferences($u1->id);
+        $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth');
+        $this->assertEmpty((array) $prefs);
+
+        // Set some preferences.
+        set_user_preference('auth_forcepasswordchange', 1, $u1);
+        set_user_preference('create_password', 1, $u1);
+        set_user_preference('login_failed_count', 18, $u1);
+        set_user_preference('login_failed_count_since_success', 7, $u1);
+        set_user_preference('login_failed_last', $now - DAYSECS, $u1);
+        set_user_preference('login_lockout', $now - HOURSECS, $u1);
+        set_user_preference('login_lockout_ignored', 0, $u1);
+        set_user_preference('login_lockout_secret', 'Hello world!', $u1);
+
+        set_user_preference('auth_forcepasswordchange', 0, $u2);
+        set_user_preference('create_password', 0, $u2);
+        set_user_preference('login_lockout_ignored', 1, $u2);
+
+        // Check user 1.
+        writer::reset();
+        provider::export_user_preferences($u1->id);
+        $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth');
+        $this->assertEquals(transform::yesno(true), $prefs->auth_forcepasswordchange->value);
+        $this->assertEquals(transform::yesno(true), $prefs->create_password->value);
+        $this->assertEquals(18, $prefs->login_failed_count->value);
+        $this->assertEquals(7, $prefs->login_failed_count_since_success->value);
+        $this->assertEquals(transform::datetime($now - DAYSECS), $prefs->login_failed_last->value);
+        $this->assertEquals(transform::datetime($now - HOURSECS), $prefs->login_lockout->value);
+        $this->assertEquals(transform::yesno(false), $prefs->login_lockout_ignored->value);
+        $this->assertEquals('Hello world!', $prefs->login_lockout_secret->value);
+
+        // Check user 2.
+        writer::reset();
+        provider::export_user_preferences($u2->id);
+        $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth');
+        $this->assertEquals(transform::yesno(false), $prefs->auth_forcepasswordchange->value);
+        $this->assertEquals(transform::yesno(false), $prefs->create_password->value);
+        $this->assertObjectNotHasAttribute('login_failed_count', $prefs);
+        $this->assertObjectNotHasAttribute('login_failed_count_since_success', $prefs);
+        $this->assertObjectNotHasAttribute('login_failed_last', $prefs);
+        $this->assertObjectNotHasAttribute('login_lockout', $prefs);
+        $this->assertEquals(transform::yesno(true), $prefs->login_lockout_ignored->value);
+        $this->assertObjectNotHasAttribute('login_lockout_secret', $prefs);
+    }
+}
diff --git a/availability/classes/privacy/provider.php b/availability/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..bb8a3a7
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for core_availability.
+ *
+ * @package    core_availability
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_availability\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for core_availability implementing null_provider.
+ *
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
diff --git a/cache/classes/privacy/provider.php b/cache/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..20cc83f
--- /dev/null
@@ -0,0 +1,66 @@
+<?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 Subsystem implementation for core_cache.
+ *
+ * @package    core_cache
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_cache\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\metadata\collection;
+
+/**
+ * Privacy Subsystem implementation for core_cache.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+        // Caches store data.
+        \core_privacy\local\metadata\provider,
+
+        // The cache subsystem stores data on behalf of other components.
+        \core_privacy\local\request\subsystem\plugin_provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * Note, although this plugin does store user data, it is not able to
+     * identify it, and that user data is typically very short lived.
+     *
+     * Therefore it is not realistically possible to export any of this
+     * data as it is only identifiable by the plugin storing it, and that
+     * plugin should already be exporting the data as part of it's own
+     * implementation.
+     *
+     * @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 {
+        // Data is stored in cache stores.
+        $collection->add_plugintype_link('cachestore', [], 'privacy:metadata:cachestore');
+
+        // Cache locks do not store any personal user data.
+
+        return $collection;
+    }
+}
diff --git a/cache/locks/file/classes/privacy/provider.php b/cache/locks/file/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..a125cc5
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for cachelock_file.
+ *
+ * @package    cachelock_file
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachelock_file\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachelock_file implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index b57188c..fa5c6df 100644 (file)
@@ -23,4 +23,5 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['pluginname'] = 'File locking';
\ No newline at end of file
+$string['pluginname'] = 'File locking';
+$string['privacy:metadata'] = 'The File locking plugin does not store any personal data.';
diff --git a/cache/stores/apcu/classes/privacy/provider.php b/cache/stores/apcu/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..ce0998f
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for cachestore_apcu.
+ *
+ * @package    cachestore_apcu
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_apcu\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_apcu implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index 9bdf82d..b9ee000 100644 (file)
@@ -30,5 +30,6 @@ $string['prefix'] = 'Prefix';
 $string['prefix_help'] = 'The above prefix gets used for all keys being stored in this APC store instance. By default the database prefix is used.';
 $string['prefixinvalid'] = 'The prefix you have selected is invalid. You can only use a-z A-Z 0-9-_.';
 $string['prefixnotunique'] = 'The prefix you have selected is not unique. Please choose a unique prefix.';
+$string['privacy:metadata'] = 'The APC user cache (APCu) plugin stores data briefly as part of its caching functionality but this data is regularly cleared and is not sent externally in any way.';
 $string['testperformance'] = 'Test performance';
 $string['testperformance_desc'] = 'If enabled, APCu performance will be included when viewing the Test performance page. Enabling this on a production site is not recommended.';
diff --git a/cache/stores/file/classes/privacy/provider.php b/cache/stores/file/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..ab08c8e
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for cachestore_file.
+ *
+ * @package    cachestore_file
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_file\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_file implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index f0097c2..2759410 100644 (file)
@@ -33,6 +33,7 @@ $string['autocreate_help'] = 'If enabled the directory specified in path will be
 $string['path'] = 'Cache path';
 $string['path_help'] = 'The directory that should be used to store files for this cache store. If left blank (default) a directory will be automatically created in the moodledata directory. This can be used to point a file store towards a directory on a better performing drive (such as one in memory).';
 $string['pluginname'] = 'File cache';
+$string['privacy:metadata'] = 'The File cache cachestore plugin stores data briefly as part of its caching functionality but this data is regularly cleared.';
 $string['prescan'] = 'Prescan directory';
 $string['prescan_help'] = 'If enabled the directory is scanned when the cache is first used and requests for files are first checked against the scan data. This can help if you have a slow file system and are finding that file operations are causing you a bottle neck.';
 $string['singledirectory'] = 'Single directory store';
@@ -49,4 +50,4 @@ It is advisable to only turn this on if the following is true:<br />
  *     directory structure is inadvisable.
  *   - the things we are caching are more expensive to calculate, so the extra
  *     time to computer a better hash is a worthwhile trade-off.
- */
\ No newline at end of file
+ */
diff --git a/cache/stores/memcache/classes/privacy/provider.php b/cache/stores/memcache/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..e7e3779
--- /dev/null
@@ -0,0 +1,87 @@
+<?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 Subsystem implementation for cachestore_memcache.
+ *
+ * @package    cachestore_memcache
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_memcache\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_memcache.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @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('memcache', [
+                'data' => 'privacy:metadata:memcache:data',
+            ], 'privacy:metadata:memcache');
+        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) : contextlist {
+        return new 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) {
+    }
+
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param \context $context A user context.
+     */
+    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.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
index 62ef3fe..9095825 100644 (file)
@@ -39,6 +39,8 @@ $string['prefix_help'] = 'This prefix is used for all key names on the memcache
 * If you only have one Moodle instance using this server, you can leave this value default.
 * Due to key length restrictions, a maximum of 5 characters is permitted.';
 $string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
+$string['privacy:metadata:memcache'] = 'The Memcache cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Memcache server where data is regularly removed.';
+$string['privacy:metadata:memcache:data'] = 'The various data stored in the cache';
 $string['servers'] = 'Servers';
 $string['servers_help'] = 'This sets the servers that should be utilised by this memcache adapter.
 Servers should be defined one per line and consist of a server address and optionally a port and weight.
diff --git a/cache/stores/memcached/classes/privacy/provider.php b/cache/stores/memcached/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..932e2ca
--- /dev/null
@@ -0,0 +1,87 @@
+<?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 Subsystem implementation for cachestore_memcached.
+ *
+ * @package    cachestore_memcached
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_memcached\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_memcached.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @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('memcached', [
+                'data' => 'privacy:metadata:memcached:data',
+            ], 'privacy:metadata:memcached');
+        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) : contextlist {
+        return new 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) {
+    }
+
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param \context $context A user context.
+     */
+    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.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
index fcfb2dc..e697808 100644 (file)
@@ -54,6 +54,8 @@ If the cache is shared by other applications then each key will be deleted indiv
 If you are running a dedicated cache for this application then the entire cache can safely be flushed without any risk of destroying another application's cache data. This should result in increased performance when purging the cache.
 ";
 $string['pluginname'] = 'Memcached';
+$string['privacy:metadata:memcached'] = 'The Memcached cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Memcache server where data is regularly removed.';
+$string['privacy:metadata:memcached:data'] = 'The various data stored in the cache';
 $string['prefix'] = 'Prefix key';
 $string['prefix_help'] = 'This can be used to create a "domain" for your item keys allowing you to create multiple memcached stores on a single memcached installation. It cannot be longer than 16 characters in order to ensure key length issues are not encountered.';
 $string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
diff --git a/cache/stores/mongodb/classes/privacy/provider.php b/cache/stores/mongodb/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..c16d200
--- /dev/null
@@ -0,0 +1,87 @@
+<?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 Subsystem implementation for cachestore_mongodb.
+ *
+ * @package    cachestore_mongodb
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_mongodb\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_mongodb.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @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('mongodb', [
+                'data' => 'privacy:metadata:mongodb:data',
+            ], 'privacy:metadata:mongodb');
+        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) : contextlist {
+        return new 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) {
+    }
+
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param \context $context A user context.
+     */
+    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.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
index 90f9912..d0ca148 100644 (file)
@@ -30,6 +30,8 @@ $string['password'] = 'Password';
 $string['password_help'] = 'The password of the user being used for the connection.';
 $string['pleaseupgrademongo'] = 'You are using an old version of the PHP Mongo extension (< 1.3). Support for old versions of the Mongo extension will be dropped in the future. Please consider upgrading.';
 $string['pluginname'] = 'MongoDB';
+$string['privacy:metadata:mongodb'] = 'The MongoDB cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an MongoDB server where data is regularly removed.';
+$string['privacy:metadata:mongodb:data'] = 'The various data stored in the cache';
 $string['replicaset'] = 'Replica set';
 $string['replicaset_help'] = 'The name of the replica set to connect to. If this is given the master will be determined by using the ismaster database command on the seeds, so the driver may end up connecting to a server that was not even listed.';
 $string['server'] = 'Server';
diff --git a/cache/stores/redis/classes/privacy/provider.php b/cache/stores/redis/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..20defff
--- /dev/null
@@ -0,0 +1,87 @@
+<?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 Subsystem implementation for cachestore_redis.
+ *
+ * @package    cachestore_redis
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_redis\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_redis.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @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('redis', [
+                'data' => 'privacy:metadata:redis:data',
+            ], 'privacy:metadata:redis');
+        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) : contextlist {
+        return new 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) {
+    }
+
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param \context $context A user context.
+     */
+    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.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
index 26aad4e..0d155be 100644 (file)
@@ -30,6 +30,8 @@ $string['prefix_help'] = 'This prefix is used for all key names on the Redis ser
 * If you only have one Moodle instance using this server, you can leave this value default.
 * Due to key length restrictions, a maximum of 5 characters is permitted.';
 $string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
+$string['privacy:metadata:redis'] = 'The Redis cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Redis server where data is regularly removed.';
+$string['privacy:metadata:redis:data'] = 'The various data stored in the cache';
 $string['serializer_igbinary'] = 'The igbinary serializer.';
 $string['serializer_php'] = 'The default PHP serializer.';
 $string['server'] = 'Server';
diff --git a/cache/stores/session/classes/privacy/provider.php b/cache/stores/session/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..e7e1644
--- /dev/null
@@ -0,0 +1,85 @@
+<?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 Subsystem implementation for cachestore_session.
+ *
+ * @package    cachestore_session
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_session\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_session.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @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_subsystem_link('core_user', [], 'privacy:metadata:core_user');
+        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) : contextlist {
+        return new 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) {
+    }
+
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param \context $context A user context.
+     */
+    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.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
index 80b9c81..c6416b2 100644 (file)
@@ -28,4 +28,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['pluginname'] = 'Session cache';
\ No newline at end of file
+$string['pluginname'] = 'Session cache';
+$string['privacy:metadata:core_user'] = 'The Session cachestore plugin stores data briefly as part of its caching functionality. This data is stored in the short-lived user session.';
diff --git a/cache/stores/static/classes/privacy/provider.php b/cache/stores/static/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..3e994ee
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for cachestore_static.
+ *
+ * @package    cachestore_static
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace cachestore_static\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for cachestore_static implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index b433999..ec687b9 100644 (file)
@@ -28,4 +28,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['pluginname'] = 'Static request cache';
\ No newline at end of file
+$string['pluginname'] = 'Static request cache';
+$string['privacy:metadata'] = 'The Static request cachestore plugin stores some data, but this is only present for the lifetime of a single HTTP request.';
index 9899b5f..c50951f 100644 (file)
@@ -92,7 +92,7 @@ $backupfile = $backupfile['backup_destination'];
 //display the sending file page
 echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('sendingcourse', 'hub'), 3, 'main');
-$renderer = $PAGE->get_renderer('core', 'publish');
+$renderer = $PAGE->get_renderer('core', 'course');
 echo $renderer->sendingbackupinfo($backupfile);
 if (ob_get_level()) {
     ob_flush();
index ad7f7d7..1ce7fee 100644 (file)
@@ -62,7 +62,7 @@ if (!empty($updatestatusid) && confirm_sesskey()) {
     }
 }
 
-$renderer = $PAGE->get_renderer('core', 'publish');
+$renderer = $PAGE->get_renderer('core', 'course');
 
 // Unpublish course.
 if ($publication = \core\hub\publication::get_publication($publicationid, $courseid)) {
index 57470a6..21ffb34 100644 (file)
@@ -179,7 +179,7 @@ echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('publishcourseon', 'hub', 'Moodle.net'), 3, 'main');
 
 // Display hub information (logo, name, description).
-$renderer = $PAGE->get_renderer('core', 'publish');
+$renderer = $PAGE->get_renderer('core', 'course');
 if ($hubinfo = \core\hub\registration::get_moodlenet_info()) {
     echo $renderer->hubinfo($hubinfo);
 }
diff --git a/course/publish/renderer.php b/course/publish/renderer.php
deleted file mode 100644 (file)
index 9dbf3b3..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-
-///////////////////////////////////////////////////////////////////////////
-//                                                                       //
-// This file is part of Moodle - http://moodle.org/                      //
-// Moodle - Modular Object-Oriented Dynamic Learning Environment         //
-//                                                                       //
-// 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/>.       //
-//                                                                       //
-///////////////////////////////////////////////////////////////////////////
-
-/**
- * Course publish renderer.
- * @package   course
- * @subpackage publish
- * @copyright 2010 Moodle Pty Ltd (http://moodle.com)
- * @author    Jerome Mouneyrac
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class core_publish_renderer extends plugin_renderer_base {
-
-    /**
-     * Display the selector to advertise or publish a course
-     */
-    public function publicationselector($courseid) {
-        $text = '';
-
-        $advertiseurl = new moodle_url("/course/publish/metadata.php",
-                        array('sesskey' => sesskey(), 'id' => $courseid, 'advertise' => true));
-        $advertisebutton = new single_button($advertiseurl, get_string('advertise', 'hub'));
-        $text .= $this->output->render($advertisebutton);
-        $text .= html_writer::tag('div', get_string('advertisepublication_help', 'hub'),
-                        array('class' => 'publishhelp'));
-
-        $text .= html_writer::empty_tag('br');  /// TODO Delete
-
-        $uploadurl = new moodle_url("/course/publish/metadata.php",
-                        array('sesskey' => sesskey(), 'id' => $courseid, 'share' => true));
-        $uploadbutton = new single_button($uploadurl, get_string('share', 'hub'));
-        $text .= $this->output->render($uploadbutton);
-        $text .= html_writer::tag('div', get_string('sharepublication_help', 'hub'),
-                        array('class' => 'publishhelp'));
-
-        return $text;
-    }
-
-    /**
-     * Display the listing of hub where a course is registered on
-     */
-    public function registeredonhublisting($courseid, $publications) {
-        global $CFG;
-        $table = new html_table();
-        $table->head = array(get_string('type', 'hub'),
-            get_string('date'), get_string('status', 'hub'), get_string('operation', 'hub'));
-        $table->size = array('20%', '30%', '%20', '%25');
-
-        $brtag = html_writer::empty_tag('br');
-
-        foreach ($publications as $publication) {
-
-            $params = array('id' => $publication->courseid, 'publicationid' => $publication->id);
-            $cancelurl = new moodle_url("/course/publish/index.php", $params);
-            $cancelbutton = new single_button($cancelurl, get_string('removefromhub', 'hub'));
-            $cancelbutton->class = 'centeredbutton';
-            $cancelbuttonhtml = $this->output->render($cancelbutton);
-
-            if ($publication->enrollable) {
-                $params = array('sesskey' => sesskey(), 'id' => $publication->courseid, 'publicationid' => $publication->id);
-                $updateurl = new moodle_url("/course/publish/metadata.php", $params);
-                $updatebutton = new single_button($updateurl, get_string('update', 'hub'));
-                $updatebutton->class = 'centeredbutton';
-                $updatebuttonhtml = $this->output->render($updatebutton);
-
-                $operations = $updatebuttonhtml . $brtag . $cancelbuttonhtml;
-            } else {
-                $operations = $cancelbuttonhtml;
-            }
-
-            //if the publication check time if bigger than May 2010, it has been checked
-            if ($publication->timechecked > 1273127954) {
-                if ($publication->status == 0) {
-                    $status = get_string('statusunpublished', 'hub');
-                } else {
-                    $status = get_string('statuspublished', 'hub');
-                    if (!empty($publication->link)) {
-                        $status = html_writer::link($publication->link, $status);
-                    }
-                }
-
-                $status .= $brtag . html_writer::tag('a', get_string('updatestatus', 'hub'),
-                                array('href' => $CFG->wwwroot . '/course/publish/index.php?id='
-                                    . $courseid . "&updatestatusid=" . $publication->id
-                                    . "&sesskey=" . sesskey())) .
-                        $brtag . get_string('lasttimechecked', 'hub') . ": "
-                        . format_time(time() - $publication->timechecked);
-            } else {
-                $status = get_string('neverchecked', 'hub') . $brtag
-                        . html_writer::tag('a', get_string('updatestatus', 'hub'),
-                                array('href' => $CFG->wwwroot . '/course/publish/index.php?id='
-                                    . $courseid . "&updatestatusid=" . $publication->id
-                                    . "&sesskey=" . sesskey()));
-            }
-            //add button cells
-            $cells = array($publication->enrollable ?
-                        get_string('advertised', 'hub') : get_string('shared', 'hub'),
-                        userdate($publication->timepublished,
-                        get_string('strftimedatetimeshort')), $status, $operations);
-            $row = new html_table_row($cells);
-            $table->data[] = $row;
-        }
-
-        $contenthtml = html_writer::table($table);
-
-        return $contenthtml;
-    }
-
-    /**
-     * Display unpublishing confirmation page
-     * @param object $publication
-     *      $publication->courseshortname
-      $publication->courseid
-      $publication->hubname
-      $publication->huburl
-      $publication->id
-     */
-    public function confirmunpublishing($publication) {
-        $optionsyes = array('sesskey' => sesskey(), 'id' => $publication->courseid,
-            'hubcourseid' => $publication->hubcourseid,
-            'cancel' => true, 'publicationid' => $publication->id, 'confirm' => true);
-        $optionsno = array('sesskey' => sesskey(), 'id' => $publication->courseid);
-        $publication->hubname = html_writer::tag('a', 'Moodle.net',
-                        array('href' => HUB_MOODLEORGHUBURL));
-        $formcontinue = new single_button(new moodle_url("/course/publish/index.php",
-                                $optionsyes), get_string('unpublish', 'hub'), 'post');
-        $formcancel = new single_button(new moodle_url("/course/publish/index.php",
-                                $optionsno), get_string('cancel'), 'get');
-        return $this->output->confirm(get_string('unpublishconfirmation', 'hub', $publication),
-                $formcontinue, $formcancel);
-    }
-
-    /**
-     * Display waiting information about backup size during uploading backup process
-     * @param object $backupfile the backup stored_file
-     * @return $html string
-     */
-    public function sendingbackupinfo($backupfile) {
-        $sizeinfo = new stdClass();
-        $sizeinfo->total = number_format($backupfile->get_filesize() / 1000000, 2);
-        $html = html_writer::tag('div', get_string('sendingsize', 'hub', $sizeinfo),
-                        array('class' => 'courseuploadtextinfo'));
-        return $html;
-    }
-
-    /**
-     * Display upload successfull message and a button to the publish index page
-     * @param int $id the course id
-     * @return $html string
-     */
-    public function sentbackupinfo($id) {
-        $html = html_writer::tag('div', get_string('sent', 'hub'),
-                        array('class' => 'courseuploadtextinfo'));
-        $publishindexurl = new moodle_url('/course/publish/index.php',
-                        array('sesskey' => sesskey(), 'id' => $id,
-                            'published' => true));
-        $continue = $this->output->render(
-                        new single_button($publishindexurl, get_string('continue')));
-        $html .= html_writer::tag('div', $continue, array('class' => 'sharecoursecontinue'));
-        return $html;
-    }
-
-    /**
-     * Hub information (logo - name - description - link)
-     * @param object $hubinfo
-     * @return string html code
-     */
-    public function hubinfo($hubinfo) {
-        $screenshothtml = html_writer::empty_tag('img',
-                        array('src' => $hubinfo['imgurl'], 'alt' => $hubinfo['name']));
-        $hubdescription = html_writer::tag('div', $screenshothtml,
-                        array('class' => 'hubscreenshot'));
-
-        $hubdescription .= html_writer::tag('a', $hubinfo['name'],
-                        array('class' => 'hublink', 'href' => $hubinfo['url'],
-                            'onclick' => 'this.target="_blank"'));
-
-        $hubdescription .= html_writer::tag('div', format_text($hubinfo['description'], FORMAT_PLAIN),
-                        array('class' => 'hubdescription'));
-        $hubdescription = html_writer::tag('div', $hubdescription, array('class' => 'hubinfo clearfix'));
-
-        return $hubdescription;
-    }
-
-}
index a086ba6..68767ff 100644 (file)
@@ -2137,6 +2137,180 @@ class core_course_renderer extends plugin_renderer_base {
         $data = $page->export_for_template($this->output);
         return $this->output->render_from_template('core_course/activity_navigation', $data);
     }
+
+    /**
+     * Display the selector to advertise or publish a course
+     * @param int $courseid
+     */
+    public function publicationselector($courseid) {
+        $text = '';
+
+        $advertiseurl = new moodle_url("/course/publish/metadata.php",
+            array('sesskey' => sesskey(), 'id' => $courseid, 'advertise' => true));
+        $advertisebutton = new single_button($advertiseurl, get_string('advertise', 'hub'));
+        $text .= $this->output->render($advertisebutton);
+        $text .= html_writer::tag('div', get_string('advertisepublication_help', 'hub'),
+            array('class' => 'publishhelp'));
+
+        $text .= html_writer::empty_tag('br');  // TODO Delete.
+
+        $uploadurl = new moodle_url("/course/publish/metadata.php",
+            array('sesskey' => sesskey(), 'id' => $courseid, 'share' => true));
+        $uploadbutton = new single_button($uploadurl, get_string('share', 'hub'));
+        $text .= $this->output->render($uploadbutton);
+        $text .= html_writer::tag('div', get_string('sharepublication_help', 'hub'),
+            array('class' => 'publishhelp'));
+
+        return $text;
+    }
+
+    /**
+     * Display the listing of hub where a course is registered on
+     * @param int $courseid
+     * @param array $publications
+     */
+    public function registeredonhublisting($courseid, $publications) {
+        global $CFG;
+        $table = new html_table();
+        $table->head = array(get_string('type', 'hub'),
+            get_string('date'), get_string('status', 'hub'), get_string('operation', 'hub'));
+        $table->size = array('20%', '30%', '%20', '%25');
+
+        $brtag = html_writer::empty_tag('br');
+
+        foreach ($publications as $publication) {
+
+            $params = array('id' => $publication->courseid, 'publicationid' => $publication->id);
+            $cancelurl = new moodle_url("/course/publish/index.php", $params);
+            $cancelbutton = new single_button($cancelurl, get_string('removefromhub', 'hub'));
+            $cancelbutton->class = 'centeredbutton';
+            $cancelbuttonhtml = $this->output->render($cancelbutton);
+
+            if ($publication->enrollable) {
+                $params = array('sesskey' => sesskey(), 'id' => $publication->courseid, 'publicationid' => $publication->id);
+                $updateurl = new moodle_url("/course/publish/metadata.php", $params);
+                $updatebutton = new single_button($updateurl, get_string('update', 'hub'));
+                $updatebutton->class = 'centeredbutton';
+                $updatebuttonhtml = $this->output->render($updatebutton);
+
+                $operations = $updatebuttonhtml . $brtag . $cancelbuttonhtml;
+            } else {
+                $operations = $cancelbuttonhtml;
+            }
+
+            // If the publication check time if bigger than May 2010, it has been checked.
+            if ($publication->timechecked > 1273127954) {
+                if ($publication->status == 0) {
+                    $status = get_string('statusunpublished', 'hub');
+                } else {
+                    $status = get_string('statuspublished', 'hub');
+                    if (!empty($publication->link)) {
+                        $status = html_writer::link($publication->link, $status);
+                    }
+                }
+
+                $status .= $brtag . html_writer::tag('a', get_string('updatestatus', 'hub'),
+                        array('href' => $CFG->wwwroot . '/course/publish/index.php?id='
+                            . $courseid . "&updatestatusid=" . $publication->id
+                            . "&sesskey=" . sesskey())) .
+                    $brtag . get_string('lasttimechecked', 'hub') . ": "
+                    . format_time(time() - $publication->timechecked);
+            } else {
+                $status = get_string('neverchecked', 'hub') . $brtag
+                    . html_writer::tag('a', get_string('updatestatus', 'hub'),
+                        array('href' => $CFG->wwwroot . '/course/publish/index.php?id='
+                            . $courseid . "&updatestatusid=" . $publication->id
+                            . "&sesskey=" . sesskey()));
+            }
+            // Add button cells.
+            $cells = array($publication->enrollable ?
+                get_string('advertised', 'hub') : get_string('shared', 'hub'),
+                userdate($publication->timepublished,
+                    get_string('strftimedatetimeshort')), $status, $operations);
+            $row = new html_table_row($cells);
+            $table->data[] = $row;
+        }
+
+        $contenthtml = html_writer::table($table);
+
+        return $contenthtml;
+    }
+
+    /**
+     * Display unpublishing confirmation page
+     * @param stdClass $publication
+     *      $publication->courseshortname
+     *      $publication->courseid
+     *      $publication->hubname
+     *      $publication->huburl
+     *      $publication->id
+     */
+    public function confirmunpublishing($publication) {
+        $optionsyes = array('sesskey' => sesskey(), 'id' => $publication->courseid,
+            'hubcourseid' => $publication->hubcourseid,
+            'cancel' => true, 'publicationid' => $publication->id, 'confirm' => true);
+        $optionsno = array('sesskey' => sesskey(), 'id' => $publication->courseid);
+        $publication->hubname = html_writer::tag('a', 'Moodle.net',
+            array('href' => HUB_MOODLEORGHUBURL));
+        $formcontinue = new single_button(new moodle_url("/course/publish/index.php",
+            $optionsyes), get_string('unpublish', 'hub'), 'post');
+        $formcancel = new single_button(new moodle_url("/course/publish/index.php",
+            $optionsno), get_string('cancel'), 'get');
+        return $this->output->confirm(get_string('unpublishconfirmation', 'hub', $publication),
+            $formcontinue, $formcancel);
+    }
+
+    /**
+     * Display waiting information about backup size during uploading backup process
+     * @param object $backupfile the backup stored_file
+     * @return $html string
+     */
+    public function sendingbackupinfo($backupfile) {
+        $sizeinfo = new stdClass();
+        $sizeinfo->total = number_format($backupfile->get_filesize() / 1000000, 2);
+        $html = html_writer::tag('div', get_string('sendingsize', 'hub', $sizeinfo),
+            array('class' => 'courseuploadtextinfo'));
+        return $html;
+    }
+
+    /**
+     * Display upload successfull message and a button to the publish index page
+     * @param int $id the course id
+     * @return $html string
+     */
+    public function sentbackupinfo($id) {
+        $html = html_writer::tag('div', get_string('sent', 'hub'),
+            array('class' => 'courseuploadtextinfo'));
+        $publishindexurl = new moodle_url('/course/publish/index.php',
+            array('sesskey' => sesskey(), 'id' => $id,
+                'published' => true));
+        $continue = $this->output->render(
+            new single_button($publishindexurl, get_string('continue')));
+        $html .= html_writer::tag('div', $continue, array('class' => 'sharecoursecontinue'));
+        return $html;
+    }
+
+    /**
+     * Hub information (logo - name - description - link)
+     * @param object $hubinfo
+     * @return string html code
+     */
+    public function hubinfo($hubinfo) {
+        $screenshothtml = html_writer::empty_tag('img',
+            array('src' => $hubinfo['imgurl'], 'alt' => $hubinfo['name']));
+        $hubdescription = html_writer::tag('div', $screenshothtml,
+            array('class' => 'hubscreenshot'));
+
+        $hubdescription .= html_writer::tag('a', $hubinfo['name'],
+            array('class' => 'hublink', 'href' => $hubinfo['url'],
+                'onclick' => 'this.target="_blank"'));
+
+        $hubdescription .= html_writer::tag('div', format_text($hubinfo['description'], FORMAT_PLAIN),
+            array('class' => 'hubdescription'));
+        $hubdescription = html_writer::tag('div', $hubdescription, array('class' => 'hubinfo clearfix'));
+
+        return $hubdescription;
+    }
 }
 
 /**
diff --git a/grade/classes/privacy/provider.php b/grade/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..4976504
--- /dev/null
@@ -0,0 +1,831 @@
+<?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    core_grades
+ * @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 core_grades\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use context_course;
+use context_system;
+use grade_item;
+use grade_grade;
+use grade_scale;
+use stdClass;
+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;
+
+require_once($CFG->libdir . '/gradelib.php');
+
+/**
+ * Data provider class.
+ *
+ * @package    core_grades
+ * @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_database_table('grade_outcomes', [
+            'timemodified' => 'privacy:metadata:outcomes:timemodified',
+            'usermodified' => 'privacy:metadata:outcomes:usermodified',
+        ], 'privacy:metadata:outcomes');
+
+        $collection->add_database_table('grade_outcomes_history', [
+            'timemodified' => 'privacy:metadata:history:timemodified',
+            'loggeduser' => 'privacy:metadata:history:loggeduser',
+        ], 'privacy:metadata:outcomeshistory');
+
+        $collection->add_database_table('grade_categories_history', [
+            'timemodified' => 'privacy:metadata:history:timemodified',
+            'loggeduser' => 'privacy:metadata:history:loggeduser',
+        ], 'privacy:metadata:categorieshistory');
+
+        $collection->add_database_table('grade_items_history', [
+            'timemodified' => 'privacy:metadata:history:timemodified',
+            'loggeduser' => 'privacy:metadata:history:loggeduser',
+        ], 'privacy:metadata:itemshistory');
+
+        $gradescommonfields = [
+            'userid' => 'privacy:metadata:grades:userid',
+            'usermodified' => 'privacy:metadata:grades:usermodified',
+            'finalgrade' => 'privacy:metadata:grades:finalgrade',
+            'feedback' => 'privacy:metadata:grades:feedback',
+            'information' => 'privacy:metadata:grades:information',
+        ];
+
+        $collection->add_database_table('grade_grades', array_merge($gradescommonfields, [
+            'timemodified' => 'privacy:metadata:grades:timemodified',
+        ]), 'privacy:metadata:grades');
+
+        $collection->add_database_table('grade_grades_history', array_merge($gradescommonfields, [
+            'timemodified' => 'privacy:metadata:history:timemodified',
+            'loggeduser' => 'privacy:metadata:history:loggeduser',
+        ]), 'privacy:metadata:gradeshistory');
+
+        // The table grade_import_values is not reported because its data is temporary and only
+        // used during an import. It's content is deleted after a successful, or failed, import.
+
+        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();
+
+        // Add where we modified outcomes.
+        $sql = "
+            SELECT DISTINCT ctx.id
+              FROM {grade_outcomes} go
+              JOIN {context} ctx
+                ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel)
+                OR (ctx.id = :syscontextid)
+             WHERE go.usermodified = :userid";
+        $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
+        $contextlist->add_from_sql($sql, $params);
+
+        // Add where appear in the history of outcomes, categories or items.
+        $sql = "
+            SELECT DISTINCT ctx.id
+              FROM {context} ctx
+         LEFT JOIN {grade_outcomes_history} goh
+                ON (goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1)
+                OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid)
+         LEFT JOIN {grade_categories_history} gch
+                ON gch.courseid = ctx.instanceid
+               AND ctx.contextlevel = :courselevel2
+         LEFT JOIN {grade_items_history} gih
+                ON gih.courseid = ctx.instanceid
+               AND ctx.contextlevel = :courselevel3
+             WHERE goh.loggeduser = :userid1
+                OR gch.loggeduser = :userid2
+                OR gih.loggeduser = :userid3";
+        $params = [
+            'syscontextid' => SYSCONTEXTID,
+            'courselevel1' => CONTEXT_COURSE,
+            'courselevel2' => CONTEXT_COURSE,
+            'courselevel3' => CONTEXT_COURSE,
+            'userid1' => $userid,
+            'userid2' => $userid,
+            'userid3' => $userid,
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
+        // Add where we were graded or modified grades, including in the history table.
+        $sql = "
+            SELECT DISTINCT ctx.id
+              FROM {grade_items} gi
+              JOIN {context} ctx
+                ON ctx.instanceid = gi.courseid
+               AND ctx.contextlevel = :courselevel
+         LEFT JOIN {grade_grades} gg
+                ON gg.itemid = gi.id
+         LEFT JOIN {grade_grades_history} ggh
+                ON ggh.itemid = gi.id
+             WHERE gg.userid = :userid1
+                OR gg.usermodified = :userid2
+                OR ggh.userid = :userid3
+                OR ggh.loggeduser = :userid4
+                OR ggh.usermodified = :userid5";
+        $params = [
+            'courselevel' => CONTEXT_COURSE,
+            'userid1' => $userid,
+            'userid2' => $userid,
+            'userid3' => $userid,
+            'userid4' => $userid,
+            'userid5' => $userid,
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
+        // Historical grades can be made orphans when the corresponding itemid is deleted. When that happens
+        // we cannot tie the historical grade to a course context, so we report the user context as a last resort.
+        $sql = "
+           SELECT DISTINCT ctx.id
+             FROM {context} ctx
+             JOIN {grade_grades_history} ggh
+               ON ctx.contextlevel = :userlevel
+              AND ggh.userid = ctx.instanceid
+        LEFT JOIN {grade_items} gi
+               ON ggh.itemid = gi.id
+            WHERE gi.id IS NULL
+              AND (
+                  ggh.userid = :userid1
+               OR ggh.usermodified = :userid2
+               OR ggh.loggeduser = :userid3
+                  )";
+        $params = [
+            'userlevel' => CONTEXT_USER,
+            'userid1' => $userid,
+            'userid2' => $userid,
+            'userid3' => $userid
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        $user = $contextlist->get_user();
+        $userid = $user->id;
+        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
+            if ($context->contextlevel == CONTEXT_COURSE) {
+                $carry[$context->contextlevel][] = $context;
+
+            } else if ($context->contextlevel == CONTEXT_USER) {
+                $carry[$context->contextlevel][] = $context;
+
+            }
+
+            return $carry;
+        }, [
+            CONTEXT_USER => [],
+            CONTEXT_COURSE => []
+        ]);
+
+        $rootpath = [get_string('grades', 'core_grades')];
+        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
+
+        // Export the outcomes.
+        static::export_user_data_outcomes_in_contexts($contextlist);
+
+        // Export the historical grades which have become orphans (their grade items were deleted).
+        // We place those in ther user context of the graded user.
+        $userids = array_values(array_map(function($context) {
+            return $context->instanceid;
+        }, $contexts[CONTEXT_USER]));
+        if (!empty($userids)) {
+
+            // Export own historical grades and related ones.
+            list($inuseridsql, $inuseridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+            list($inusermodifiedsql, $inusermodifiedparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+            list($inloggedusersql, $inloggeduserparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+            $usercontext = $contexts[CONTEXT_USER];
+            $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
+            $sql = "
+                SELECT $gghfields, ctx.id as ctxid
+                  FROM {grade_grades_history} ggh
+                  JOIN {context} ctx
+                    ON ctx.instanceid = ggh.userid
+                   AND ctx.contextlevel = :userlevel
+             LEFT JOIN {grade_items} gi
+                    ON gi.id = ggh.itemid
+                 WHERE gi.id IS NULL
+                   AND (ggh.userid $inuseridsql
+                    OR ggh.usermodified $inusermodifiedsql
+                    OR ggh.loggeduser $inloggedusersql)
+                   AND (ggh.userid = :userid1
+                    OR ggh.usermodified = :userid2
+                    OR ggh.loggeduser = :userid3)
+              ORDER BY ggh.userid, ggh.timemodified, ggh.id";
+            $params = array_merge($inuseridparams, $inusermodifiedparams, $inloggeduserparams,
+                ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'userlevel' => CONTEXT_USER]);
+
+            $deletedstr = get_string('privacy:request:unknowndeletedgradeitem', 'core_grades');
+            $recordset = $DB->get_recordset_sql($sql, $params);
+            static::recordset_loop_and_export($recordset, 'ctxid', [], function($carry, $record) use ($deletedstr, $userid) {
+                $context = context::instance_by_id($record->ctxid);
+                $gghrecord = static::extract_record($record, 'ggh_');
+
+                // Orphan grades do not have items, so we do not recreate a grade_grade item, and we do not format grades.
+                $carry[] = [
+                    'name' => $deletedstr,
+                    'graded_user_was_you' => transform::yesno($userid == $gghrecord->userid),
+                    'grade' => $gghrecord->finalgrade,
+                    'feedback' => format_text($gghrecord->feedback, $gghrecord->feedbackformat, ['context' => $context]),
+                    'information' => format_text($gghrecord->information, $gghrecord->informationformat, ['context' => $context]),
+                    'timemodified' => transform::datetime($gghrecord->timemodified),
+                    'logged_in_user_was_you' => transform::yesno($userid == $gghrecord->loggeduser),
+                    'author_of_change_was_you' => transform::yesno($userid == $gghrecord->usermodified),
+                    'action' => static::transform_history_action($gghrecord->action)
+                ];
+
+                return $carry;
+
+            }, function($ctxid, $data) use ($rootpath) {
+                $context = context::instance_by_id($ctxid);
+                writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
+            });
+        }
+
+        // Find out the course IDs.
+        $courseids = array_values(array_map(function($context) {
+            return $context->instanceid;
+        }, $contexts[CONTEXT_COURSE]));
+        if (empty($courseids)) {
+            return;
+        }
+        list($incoursesql, $incourseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+
+        // Ensure that the grades are final and do not need regrading.
+        array_walk($courseids, function($courseid) {
+            grade_regrade_final_grades($courseid);
+        });
+
+        // Export own grades.
+        $ggfields = static::get_fields_sql('grade_grade', 'gg', 'gg_');
+        $gifields = static::get_fields_sql('grade_item', 'gi', 'gi_');
+        $scalefields = static::get_fields_sql('grade_scale', 'sc', 'sc_');
+        $sql = "
+            SELECT $ggfields, $gifields, $scalefields
+              FROM {grade_grades} gg
+              JOIN {grade_items} gi
+                ON gi.id = gg.itemid
+         LEFT JOIN {scale} sc
+                ON sc.id = gi.scaleid
+             WHERE gi.courseid $incoursesql
+               AND gg.userid = :userid
+          ORDER BY gi.courseid, gi.id, gg.id";
+        $params = array_merge($incourseparams, ['userid' => $userid]);
+
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
+            $context = context_course::instance($record->gi_courseid);
+            $gg = static::extract_grade_grade_from_record($record);
+            $carry[] = static::transform_grade($gg, $context);
+            return $carry;
+
+        }, function($courseid, $data) use ($rootpath) {
+            $context = context_course::instance($courseid);
+            writer::with_context($context)->export_data($rootpath, (object) ['grades' => $data]);
+        });
+
+        // Export own historical grades in courses.
+        $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
+        $sql = "
+            SELECT $gghfields, $gifields, $scalefields
+              FROM {grade_grades_history} ggh
+              JOIN {grade_items} gi
+                ON gi.id = ggh.itemid
+         LEFT JOIN {scale} sc
+                ON sc.id = gi.scaleid
+             WHERE gi.courseid $incoursesql
+               AND ggh.userid = :userid
+          ORDER BY gi.courseid, ggh.timemodified, ggh.id";
+        $params = array_merge($incourseparams, ['userid' => $userid]);
+
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
+            $context = context_course::instance($record->gi_courseid);
+            $gg = static::extract_grade_grade_from_record($record, true);
+            $carry[] = array_merge(static::transform_grade($gg, $context), [
+                'action' => static::transform_history_action($record->ggh_action)
+            ]);
+            return $carry;
+
+        }, function($courseid, $data) use ($rootpath) {
+            $context = context_course::instance($courseid);
+            writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
+        });
+
+        // Export edits of categories history.
+        $sql = "
+            SELECT gch.id, gch.courseid, gch.fullname, gch.timemodified, gch.action
+              FROM {grade_categories_history} gch
+             WHERE gch.courseid $incoursesql
+               AND gch.loggeduser = :userid
+          ORDER BY gch.courseid, gch.timemodified, gch.id";
+        $params = array_merge($incourseparams, ['userid' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
+            $carry[] = [
+                'name' => $record->fullname,
+                'timemodified' => transform::datetime($record->timemodified),
+                'logged_in_user_was_you' => transform::yesno(true),
+                'action' => static::transform_history_action($record->action),
+            ];
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = context_course::instance($courseid);
+            writer::with_context($context)->export_related_data($relatedtomepath, 'categories_history',
+                (object) ['modified_records' => $data]);
+        });
+
+        // Export edits of items history.
+        $sql = "
+            SELECT gih.id, gih.courseid, gih.itemname, gih.itemmodule, gih.iteminfo, gih.timemodified, gih.action
+              FROM {grade_items_history} gih
+             WHERE gih.courseid $incoursesql
+               AND gih.loggeduser = :userid
+          ORDER BY gih.courseid, gih.timemodified, gih.id";
+        $params = array_merge($incourseparams, ['userid' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
+            $carry[] = [
+                'name' => $record->itemname,
+                'module' => $record->itemmodule,
+                'info' => $record->iteminfo,
+                'timemodified' => transform::datetime($record->timemodified),
+                'logged_in_user_was_you' => transform::yesno(true),
+                'action' => static::transform_history_action($record->action),
+            ];
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = context_course::instance($courseid);
+            writer::with_context($context)->export_related_data($relatedtomepath, 'items_history',
+                (object) ['modified_records' => $data]);
+        });
+
+        // Export edits of grades in course.
+        $sql = "
+            SELECT $ggfields, $gifields, $scalefields
+              FROM {grade_grades} gg
+              JOIN {grade_items} gi
+                ON gg.itemid = gi.id
+         LEFT JOIN {scale} sc
+                ON sc.id = gi.scaleid
+             WHERE gi.courseid $incoursesql
+               AND gg.userid <> :userid1    -- Our grades have already been exported.
+               AND gg.usermodified = :userid2
+          ORDER BY gi.courseid, gg.timemodified, gg.id";
+        $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
+            $context = context_course::instance($record->gi_courseid);
+            $gg = static::extract_grade_grade_from_record($record);
+            $carry[] = array_merge(static::transform_grade($gg, $context), [
+                'userid' => transform::user($gg->userid),
+                'created_or_modified_by_you' => transform::yesno(true),
+            ]);
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = context_course::instance($courseid);
+            writer::with_context($context)->export_related_data($relatedtomepath, 'grades', (object) ['grades' => $data]);
+        });
+
+        // Export edits of grades history in course.
+        $sql = "
+            SELECT $gghfields, $gifields, $scalefields, ggh.loggeduser AS loggeduser
+              FROM {grade_grades_history} ggh
+              JOIN {grade_items} gi
+                ON ggh.itemid = gi.id
+         LEFT JOIN {scale} sc
+                ON sc.id = gi.scaleid
+             WHERE gi.courseid $incoursesql
+               AND ggh.userid <> :userid1   -- We've already exported our history.
+               AND (ggh.loggeduser = :userid2
+                OR ggh.usermodified = :userid3)
+          ORDER BY gi.courseid, ggh.timemodified, ggh.id";
+        $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) use ($userid) {
+            $context = context_course::instance($record->gi_courseid);
+            $gg = static::extract_grade_grade_from_record($record, true);
+            $carry[] = array_merge(static::transform_grade($gg, $context), [
+                'userid' => transform::user($gg->userid),
+                'logged_in_user_was_you' => transform::yesno($userid == $record->loggeduser),
+                'author_of_change_was_you' => transform::yesno($userid == $gg->usermodified),
+                'action' => static::transform_history_action($record->ggh_action),
+            ]);
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = context_course::instance($courseid);
+            writer::with_context($context)->export_related_data($relatedtomepath, 'grades_history',
+                (object) ['modified_records' => $data]);
+        });
+    }
+
+    /**
+     * 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;
+
+        switch ($context->contextlevel) {
+            case CONTEXT_USER:
+                // The user context is only reported when there are orphan historical grades, so we only delete those.
+                static::delete_orphan_historical_grades($context->instanceid);
+                break;
+
+            case CONTEXT_COURSE:
+                // We must not change the structure of the course, so we only delete user content.
+                $itemids = static::get_item_ids_from_course_ids([$context->instanceid]);
+                if (empty($itemids)) {
+                    return;
+                }
+                list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
+                $DB->delete_records_select('grade_grades', "itemid $insql", $inparams);
+                $DB->delete_records_select('grade_grades_history', "itemid $insql", $inparams);
+                break;
+        }
+
+    }
+
+    /**
+     * 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;
+        $userid = $contextlist->get_user()->id;
+
+        $courseids = [];
+        foreach ($contextlist->get_contexts() as $context) {
+            if ($context->contextlevel == CONTEXT_USER && $userid == $context->instanceid) {
+                // User attempts to delete data in their own context.
+                static::delete_orphan_historical_grades($userid);
+
+            } else if ($context->contextlevel == CONTEXT_COURSE) {
+                // Log the list of course IDs.
+                $courseids[] = $context->instanceid;
+            }
+        }
+
+        $itemids = static::get_item_ids_from_course_ids($courseids);
+        if (empty($itemids)) {
+            // Our job here is done!
+            return;
+        }
+
+        // Delete all the grades.
+        list($insql, $inparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
+        $params = array_merge($inparams, ['userid' => $userid]);
+        $DB->delete_records_select('grade_grades', "itemid $insql AND userid = :userid", $params);
+        $DB->delete_records_select('grade_grades_history', "itemid $insql AND userid = :userid", $params);
+    }
+
+    /**
+     * Delete orphan historical grades.
+     *
+     * @param int $userid The user ID.
+     * @return void
+     */
+    protected static function delete_orphan_historical_grades($userid) {
+        global $DB;
+        $sql = "
+            SELECT ggh.id
+              FROM {grade_grades_history} ggh
+         LEFT JOIN {grade_items} gi
+                ON ggh.itemid = gi.id
+             WHERE gi.id IS NULL
+               AND ggh.userid = :userid";
+        $ids = $DB->get_fieldset_sql($sql, ['userid' => $userid]);
+        if (empty($ids)) {
+            return;
+        }
+        list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
+        $DB->delete_records_select('grade_grades_history', "id $insql", $inparams);
+    }
+
+    /**
+     * Export the user data related to outcomes.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     * @return void
+     */
+    protected static function export_user_data_outcomes_in_contexts(approved_contextlist $contextlist) {
+        global $DB;
+
+        $rootpath = [get_string('grades', 'core_grades')];
+        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
+        $userid = $contextlist->get_user()->id;
+
+        // Reorganise the contexts.
+        $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_SYSTEM) {
+                $carry['in_system'] = true;
+            } else if ($context->contextlevel == CONTEXT_COURSE) {
+                $carry['courseids'][] = $context->instanceid;
+            }
+            return $carry;
+        }, [
+            'in_system' => false,
+            'courseids' => []
+        ]);
+
+        // Construct SQL.
+        $sqltemplateparts = [];
+        $templateparams = [];
+        if ($reduced['in_system']) {
+            $sqltemplateparts[] = '{prefix}.courseid IS NULL';
+        }
+        if (!empty($reduced['courseids'])) {
+            list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED);
+            $sqltemplateparts[] = "{prefix}.courseid $insql";
+            $templateparams = array_merge($templateparams, $inparams);
+        }
+        if (empty($sqltemplateparts)) {
+            return;
+        }
+        $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
+
+        // Export edited outcomes.
+        $sqlwhere = str_replace('{prefix}', 'go', $sqltemplate);
+        $sql = "
+            SELECT go.id, COALESCE(go.courseid, 0) AS courseid, go.shortname, go.fullname, go.timemodified
+              FROM {grade_outcomes} go
+             WHERE $sqlwhere
+               AND go.usermodified = :userid
+          ORDER BY go.courseid, go.timemodified, go.id";
+        $params = array_merge($templateparams, ['userid' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
+            $carry[] = [
+                'shortname' => $record->shortname,
+                'fullname' => $record->fullname,
+                'timemodified' => transform::datetime($record->timemodified),
+                'created_or_modified_by_you' => transform::yesno(true)
+            ];
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
+            writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes',
+                (object) ['outcomes' => $data]);
+        });
+
+        // Export edits of outcomes history.
+        $sqlwhere = str_replace('{prefix}', 'goh', $sqltemplate);
+        $sql = "
+            SELECT goh.id, COALESCE(goh.courseid, 0) AS courseid, goh.shortname, goh.fullname, goh.timemodified, goh.action
+              FROM {grade_outcomes_history} goh
+             WHERE $sqlwhere
+               AND goh.loggeduser = :userid
+          ORDER BY goh.courseid, goh.timemodified, goh.id";
+        $params = array_merge($templateparams, ['userid' => $userid]);
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
+            $carry[] = [
+                'shortname' => $record->shortname,
+                'fullname' => $record->fullname,
+                'timemodified' => transform::datetime($record->timemodified),
+                'logged_in_user_was_you' => transform::yesno(true),
+                'action' => static::transform_history_action($record->action)
+            ];
+            return $carry;
+
+        }, function($courseid, $data) use ($relatedtomepath) {
+            $context = $courseid ? context_course::instance($courseid) : context_system::instance();
+            writer::with_context($context)->export_related_data($relatedtomepath, 'outcomes_history',
+                (object) ['modified_records' => $data]);
+        });
+    }
+
+    /**
+     * Extract grade_grade from a record.
+     *
+     * @param stdClass $record The record.
+     * @param bool $ishistory Whether we're extracting a historical grade.
+     * @return grade_grade
+     */
+    protected static function extract_grade_grade_from_record(stdClass $record, $ishistory = false) {
+        $prefix = $ishistory ? 'ggh_' : 'gg_';
+        $ggrecord = static::extract_record($record, $prefix);
+        if ($ishistory) {
+            // The grade history is not a real grade_grade so we remove the ID.
+            unset($ggrecord->id);
+        }
+        $gg = new grade_grade($ggrecord, false);
+
+        // There is a grade item in the record.
+        if (!empty($record->gi_id)) {
+            $gi = new grade_item(static::extract_record($record, 'gi_'), false);
+            $gg->grade_item = $gi;  // This is a common hack throughout the grades API.
+        }
+
+        // Load the scale, when it still exists.
+        if (!empty($gi->scaleid) && !empty($record->sc_id)) {
+            $scalerec = static::extract_record($record, 'sc_');
+            $gi->scale = new grade_scale($scalerec, false);
+            $gi->scale->load_items();
+        }
+
+        return $gg;
+    }
+
+    /**
+     * Extract a record from another one.
+     *
+     * @param object $record The record to extract from.
+     * @param string $prefix The prefix used.
+     * @return object
+     */
+    protected static function extract_record($record, $prefix) {
+        $result = [];
+        $prefixlength = strlen($prefix);
+        foreach ($record as $key => $value) {
+            if (strpos($key, $prefix) === 0) {
+                $result[substr($key, $prefixlength)] = $value;
+            }
+        }
+        return (object) $result;
+    }
+
+    /**
+     * Get fields SQL for a grade related object.
+     *
+     * @param string $target The related object.
+     * @param string $alias The table alias.
+     * @param string $prefix A prefix.
+     * @return string
+     */
+    protected static function get_fields_sql($target, $alias, $prefix) {
+        switch ($target) {
+            case 'grade_category':
+            case 'grade_grade':
+            case 'grade_item':
+            case 'grade_outcome':
+            case 'grade_scale':
+                $obj = new $target([], false);
+                $fields = array_merge(array_keys($obj->optional_fields), $obj->required_fields);
+                break;
+
+            case 'grade_grades_history':
+                $fields = ['id', 'action', 'oldid', 'source', 'timemodified', 'loggeduser', 'itemid', 'userid', 'rawgrade',
+                    'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime',
+                    'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat'];
+                break;
+
+            default:
+                throw new \coding_exception('Unrecognised target: ' . $target);
+                break;
+        }
+
+        return implode(', ', array_map(function($field) use ($alias, $prefix) {
+            return "{$alias}.{$field} AS {$prefix}{$field}";
+        }, $fields));
+    }
+
+    /**
+     * Get all the items IDs from course IDs.
+     *
+     * @param array $courseids The course IDs.
+     * @return array
+     */
+    protected static function get_item_ids_from_course_ids($courseids) {
+        global $DB;
+        if (empty($courseids)) {
+            return [];
+        }
+        list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+        return $DB->get_fieldset_select('grade_items', 'id', "courseid $insql", $inparams);
+    }
+
+    /**
+     * Loop and export from a recordset.
+     *
+     * @param moodle_recordset $recordset The recordset.
+     * @param string $splitkey The record key to determine when to export.
+     * @param mixed $initial The initial data to reduce from.
+     * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
+     * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
+     * @return void
+     */
+    protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
+            callable $reducer, callable $export) {
+
+        $data = $initial;
+        $lastid = null;
+
+        foreach ($recordset as $record) {
+            if ($lastid !== null && $record->{$splitkey} != $lastid) {
+                $export($lastid, $data);
+                $data = $initial;
+            }
+            $data = $reducer($data, $record);
+            $lastid = $record->{$splitkey};
+        }
+        $recordset->close();
+
+        if ($lastid !== null) {
+            $export($lastid, $data);
+        }
+    }
+
+    /**
+     * Transform an history action.
+     *
+     * @param int $action The action.
+     * @return string
+     */
+    protected static function transform_history_action($action) {
+        switch ($action) {
+            case GRADE_HISTORY_INSERT:
+                return get_string('privacy:request:historyactioninsert', 'core_grades');
+                break;
+            case GRADE_HISTORY_UPDATE:
+                return get_string('privacy:request:historyactionupdate', 'core_grades');
+                break;
+            case GRADE_HISTORY_DELETE:
+                return get_string('privacy:request:historyactiondelete', 'core_grades');
+                break;
+        }
+
+        return '?';
+    }
+
+    /**
+     * Transform a grade.
+     *
+     * @param grade_grade $gg The grade object.
+     * @param context $context The context.
+     * @return array
+     */
+    protected static function transform_grade(grade_grade $gg, context $context) {
+        $gi = $gg->load_grade_item();
+        $timemodified = $gg->timemodified ? transform::datetime($gg->timemodified) : null;
+        $timecreated = $gg->timecreated ? transform::datetime($gg->timecreated) : $timemodified; // When null we use timemodified.
+        return [
+            'item' => $gi->get_name(),
+            'grade' => $gg->finalgrade,
+            'grade_formatted' => grade_format_gradevalue($gg->finalgrade, $gi),
+            'feedback' => format_text($gg->feedback, $gg->feedbackformat, ['context' => $context]),
+            'information' => format_text($gg->information, $gg->informationformat, ['context' => $context]),
+            'timecreated' => $timecreated,
+            'timemodified' => $timemodified,
+        ];
+    }
+
+}
diff --git a/grade/tests/privacy_test.php b/grade/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..83605a1
--- /dev/null
@@ -0,0 +1,782 @@
+<?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    core_grades
+ * @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\approved_contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use core_grades\privacy\provider;
+
+require_once($CFG->libdir . '/gradelib.php');
+
+/**
+ * Data provider testcase class.
+ *
+ * @package    core_grades
+ * @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 core_grades_privacy_testcase extends provider_testcase {
+
+    public function setUp() {
+        global $PAGE;
+        $this->resetAfterTest();
+        $PAGE->get_renderer('core');
+    }
+
+    public function test_get_contexts_for_userid_gradebook_edits() {
+        $dg = $this->getDataGenerator();
+
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $u5 = $dg->create_user();
+        $u6 = $dg->create_user();
+
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        // Create some stuff.
+        $gi1a = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi1b = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi2a = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+        $gc1a = new grade_category($dg->create_grade_category(['courseid' => $c1->id]), false);
+        $gc1b = new grade_category($dg->create_grade_category(['courseid' => $c1->id]), false);
+        $gc2a = new grade_category($dg->create_grade_category(['courseid' => $c2->id]), false);
+        $go2 = new grade_outcome($dg->create_grade_outcome(['courseid' => $c2->id, 'shortname' => 'go2',
+            'fullname' => 'go2']), false);
+
+        // Nothing as of now.
+        foreach ([$u1, $u2, $u3, $u4] as $u) {
+            $contexts = array_flip(provider::get_contexts_for_userid($u->id)->get_contextids());
+            $this->assertEmpty($contexts);
+        }
+
+        $go0 = new grade_outcome(['shortname' => 'go0', 'fullname' => 'go0', 'usermodified' => $u1->id]);
+        $go0->insert();
+        $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]);
+        $go1->insert();
+
+        // User 2 creates history.
+        $this->setUser($u2);
+        $go0->shortname .= ' edited';
+        $go0->update();
+        $gc1a->fullname .= ' edited';
+        $gc1a->update();
+
+        // User 3 creates history.
+        $this->setUser($u3);
+        $go1->shortname .= ' edited';
+        $go1->update();
+        $gc2a->fullname .= ' a';
+        $gc2a->update();
+
+        // User 4 updates an outcome in course (creates history).
+        $this->setUser($u4);
+        $go2->shortname .= ' edited';
+        $go2->update();
+
+        // User 5 updates an item.
+        $this->setUser($u5);
+        $gi1a->itemname .= ' edited';
+        $gi1a->update();
+
+        // User 6 creates history.
+        $this->setUser($u6);
+        $gi2a->delete();
+
+        // Assert contexts.
+        $contexts = array_flip(provider::get_contexts_for_userid($u1->id)->get_contextids());
+        $this->assertCount(2, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $this->assertArrayHasKey($sysctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u2->id)->get_contextids());
+        $this->assertCount(2, $contexts);
+        $this->assertArrayHasKey($sysctx->id, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u3->id)->get_contextids());
+        $this->assertCount(2, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $this->assertArrayHasKey($c2ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u4->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c2ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u5->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u6->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c2ctx->id, $contexts);
+    }
+
+    public function test_get_contexts_for_userid_grades_and_history() {
+        $dg = $this->getDataGenerator();
+
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $u5 = $dg->create_user();
+        $u6 = $dg->create_user();
+
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        // Create some stuff.
+        $gi1a = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi1b = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi2a = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+        $gi2b = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+
+        // Nothing as of now.
+        foreach ([$u1, $u2, $u3, $u4, $u5, $u6] as $u) {
+            $contexts = array_flip(provider::get_contexts_for_userid($u->id)->get_contextids());
+            $this->assertEmpty($contexts);
+        }
+
+        // User 1 is graded in course 1.
+        $gi1a->update_final_grade($u1->id, 1, 'test');
+
+        // User 2 is graded in course 2.
+        $gi2a->update_final_grade($u2->id, 10, 'test');
+
+        // User 3 is set as modifier.
+        $gi1a->update_final_grade($u1->id, 1, 'test', '', FORMAT_MOODLE, $u3->id);
+
+        // User 4 is set as modifier, and creates history..
+        $this->setUser($u4);
+        $gi1a->update_final_grade($u2->id, 1, 'test');
+
+        // User 5 creates history, user 6 is the known modifier, and we delete the item.
+        $this->setUser($u5);
+        $gi2b->update_final_grade($u2->id, 1, 'test', '', FORMAT_PLAIN, $u6->id);
+        $gi2b->delete();
+
+        // Assert contexts.
+        $contexts = array_flip(provider::get_contexts_for_userid($u1->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u2->id)->get_contextids());
+        $this->assertCount(3, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $this->assertArrayHasKey($c2ctx->id, $contexts);
+        $this->assertArrayHasKey(context_user::instance($u2->id)->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u3->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u4->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey($c1ctx->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u5->id)->get_contextids());
+        $this->assertCount(2, $contexts);
+        $this->assertArrayHasKey($c2ctx->id, $contexts);
+        $this->assertArrayHasKey(context_user::instance($u2->id)->id, $contexts);
+        $contexts = array_flip(provider::get_contexts_for_userid($u6->id)->get_contextids());
+        $this->assertCount(1, $contexts);
+        $this->assertArrayHasKey(context_user::instance($u2->id)->id, $contexts);
+    }
+
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u1ctx = context_user::instance($u1->id);
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        // Create some stuff.
+        $gi1a = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi1b = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi2a = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+        $gi2b = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+
+        $gi1a->update_final_grade($u1->id, 1, 'test');
+        $gi1a->update_final_grade($u2->id, 1, 'test');
+        $gi1b->update_final_grade($u1->id, 1, 'test');
+        $gi2a->update_final_grade($u1->id, 1, 'test');
+        $gi2a->update_final_grade($u2->id, 1, 'test');
+        $gi2b->update_final_grade($u1->id, 1, 'test');
+        $gi2b->update_final_grade($u2->id, 1, 'test');
+        $gi2b->delete();
+
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi1a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u1->id, 'itemid' => $gi2b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+
+        provider::delete_data_for_all_users_in_context($c1ctx);
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi1a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u1->id, 'itemid' => $gi2b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+
+        provider::delete_data_for_all_users_in_context($u1ctx);
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades_history', ['userid' => $u1->id, 'itemid' => $gi2b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+
+        provider::delete_data_for_all_users_in_context($c2ctx);
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+    }
+
+    public function test_delete_data_for_user() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        // Create some stuff.
+        $gi1a = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi1b = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi2a = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+        $gi2b = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+
+        $gi1a->update_final_grade($u1->id, 1, 'test');
+        $gi1a->update_final_grade($u2->id, 1, 'test');
+        $gi1b->update_final_grade($u1->id, 1, 'test');
+        $gi2a->update_final_grade($u1->id, 1, 'test');
+        $gi2a->update_final_grade($u2->id, 1, 'test');
+        $gi2b->update_final_grade($u1->id, 1, 'test');
+        $gi2b->update_final_grade($u2->id, 1, 'test');
+        $gi2b->delete();
+
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi1a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u1->id, 'itemid' => $gi2b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+
+        provider::delete_data_for_user(new approved_contextlist($u1, 'core_grades', [$c1ctx->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi1a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u1->id, 'itemid' => $gi2b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+
+        provider::delete_data_for_user(new approved_contextlist($u1, 'core_grades', [$u1ctx->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi1a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades_history', ['userid' => $u1->id, 'itemid' => $gi2b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+
+        provider::delete_data_for_user(new approved_contextlist($u1, 'core_grades', [$u2ctx->id, $c2ctx->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi1a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi1b->id]));
+        $this->assertFalse($DB->record_exists('grade_grades', ['userid' => $u1->id, 'itemid' => $gi2a->id]));
+        $this->assertTrue($DB->record_exists('grade_grades', ['userid' => $u2->id, 'itemid' => $gi2a->id]));
+        $this->assertFalse($DB->record_exists('grade_grades_history', ['userid' => $u1->id, 'itemid' => $gi2b->id]));
+        $this->assertTrue($DB->record_exists('grade_grades_history', ['userid' => $u2->id, 'itemid' => $gi2b->id]));
+    }
+
+    public function test_export_data_for_user_about_grades_and_history() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+
+        // Users being graded.
+        $ug1 = $dg->create_user();
+        $ug2 = $dg->create_user();
+        $ug3 = $dg->create_user();
+        // Users performing actions.
+        $ua1 = $dg->create_user();
+        $ua2 = $dg->create_user();
+        $ua3 = $dg->create_user();
+
+        $ug1ctx = context_user::instance($ug1->id);
+        $ug2ctx = context_user::instance($ug2->id);
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        $rootpath = [get_string('grades', 'core_grades')];
+        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
+
+        // Create the course minimal stuff.
+        grade_category::fetch_course_category($c1->id);
+        $ci1 = grade_item::fetch_course_item($c1->id);
+        grade_category::fetch_course_category($c2->id);
+        $ci2 = grade_item::fetch_course_item($c2->id);
+
+        // Create data that will sit in the user context because we will delete the grate item.
+        $gi1 = new grade_item($dg->create_grade_item(['courseid' => $c1->id, 'aggregationcoef2' => 1]), false);
+        $gi1->update_final_grade($ug1->id, 100, 'test', 'Well done!', FORMAT_PLAIN, $ua2->id);
+        $gi1->update_final_grade($ug1->id, 1, 'test', 'Hi', FORMAT_PLAIN, $ua2->id);
+        $gi1->update_final_grade($ug3->id, 12, 'test', 'Hello', FORMAT_PLAIN, $ua2->id);
+
+        // Create another set for another user.
+        $gi2a = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+        $gi2a->update_final_grade($ug1->id, 15, 'test', '', FORMAT_PLAIN, $ua2->id);
+        $gi2b = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+        $gi2b->update_final_grade($ug1->id, 30, 'test', 'Well played!', FORMAT_PLAIN, $ua2->id);
+
+        // Export action user 1 everywhere.
+        provider::export_user_data(new approved_contextlist($ua1, 'core_grades', [$ug1ctx->id, $ug2ctx->id,
+            $c1ctx->id, $c2ctx->id]));
+        $this->assert_context_has_no_data($ug1ctx);
+        $this->assert_context_has_no_data($ug2ctx);
+        $this->assert_context_has_no_data($c1ctx);
+        $this->assert_context_has_no_data($c2ctx);
+
+        // Export action user 2 in course 1.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($ua2, 'core_grades', [$c1ctx->id]));
+        $this->assert_context_has_no_data($ug1ctx);
+        $this->assert_context_has_no_data($ug2ctx);
+        $this->assert_context_has_no_data($c2ctx);
+        $data = writer::with_context($c1ctx)->get_data($rootpath);
+        $this->assertEmpty($data);
+
+        // Here we are testing the export of grades that we've changed.
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'grades');
+        $this->assertCount(2, $data->grades);
+        $this->assertEquals($gi1->get_name(), $data->grades[0]['item']);
+        $this->assertEquals(1, $data->grades[0]['grade']);
+        $this->assertEquals('Hi', $data->grades[0]['feedback']);
+        $this->assertEquals(transform::yesno(true), $data->grades[0]['created_or_modified_by_you']);
+        $this->assertEquals($gi1->get_name(), $data->grades[1]['item']);
+        $this->assertEquals(12, $data->grades[1]['grade']);
+        $this->assertEquals('Hello', $data->grades[1]['feedback']);
+        $this->assertEquals(transform::yesno(true), $data->grades[1]['created_or_modified_by_you']);
+
+        // Here we are testing the export of history of grades that we've changed.
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'grades_history');
+        $this->assertCount(3, $data->modified_records);
+        $grade = $data->modified_records[0];
+        $this->assertEquals($ug1->id, $grade['userid']);
+        $this->assertEquals($gi1->get_name(), $grade['item']);
+        $this->assertEquals(100, $grade['grade']);
+        $this->assertEquals('Well done!', $grade['feedback']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(true), $grade['author_of_change_was_you']);
+        $grade = $data->modified_records[1];
+        $this->assertEquals($ug1->id, $grade['userid']);
+        $this->assertEquals($gi1->get_name(), $grade['item']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(true), $grade['author_of_change_was_you']);
+        $grade = $data->modified_records[2];
+        $this->assertEquals($ug3->id, $grade['userid']);
+        $this->assertEquals($gi1->get_name(), $grade['item']);
+        $this->assertEquals(12, $grade['grade']);
+        $this->assertEquals('Hello', $grade['feedback']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(true), $grade['author_of_change_was_you']);
+
+        // Create a history record with logged user.
+        $this->setUser($ua3);
+        $gi1->update_final_grade($ug3->id, 50, 'test', '...', FORMAT_PLAIN, $ua2->id);
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($ua3, 'core_grades', [$c1ctx->id]));
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'grades_history');
+        $this->assertCount(1, $data->modified_records);
+        $grade = $data->modified_records[0];
+        $this->assertEquals($ug3->id, $grade['userid']);
+        $this->assertEquals($gi1->get_name(), $grade['item']);
+        $this->assertEquals(50, $grade['grade']);
+        $this->assertEquals('...', $grade['feedback']);
+        $this->assertEquals(transform::yesno(true), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['author_of_change_was_you']);
+
+        // Test that we export our own grades.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($ug1, 'core_grades', [$c1ctx->id]));
+        $data = writer::with_context($c1ctx)->get_data($rootpath);
+        $this->assert_context_has_no_data($c2ctx);
+        $this->assertCount(2, $data->grades);
+        $grade = $data->grades[0];
+        $this->assertEquals($ci1->get_name(), $grade['item']);
+        $this->assertEquals(1, $grade['grade']);
+        $grade = $data->grades[1];
+        $this->assertEquals($gi1->get_name(), $grade['item']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+
+        // Test that we export our own grades in two courses.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($ug1, 'core_grades', [$ug1ctx->id, $c1ctx->id, $c2ctx->id]));
+        $this->assert_context_has_no_data($ug1ctx);
+        $data = writer::with_context($c1ctx)->get_data($rootpath);
+        $this->assertCount(2, $data->grades);
+        $grade = $data->grades[0];
+        $this->assertEquals($ci1->get_name(), $grade['item']);
+        $this->assertEquals(1, $grade['grade']);
+        $grade = $data->grades[1];
+        $this->assertEquals($gi1->get_name(), $grade['item']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+
+        $data = writer::with_context($c2ctx)->get_data($rootpath);
+        $this->assertCount(3, $data->grades);
+        $grade = $data->grades[0];
+        $this->assertEquals($ci2->get_name(), $grade['item']);
+        $grade = $data->grades[1];
+        $this->assertEquals($gi2a->get_name(), $grade['item']);
+        $this->assertEquals(15, $grade['grade']);
+        $this->assertEquals('', $grade['feedback']);
+        $grade = $data->grades[2];
+        $this->assertEquals($gi2b->get_name(), $grade['item']);
+        $this->assertEquals(30, $grade['grade']);
+        $this->assertEquals('Well played!', $grade['feedback']);
+
+        // Delete a grade item.
+        $this->setUser($ua3);
+        $gi1->delete();
+
+        // Now, we should find history of grades in our own context.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($ug1, 'core_grades', [$ug1ctx->id, $c1ctx->id, $c2ctx->id]));
+        $data = writer::with_context($c1ctx)->get_data($rootpath);
+        $this->assertCount(1, $data->grades);
+        $this->assertEquals($ci1->get_name(), $data->grades[0]['item']);
+        $data = writer::with_context($c2ctx)->get_data($rootpath);
+        $this->assertCount(3, $data->grades);
+        $data = writer::with_context($ug1ctx)->get_related_data($rootpath, 'history');
+        $this->assertCount(3, $data->grades);
+        $grade = $data->grades[0];
+        $this->assertEquals(get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'), $grade['name']);
+        $this->assertEquals(100, $grade['grade']);
+        $this->assertEquals('Well done!', $grade['feedback']);
+        $this->assertEquals(transform::yesno(true), $grade['graded_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['author_of_change_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactioninsert', 'core_grades'), $grade['action']);
+        $grade = $data->grades[1];
+        $this->assertEquals(get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'), $grade['name']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+        $this->assertEquals(transform::yesno(true), $grade['graded_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['author_of_change_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactionupdate', 'core_grades'), $grade['action']);
+        $grade = $data->grades[2];
+        $this->assertEquals(get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'), $grade['name']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+        $this->assertEquals(transform::yesno(true), $grade['graded_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['author_of_change_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), $grade['action']);
+
+        // The action user 3 should have a record of the deletion in the user's context.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($ua3, 'core_grades', [$ug1ctx->id]));
+        $data = writer::with_context($ug1ctx)->get_related_data($rootpath, 'history');
+        $this->assertCount(1, $data->grades);
+        $grade = $data->grades[0];
+        $this->assertEquals(get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'), $grade['name']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+        $this->assertEquals(transform::yesno(true), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(false), $grade['author_of_change_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), $grade['action']);
+
+        // The action user 2 should have a record of their edits in the user's context.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($ua2, 'core_grades', [$ug1ctx->id]));
+        $data = writer::with_context($ug1ctx)->get_related_data($rootpath, 'history');
+        $this->assertCount(3, $data->grades);
+        $grade = $data->grades[0];
+        $this->assertEquals(get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'), $grade['name']);
+        $this->assertEquals(100, $grade['grade']);
+        $this->assertEquals('Well done!', $grade['feedback']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(true), $grade['author_of_change_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactioninsert', 'core_grades'), $grade['action']);
+        $grade = $data->grades[1];
+        $this->assertEquals(get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'), $grade['name']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(true), $grade['author_of_change_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactionupdate', 'core_grades'), $grade['action']);
+        $grade = $data->grades[2];
+        $this->assertEquals(get_string('privacy:request:unknowndeletedgradeitem', 'core_grades'), $grade['name']);
+        $this->assertEquals(1, $grade['grade']);
+        $this->assertEquals('Hi', $grade['feedback']);
+        $this->assertEquals(transform::yesno(false), $grade['logged_in_user_was_you']);
+        $this->assertEquals(transform::yesno(true), $grade['author_of_change_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), $grade['action']);
+    }
+
+    public function test_export_data_for_user_with_scale() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course();
+        $scale = $dg->create_scale(['scale' => 'Awesome,OK,Reasonable,Bad']);
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+
+        $u1ctx = context_user::instance($u1->id);
+        $c1ctx = context_course::instance($c1->id);
+
+        $rootpath = [get_string('grades', 'core_grades')];
+
+        // Create another set for another user.
+        $gi1 = new grade_item($dg->create_grade_item(['courseid' => $c1->id, 'scaleid' => $scale->id]), false);
+        $gi1->update_final_grade($u1->id, 1, 'test', '', FORMAT_PLAIN, $u2->id);
+        $gi2 = new grade_item($dg->create_grade_item(['courseid' => $c1->id, 'scaleid' => $scale->id]), false);
+        $gi2->update_final_grade($u1->id, 3, 'test', '', FORMAT_PLAIN, $u2->id);
+
+        // Export user's data.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u1, 'core_grades', [$c1ctx->id]));
+        $data = writer::with_context($c1ctx)->get_data($rootpath);
+        $this->assertCount(3, $data->grades);
+        $this->assertEquals(grade_item::fetch_course_item($c1->id)->get_name(), $data->grades[0]['item']);
+        $this->assertEquals($gi1->get_name(), $data->grades[1]['item']);
+        $this->assertEquals(1, $data->grades[1]['grade']);
+        $this->assertEquals('Awesome', $data->grades[1]['grade_formatted']);
+        $this->assertEquals($gi2->get_name(), $data->grades[2]['item']);
+        $this->assertEquals(3, $data->grades[2]['grade']);
+        $this->assertEquals('Reasonable', $data->grades[2]['grade_formatted']);
+    }
+
+    public function test_export_data_for_user_about_gradebook_edits() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $u5 = $dg->create_user();
+        $u6 = $dg->create_user();
+
+        $sysctx = context_system::instance();
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+        $u3ctx = context_user::instance($u3->id);
+        $u4ctx = context_user::instance($u4->id);
+        $u5ctx = context_user::instance($u5->id);
+        $u6ctx = context_user::instance($u6->id);
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        $rootpath = [get_string('grades', 'core_grades')];
+        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
+        $allcontexts = [$sysctx->id, $c1ctx->id, $c2ctx->id, $u1ctx->id, $u2ctx->id, $u3ctx->id, $u4ctx->id,
+            $u5ctx->id, $u6ctx->id];
+        $updateactionstr = get_string('privacy:request:historyactionupdate', 'core_grades');
+
+        // Create some stuff.
+        $gi1a = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi1b = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi2a = new grade_item($dg->create_grade_item(['courseid' => $c2->id]), false);
+        $gc1a = new grade_category($dg->create_grade_category(['courseid' => $c1->id]), false);
+        $gc1b = new grade_category($dg->create_grade_category(['courseid' => $c1->id]), false);
+        $gc2a = new grade_category($dg->create_grade_category(['courseid' => $c2->id]), false);
+        $go2 = new grade_outcome($dg->create_grade_outcome(['courseid' => $c2->id, 'shortname' => 'go2',
+            'fullname' => 'go2']), false);
+
+        $go0 = new grade_outcome(['shortname' => 'go0', 'fullname' => 'go0', 'usermodified' => $u1->id]);
+        $go0->insert();
+        $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]);
+        $go1->insert();
+
+        // User 2 creates history.
+        $this->setUser($u2);
+        $go0->shortname .= ' edited';
+        $go0->update();
+        $gc1a->fullname .= ' edited';
+        $gc1a->update();
+
+        // User 3 creates history.
+        $this->setUser($u3);
+        $go1->shortname .= ' edited';
+        $go1->update();
+        $gc2a->fullname .= ' a';
+        $gc2a->update();
+
+        // User 4 updates an outcome in course (creates history).
+        $this->setUser($u4);
+        $go2->shortname .= ' edited';
+        $go2->update();
+
+        // User 5 updates an item.
+        $this->setUser($u5);
+        $gi1a->itemname .= ' edited';
+        $gi1a->update();
+
+        // User 6 creates history.
+        $this->setUser($u6);
+        $gi2a->delete();
+
+        $this->setAdminUser();
+
+        // Export data for u1.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u1, 'core_grades', $allcontexts));
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'outcomes');
+        $this->assertCount(1, $data->outcomes);
+        $this->assertEquals($go0->shortname, $data->outcomes[0]['shortname']);
+        $this->assertEquals($go0->fullname, $data->outcomes[0]['fullname']);
+        $this->assertEquals(transform::yesno(true), $data->outcomes[0]['created_or_modified_by_you']);
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'outcomes');
+        $this->assertCount(1, $data->outcomes);
+        $this->assertEquals($go1->shortname, $data->outcomes[0]['shortname']);
+        $this->assertEquals($go1->fullname, $data->outcomes[0]['fullname']);
+        $this->assertEquals(transform::yesno(true), $data->outcomes[0]['created_or_modified_by_you']);
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'outcomes_history');
+        $this->assertEmpty($data);
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'outcomes_history');
+        $this->assertEmpty($data);
+
+        // Export data for u2.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u2, 'core_grades', $allcontexts));
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'outcomes');
+        $this->assertEmpty($data);
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'outcomes');
+        $this->assertEmpty($data);
+        $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'outcomes_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($go0->shortname, $data->modified_records[0]['shortname']);
+        $this->assertEquals($go0->fullname, $data->modified_records[0]['fullname']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
+        $this->assertEquals($updateactionstr, $data->modified_records[0]['action']);
+
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'categories_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($gc1a->fullname, $data->modified_records[0]['name']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
+        $this->assertEquals($updateactionstr, $data->modified_records[0]['action']);
+
+        // Export data for u3.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u3, 'core_grades', $allcontexts));
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'outcomes_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($go1->shortname, $data->modified_records[0]['shortname']);
+        $this->assertEquals($go1->fullname, $data->modified_records[0]['fullname']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
+        $this->assertEquals($updateactionstr, $data->modified_records[0]['action']);
+
+        $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'categories_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($gc2a->fullname, $data->modified_records[0]['name']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
+        $this->assertEquals($updateactionstr, $data->modified_records[0]['action']);
+
+        // Export data for u4.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u4, 'core_grades', $allcontexts));
+        $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'outcomes_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($go2->shortname, $data->modified_records[0]['shortname']);
+        $this->assertEquals($go2->fullname, $data->modified_records[0]['fullname']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
+        $this->assertEquals($updateactionstr, $data->modified_records[0]['action']);
+
+        // Export data for u5.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u5, 'core_grades', $allcontexts));
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'items_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($gi1a->itemname, $data->modified_records[0]['name']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
+        $this->assertEquals($updateactionstr, $data->modified_records[0]['action']);
+
+        // Export data for u6.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u6, 'core_grades', $allcontexts));
+        $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'items_history');
+        $this->assertEmpty($data);
+        $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'items_history');
+        $this->assertCount(1, $data->modified_records);
+        $this->assertEquals($gi2a->itemname, $data->modified_records[0]['name']);
+        $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
+        $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'),
+            $data->modified_records[0]['action']);
+    }
+
+    /**
+     * Assert there is no grade data in the context.
+     *
+     * @param context $context The context.
+     * @return void
+     */
+    protected function assert_context_has_no_data(context $context) {
+        $rootpath = [get_string('grades', 'core_grades')];
+        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
+
+        $data = writer::with_context($context)->get_data($rootpath);
+        $this->assertEmpty($data);
+
+        $data = writer::with_context($context)->get_related_data($rootpath, 'history');
+        $this->assertEmpty($data);
+
+        $files = ['categories_history', 'items_history', 'outcomes', 'outcomes_history', 'grades', 'grades_history', 'history'];
+        foreach ($files as $file) {
+            $data = writer::with_context($context)->get_related_data($relatedtomepath, $file);
+            $this->assertEmpty($data);
+        }
+    }
+}
index c5185e2..b358466 100644 (file)
@@ -138,6 +138,14 @@ $string['passwordhandling'] = 'Password field handling';
 $string['plaintext'] = 'Plain text';
 $string['pluginnotenabled'] = 'Authentication plugin \'{$a}\' is not enabled.';
 $string['pluginnotinstalled'] = 'Authentication plugin \'{$a}\' is not installed.';
+$string['privacy:metadata:userpref:createpassword'] = 'Indicates that a password should be generated for the user';
+$string['privacy:metadata:userpref:forcepasswordchange'] = 'Indicates whether the user should change their password upon logging in';
+$string['privacy:metadata:userpref:loginfailedcount'] = 'The number of times the user failed to log in';
+$string['privacy:metadata:userpref:loginfailedcountsincesuccess'] = 'The number of times the user failed to login since their last successful login';
+$string['privacy:metadata:userpref:loginfailedlast'] = 'The date at which the last failed login attempt was recorded';
+$string['privacy:metadata:userpref:loginlockout'] = 'Indicates whether the user\'s account is locked due to failed login attempts, and the date at which the account entered the lockout state';
+$string['privacy:metadata:userpref:loginlockoutignored'] = 'Indicates that a user\'s account should never be subject to lockouts';
+$string['privacy:metadata:userpref:loginlockoutsecret'] = 'When locked, the secret the user must use for unlocking their account';
 $string['potentialidps'] = 'Log in using your account on:';
 $string['recaptcha'] = 'reCAPTCHA';
 $string['recaptcha_help'] = 'The CAPTCHA is for preventing abuse from automated programs. Follow the instructions to verify you are a person. This could be a box to check, characters presented in an image you must enter or a set of images to select from.
index f4ed7e4..3c95526 100644 (file)
@@ -61,6 +61,7 @@ $string['listheader_multi_or'] = 'any';
 $string['manageplugins'] = 'Manage restrictions';
 $string['missingplugin'] = 'Missing restriction plugin';
 $string['or'] = 'or';
+$string['privacy:metadata'] = 'The Access restriction subsystem does not store any personal data.';
 $string['restrictaccess'] = 'Restrict access';
 $string['restrictbygroup'] = 'Add group/grouping access restriction';
 $string['setheading'] = '{$a->number} Set of {$a->count} restriction(s)';
index d35ddcb..70906e1 100644 (file)
@@ -137,6 +137,7 @@ $string['nativelocking'] = 'This plugin handles its own locking.';
 $string['none'] = 'None';
 $string['plugin'] = 'Plugin';
 $string['pluginsummaries'] = 'Installed cache stores';
+$string['privacy:metadata:cachestore'] = 'The Cache subsystem stores data temporarily on behalf of other parts of Moodle. This data is not easily identifiable, and is very short lived. It serves as a cache of data stored elsewhere in Moodle, and should therefore already be handled by those Moodle components.';
 $string['purge'] = 'Purge';
 $string['purgedefinitionsuccess'] = 'Successfully purged the requested definition.';
 $string['purgestoresuccess'] = 'Successfully purged the requested store.';
index 0e8aff2..aec0710 100644 (file)
@@ -614,6 +614,29 @@ $string['prefletters'] = 'Grade letters and boundaries';
 $string['prefrows'] = 'Special rows';
 $string['prefshow'] = 'Show/hide toggles';
 $string['previewrows'] = 'Preview rows';
+$string['privacy:metadata:categorieshistory'] = 'A record of previous versions of grade categories';
+$string['privacy:metadata:grades'] = 'A record of grades';
+$string['privacy:metadata:grades:aggregationstatus'] = 'The aggregation status';
+$string['privacy:metadata:grades:aggregationweight'] = 'The weight in aggregation';
+$string['privacy:metadata:grades:feedback'] = 'The feedback';
+$string['privacy:metadata:grades:finalgrade'] = 'The grade';
+$string['privacy:metadata:grades:information'] = 'Some information additional information';
+$string['privacy:metadata:grades:timemodified'] = 'Time at which the grade was last modified';
+$string['privacy:metadata:grades:userid'] = 'The ID of the user whose grade it is';
+$string['privacy:metadata:grades:usermodified'] = 'The ID of the user who last modified the record';
+$string['privacy:metadata:gradeshistory'] = 'A record of the previous grades';
+$string['privacy:metadata:history:loggeduser'] = 'The ID of the user who was logged in when the versioning occurred';
+$string['privacy:metadata:history:timemodified'] = 'Time at which the versioning occurred';
+$string['privacy:metadata:itemshistory'] = 'A record of previous versions of grade items';
+$string['privacy:metadata:outcomes'] = 'A record of outcomes';
+$string['privacy:metadata:outcomes:timemodified'] = 'Time at which the record was modified';
+$string['privacy:metadata:outcomes:usermodified'] = 'The user who last modified the record';
+$string['privacy:metadata:outcomeshistory'] = 'A record of previous versions of outcomes';
+$string['privacy:path:relatedtome'] = 'Related to me';
+$string['privacy:request:historyactiondelete'] = 'Delete';
+$string['privacy:request:historyactioninsert'] = 'Insert';
+$string['privacy:request:historyactionupdate'] = 'Update';
+$string['privacy:request:unknowndeletedgradeitem'] = 'Unknown (the grade item was deleted)';
 $string['profilereport'] = 'User profile report';
 $string['profilereport_help'] = 'Grade report used on user profile page.';
 $string['publishing'] = 'Publishing';
index aeb331f..9c1f8d8 100644 (file)
@@ -141,6 +141,22 @@ $string['postrestparam'] = 'PHP code for REST (POST request)';
 $string['potusers'] = 'Not authorised users';
 $string['potusersmatching'] = 'Not authorised users matching';
 $string['print'] = 'Print all';
+$string['privacy:metadata:serviceusers'] = 'A list of users who can use a certain external services';
+$string['privacy:metadata:serviceusers:iprestriction'] = 'IP restricted to use the service';
+$string['privacy:metadata:serviceusers:timecreated'] = 'The date at which the record was created';
+$string['privacy:metadata:serviceusers:userid'] = 'The ID of the user';
+$string['privacy:metadata:serviceusers:validuntil'] = 'The date at which the authorisation ends';
+$string['privacy:metadata:tokens'] = 'A record of tokens for interacting with Moodle through web services or Mobile applications.';
+$string['privacy:metadata:tokens:creatorid'] = 'The ID of the user who created the token';
+$string['privacy:metadata:tokens:iprestriction'] = 'IP restricted to use this token';
+$string['privacy:metadata:tokens:lastaccess'] = 'The date at which the token was last used';
+$string['privacy:metadata:tokens:privatetoken'] = 'A more private token occasionally used to validate certain operations, such as SSO.';
+$string['privacy:metadata:tokens:timecreated'] = 'The date at which the token was created';
+$string['privacy:metadata:tokens:token'] = 'The user\'s token';
+$string['privacy:metadata:tokens:tokentype'] = 'The type of token';
+$string['privacy:metadata:tokens:userid'] = 'The ID of the user whose token it is';
+$string['privacy:metadata:tokens:validuntil'] = 'The date at which the token becomes invalid';
+$string['privacy:request:notexportedsecurity'] = 'Not exported for security reasons';
 $string['protocol'] = 'Protocol';
 $string['removefunction'] = 'Remove';
 $string['removefunctionconfirm'] = 'Do you really want to remove function "{$a->function}" from service "{$a->service}"?';
index c569cf6..360fee1 100644 (file)
@@ -24,6 +24,11 @@ information provided here is intended especially for developers.
   act as a site policy handler.
 * There is a new is_fulltext_search_supported() DML function. The default implementation returns false. This function
   is used by 'Simple search' global search engine to determine if the database full-text search capabilities can be used.
+* The following have been removed from the list of core subsystems:
+   - core_register
+   - core_publish
+  Following this change, \core_register_renderer and \core_publish_renderer have been removed and their methods have been
+  moved to \core_admin_renderer and \core_course_renderer respectively.
 
 === 3.4 ===
 
index c930d4f..c0d82a8 100644 (file)
@@ -63,6 +63,21 @@ function profiling_is_saved($value = null) {
     return $saved;
 }
 
+/**
+ * Whether PHP profiling is available.
+ *
+ * This check ensures that one of the available PHP Profiling extensions is available.
+ *
+ * @return  bool
+ */
+function profiling_available() {
+    $hasextension = extension_loaded('tideways_xhprof');
+    $hasextension = $hasextension || extension_loaded('tideways');
+    $hasextension = $hasextension || extension_loaded('xhprof');
+
+    return $hasextension;
+}
+
 /**
  * Start profiling observing all the configuration
  */
@@ -70,7 +85,7 @@ function profiling_start() {
     global $CFG, $SESSION, $SCRIPT;
 
     // If profiling isn't available, nothing to start
-    if (!extension_loaded('xhprof') && !extension_loaded('tideways')) {
+    if (!profiling_available()) {
         return false;
     }
 
@@ -147,7 +162,9 @@ function profiling_start() {
 
     // Arrived here, the script is going to be profiled, let's do it
     $ignore = array('call_user_func', 'call_user_func_array');
-    if (extension_loaded('tideways')) {
+    if (extension_loaded('tideways_xhprof')) {
+        tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU + TIDEWAYS_XHPROF_FLAGS_MEMORY);
+    } else if (extension_loaded('tideways')) {
         tideways_enable(TIDEWAYS_FLAGS_CPU + TIDEWAYS_FLAGS_MEMORY, array('ignored_functions' =>  $ignore));
     } else {
         xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY, array('ignored_functions' => $ignore));
@@ -165,7 +182,7 @@ function profiling_stop() {
     global $CFG, $DB, $SCRIPT;
 
     // If profiling isn't available, nothing to stop
-    if (!extension_loaded('xhprof') && !extension_loaded('tideways')) {
+    if (!profiling_available()) {
         return false;
     }
 
@@ -184,7 +201,9 @@ function profiling_stop() {
 
     // Arrived here, profiling is running, stop and save everything
     profiling_is_running(false);
-    if (extension_loaded('tideways')) {
+    if (extension_loaded('tideways_xhprof')) {
+        $data = tideways_xhprof_disable();
+    } else if (extension_loaded('tideways')) {
         $data = tideways_disable();
     } else {
         $data = xhprof_disable();
index 01c5a3b..c04eba9 100644 (file)
@@ -19,7 +19,7 @@
         {{blocktime}}
     </div>
 {{/displayblocktime}}
-<div class="message row-fluid"
+<div class="message d-flex row-fluid"
     data-region="message"
     data-blocktime="{{blocktime}}"
     data-id="{{id}}{{isread}}"
diff --git a/mod/lti/classes/privacy/provider.php b/mod/lti/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..1afabf6
--- /dev/null
@@ -0,0 +1,369 @@
+<?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 Subsystem implementation for mod_lti.
+ *
+ * @package    mod_lti
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_lti\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\helper;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem implementation for mod_lti.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Return the fields which contain personal data.
+     *
+     * @param collection $items a reference to the collection to use to store the metadata.
+     * @return collection the updated collection of metadata items.
+     */
+    public static function get_metadata(collection $items) : collection {
+        $items->add_database_table(
+            'lti_submission',
+            [
+                'userid' => 'privacy:metadata:lti_submission:userid',
+                'datesubmitted' => 'privacy:metadata:lti_submission:datesubmitted',
+                'dateupdated' => 'privacy:metadata:lti_submission:dateupdated',
+                'gradepercent' => 'privacy:metadata:lti_submission:gradepercent',
+                'originalgrade' => 'privacy:metadata:lti_submission:originalgrade',
+            ],
+            'privacy:metadata:lti_submission'
+        );
+
+        $items->add_database_table(
+            'lti_tool_proxies',
+            [
+                'name' => 'privacy:metadata:lti_tool_proxies:name',
+                'createdby' => 'privacy:metadata:createdby',
+                'timecreated' => 'privacy:metadata:timecreated',
+                'timemodified' => 'privacy:metadata:timemodified'
+            ],
+            'privacy:metadata:lti_tool_proxies'
+        );
+
+        $items->add_database_table(
+            'lti_types',
+            [
+                'name' => 'privacy:metadata:lti_types:name',
+                'createdby' => 'privacy:metadata:createdby',
+                'timecreated' => 'privacy:metadata:timecreated',
+                'timemodified' => 'privacy:metadata:timemodified'
+            ],
+            'privacy:metadata:lti_types'
+        );
+
+        return $items;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid the userid.
+     * @return contextlist the list of contexts containing user info for the user.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        // Fetch all LTI submissions.
+        $sql = "SELECT c.id
+                  FROM {context} c
+            INNER JOIN {course_modules} cm
+                    ON cm.id = c.instanceid
+                   AND c.contextlevel = :contextlevel
+            INNER JOIN {modules} m
+                    ON m.id = cm.module
+                   AND m.name = :modname
+            INNER JOIN {lti} lti
+                    ON lti.id = cm.instance
+            INNER JOIN {lti_submission} ltisub
+                    ON ltisub.ltiid = lti.id
+                 WHERE ltisub.userid = :userid";
+
+        $params = [
+            'modname' => 'lti',
+            'contextlevel' => CONTEXT_MODULE,
+            'userid' => $userid,
+        ];
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+
+        // Fetch all LTI types.
+        $sql = "SELECT c.id
+                 FROM {context} c
+                 JOIN {course} course
+                   ON c.contextlevel = :contextlevel
+                  AND c.instanceid = course.id
+                 JOIN {lti_types} ltit
+                   ON ltit.course = course.id
+                WHERE ltit.createdby = :userid";
+
+        $params = [
+            'contextlevel' => CONTEXT_COURSE,
+            'userid' => $userid
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
+        // The LTI tool proxies sit in the system context.
+        $contextlist->add_system_context();
+
+        return $contextlist;
+    }
+
+    /**
+     * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
+     *
+     * @param approved_contextlist $contextlist a list of contexts approved for export.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        self::export_user_data_lti_submissions($contextlist);
+
+        self::export_user_data_lti_types($contextlist);
+
+        self::export_user_data_lti_tool_proxies($contextlist);
+    }
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param \context $context the context to delete in.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        global $DB;
+
+        if (!$context instanceof \context_module) {
+            return;
+        }
+
+        $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
+        $DB->delete_records('lti_submission', ['ltiid' => $instanceid]);
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist a list of contexts approved for deletion.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        global $DB;
+
+        if (empty($contextlist->count())) {
+            return;
+        }
+
+        $userid = $contextlist->get_user()->id;
+        foreach ($contextlist->get_contexts() as $context) {
+            if (!$context instanceof \context_module) {
+                return;
+            }
+            $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
+            $DB->delete_records('lti_submission', ['ltiid' => $instanceid, 'userid' => $userid]);
+        }
+    }
+
+    /**
+     * Export personal data for the given approved_contextlist related to LTI submissions.
+     *
+     * @param approved_contextlist $contextlist a list of contexts approved for export.
+     */
+    protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) {
+        global $DB;
+
+        // Filter out any contexts that are not related to modules.
+        $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_MODULE) {
+                $carry[] = $context->instanceid;
+            }
+            return $carry;
+        }, []);
+
+        if (empty($cmids)) {
+            return;
+        }
+
+        $user = $contextlist->get_user();
+
+        // Get all the LTI activities associated with the above course modules.
+        $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids);
+        $ltiids = array_keys($ltiidstocmids);
+
+        list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED);
+        $params = array_merge($inparams, ['userid' => $user->id]);
+        $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id');
+        self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) {
+            $carry[] = [
+                'gradepercent' => $record->gradepercent,
+                'originalgrade' => $record->originalgrade,
+                'datesubmitted' => transform::datetime($record->datesubmitted),
+                'dateupdated' => transform::datetime($record->dateupdated)
+            ];
+            return $carry;
+        }, function($ltiid, $data) use ($user, $ltiidstocmids) {
+            $context = \context_module::instance($ltiidstocmids[$ltiid]);
+            $contextdata = helper::get_context_data($context, $user);
+            $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]);
+            helper::export_context_files($context, $user);
+            writer::with_context($context)->export_data([], $finaldata);
+        });
+    }
+
+    /**
+     * Export personal data for the given approved_contextlist related to LTI types.
+     *
+     * @param approved_contextlist $contextlist a list of contexts approved for export.
+     */
+    protected static function export_user_data_lti_types(approved_contextlist $contextlist) {
+        global $DB;
+
+        // Filter out any contexts that are not related to courses.
+        $courseids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_COURSE) {
+                $carry[] = $context->instanceid;
+            }
+            return $carry;
+        }, []);
+
+        if (empty($courseids)) {
+            return;
+        }
+
+        $user = $contextlist->get_user();
+
+        list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+        $params = array_merge($inparams, ['userid' => $user->id]);
+        $ltitypes = $DB->get_recordset_select('lti_types', "course $insql AND createdby = :userid", $params, 'timecreated ASC');
+        self::recordset_loop_and_export($ltitypes, 'course', [], function($carry, $record) {
+            $context = \context_course::instance($record->course);
+            $options = ['context' => $context];
+            $carry[] = [
+                'name' => format_string($record->name, true, $options),
+                'createdby' => transform::user($record->createdby),
+                'timecreated' => transform::datetime($record->timecreated),
+                'timemodified' => transform::datetime($record->timemodified)
+            ];
+            return $carry;
+        }, function($courseid, $data) {
+            $context = \context_course::instance($courseid);
+            $finaldata = (object) ['lti_types' => $data];
+            writer::with_context($context)->export_data([], $finaldata);
+        });
+    }
+
+    /**
+     * Export personal data for the given approved_contextlist related to LTI tool proxies.
+     *
+     * @param approved_contextlist $contextlist a list of contexts approved for export.
+     */
+    protected static function export_user_data_lti_tool_proxies(approved_contextlist $contextlist) {
+        global $DB;
+
+        // Filter out any contexts that are not related to system context.
+        $systemcontexts = array_filter($contextlist->get_contexts(), function($context) {
+            return $context->contextlevel == CONTEXT_SYSTEM;
+        });
+
+        if (empty($systemcontexts)) {
+            return;
+        }
+
+        $user = $contextlist->get_user();
+
+        $systemcontext = \context_system::instance();
+
+        $data = [];
+        $ltiproxies = $DB->get_recordset('lti_tool_proxies', ['createdby' => $user->id], 'timecreated ASC');
+        foreach ($ltiproxies as $ltiproxy) {
+            $data[] = [
+                'name' => format_string($ltiproxy->name, true, $systemcontext),
+                'createdby' => transform::user($ltiproxy->createdby),
+                'timecreated' => transform::datetime($ltiproxy->timecreated),
+                'timemodified' => transform::datetime($ltiproxy->timemodified)
+            ];
+        }
+        $ltiproxies->close();
+
+        $finaldata = (object) ['lti_tool_proxies' => $data];
+        writer::with_context($systemcontext)->export_data([], $finaldata);
+    }
+
+    /**
+     * Return a dict of LTI IDs mapped to their course module ID.
+     *
+     * @param array $cmids The course module IDs.
+     * @return array In the form of [$ltiid => $cmid].
+     */
+    protected static function get_lti_ids_to_cmids_from_cmids(array $cmids) {
+        global $DB;
+
+        list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
+        $sql = "SELECT lti.id, cm.id AS cmid
+                 FROM {lti} lti
+                 JOIN {modules} m
+                   ON m.name = :lti
+                 JOIN {course_modules} cm
+                   ON cm.instance = lti.id
+                  AND cm.module = m.id
+                WHERE cm.id $insql";
+        $params = array_merge($inparams, ['lti' => 'lti']);
+
+        return $DB->get_records_sql_menu($sql, $params);
+    }
+
+    /**
+     * Loop and export from a recordset.
+     *
+     * @param \moodle_recordset $recordset The recordset.
+     * @param string $splitkey The record key to determine when to export.
+     * @param mixed $initial The initial data to reduce from.
+     * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
+     * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
+     * @return void
+     */
+    protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
+                                                        callable $reducer, callable $export) {
+        $data = $initial;
+        $lastid = null;
+
+        foreach ($recordset as $record) {
+            if ($lastid && $record->{$splitkey} != $lastid) {
+                $export($lastid, $data);
+                $data = $initial;
+            }
+            $data = $reducer($data, $record);
+            $lastid = $record->{$splitkey};
+        }
+        $recordset->close();
+
+        if (!empty($lastid)) {
+            $export($lastid, $data);
+        }
+    }
+}
index 12e6800..b567b3a 100644 (file)
@@ -356,6 +356,19 @@ $string['preferwidget'] = 'Prefer widget launch';
 $string['preferwidth'] = 'Preferred width';
 $string['press_to_submit'] = 'Press to launch this activity';
 $string['privacy'] = 'Privacy';
+$string['privacy:metadata:createdby'] = 'The user who created the record';
+$string['privacy:metadata:lti_submission'] = 'LTI submission';
+$string['privacy:metadata:lti_submission:datesubmitted'] = 'The timestamp indicating when the submission was made';
+$string['privacy:metadata:lti_submission:dateupdated'] = 'The timestamp indicating when the submission was modified';
+$string['privacy:metadata:lti_submission:gradepercent'] = 'The grade for the user as a percentage';
+$string['privacy:metadata:lti_submission:originalgrade'] = 'The original grade for the user';
+$string['privacy:metadata:lti_submission:userid'] = 'The ID of the user who submitted for the LTI activity';
+$string['privacy:metadata:lti_tool_proxies'] = 'LTI proxies';
+$string['privacy:metadata:lti_tool_proxies:name'] = 'LTI proxy name';
+$string['privacy:metadata:lti_types'] = 'LTI types';
+$string['privacy:metadata:lti_types:name'] = 'LTI type name';
+$string['privacy:metadata:timecreated'] = 'The date at which the record was created';
+$string['privacy:metadata:timemodified'] = 'The date at which the record was modified';
 $string['quickgrade'] = 'Allow quick grading';
 $string['quickgrade_help'] = 'If enabled, multiple tools can be graded on one page. Add grades and comments then click the "Save all my feedback" button to save all changes for that page.';
 $string['redirect'] = 'You will be redirected in few seconds. If you are not, press the button.';
diff --git a/mod/lti/service/gradebookservices/classes/privacy/provider.php b/mod/lti/service/gradebookservices/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..878fe6b
--- /dev/null
@@ -0,0 +1,94 @@
+<?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 Subsystem implementation for ltiservice_gradebookservices.
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_gradebookservices\privacy;
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for ltiservice_gradebookservices.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @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->link_external_location('External LTI provider.', [
+            'userid' => 'privacy:metadata:userid',
+            'grade' => 'privacy:metadata:grade',
+            'maxgrade' => 'privacy:metadata:maxgrade',
+            'feedback' => 'privacy:metadata:feedback',
+            'timemodified' => 'privacy:metadata:timemodified'
+        ], 'privacy:metadata:externalpurpose');
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        return new 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) {
+    }
+
+    /**
+     * Delete all user data which matches the specified context.
+     *
+     * @param \context $context A user context.
+     */
+    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.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
index e68fdc5..7d9a5d1 100644 (file)
@@ -34,5 +34,11 @@ $string['modulename'] = 'LTI Grades';
 $string['nevergs'] = 'Do not use this service';
 $string['partialgs'] = 'Use this service for grade sync only';
 $string['pluginname'] = 'LTI Assignment and Grade Services';
+$string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.';
+$string['privacy:metadata:feedback'] = 'The feedback the user received for this LTI activity.';
+$string['privacy:metadata:grade'] = 'The grade the user received in Moodle for this LTI activity.';
+$string['privacy:metadata:maxgrade'] = 'The max grade that can be achieved for this LTI activity.';
+$string['privacy:metadata:timemodified'] = 'The last time the grade was updated';
+$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
 $string['servicename'] = 'LTI Assignment and Grade Services';
 $string['taskcleanup'] = 'LTI Assignment and Grade Services table cleanup';
diff --git a/mod/lti/service/gradebookservices/tests/privacy_provider_test.php b/mod/lti/service/gradebookservices/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..d24c59c
--- /dev/null
@@ -0,0 +1,51 @@
+<?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/>.
+/**
+ * Unit tests for ltiservice_gradebookservices privacy provider.
+ *
+ * @package    ltiservice_gradebookservices
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\tests\provider_testcase;
+
+/**
+ * Unit tests for ltiservice_gradebookservices privacy provider.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class ltiservice_gradebookservices_privacy_provider_testcase extends provider_testcase {
+
+    /**
+     * Basic setup for these tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+        $user = $this->getDataGenerator()->create_user();
+        $contextlist = \ltiservice_gradebookservices\privacy\provider::get_contexts_for_userid($user->id);
+        $this->assertEmpty($contextlist);
+    }
+}
diff --git a/mod/lti/service/memberships/classes/privacy/provider.php b/mod/lti/service/memberships/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..83666d0
--- /dev/null
@@ -0,0 +1,95 @@
+<?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 Subsystem implementation for ltiservice_memberships.
+ *
+ * @package    ltiservice_memberships
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_memberships\privacy;
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for ltiservice_memberships.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @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->link_external_location('External LTI provider.', [
+            'userid' => 'privacy:metadata:userid',
+            'useridnumber' => 'privacy:metadata:useridnumber',
+            'fullname' => 'privacy:metadata:fullname',
+            'firstname' => 'privacy:metadata:firstname',
+            'lastname' => 'privacy:metadata:lastname',
+            'email' => 'privacy:metadata:email'
+        ], 'privacy:metadata:externalpurpose');
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        return new 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) {
+    }
+
+    /**
+     * Delete all user data which matches the specified context.
+     *
+     * @param \context $context A user context.
+     */
+    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.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
index cd8d220..8798cab 100644 (file)
@@ -28,4 +28,11 @@ $string['ltiservice_memberships'] = 'IMS LTI Membership: ';
 $string['ltiservice_memberships_help'] = 'Allow the tool to retrieve member\'s info from the course using the IMS LTI Membership Service. The privacy settings will apply.';
 $string['notallow'] = 'Do not use this service';
 $string['pluginname'] = 'Memberships LTI Service';
+$string['privacy:metadata:email'] = 'The email of the user using the LTI consumer.';
+$string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.';
+$string['privacy:metadata:firstname'] = 'The firstname of the user using the LTI consumer.';
+$string['privacy:metadata:fullname'] = 'The fullname of the user using the LTI consumer.';
+$string['privacy:metadata:lastname'] = 'The lastname of the user using the LTI consumer.';
+$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
+$string['privacy:metadata:useridnumber'] = 'The idnumber of the user using the LTI consumer.';
 $string['servicename'] = 'Memberships';
diff --git a/mod/lti/service/memberships/tests/privacy_provider_test.php b/mod/lti/service/memberships/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..a2e8d87
--- /dev/null
@@ -0,0 +1,51 @@
+<?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/>.
+/**
+ * Unit tests for ltiservice_memberships privacy provider.
+ *
+ * @package    ltiservice_memberships
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\tests\provider_testcase;
+
+/**
+ * Unit tests for ltiservice_memberships privacy provider.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class ltiservice_memberships_privacy_provider_testcase extends provider_testcase {
+
+    /**
+     * Basic setup for these tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+        $user = $this->getDataGenerator()->create_user();
+        $contextlist = \ltiservice_memberships\privacy\provider::get_contexts_for_userid($user->id);
+        $this->assertEmpty($contextlist);
+    }
+}
diff --git a/mod/lti/service/profile/classes/privacy/provider.php b/mod/lti/service/profile/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..f3407b1
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for ltiservice_profile.
+ *
+ * @package    ltiservice_profile
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_profile\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for ltiservice_profile implementing null_provider.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index 9001c1c..f7ab587 100644 (file)
@@ -24,3 +24,4 @@
  */
 
 $string['pluginname'] = 'Tool Consumer Profile LTI Service';
+$string['privacy:metadata'] = 'The Tool Consumer Profile LTI Service plugin does not store any personal data.';
diff --git a/mod/lti/service/toolproxy/classes/privacy/provider.php b/mod/lti/service/toolproxy/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..e73edf0
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for ltiservice_toolproxy.
+ *
+ * @package    ltiservice_toolproxy
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_toolproxy\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for ltiservice_toolproxy implementing null_provider.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index 4e61740..5b389a4 100644 (file)
@@ -24,3 +24,4 @@
  */
 
 $string['pluginname'] = 'Tool Proxy Service';
+$string['privacy:metadata'] = 'The Tool Proxy Service plugin does not store any personal data.';
diff --git a/mod/lti/service/toolsettings/classes/privacy/provider.php b/mod/lti/service/toolsettings/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..2161181
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 Subsystem implementation for ltiservice_toolsettings.
+ *
+ * @package    ltiservice_toolsettings
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_toolsettings\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for ltiservice_toolsettings implementing null_provider.
+ *
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index b4043a7..59f4bbe 100644 (file)
@@ -24,3 +24,4 @@
  */
 
 $string['pluginname'] = 'Tool Settings Service';
+$string['privacy:metadata'] = 'The Tool Settings Service plugin does not store any personal data.';
diff --git a/mod/lti/tests/privacy_provider_test.php b/mod/lti/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..63a545c
--- /dev/null
@@ -0,0 +1,313 @@<