Merge branch 'MDL-62135-master' of git://github.com/FMCorz/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 2 May 2018 09:37:47 +0000 (17:37 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 2 May 2018 09:37:47 +0000 (17:37 +0800)
146 files changed:
admin/tool/dataprivacy/classes/api.php
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]
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 [moved from admin/registration/classes/privacy/provider.php with 80% similarity]
badges/classes/privacy/provider.php [new file with mode: 0644]
badges/tests/privacy_test.php [new file with mode: 0644]
blocks/classes/privacy/provider.php [new file with mode: 0644]
blocks/comments/classes/privacy/provider.php [new file with mode: 0644]
blocks/comments/lang/en/block_comments.php
blocks/comments/tests/privacy_provider_test.php [new file with mode: 0644]
blocks/recent_activity/classes/privacy/provider.php [new file with mode: 0644]
blocks/recent_activity/lang/en/block_recent_activity.php
blocks/tests/privacy_test.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/moodleform_mod.php
files/classes/conversion.php
files/classes/privacy/provider.php [new file with mode: 0644]
files/classes/task/conversion_cleanup_task.php
files/converter/classes/privacy/provider.php [new file with mode: 0644]
files/tests/conversion_test.php
install/lang/el/error.php
install/lang/te/moodle.php
install/lang/tl/moodle.php
lang/en/auth.php
lang/en/availability.php
lang/en/badges.php
lang/en/block.php
lang/en/cache.php
lang/en/fileconverter.php [moved from lang/en/register.php with 68% similarity]
lang/en/files.php [new file with mode: 0644]
lang/en/webservice.php
lib/classes/component.php
lib/tests/component_test.php
message/templates/message_area_message.mustache
mod/assign/classes/privacy/assign_plugin_request_data.php [new file with mode: 0644]
mod/assign/classes/privacy/assignfeedback_provider.php [new file with mode: 0644]
mod/assign/classes/privacy/assignsubmission_provider.php [new file with mode: 0644]
mod/assign/classes/privacy/feedback_legacy_polyfill.php [new file with mode: 0644]
mod/assign/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/classes/privacy/submission_legacy_polyfill.php [new file with mode: 0644]
mod/assign/classes/privacy/useridlist.php [new file with mode: 0644]
mod/assign/feedback/comments/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/comments/lang/en/assignfeedback_comments.php
mod/assign/feedback/comments/tests/privacy_test.php [new file with mode: 0644]
mod/assign/feedback/editpdf/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/tests/privacy_test.php [new file with mode: 0644]
mod/assign/feedback/file/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/file/lang/en/assignfeedback_file.php
mod/assign/feedback/file/tests/privacy_test.php [new file with mode: 0644]
mod/assign/feedback/offline/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/feedback/offline/lang/en/assignfeedback_offline.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/submission/comments/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/submission/comments/lang/en/assignsubmission_comments.php
mod/assign/submission/comments/tests/privacy_test.php [new file with mode: 0644]
mod/assign/submission/file/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/tests/privacy_test.php [new file with mode: 0644]
mod/assign/submission/onlinetext/classes/privacy/provider.php [new file with mode: 0644]
mod/assign/submission/onlinetext/lang/en/assignsubmission_onlinetext.php
mod/assign/submission/onlinetext/tests/privacy_test.php [new file with mode: 0644]
mod/assign/tests/privacy_feedback_legacy_polyfill_test.php [new file with mode: 0644]
mod/assign/tests/privacy_submission_legacy_polyfill_test.php [new file with mode: 0644]
mod/assign/tests/privacy_test.php [new file with mode: 0644]
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
mod/scorm/classes/privacy/provider.php [new file with mode: 0644]
mod/scorm/lang/en/scorm.php
mod/scorm/report/basic/classes/privacy/provider.php [new file with mode: 0644]
mod/scorm/report/basic/lang/en/scormreport_basic.php
mod/scorm/report/basic/tests/privacy_test.php [new file with mode: 0644]
mod/scorm/report/graphs/classes/privacy/provider.php [new file with mode: 0644]
mod/scorm/report/graphs/lang/en/scormreport_graphs.php
mod/scorm/report/interactions/classes/privacy/provider.php [new file with mode: 0644]
mod/scorm/report/interactions/lang/en/scormreport_interactions.php
mod/scorm/report/interactions/tests/privacy_test.php [new file with mode: 0644]
mod/scorm/report/objectives/classes/privacy/provider.php [new file with mode: 0644]
mod/scorm/report/objectives/lang/en/scormreport_objectives.php
mod/scorm/report/objectives/tests/privacy_test.php [new file with mode: 0644]
mod/scorm/tests/privacy_test.php [new file with mode: 0644]
privacy/classes/local/request/moodle_content_writer.php
privacy/classes/tests/request/content_writer.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
privacy/tests/tests_content_writer_test.php
theme/boost/amd/build/drawer.min.js
theme/boost/amd/src/drawer.js
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/modules.scss
theme/boost/templates/core/filemanager_modal_generallayout.mustache
theme/boost/tests/behat/behat_theme_boost_behat_repository_upload.php
theme/bootstrapbase/less/moodle/backup-restore.less
theme/bootstrapbase/style/moodle.css
version.php
webservice/classes/privacy/provider.php [new file with mode: 0644]
webservice/tests/privacy_test.php [new file with mode: 0644]

index 9ec5f45..09583b2 100644 (file)
@@ -220,6 +220,7 @@ class api {
     public static function get_data_requests($userid = 0) {
         global $USER;
         $results = [];
+        $sort = 'status ASC, timemodified ASC';
         if ($userid) {
             // Get the data requests for the user or data requests made by the user.
             $select = "userid = :userid OR requestedby = :requestedby";
@@ -227,11 +228,11 @@ class api {
                 'userid' => $userid,
                 'requestedby' => $userid
             ];
-            $results = data_request::get_records_select($select, $params, 'status DESC, timemodified DESC');
+            $results = data_request::get_records_select($select, $params, $sort);
         } else {
             // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
             if (self::is_site_dpo($USER->id)) {
-                $results = data_request::get_records(null, 'status DESC, timemodified DESC', '');
+                $results = data_request::get_records(null, $sort, '');
             }
         }
 
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;
+    }
+
+}
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);
+    }
+}
similarity index 80%
rename from admin/registration/classes/privacy/provider.php
rename to availability/classes/privacy/provider.php
index c1c4ea4..bb8a3a7 100644 (file)
 //
 // 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_register.
+ * Privacy Subsystem implementation for core_availability.
  *
- * @package    core_register
- * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @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_register\privacy;
+
+namespace core_availability\privacy;
+
 defined('MOODLE_INTERNAL') || die();
+
 /**
- * Privacy Subsystem for core_register implementing null_provider.
+ * Privacy Subsystem for core_availability implementing null_provider.
  *
- * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @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.
diff --git a/badges/classes/privacy/provider.php b/badges/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..1b198cb
--- /dev/null
@@ -0,0 +1,511 @@
+<?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_badges
+ * @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_badges\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use badge;
+use context;
+use context_course;
+use context_helper;
+use context_system;
+use context_user;
+use core_text;
+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 . '/badgeslib.php');
+
+/**
+ * Data provider class.
+ *
+ * @package    core_badges
+ * @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('badge', [
+            'usercreated' => 'privacy:metadata:badge:usercreated',
+            'usermodified' => 'privacy:metadata:badge:usermodified',
+            'timecreated' => 'privacy:metadata:badge:timecreated',
+            'timemodified' => 'privacy:metadata:badge:timemodified',
+        ], 'privacy:metadata:badge');
+
+        $collection->add_database_table('badge_issued', [
+            'userid' => 'privacy:metadata:issued:userid',
+            'dateissued' => 'privacy:metadata:issued:dateissued',
+            'dateexpire' => 'privacy:metadata:issued:dateexpire',
+        ], 'privacy:metadata:issued');
+
+        $collection->add_database_table('badge_criteria_met', [
+            'userid' => 'privacy:metadata:criteriamet:userid',
+            'datemet' => 'privacy:metadata:criteriamet:datemet',
+        ], 'privacy:metadata:criteriamet');
+
+        $collection->add_database_table('badge_manual_award', [
+            'recipientid' => 'privacy:metadata:manualaward:recipientid',
+            'issuerid' => 'privacy:metadata:manualaward:issuerid',
+            'issuerrole' => 'privacy:metadata:manualaward:issuerrole',
+            'datemet' => 'privacy:metadata:manualaward:datemet',
+        ], 'privacy:metadata:manualaward');
+
+        $collection->add_database_table('badge_backpack', [
+            'userid' => 'privacy:metadata:backpack:userid',
+            'email' => 'privacy:metadata:backpack:email',
+            'backpackurl' => 'privacy:metadata:backpack:backpackurl',
+            'backpackuid' => 'privacy:metadata:backpack:backpackuid',
+            // The columns autosync and password are not used.
+        ], 'privacy:metadata:backpack');
+
+        $collection->add_external_location_link('backpacks', [
+            'name' => 'privacy:metadata:external:backpacks:badge',
+            'description' => 'privacy:metadata:external:backpacks:description',
+            'image' => 'privacy:metadata:external:backpacks:image',
+            'url' => 'privacy:metadata:external:backpacks:url',
+            'issuer' => 'privacy:metadata:external:backpacks:issuer',
+        ], 'privacy:metadata:external:backpacks');
+
+        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();
+
+        // Find the modifications we made on badges (course & system).
+        $sql = "
+            SELECT ctx.id
+              FROM {badge} b
+              JOIN {context} ctx
+                ON (b.type = :typecourse AND b.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel)
+                OR (b.type = :typesite AND ctx.id = :syscontextid)
+             WHERE b.usermodified = :userid1
+                OR b.usercreated = :userid2";
+        $params = [
+            'courselevel' => CONTEXT_COURSE,
+            'syscontextid' => SYSCONTEXTID,
+            'typecourse' => BADGE_TYPE_COURSE,
+            'typesite' => BADGE_TYPE_SITE,
+            'userid1' => $userid,
+            'userid2' => $userid,
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
+        // Find where we've manually awarded a badge (recipient user context).
+        $sql = "
+            SELECT ctx.id
+              FROM {badge_manual_award} bma
+              JOIN {context} ctx
+                ON ctx.instanceid = bma.recipientid
+               AND ctx.contextlevel = :userlevel
+             WHERE bma.issuerid = :userid";
+        $params = [
+            'userlevel' => CONTEXT_USER,
+            'userid' => $userid,
+        ];
+        $contextlist->add_from_sql($sql, $params);
+
+        // Now find where there is real user data (user context).
+        $sql = "
+            SELECT ctx.id
+              FROM {context} ctx
+         LEFT JOIN {badge_manual_award} bma
+                ON bma.recipientid = ctx.instanceid
+         LEFT JOIN {badge_issued} bi
+                ON bi.userid = ctx.instanceid
+         LEFT JOIN {badge_criteria_met} bcm
+                ON bcm.userid = ctx.instanceid
+         LEFT JOIN {badge_backpack} bb
+                ON bb.userid = ctx.instanceid
+             WHERE ctx.contextlevel = :userlevel
+               AND ctx.instanceid = :userid
+               AND (bma.id IS NOT NULL
+                OR bi.id IS NOT NULL
+                OR bcm.id IS NOT NULL
+                OR bb.id IS NOT NULL)";
+        $params = [
+            'userlevel' => CONTEXT_USER,
+            'userid' => $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;
+
+        $userid = $contextlist->get_user()->id;
+        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            $level = $context->contextlevel;
+            if ($level == CONTEXT_USER || $level == CONTEXT_COURSE) {
+                $carry[$level][] = $context->instanceid;
+            } else if ($level == CONTEXT_SYSTEM) {
+                $carry[$level] = SYSCONTEXTID;
+            }
+            return $carry;
+        }, [
+            CONTEXT_COURSE => [],
+            CONTEXT_USER => [],
+            CONTEXT_SYSTEM => null,
+        ]);
+
+        $path = [get_string('badges', 'core_badges')];
+        $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
+
+        // Export the badges we've created or modified.
+        if (!empty($contexts[CONTEXT_SYSTEM]) || !empty($contexts[CONTEXT_COURSE])) {
+            $sqls = [];
+            $params = [];
+
+            if (!empty($contexts[CONTEXT_SYSTEM])) {
+                $sqls[] = "b.type = :typesite";
+                $params['typesite'] = BADGE_TYPE_SITE;
+            }
+
+            if (!empty($contexts[CONTEXT_COURSE])) {
+                list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_COURSE], SQL_PARAMS_NAMED);
+                $sqls[] = "(b.type = :typecourse AND b.courseid $insql)";
+                $params = array_merge($params, ['typecourse' => BADGE_TYPE_COURSE], $inparams);
+            }
+
+            $sqlwhere = '(' . implode(' OR ', $sqls) . ')';
+            $sql = "
+                SELECT b.*, COALESCE(b.courseid, 0) AS normalisedcourseid
+                  FROM {badge} b
+                 WHERE (b.usermodified = :userid1 OR b.usercreated = :userid2)
+                   AND $sqlwhere
+              ORDER BY b.courseid, b.id";
+            $params = array_merge($params, ['userid1' => $userid, 'userid2' => $userid]);
+            $recordset = $DB->get_recordset_sql($sql, $params);
+            static::recordset_loop_and_export($recordset, 'normalisedcourseid', [], function($carry, $record) use ($userid) {
+                $carry[] = [
+                    'name' => $record->name,
+                    'created_on' => transform::datetime($record->timecreated),
+                    'created_by_you' => transform::yesno($record->usercreated == $userid),
+                    'modified_on' => transform::datetime($record->timemodified),
+                    'modified_by_you' => transform::yesno($record->usermodified == $userid),
+                ];
+                return $carry;
+            }, function($courseid, $data) use ($path) {
+                $context = $courseid ? context_course::instance($courseid) : context_system::instance();
+                writer::with_context($context)->export_data($path, (object) ['badges' => $data]);
+            });
+        }
+
+        // Export the badges we've manually awarded.
+        if (!empty($contexts[CONTEXT_USER])) {
+            list($insql, $inparams) = $DB->get_in_or_equal($contexts[CONTEXT_USER], SQL_PARAMS_NAMED);
+            $sql = "
+                SELECT bma.id, bma.recipientid, bma.datemet, b.name, b.courseid,
+                       r.id AS roleid,
+                       r.name AS rolename,
+                       r.shortname AS roleshortname,
+                       r.archetype AS rolearchetype,
+                       $ctxfields
+                  FROM {badge_manual_award} bma
+                  JOIN {badge} b
+                    ON b.id = bma.badgeid
+                  JOIN {role} r
+                    ON r.id = bma.issuerrole
+                  JOIN {context} ctx
+                    ON (COALESCE(b.courseid, 0) > 0 AND ctx.instanceid = b.courseid AND ctx.contextlevel = :courselevel)
+                    OR (COALESCE(b.courseid, 0) = 0 AND ctx.id = :syscontextid)
+                 WHERE bma.recipientid $insql
+                   AND bma.issuerid = :userid
+              ORDER BY bma.recipientid, bma.id";
+            $params = array_merge($inparams, [
+                'courselevel' => CONTEXT_COURSE,
+                'syscontextid' => SYSCONTEXTID,
+                'userid' => $userid
+            ]);
+            $recordset = $DB->get_recordset_sql($sql, $params);
+            static::recordset_loop_and_export($recordset, 'recipientid', [], function($carry, $record) use ($userid) {
+
+                // The only reason we fetch the context and role is to format the name of the role, which could be
+                // different to the standard name if the badge was created in a course.
+                context_helper::preload_from_record($record);
+                $context = $record->courseid ? context_course::instance($record->courseid) : context_system::instance();
+                $role = (object) [
+                    'id' => $record->roleid,
+                    'name' => $record->rolename,
+                    'shortname' => $record->roleshortname,
+                    'archetype' => $record->rolearchetype,
+                    // Mock those two fields as they do not matter.
+                    'sortorder' => 0,
+                    'description' => ''
+                ];
+
+                $carry[] = [
+                    'name' => $record->name,
+                    'issued_by_you' => transform::yesno(true),
+                    'issued_on' => transform::datetime($record->datemet),
+                    'issuer_role' => role_get_name($role, $context),
+                ];
+                return $carry;
+            }, function($userid, $data) use ($path) {
+                $context = context_user::instance($userid);
+                writer::with_context($context)->export_related_data($path, 'manual_awards', (object) ['badges' => $data]);
+            });
+        }
+
+        // Export our data.
+        if (in_array($userid, $contexts[CONTEXT_USER])) {
+
+            // Export the badges.
+            $uniqueid = $DB->sql_concat_join("'-'", ['b.id', 'COALESCE(bc.id, 0)', 'COALESCE(bi.id, 0)',
+                'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)']);
+            $sql = "
+                SELECT $uniqueid AS uniqueid, b.id,
+                       bi.id AS biid, bi.dateissued, bi.dateexpire, bi.uniquehash,
+                       bma.id AS bmaid, bma.datemet, bma.issuerid,
+                       bcm.id AS bcmid,
+                       c.fullname AS coursename,
+                       $ctxfields
+                  FROM {badge} b
+             LEFT JOIN {badge_issued} bi
+                    ON bi.badgeid = b.id
+                   AND bi.userid = :userid1
+             LEFT JOIN {badge_manual_award} bma
+                    ON bma.badgeid = b.id
+                   AND bma.recipientid = :userid2
+             LEFT JOIN {badge_criteria} bc
+                    ON bc.badgeid = b.id
+             LEFT JOIN {badge_criteria_met} bcm
+                    ON bcm.critid = bc.id
+                   AND bcm.userid = :userid3
+             LEFT JOIN {course} c
+                    ON c.id = b.courseid
+                   AND b.type = :typecourse
+             LEFT JOIN {context} ctx
+                    ON ctx.instanceid = c.id
+                   AND ctx.contextlevel = :courselevel
+                 WHERE bi.id IS NOT NULL
+                    OR bma.id IS NOT NULL
+                    OR bcm.id IS NOT NULL
+              ORDER BY b.id";
+            $params = [
+                'userid1' => $userid,
+                'userid2' => $userid,
+                'userid3' => $userid,
+                'courselevel' => CONTEXT_COURSE,
+                'typecourse' => BADGE_TYPE_COURSE,
+            ];
+            $recordset = $DB->get_recordset_sql($sql, $params);
+            static::recordset_loop_and_export($recordset, 'id', null, function($carry, $record) use ($userid) {
+                $badge = new badge($record->id);
+
+                // Export details of the badge.
+                if ($carry === null) {
+                    $carry = [
+                        'name' => $badge->name,
+                        'issued' => null,
+                        'manual_award' => null,
+                        'criteria_met' => []
+                    ];
+
+                    if ($badge->type == BADGE_TYPE_COURSE) {
+                        context_helper::preload_from_record($record);
+                        $carry['course'] = format_string($record->coursename, true, ['context' => $badge->get_context()]);
+                    }
+
+                    if (!empty($record->biid)) {
+                        $carry['issued'] = [
+                            'issued_on' => transform::datetime($record->dateissued),
+                            'expires_on' => $record->dateexpire ? transform::datetime($record->dateexpire) : null,
+                            'unique_hash' => $record->uniquehash,
+                        ];
+                    }
+
+                    if (!empty($record->bmaid)) {
+                        $carry['manual_award'] = [
+                            'awarded_on' => transform::datetime($record->datemet),
+                            'issuer' => transform::user($record->issuerid)
+                        ];
+                    }
+                }
+
+                // Export the details of the criteria met.
+                // We only do that once, when we find that a least one criteria was met.
+                // This is heavily based on the logic present in core_badges_renderer::render_issued_badge.
+                if (!empty($record->bcmid) && empty($carry['criteria_met'])) {
+
+                    $agg = $badge->get_aggregation_methods();
+                    $evidenceids = array_map(function($record) {
+                        return $record->critid;
+                    }, $badge->get_criteria_completions($userid));
+
+                    $criteria = $badge->criteria;
+                    unset($criteria[BADGE_CRITERIA_TYPE_OVERALL]);
+
+                    $items = [];
+                    foreach ($criteria as $type => $c) {
+                        if (in_array($c->id, $evidenceids)) {
+                            $details = $c->get_details(true);
+                            if (count($c->params) == 1) {
+                                $items[] = get_string('criteria_descr_single_' . $type , 'core_badges') . ' ' . $details;
+                            } else {
+                                $items[] = get_string('criteria_descr_' . $type , 'core_badges',
+                                    core_text::strtoupper($agg[$badge->get_aggregation_method($type)])) . ' ' . $details;
+                            }
+                        }
+                    }
+                    $carry['criteria_met'] = $items;
+                }
+                return $carry;
+            }, function($badgeid, $data) use ($path, $userid) {
+                $path = array_merge($path, ["{$data['name']} ({$badgeid})"]);
+                $writer = writer::with_context(context_user::instance($userid));
+                $writer->export_data($path, (object) $data);
+                $writer->export_area_files($path, 'badges', 'userbadge', $badgeid);
+            });
+
+            // Export the backpacks.
+            $data = [];
+            $recordset = $DB->get_recordset_select('badge_backpack', 'userid = :userid', ['userid' => $userid]);
+            foreach ($recordset as $record) {
+                $data[] = [
+                    'email' => $record->email,
+                    'url' => $record->backpackurl,
+                    'uid' => $record->backpackuid
+                ];
+            }
+            $recordset->close();
+            if (!empty($data)) {
+                writer::with_context(context_user::instance($userid))->export_related_data($path, 'backpacks',
+                    (object) ['backpacks' => $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) {
+        // We cannot delete the course or system data as it is needed by the system.
+        if ($context->contextlevel != CONTEXT_USER) {
+            return;
+        }
+
+        // Delete all the user data.
+        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) {
+        $userid = $contextlist->get_user()->id;
+        foreach ($contextlist->get_contexts() as $context) {
+            if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
+                // We can only delete our own data in the user context, nothing in course or system.
+                static::delete_user_data($userid);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Delete all the data for a user.
+     *
+     * @param int $userid The user ID.
+     * @return void
+     */
+    protected static function delete_user_data($userid) {
+        global $DB;
+
+        // Delete the stuff.
+        $DB->delete_records('badge_manual_award', ['recipientid' => $userid]);
+        $DB->delete_records('badge_criteria_met', ['userid' => $userid]);
+        $DB->delete_records('badge_issued', ['userid' => $userid]);
+
+        // Delete the backpacks and related stuff.
+        $backpackids = $DB->get_fieldset_select('badge_backpack', 'id', 'userid = :userid', ['userid' => $userid]);
+        if (!empty($backpackids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($backpackids, SQL_PARAMS_NAMED);
+            $DB->delete_records_select('badge_external', "backpackid $insql", $inparams);
+            $DB->delete_records_select('badge_backpack', "id $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);
+        }
+    }
+}
diff --git a/badges/tests/privacy_test.php b/badges/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..bf3b084
--- /dev/null
@@ -0,0 +1,557 @@
+<?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_badges
+ * @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_badges\privacy\provider;
+
+require_once($CFG->libdir . '/badgeslib.php');
+
+/**
+ * Data provider testcase class.
+ *
+ * @package    core_badges
+ * @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_badges_privacy_testcase extends provider_testcase {
+
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    public function test_get_contexts_for_userid_for_badge_editing() {
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $u5 = $dg->create_user();
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+
+        // Assert that we find contexts where we created/modified a badge.
+        $this->create_badge(['usercreated' => $u1->id, 'usermodified' => $u5->id]);
+        $this->create_badge(['usercreated' => $u2->id, 'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
+        $this->create_badge(['usermodified' => $u3->id]);
+        $this->create_badge(['usermodified' => $u4->id, 'type' => BADGE_TYPE_COURSE, 'courseid' => $c2->id,
+            'usercreated' => $u5->id]);
+
+        $contexts = provider::get_contexts_for_userid($u1->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($sysctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u2->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($c1ctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u3->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($sysctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u4->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($c2ctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u5->id)->get_contextids();
+        $this->assertCount(2, $contexts);
+        $this->assertTrue(in_array($sysctx->id, $contexts));
+        $this->assertTrue(in_array($c2ctx->id, $contexts));
+    }
+
+    public function test_get_contexts_for_userid_for_manual_award() {
+        global $DB;
+
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $c1 = $dg->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+        $u3ctx = context_user::instance($u3->id);
+        $u4ctx = context_user::instance($u4->id);
+        $b1 = $this->create_badge();
+        $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
+
+        $this->create_manual_award(['recipientid' => $u4->id, 'issuerid' => $u1->id, 'badgeid' => $b1->id]);
+        $this->create_manual_award(['recipientid' => $u3->id, 'issuerid' => $u2->id, 'badgeid' => $b1->id]);
+        $this->create_manual_award(['recipientid' => $u3->id, 'issuerid' => $u2->id, 'badgeid' => $b2->id]);
+
+        $contexts = provider::get_contexts_for_userid($u1->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u4ctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u2->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u3ctx->id, $contexts[0]);
+    }
+
+    public function test_get_contexts_for_userid_for_my_stuff() {
+        global $DB;
+
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $c1 = $dg->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+        $u3ctx = context_user::instance($u3->id);
+        $u4ctx = context_user::instance($u4->id);
+        $b1 = $this->create_badge();
+        $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
+
+        $this->create_backpack(['userid' => $u1->id]);
+        $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id]);
+        $this->create_issued(['badgeid' => $b2->id, 'userid' => $u3->id]);
+
+        $crit = $this->create_criteria_manual($b1->id);
+        $crit->mark_complete($u4->id);
+
+        $contexts = provider::get_contexts_for_userid($u1->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u1ctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u2->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u2ctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u3->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u3ctx->id, $contexts[0]);
+
+        $contexts = provider::get_contexts_for_userid($u4->id)->get_contextids();
+        $this->assertCount(1, $contexts);
+        $this->assertEquals($u4ctx->id, $contexts[0]);
+    }
+
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $c1 = $dg->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $b1 = $this->create_badge(['usercreated' => $u1->id, 'usermodified' => $u2->id]);
+        $b2 = $this->create_badge(['usercreated' => $u2->id, 'usermodified' => $u1->id,
+            'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
+
+        $this->create_backpack(['userid' => $u1->id]);
+        $this->create_backpack(['userid' => $u2->id]);
+        $this->create_manual_award(['recipientid' => $u1->id, 'badgeid' => $b1->id]);
+        $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id, 'issuerid' => $u1->id]);
+        $this->create_issued(['badgeid' => $b2->id, 'userid' => $u1->id]);
+        $this->create_issued(['badgeid' => $b2->id, 'userid' => $u2->id]);
+
+        $crit = $this->create_criteria_manual($b1->id);
+        $crit->mark_complete($u2->id);
+        $crit = $this->create_criteria_manual($b2->id);
+        $crit->mark_complete($u1->id);
+
+        $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id]));
+        $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id]));
+        $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id]));
+        $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u2->id]));
+
+        provider::delete_data_for_user(new approved_contextlist($u1, 'core_badges', [$sysctx->id, $c1ctx->id,
+            $u1ctx->id, $u2ctx->id]));
+
+        $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_backpack', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_issued', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_criteria_met', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_criteria_met', ['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();
+        $c1 = $dg->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $b1 = $this->create_badge(['usercreated' => $u1->id, 'usermodified' => $u2->id]);
+        $b2 = $this->create_badge(['usercreated' => $u2->id, 'usermodified' => $u1->id,
+            'type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id]);
+
+        $this->create_backpack(['userid' => $u1->id]);
+        $this->create_backpack(['userid' => $u2->id]);
+        $this->create_manual_award(['recipientid' => $u1->id, 'badgeid' => $b1->id]);
+        $this->create_manual_award(['recipientid' => $u2->id, 'badgeid' => $b1->id, 'issuerid' => $u1->id]);
+        $this->create_issued(['badgeid' => $b2->id, 'userid' => $u1->id]);
+        $this->create_issued(['badgeid' => $b2->id, 'userid' => $u2->id]);
+
+        $crit = $this->create_criteria_manual($b1->id);
+        $crit->mark_complete($u2->id);
+        $crit = $this->create_criteria_manual($b2->id);
+        $crit->mark_complete($u1->id);
+
+        $assertnochange = function() use ($DB, $u1, $u2) {
+            $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u1->id]));
+            $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id]));
+            $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id]));
+            $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id]));
+            $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id]));
+            $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id]));
+            $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id]));
+            $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id]));
+            $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u1->id]));
+            $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id]));
+            $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u1->id]));
+            $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u2->id]));
+        };
+        $assertnochange();
+
+        provider::delete_data_for_all_users_in_context($sysctx);
+        $assertnochange();
+
+        provider::delete_data_for_all_users_in_context($c1ctx);
+        $assertnochange();
+
+        provider::delete_data_for_all_users_in_context($u1ctx);
+        $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usercreated' => $u2->id]));
+        $this->assertTrue($DB->record_exists('badge', ['usermodified' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_backpack', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_backpack', ['userid' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_manual_award', ['recipientid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_manual_award', ['recipientid' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_issued', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_issued', ['userid' => $u2->id]));
+        $this->assertFalse($DB->record_exists('badge_criteria_met', ['userid' => $u1->id]));
+        $this->assertTrue($DB->record_exists('badge_criteria_met', ['userid' => $u2->id]));
+    }
+
+    public function test_export_data_for_user() {
+        global $DB;
+
+        $yes = transform::yesno(true);
+        $no = transform::yesno(false);
+
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $c1 = $dg->create_course();
+        $sysctx = context_system::instance();
+        $c1ctx = context_course::instance($c1->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $b1 = $this->create_badge(['usercreated' => $u3->id]);
+        $b2 = $this->create_badge(['type' => BADGE_TYPE_COURSE, 'courseid' => $c1->id, 'usermodified' => $u3->id]);
+        $b3 = $this->create_badge();
+        $b3crit = $this->create_criteria_manual($b3->id);
+        $b4 = $this->create_badge();
+
+        // Create things for user 2, to check it's not exported it.
+        $this->create_issued(['badgeid' => $b4->id, 'userid' => $u2->id]);
+        $this->create_backpack(['userid' => $u2->id, 'email' => $u2->email]);
+        $this->create_manual_award(['badgeid' => $b1->id, 'recipientid' => $u2->id, 'issuerid' => $u3->id]);
+
+        // Create a set of stuff for u1.
+        $this->create_issued(['badgeid' => $b1->id, 'userid' => $u1->id, 'uniquehash' => 'yoohoo']);
+        $this->create_manual_award(['badgeid' => $b2->id, 'recipientid' => $u1->id, 'issuerid' => $u3->id]);
+        $b3crit->mark_complete($u1->id);
+        $this->create_backpack(['userid' => $u1->id, 'email' => $u1->email]);
+
+        // Check u1.
+        writer::reset();
+        provider::export_user_data(new approved_contextlist($u1, 'core_badges', [$u1ctx->id, $u2ctx->id,
+            $sysctx->id, $c1ctx->id]));
+        $this->assertFalse(writer::with_context($u2ctx)->has_any_data());
+        $this->assertFalse(writer::with_context($sysctx)->has_any_data());
+        $this->assertFalse(writer::with_context($c1ctx)->has_any_data());
+
+        $path = [get_string('badges', 'core_badges'), "{$b1->name} ({$b1->id})"];
+        $data = writer::with_context($u1ctx)->get_data($path);
+        $this->assertEquals($b1->name, $data->name);
+        $this->assertNotEmpty($data->issued);
+        $this->assertEmpty($data->manual_award);
+        $this->assertEmpty($data->criteria_met);
+        $this->assertFalse(isset($data->course));
+        $this->assertEquals('yoohoo', $data->issued['unique_hash']);
+        $this->assertNull($data->issued['expires_on']);
+
+        $path = [get_string('badges', 'core_badges'), "{$b2->name} ({$b2->id})"];
+        $data = writer::with_context($u1ctx)->get_data($path);
+        $this->assertEquals($b2->name, $data->name);
+        $this->assertEmpty($data->issued);
+        $this->assertNotEmpty($data->manual_award);
+        $this->assertEmpty($data->criteria_met);
+        $this->assertEquals($c1->fullname, $data->course);
+        $this->assertEquals($u3->id, $data->manual_award['issuer']);
+
+        $path = [get_string('badges', 'core_badges'), "{$b3->name} ({$b3->id})"];
+        $data = writer::with_context($u1ctx)->get_data($path);
+        $this->assertEquals($b3->name, $data->name);
+        $this->assertEmpty($data->issued);
+        $this->assertEmpty($data->manual_award);
+        $this->assertNotEmpty($data->criteria_met);
+        $this->assertNotFalse(strpos($data->criteria_met[0], get_string('criteria_descr_2', 'core_badges', 'ALL')));
+
+        $path = [get_string('badges', 'core_badges')];
+        $data = writer::with_context($u1ctx)->get_related_data($path, 'backpacks');
+        $this->assertCount(1, $data->backpacks);
+        $this->assertEquals($u1->email, $data->backpacks[0]['email']);
+
+        // Confirm we do not have u2.
+        $path = [get_string('badges', 'core_badges'), "{$b4->name} ({$b4->id})"];
+        $data = writer::with_context($u1ctx)->get_data($path);
+        $this->assertEmpty($data);
+        $data = writer::with_context($u2ctx)->get_data($path);
+        $this->assertEmpty($data);
+
+        // Export for u3.
+        writer::reset();
+        $path = [get_string('badges', 'core_badges')];
+        provider::export_user_data(new approved_contextlist($u3, 'core_badges', [$u1ctx->id, $u2ctx->id,
+            $sysctx->id, $c1ctx->id]));
+
+        $data = writer::with_context($u2ctx)->get_related_data($path, 'manual_awards');
+        $this->assertCount(1, $data->badges);
+        $this->assertEquals($b1->name, $data->badges[0]['name']);
+        $this->assertEquals($yes, $data->badges[0]['issued_by_you']);
+        $this->assertEquals('Manager', $data->badges[0]['issuer_role']);
+
+        $data = writer::with_context($sysctx)->get_data($path);
+        $this->assertCount(1, $data->badges);
+        $this->assertEquals($b1->name, $data->badges[0]['name']);
+        $this->assertEquals($yes, $data->badges[0]['created_by_you']);
+        $this->assertEquals($no, $data->badges[0]['modified_by_you']);
+
+        $data = writer::with_context($c1ctx)->get_data($path);
+        $this->assertCount(1, $data->badges);
+        $this->assertEquals($b2->name, $data->badges[0]['name']);
+        $this->assertEquals($no, $data->badges[0]['created_by_you']);
+        $this->assertEquals($yes, $data->badges[0]['modified_by_you']);
+
+        $data = writer::with_context($u1ctx)->get_related_data($path, 'manual_awards');
+        $this->assertCount(1, $data->badges);
+        $this->assertEquals($b3->name, $data->badges[0]['name']);
+        $this->assertEquals($yes, $data->badges[0]['issued_by_you']);
+        $this->assertEquals('Manager', $data->badges[0]['issuer_role']);
+    }
+
+    /**
+     * Create a badge.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function create_badge(array $params = []) {
+        global $DB, $USER;
+        $record = (object) array_merge([
+            'name' => "Test badge with 'apostrophe' and other friends (<>&@#)",
+            'description' => "Testing badges",
+            'timecreated' => time(),
+            'timemodified' => time(),
+            'usercreated' => $USER->id,
+            'usermodified' => $USER->id,
+            'issuername' => "Test issuer",
+            'issuerurl' => "http://issuer-url.domain.co.nz",
+            'issuercontact' => "issuer@example.com",
+            'expiredate' => null,
+            'expireperiod' => null,
+            'type' => BADGE_TYPE_SITE,
+            'courseid' => null,
+            'messagesubject' => "Test message subject",
+            'message' => "Test message body",
+            'attachment' => 1,
+            'notification' => 0,
+            'status' => BADGE_STATUS_ACTIVE,
+        ], $params);
+        $record->id = $DB->insert_record('badge', $record);
+
+        return $record;
+    }
+
+    /**
+     * Create a backpack.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function create_backpack(array $params = []) {
+        global $DB;
+        $record = (object) array_merge([
+            'userid' => null,
+            'email' => 'test@example.com',
+            'backpackurl' => "http://here.there.com",
+            'backpackuid' => "12345",
+            'autosync' => 0,
+            'password' => '',
+        ], $params);
+        $record->id = $DB->insert_record('badge_backpack', $record);
+        return $record;
+    }
+
+    /**
+     * Create a criteria of type badge.
+     *
+     * @param int $badgeid The badge ID.
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function create_criteria_badge($badgeid, array $params = []) {
+        $badge = new badge($badgeid);
+        if (empty($badge->criteria)) {
+            $overall = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id]);
+            $overall->save(['agg' => BADGE_CRITERIA_AGGREGATION_ALL]);
+        }
+
+        $criteria = award_criteria::build([
+            'badgeid' => $badge->id,
+            'criteriatype' => BADGE_CRITERIA_TYPE_BADGE,
+        ]);
+
+        if (isset($params['badgeid'])) {
+            $params['badge_' . $params['badgeid']] = $params['badgeid'];
+            unset($params['badgeid']);
+        }
+
+        $criteria->save($params);
+        $badge = new badge($badgeid);
+        return $badge->criteria[BADGE_CRITERIA_TYPE_BADGE];
+    }
+
+    /**
+     * Create a criteria of type manual.
+     *
+     * @param int $badgeid The badge ID.
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function create_criteria_manual($badgeid, array $params = []) {
+        global $DB;
+
+        $badge = new badge($badgeid);
+        if (empty($badge->criteria)) {
+            $overall = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id]);
+            $overall->save(['agg' => BADGE_CRITERIA_AGGREGATION_ALL]);
+        }
+
+        $criteria = award_criteria::build([
+            'badgeid' => $badge->id,
+            'criteriatype' => BADGE_CRITERIA_TYPE_MANUAL,
+        ]);
+
+        $managerroleid = $DB->get_field_select('role', 'id', 'shortname = ?', ['manager'], IGNORE_MULTIPLE);
+        if (empty($params)) {
+            $params = [
+                'role_' . $managerroleid = $managerroleid
+            ];
+        }
+
+        $criteria->save($params);
+        $badge = new badge($badgeid);
+        return $badge->criteria[BADGE_CRITERIA_TYPE_MANUAL];
+    }
+
+    /**
+     * Create a badge issued.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function create_issued(array $params = []) {
+        global $DB, $USER;
+        $record = (object) array_merge([
+            'badgeid' => null,
+            'userid' => null,
+            'uniquehash' => random_string(40),
+            'dateissued' => time(),
+            'dateexpire' => null,
+            'visible' => 1,
+            'issuernotified' => null,
+        ], $params);
+        $record->id = $DB->insert_record('badge_issued', $record);
+        return $record;
+    }
+
+    /**
+     * Create a manual award.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    protected function create_manual_award(array $params = []) {
+        global $DB, $USER;
+        $record = (object) array_merge([
+            'badgeid' => null,
+            'recipientid' => null,
+            'issuerid' => $USER->id,
+            'issuerrole' => $DB->get_field_select('role', 'id', 'shortname = ?', ['manager'], IGNORE_MULTIPLE),
+            'datemet' => time()
+        ], $params);
+        $record->id = $DB->insert_record('badge_manual_award', $record);
+        return $record;
+    }
+
+}
diff --git a/blocks/classes/privacy/provider.php b/blocks/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..cbee644
--- /dev/null
@@ -0,0 +1,227 @@
+<?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_block
+ * @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_block\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use context_block;
+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    core_block
+ * @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,
+    \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('blockIDhidden', 'privacy:metadata:userpref:hiddenblock');
+        $collection->add_user_preference('docked_block_instance_ID', 'privacy:metadata:userpref:dockedinstance');
+        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 {
+        global $DB;
+        $contextlist = new \core_privacy\local\request\contextlist();
+
+        // Fetch the block instance IDs.
+        $likehidden = $DB->sql_like('name', ':hidden', false, false);
+        $likedocked = $DB->sql_like('name', ':docked', false, false);
+        $sql = "userid = :userid AND ($likehidden OR $likedocked)";
+        $params = [
+            'userid' => $userid,
+            'hidden' => 'block%hidden',
+            'docked' => 'docked_block_instance_%',
+        ];
+        $prefs = $DB->get_fieldset_select('user_preferences', 'name', $sql, $params);
+
+        $instanceids = array_unique(array_map(function($prefname) {
+            if (preg_match('/^block(\d+)hidden$/', $prefname, $matches)) {
+                return $matches[1];
+            } else if (preg_match('/^docked_block_instance_(\d+)$/', $prefname, $matches)) {
+                return $matches[1];
+            }
+            return 0;
+        }, $prefs));
+
+        // Find the context of the instances.
+        if (!empty($instanceids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED);
+            $sql = "
+                SELECT ctx.id
+                  FROM {context} ctx
+                 WHERE ctx.instanceid $insql
+                   AND ctx.contextlevel = :blocklevel";
+            $params = array_merge($inparams, ['blocklevel' => CONTEXT_BLOCK]);
+            $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;
+        $userid = $contextlist->get_user()->id;
+
+        // Extract the block instance IDs.
+        $instanceids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_BLOCK) {
+                $carry[] = $context->instanceid;
+            }
+            return $carry;
+        }, []);
+        if (empty($instanceids)) {
+            return;
+        }
+
+        // Query the blocks and their preferences.
+        list($insql, $inparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED);
+        $hiddenkey = $DB->sql_concat("'block'", 'bi.id', "'hidden'");
+        $dockedkey = $DB->sql_concat("'docked_block_instance_'", 'bi.id');
+        $sql = "
+            SELECT bi.id, h.value AS prefhidden, d.value AS prefdocked
+              FROM {block_instances} bi
+         LEFT JOIN {user_preferences} h
+                ON h.userid = :userid1
+               AND h.name = $hiddenkey
+         LEFT JOIN {user_preferences} d
+                ON d.userid = :userid2
+               AND d.name = $dockedkey
+             WHERE bi.id $insql
+               AND (h.id IS NOT NULL
+                OR d.id IS NOT NULL)";
+        $params = array_merge($inparams, [
+            'userid1' => $userid,
+            'userid2' => $userid,
+        ]);
+
+        // Export all the things.
+        $dockedstr = get_string('privacy:request:blockisdocked', 'core_block');
+        $hiddenstr = get_string('privacy:request:blockishidden', 'core_block');
+        $recordset = $DB->get_recordset_sql($sql, $params);
+        foreach ($recordset as $record) {
+            $context = context_block::instance($record->id);
+            if ($record->prefdocked !== null) {
+                writer::with_context($context)->export_user_preference(
+                    'core_block',
+                    'block_is_docked',
+                    transform::yesno($record->prefdocked),
+                    $dockedstr
+                );
+            }
+            if ($record->prefhidden !== null) {
+                writer::with_context($context)->export_user_preference(
+                    'core_block',
+                    'block_is_hidden',
+                    transform::yesno($record->prefhidden),
+                    $hiddenstr
+                );
+            }
+        }
+        $recordset->close();
+    }
+
+    /**
+     * 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) {
+      // Our preferences aren't site-wide so they are exported in export_user_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_BLOCK) {
+            return;
+        }
+
+        // Delete the user preferences.
+        $instanceid = $context->instanceid;
+        $DB->delete_records_list('user_preferences', 'name', [
+            "block{$instanceid}hidden",
+            "docked_block_instance_{$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;
+        $userid = $contextlist->get_user()->id;
+        $prefnames = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_BLOCK) {
+                $carry[] = "block{$context->instanceid}hidden";
+                $carry[] = "docked_block_instance_{$context->instanceid}";
+            }
+            return $carry;
+        }, []);
+
+        if (empty($prefnames)) {
+            return;
+        }
+
+        list($insql, $inparams) = $DB->get_in_or_equal($prefnames, SQL_PARAMS_NAMED);
+        $sql = "userid = :userid AND name $insql";
+        $params = array_merge($inparams, ['userid' => $userid]);
+        $DB->delete_records_select('user_preferences', $sql, $params);
+    }
+
+}
diff --git a/blocks/comments/classes/privacy/provider.php b/blocks/comments/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..d758293
--- /dev/null
@@ -0,0 +1,115 @@
+<?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 block_comments.
+ *
+ * @package    block_comments
+ * @category   privacy
+ * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_comments\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+
+/**
+ * Privacy Subsystem implementation for block_comments.
+ *
+ * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+        // The block_comments block stores user provided data.
+        \core_privacy\local\metadata\provider,
+
+        // The block_comments block provides data directly to core.
+        \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @param collection $collection
+     * @return collection
+     */
+    public static function get_metadata(collection $collection) : collection {
+        return $collection->add_subsystem_link('core_comment', [], 'privacy:metadata:core_comment');
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid
+     * @return contextlist
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $contextlist = new contextlist();
+
+        $sql = "SELECT contextid
+                  FROM {comments}
+                 WHERE component = :component
+                   AND userid = :userid";
+        $params = [
+            'component' => 'block_comments',
+            'userid' => $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
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        $contexts = $contextlist->get_contexts();
+        foreach ($contexts as $context) {
+            \core_comment\privacy\provider::export_comments(
+                    $context,
+                    'block_comments',
+                    'page_comments',
+                    0,
+                    []
+            );
+        }
+    }
+
+    /**
+     * Delete all data for all users in the specified context.
+     *
+     * @param \context $context
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        \core_comment\privacy\provider::delete_comments_for_all_users($context, 'block_comments');
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'block_comments');
+    }
+}
index f64e5f6..116f10e 100644 (file)
@@ -26,3 +26,4 @@
 $string['comments:myaddinstance'] = 'Add a new comments block to Dashboard';
 $string['comments:addinstance'] = 'Add a new comments block';
 $string['pluginname'] = 'Comments';
+$string['privacy:metadata:core_comment'] = 'A record of comments added.';
diff --git a/blocks/comments/tests/privacy_provider_test.php b/blocks/comments/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..9cc028b
--- /dev/null
@@ -0,0 +1,468 @@
+<?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 provider tests.
+ *
+ * @package    block_comments
+ * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use core_privacy\local\metadata\collection;
+use block_comments\privacy\provider;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class block_comments_privacy_provider_testcase.
+ *
+ * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_comments_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
+
+    /** @var stdClass A student who is only enrolled in course1. */
+    protected $student1;
+
+    /** @var stdClass A student who is only enrolled in course2. */
+    protected $student2;
+
+    /** @var stdClass A student who is enrolled in both course1 and course2. */
+    protected $student12;
+
+    /** @var stdClass A test course. */
+    protected $course1;
+
+    /** @var stdClass A test course. */
+    protected $course2;
+
+    protected function setUp() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create courses.
+        $generator = $this->getDataGenerator();
+        $this->course1 = $generator->create_course();
+        $this->course2 = $generator->create_course();
+
+        // Create and enrol students.
+        $this->student1 = $generator->create_user();
+        $this->student2 = $generator->create_user();
+        $this->student12 = $generator->create_user();
+
+        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+        $generator->enrol_user($this->student1->id,  $this->course1->id, $studentrole->id);
+        $generator->enrol_user($this->student2->id,  $this->course2->id, $studentrole->id);
+        $generator->enrol_user($this->student12->id,  $this->course1->id, $studentrole->id);
+        $generator->enrol_user($this->student12->id,  $this->course2->id, $studentrole->id);
+
+        // Comment block on course pages.
+        $block = $this->add_comments_block_in_context(context_course::instance($this->course1->id));
+        $block = $this->add_comments_block_in_context(context_course::instance($this->course2->id));
+    }
+
+    /**
+     * Posts a comment on a given context.
+     *
+     * @param string $text The comment's text.
+     * @param context $context The context on which we want to put the comment.
+     */
+    protected function add_comment($text, context $context) {
+        $args = new stdClass;
+        $args->context = $context;
+        $args->area = 'page_comments';
+        $args->itemid = 0;
+        $args->component = 'block_comments';
+        $args->linktext = get_string('showcomments');
+        $args->notoggle = true;
+        $args->autostart = true;
+        $args->displaycancel = false;
+        $comment = new comment($args);
+
+        $comment->add($text);
+    }
+
+    /**
+     * Creates a comments block on a context.
+     *
+     * @param context $context The context on which we want to put the block.
+     * @return block_base The created block instance.
+     * @throws coding_exception
+     */
+    protected function add_comments_block_in_context(context $context) {
+        global $DB;
+
+        $course = null;
+
+        $page = new \moodle_page();
+        $page->set_context($context);
+
+        switch ($context->contextlevel) {
+            case CONTEXT_SYSTEM:
+                $page->set_pagelayout('frontpage');
+                $page->set_pagetype('site-index');
+                break;
+            case CONTEXT_COURSE:
+                $page->set_pagelayout('standard');
+                $page->set_pagetype('course-view');
+                $course = $DB->get_record('course', ['id' => $context->instanceid]);
+                $page->set_course($course);
+                break;
+            case CONTEXT_MODULE:
+                $page->set_pagelayout('standard');
+                $mod = $DB->get_field_sql("SELECT m.name
+                                             FROM {modules} m
+                                             JOIN {course_modules} cm on cm.module = m.id
+                                            WHERE cm.id = ?", [$context->instanceid]);
+                $page->set_pagetype("mod-$mod-view");
+                break;
+            case CONTEXT_USER:
+                $page->set_pagelayout('mydashboard');
+                $page->set_pagetype('my-index');
+                break;
+            default:
+                throw new coding_exception('Unsupported context for test');
+        }
+
+        $page->blocks->load_blocks();
+
+        $page->blocks->add_block_at_end_of_default_region('comments');
+
+        // We need to use another page object as load_blocks() only loads the blocks once.
+        $page2 = new \moodle_page();
+        $page2->set_context($page->context);
+        $page2->set_pagelayout($page->pagelayout);
+        $page2->set_pagetype($page->pagetype);
+        if ($course) {
+            $page2->set_course($course);
+        }
+
+        $page->blocks->load_blocks();
+        $page2->blocks->load_blocks();
+        $blocks = $page2->blocks->get_blocks_for_region($page2->blocks->get_default_region());
+        $block = end($blocks);
+
+        $block = block_instance('comments', $block->instance);
+
+        return $block;
+    }
+
+    /**
+     * Test for provider::get_metadata().
+     */
+    public function test_get_metadata() {
+        $collection = new collection('block_comments');
+        $newcollection = provider::get_metadata($collection);
+        $itemcollection = $newcollection->get_collection();
+        $this->assertCount(1, $itemcollection);
+
+        $link = reset($itemcollection);
+
+        $this->assertEquals('core_comment', $link->get_name());
+        $this->assertEmpty($link->get_privacy_fields());
+        $this->assertEquals('privacy:metadata:core_comment', $link->get_summary());
+    }
+
+    /**
+     * Test for provider::get_contexts_for_userid() when user had not posted any comments..
+     */
+    public function test_get_contexts_for_userid_no_comment() {
+        $this->setUser($this->student1);
+        $coursecontext1 = context_course::instance($this->course1->id);
+        $this->add_comment('New comment', $coursecontext1);
+
+        $this->setUser($this->student2);
+        $contextlist = provider::get_contexts_for_userid($this->student2->id);
+        $this->assertCount(0, $contextlist);
+    }
+
+    /**
+     * Test for provider::get_contexts_for_userid().
+     */
+    public function test_get_contexts_for_userid() {
+        $coursecontext1 = context_course::instance($this->course1->id);
+        $coursecontext2 = context_course::instance($this->course2->id);
+
+        $this->setUser($this->student12);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext2);
+
+        $contextlist = provider::get_contexts_for_userid($this->student12->id);
+        $this->assertCount(2, $contextlist);
+
+        $contextids = $contextlist->get_contextids();
+        $this->assertEquals([$coursecontext1->id, $coursecontext2->id], $contextids, '', 0.0, 10, true);
+    }
+
+    /**
+     * Test for provider::export_user_data() when the user has not posted any comments.
+     */
+    public function test_export_for_context_no_comment() {
+        $coursecontext1 = context_course::instance($this->course1->id);
+        $coursecontext2 = context_course::instance($this->course2->id);
+
+        $this->setUser($this->student1);
+        $this->add_comment('New comment', $coursecontext1);
+
+        $this->setUser($this->student2);
+
+        $this->setUser($this->student2);
+        $this->export_context_data_for_user($this->student2->id, $coursecontext2, 'block_comments');
+        $writer = \core_privacy\local\request\writer::with_context($coursecontext2);
+        $this->assertFalse($writer->has_any_data());
+
+    }
+
+    /**
+     * Test for provider::export_user_data().
+     */
+    public function test_export_for_context() {
+        $coursecontext1 = context_course::instance($this->course1->id);
+        $coursecontext2 = context_course::instance($this->course2->id);
+
+        $this->setUser($this->student12);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext2);
+
+        // Export all of the data for the context.
+        $this->export_context_data_for_user($this->student12->id, $coursecontext1, 'block_comments');
+        $writer = \core_privacy\local\request\writer::with_context($coursecontext1);
+        $this->assertTrue($writer->has_any_data());
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        $coursecontext1 = context_course::instance($this->course1->id);
+        $coursecontext2 = context_course::instance($this->course2->id);
+
+        $this->setUser($this->student1);
+        $this->add_comment('New comment', $coursecontext1);
+
+        $this->setUser($this->student2);
+        $this->add_comment('New comment', $coursecontext2);
+
+        $this->setUser($this->student12);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext2);
+
+        // Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2.
+        $this->assertEquals(
+                3,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id])
+        );
+        $this->assertEquals(
+                2,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id])
+        );
+
+        // Delete data based on context.
+        provider::delete_data_for_all_users_in_context($coursecontext1);
+
+        // After deletion, the comments for $coursecontext1 should have been deleted.
+        $this->assertEquals(
+                0,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id])
+        );
+        $this->assertEquals(
+                2,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id])
+        );
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context() when there are also comments from other plugins.
+     */
+    public function test_delete_data_for_all_users_in_context_with_comments_from_other_plugins() {
+        global $DB;
+
+        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $instance = $assigngenerator->create_instance(['course' => $this->course1]);
+        $cm = get_coursemodule_from_instance('assign', $instance->id);
+        $assigncontext = \context_module::instance($cm->id);
+        $assign = new \assign($assigncontext, $cm, $this->course1);
+
+        // Add a comments block in the assignment page.
+        $this->add_comments_block_in_context($assigncontext);
+
+        $submission = $assign->get_user_submission($this->student1->id, true);
+
+        $options = new stdClass();
+        $options->area = 'submission_comments';
+        $options->course = $assign->get_course();
+        $options->context = $assigncontext;
+        $options->itemid = $submission->id;
+        $options->component = 'assignsubmission_comments';
+        $options->showcount = true;
+        $options->displaycancel = true;
+
+        $comment = new comment($options);
+        $comment->set_post_permission(true);
+
+        $this->setUser($this->student1);
+        $comment->add('Comment from student 1');
+
+        $this->add_comment('New comment', $assigncontext);
+
+        $this->setUser($this->student2);
+        $this->add_comment('New comment', $assigncontext);
+
+        // Before deletion, we should have 3 comments in $assigncontext.
+        // One comment is for the assignment submission and 2 are for the comments block.
+        $this->assertEquals(
+                3,
+                $DB->count_records('comments', ['contextid' => $assigncontext->id])
+        );
+        $this->assertEquals(
+                2,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $assigncontext->id])
+        );
+
+        provider::delete_data_for_all_users_in_context($assigncontext);
+
+        // After deletion, the comments for $assigncontext in the comment block should have been deleted,
+        // but the assignment submission comment should be left.
+        $this->assertEquals(
+                1,
+                $DB->count_records('comments', ['contextid' => $assigncontext->id])
+        );
+        $this->assertEquals(
+                0,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $assigncontext->id])
+        );
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $coursecontext1 = context_course::instance($this->course1->id);
+        $coursecontext2 = context_course::instance($this->course2->id);
+
+        $this->setUser($this->student1);
+        $this->add_comment('New comment', $coursecontext1);
+
+        $this->setUser($this->student2);
+        $this->add_comment('New comment', $coursecontext2);
+
+        $this->setUser($this->student12);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext1);
+        $this->add_comment('New comment', $coursecontext2);
+
+        // Before deletion, we should have 3 comments in $coursecontext1 and 2 comments in $coursecontext2,
+        // and 3 comments by student12 in $coursecontext1 and $coursecontext2 combined.
+        $this->assertEquals(
+                3,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id])
+        );
+        $this->assertEquals(
+                2,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id])
+        );
+        $this->assertEquals(
+                3,
+                $DB->count_records('comments', ['component' => 'block_comments', 'userid' => $this->student12->id])
+        );
+
+        $contextlist = new \core_privacy\local\request\approved_contextlist($this->student12, 'block_comments',
+                [$coursecontext1->id, $coursecontext2->id]);
+        provider::delete_data_for_user($contextlist);
+
+        // After deletion, the comments for the student12 should have been deleted.
+        $this->assertEquals(
+                1,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext1->id])
+        );
+        $this->assertEquals(
+                1,
+                $DB->count_records('comments', ['component' => 'block_comments', 'contextid' => $coursecontext2->id])
+        );
+        $this->assertEquals(
+                0,
+                $DB->count_records('comments', ['component' => 'block_comments', 'userid' => $this->student12->id])
+        );
+    }
+
+    /**
+     * Test for provider::delete_data_for_user() when there are also comments from other plugins.
+     */
+    public function test_delete_data_for_user_with_comments_from_other_plugins() {
+        global $DB;
+
+        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $instance = $assigngenerator->create_instance(['course' => $this->course1]);
+        $cm = get_coursemodule_from_instance('assign', $instance->id);
+        $assigncontext = \context_module::instance($cm->id);
+        $assign = new \assign($assigncontext, $cm, $this->course1);
+
+        // Add a comments block in the assignment page.
+        $this->add_comments_block_in_context($assigncontext);
+
+        $submission = $assign->get_user_submission($this->student1->id, true);
+
+        $options = new stdClass();
+        $options->area = 'submission_comments';
+        $options->course = $assign->get_course();
+        $options->context = $assigncontext;
+        $options->itemid = $submission->id;
+        $options->component = 'assignsubmission_comments';
+        $options->showcount = true;
+        $options->displaycancel = true;
+
+        $comment = new comment($options);
+        $comment->set_post_permission(true);
+
+        $this->setUser($this->student1);
+        $comment->add('Comment from student 1');
+
+        $this->add_comment('New comment', $assigncontext);
+        $this->add_comment('New comment', $assigncontext);
+
+        // Before deletion, we should have 3 comments in $assigncontext.
+        // one comment is for the assignment submission and 2 are for the comments block.
+        $this->assertEquals(
+                3,
+                $DB->count_records('comments', ['contextid' => $assigncontext->id])
+        );
+
+        $contextlist = new \core_privacy\local\request\approved_contextlist($this->student1, 'block_comments',
+                [$assigncontext->id]);
+        provider::delete_data_for_user($contextlist);
+
+        // After deletion, the comments for the student1 in the comment block should have been deleted,
+        // but the assignment submission comment should be left.
+        $this->assertEquals(
+                1,
+                $DB->count_records('comments', ['contextid' => $assigncontext->id])
+        );
+        $this->assertEquals(
+                0,
+                $DB->count_records('comments', ['component' => 'block_comments', 'userid' => $this->student1->id])
+        );
+    }
+}
diff --git a/blocks/recent_activity/classes/privacy/provider.php b/blocks/recent_activity/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..8e998d5
--- /dev/null
@@ -0,0 +1,47 @@
+<?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 block_recent_activity.
+ *
+ * @package    block_recent_activity
+ * @category   privacy
+ * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_recent_activity\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The block_recent_activity does not keep any data for more than COURSE_MAX_RECENT_PERIOD.
+ *
+ * @copyright  2018 Shamim Rezaie <shamim@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 f7d1333..3f6eadb 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['pluginname'] = 'Recent activity';
+$string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.';
 $string['recent_activity:addinstance'] = 'Add a new recent activity block';
 $string['recent_activity:viewaddupdatemodule'] = 'View added and updated modules in recent activity block';
 $string['recent_activity:viewdeletemodule'] = 'View deleted modules in recent activity block';
diff --git a/blocks/tests/privacy_test.php b/blocks/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..8c6327e
--- /dev/null
@@ -0,0 +1,364 @@
+<?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_block
+ * @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_block\privacy\provider;
+
+/**
+ * Data provider testcase class.
+ *
+ * @package    core_block
+ * @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_block_privacy_testcase extends provider_testcase {
+
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    public function test_get_contexts_for_userid() {
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $manager = $this->get_block_manager(['region-a'], $c1ctx);
+        $manager->add_block('myprofile', 'region-a', 0, false);
+        $manager->load_blocks();
+        $blockmyprofile = $manager->get_blocks_for_region('region-a')[0];
+
+        $manager = $this->get_block_manager(['region-a'], $c2ctx);
+        $manager->add_block('login', 'region-a', 0, false);
+        $manager->add_block('mentees', 'region-a', 1, false);
+        $manager->load_blocks();
+        list($blocklogin, $blockmentees) = $manager->get_blocks_for_region('region-a');
+
+        $manager = $this->get_block_manager(['region-a'], $u1ctx);
+        $manager->add_block('private_files', 'region-a', 0, false);
+        $manager->load_blocks();
+        $blockprivatefiles = $manager->get_blocks_for_region('region-a')[0];
+
+        $this->set_hidden_pref($blocklogin, true, $u1->id);
+        $this->set_hidden_pref($blockprivatefiles, true, $u1->id);
+        $this->set_docked_pref($blockmyprofile, true, $u1->id);
+        $this->set_docked_pref($blockmentees, true, $u1->id);
+        $this->set_docked_pref($blockmentees, true, $u2->id);
+
+        $contextids = provider::get_contexts_for_userid($u1->id)->get_contextids();
+        $this->assertCount(4, $contextids);
+        $this->assertTrue(in_array($blocklogin->context->id, $contextids));
+        $this->assertTrue(in_array($blockprivatefiles->context->id, $contextids));
+        $this->assertTrue(in_array($blockmyprofile->context->id, $contextids));
+        $this->assertTrue(in_array($blockmentees->context->id, $contextids));
+
+        $contextids = provider::get_contexts_for_userid($u2->id)->get_contextids();
+        $this->assertCount(1, $contextids);
+        $this->assertTrue(in_array($blockmentees->context->id, $contextids));
+    }
+
+    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();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $manager = $this->get_block_manager(['region-a'], $c1ctx);
+        $manager->add_block('myprofile', 'region-a', 0, false);
+        $manager->load_blocks();
+        $blockmyprofile = $manager->get_blocks_for_region('region-a')[0];
+
+        $manager = $this->get_block_manager(['region-a'], $c2ctx);
+        $manager->add_block('login', 'region-a', 0, false);
+        $manager->add_block('mentees', 'region-a', 1, false);
+        $manager->load_blocks();
+        list($blocklogin, $blockmentees) = $manager->get_blocks_for_region('region-a');
+
+        $manager = $this->get_block_manager(['region-a'], $u1ctx);
+        $manager->add_block('private_files', 'region-a', 0, false);
+        $manager->load_blocks();
+        $blockprivatefiles = $manager->get_blocks_for_region('region-a')[0];
+
+        $this->set_hidden_pref($blocklogin, true, $u1->id);
+        $this->set_hidden_pref($blocklogin, true, $u2->id);
+        $this->set_hidden_pref($blockprivatefiles, true, $u1->id);
+        $this->set_hidden_pref($blockmyprofile, true, $u1->id);
+        $this->set_docked_pref($blockmyprofile, true, $u1->id);
+        $this->set_docked_pref($blockmentees, true, $u1->id);
+        $this->set_docked_pref($blockmentees, true, $u2->id);
+
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockprivatefiles->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockmyprofile->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmyprofile->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+
+        provider::delete_data_for_user(new approved_contextlist($u1, 'core_block', [$blocklogin->context->id,
+            $blockmyprofile->context->id, $blockmentees->context->id]));
+
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockprivatefiles->instance->id}hidden"]));
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockmyprofile->instance->id}hidden"]));
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmyprofile->instance->id}"]));
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+    }
+
+    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();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+
+        $manager = $this->get_block_manager(['region-a'], $c1ctx);
+        $manager->add_block('myprofile', 'region-a', 0, false);
+        $manager->load_blocks();
+        $blockmyprofile = $manager->get_blocks_for_region('region-a')[0];
+
+        $manager = $this->get_block_manager(['region-a'], $c2ctx);
+        $manager->add_block('login', 'region-a', 0, false);
+        $manager->add_block('mentees', 'region-a', 1, false);
+        $manager->load_blocks();
+        list($blocklogin, $blockmentees) = $manager->get_blocks_for_region('region-a');
+
+        $manager = $this->get_block_manager(['region-a'], $u1ctx);
+        $manager->add_block('private_files', 'region-a', 0, false);
+        $manager->load_blocks();
+        $blockprivatefiles = $manager->get_blocks_for_region('region-a')[0];
+
+        $this->set_hidden_pref($blocklogin, true, $u1->id);
+        $this->set_hidden_pref($blocklogin, true, $u2->id);
+        $this->set_hidden_pref($blockprivatefiles, true, $u1->id);
+        $this->set_hidden_pref($blockmyprofile, true, $u1->id);
+        $this->set_docked_pref($blockmyprofile, true, $u1->id);
+        $this->set_docked_pref($blockmentees, true, $u1->id);
+        $this->set_docked_pref($blockmentees, true, $u2->id);
+
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockprivatefiles->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockmyprofile->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmyprofile->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+
+        // Nothing happens.
+        provider::delete_data_for_all_users_in_context($c1ctx);
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockprivatefiles->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockmyprofile->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmyprofile->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+
+        // Delete one block.
+        provider::delete_data_for_all_users_in_context($blocklogin->context);
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockprivatefiles->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockmyprofile->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmyprofile->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+
+        // Delete another block.
+        provider::delete_data_for_all_users_in_context($blockmyprofile->context);
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "block{$blocklogin->instance->id}hidden"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockprivatefiles->instance->id}hidden"]));
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "block{$blockmyprofile->instance->id}hidden"]));
+        $this->assertFalse($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmyprofile->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u1->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+        $this->assertTrue($DB->record_exists('user_preferences', ['userid' => $u2->id,
+            'name' => "docked_block_instance_{$blockmentees->instance->id}"]));
+    }
+
+    public function test_export_data_for_user() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $c1ctx = context_course::instance($c1->id);
+        $c2ctx = context_course::instance($c2->id);
+        $u1ctx = context_user::instance($u1->id);
+        $u2ctx = context_user::instance($u2->id);
+        $yes = transform::yesno(true);
+        $no = transform::yesno(false);
+
+        $manager = $this->get_block_manager(['region-a'], $c1ctx);
+        $manager->add_block('myprofile', 'region-a', 0, false);
+        $manager->add_block('login', 'region-a', 1, false);
+        $manager->add_block('mentees', 'region-a', 2, false);
+        $manager->add_block('private_files', 'region-a', 3, false);
+        $manager->load_blocks();
+        list($bmyprofile, $blogin, $bmentees, $bprivatefiles) = $manager->get_blocks_for_region('region-a');
+
+        // Set some user preferences.
+        $this->set_hidden_pref($blogin, true, $u1->id);
+        $this->set_docked_pref($blogin, false, $u1->id);
+        $this->set_docked_pref($blogin, true, $u2->id);
+        $this->set_hidden_pref($bprivatefiles, false, $u1->id);
+        $this->set_docked_pref($bprivatefiles, true, $u2->id);
+        $this->set_docked_pref($bmyprofile, true, $u1->id);
+        $this->set_docked_pref($bmentees, true, $u2->id);
+
+        // Export data.
+        provider::export_user_data(new approved_contextlist($u1, 'core_block', [$bmyprofile->context->id, $blogin->context->id,
+            $bmentees->context->id, $bprivatefiles->context->id]));
+        $prefs = writer::with_context($bmentees->context)->get_user_context_preferences('core_block');
+        $this->assertEmpty((array) $prefs);
+
+        $prefs = writer::with_context($blogin->context)->get_user_context_preferences('core_block');
+        $this->assertEquals($no, $prefs->block_is_docked->value);
+        $this->assertEquals($yes, $prefs->block_is_hidden->value);
+
+        $prefs = writer::with_context($bprivatefiles->context)->get_user_context_preferences('core_block');
+        $this->assertObjectNotHasAttribute('block_is_docked', $prefs);
+        $this->assertEquals($no, $prefs->block_is_hidden->value);
+
+        $prefs = writer::with_context($bmyprofile->context)->get_user_context_preferences('core_block');
+        $this->assertEquals($yes, $prefs->block_is_docked->value);
+        $this->assertObjectNotHasAttribute('block_is_hidden', $prefs);
+    }
+
+    /**
+     * Get the block manager.
+     *
+     * @param array $regions The regions.
+     * @param context $context The context.
+     * @param string $pagetype The page type.
+     * @param string $subpage The sub page.
+     * @return block_manager
+     */
+    protected function get_block_manager($regions, $context, $pagetype = 'page-type', $subpage = '') {
+        $page = new moodle_page();
+        $page->set_context($context);
+        $page->set_pagetype($pagetype);
+        $page->set_subpage($subpage);
+        $page->set_url(new moodle_url('/'));
+
+        $blockmanager = new block_manager($page);
+        $blockmanager->add_regions($regions, false);
+        $blockmanager->set_default_region($regions[0]);
+
+        return $blockmanager;
+    }
+
+    /**
+     * Set a docked preference.
+     *
+     * @param block_base $block The block.
+     * @param bool $value The value.
+     * @param int $userid The user ID.
+     */
+    protected function set_docked_pref($block, $value, $userid) {
+        set_user_preference("docked_block_instance_{$block->instance->id}", $value, $userid);
+    }
+
+    /**
+     * Set a hidden preference.
+     *
+     * @param block_base $block The block.
+     * @param bool $value The value.
+     * @param int $userid The user ID.
+     */
+    protected function set_hidden_pref($block, $value, $userid) {
+        set_user_preference("block{$block->instance->id}hidden", $value, $userid);
+    }
+
+}
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 74d417b..b38af38 100644 (file)
@@ -50,7 +50,7 @@ abstract class moodleform_mod extends moodleform {
      */
     protected $_customcompletionelements;
     /**
-     * @var string name of module
+     * @var string name of module.
      */
     protected $_modname;
     /** current context, course or module depends if already exists*/
@@ -86,13 +86,15 @@ abstract class moodleform_mod extends moodleform {
         require_once($CFG->dirroot . '/course/format/lib.php');
         $this->courseformat = course_get_format($course);
 
-        // Guess module name
-        $matches = array();
-        if (!preg_match('/^mod_([^_]+)_mod_form$/', get_class($this), $matches)) {
-            debugging('Use $modname parameter or rename form to mod_xx_mod_form, where xx is name of your module');
-            print_error('unknownmodulename');
+        // Guess module name if not set.
+        if (is_null($this->_modname)) {
+            $matches = array();
+            if (!preg_match('/^mod_([^_]+)_mod_form$/', get_class($this), $matches)) {
+                debugging('Rename form to mod_xx_mod_form, where xx is name of your module');
+                print_error('unknownmodulename');
+            }
+            $this->_modname = $matches[1];
         }
-        $this->_modname = $matches[1];
         $this->init_features();
         parent::__construct('modedit.php');
     }
index d207d6c..9c61530 100644 (file)
@@ -199,6 +199,32 @@ class conversion extends \core\persistent {
         ]);
     }
 
+    /**
+     * Remove orphan records.
+     *
+     * Records are considered orphans when their source file not longer exists.
+     * In this scenario we do not want to keep the converted file any longer,
+     * in particular to be compliant with privacy laws.
+     */
+    public static function remove_orphan_records() {
+        global $DB;
+
+        $sql = "
+            SELECT c.id
+              FROM {" . self::TABLE . "} c
+         LEFT JOIN {files} f
+                ON f.id = c.sourcefileid
+             WHERE f.id IS NULL";
+        $ids = $DB->get_fieldset_sql($sql, []);
+
+        if (empty($ids)) {
+            return;
+        }
+
+        list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
+        $DB->delete_records_select(self::TABLE, "id $insql", $inparams);
+    }
+
     /**
      * Set the source file id for the conversion.
      *
diff --git a/files/classes/privacy/provider.php b/files/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..2bf2dbb
--- /dev/null
@@ -0,0 +1,71 @@
+<?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_files
+ * @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_files\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\collection;
+
+/**
+ * Data provider class.
+ *
+ * This only describes the files table, all components must handle the file exporting
+ * and deletion themselves.
+ *
+ * @package    core_files
+ * @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\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('files', [
+            'contenthash' => 'privacy:metadata:files:contenthash',
+            'filepath' => 'privacy:metadata:files:filepath',
+            'filename' => 'privacy:metadata:files:filename',
+            'userid' => 'privacy:metadata:files:userid',
+            'filesize' => 'privacy:metadata:files:filesize',
+            'mimetype' => 'privacy:metadata:files:mimetype',
+            'source' => 'privacy:metadata:files:source',
+            'author' => 'privacy:metadata:files:author',
+            'license' => 'privacy:metadata:files:license',
+            'timecreated' => 'privacy:metadata:files:timecreated',
+            'timemodified' => 'privacy:metadata:files:timemodified',
+        ], 'privacy:metadata:files');
+
+        return $collection;
+    }
+
+}
index 4f774f1..182260f 100644 (file)
@@ -48,6 +48,7 @@ class conversion_cleanup_task extends \core\task\scheduled_task {
      */
     public function execute() {
         \core_files\conversion::remove_old_conversion_records();
+        \core_files\conversion::remove_orphan_records();
     }
 
 }
diff --git a/files/converter/classes/privacy/provider.php b/files/converter/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..cd1874b
--- /dev/null
@@ -0,0 +1,96 @@
+<?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_fileconverter
+ * @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_fileconverter\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+use context;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+
+/**
+ * Data provider class.
+ *
+ * @package    core_fileconverter
+ * @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('fileconverter', [], 'privacy:metadata:plugintypefileconverter');
+        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 {
+        // We cannot associate files with a particular user as it would require to know all the details about
+        // the source file, which only the component owning it knows about. And, the 'usermodified' attribute
+        // of the conversion class is not an identifier of data ownership, merely that this user was logged in
+        // when the conversion was requested. As such, as the owning component should declare the source file,
+        // and we can't make sense of 'usermodified', we will not be reporting anything here. Also note that
+        // the clean up task will ensure that whenever a source file is delete, its conversions also are.
+        return new \core_privacy\local\request\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 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) {
+    }
+
+    /**
+     * 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 662ccc3..3293bcc 100644 (file)
@@ -412,4 +412,47 @@ class core_files_conversion_testcase extends advanced_testcase {
 
         $this->assertEquals(1, $DB->count_records(conversion::TABLE));
     }
+
+    /**
+     * Test orphan records are removed.
+     */
+    public function test_remove_orphan_records() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $sf1 = $this->create_stored_file('1', '1');
+        $sf2 = $this->create_stored_file('2', '2');
+        $sf3 = $this->create_stored_file('3', '3');
+        $c1 = new conversion(0, (object) ['sourcefileid' => $sf1->get_id(), 'targetformat' => 'pdf']);
+        $c1->create();
+        $c2 = new conversion(0, (object) ['sourcefileid' => $sf2->get_id(), 'targetformat' => 'pdf']);
+        $c2->create();
+        $c3 = new conversion(0, (object) ['sourcefileid' => $sf3->get_id(), 'targetformat' => 'pdf']);
+        $c3->create();
+
+        $this->assertTrue(conversion::record_exists($c1->get('id')));
+        $this->assertTrue(conversion::record_exists($c2->get('id')));
+        $this->assertTrue(conversion::record_exists($c3->get('id')));
+
+        // Nothing should happen here.
+        conversion::remove_orphan_records();
+        $this->assertTrue(conversion::record_exists($c1->get('id')));
+        $this->assertTrue(conversion::record_exists($c2->get('id')));
+        $this->assertTrue(conversion::record_exists($c3->get('id')));
+
+        // Delete file #2.
+        $sf2->delete();
+        conversion::remove_orphan_records();
+        $this->assertTrue(conversion::record_exists($c1->get('id')));
+        $this->assertFalse(conversion::record_exists($c2->get('id')));
+        $this->assertTrue(conversion::record_exists($c3->get('id')));
+
+        // Delete file #1, #3.
+        $sf1->delete();
+        $sf3->delete();
+        conversion::remove_orphan_records();
+        $this->assertFalse(conversion::record_exists($c1->get('id')));
+        $this->assertFalse(conversion::record_exists($c2->get('id')));
+        $this->assertFalse(conversion::record_exists($c3->get('id')));
+    }
 }
index 2e5ba59..63f6c6a 100644 (file)
@@ -30,6 +30,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotcreatedboninstall'] = '<p>Αδυναμία δημιουργίας βάσης δεδομένων.</p>
+<p>Η βάση δεδομένων που προσδιορίστηκε δεν υπάρχει και ο χρήστης που δόθηκε δεν έχει δικαίωμα να δημιουργήσει την βάση δεδομένων.</p>
+<p>Ο διαχειριστής του ιστοτόπου πρέπει να επαληθεύσει την ρύθμιση της βάσης δεδομένων.</p>';
 $string['cannotcreatelangdir'] = 'Δε δημιουργήθηκε φάκελος γλώσσας.';
 $string['cannotcreatetempdir'] = 'Δε δημιουργήθηκε φάκελος temp.';
 $string['cannotdownloadcomponents'] = 'Δεν κατέβηκαν τα τμήματα.';
index 103843d..a55a0dd 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'భాష';
+$string['moodlelogo'] = 'మూడ్లె లోగో';
 $string['next'] = 'తరువాతి';
 $string['previous'] = 'గత';
 $string['reload'] = 'మళ్ళీ లోడు చెయ్యి';
index d4054b6..4dcef01 100644 (file)
@@ -31,5 +31,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Wikà';
+$string['moodlelogo'] = 'Logo ng Moodle';
 $string['next'] = 'Susunod';
 $string['previous'] = 'Nakaraan';
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 cb7597e..0b92684 100644 (file)
@@ -388,6 +388,34 @@ $string['numawardstat'] = 'This badge has been issued {$a} user(s).';
 $string['overallcrit'] = 'of the selected criteria are complete.';
 $string['potentialrecipients'] = 'Potential badge recipients';
 $string['preferences'] = 'Badge preferences';
+$string['privacy:metadata:backpack'] = 'A record of user\'s backpacks';
+$string['privacy:metadata:backpack:backpackuid'] = 'The backpack unique identifier';
+$string['privacy:metadata:backpack:backpackurl'] = 'The backpack URL';
+$string['privacy:metadata:backpack:email'] = 'The email associated with the backpack';
+$string['privacy:metadata:backpack:userid'] = 'The ID of the user whose backpack it is';
+$string['privacy:metadata:badge'] = 'A collection of badges';
+$string['privacy:metadata:badge:timecreated'] = 'The date at which the badge was created';
+$string['privacy:metadata:badge:timemodified'] = 'The date at which the badge was last modified';
+$string['privacy:metadata:badge:usercreated'] = 'The ID of the user who created the badge';
+$string['privacy:metadata:badge:usermodified'] = 'The ID of the user who modified the badge';
+$string['privacy:metadata:criteriamet'] = 'A collection of criteria met';
+$string['privacy:metadata:criteriamet:datemet'] = 'The date at which the criteria was met';
+$string['privacy:metadata:criteriamet:userid'] = 'The ID of the user whose criteria is met';
+$string['privacy:metadata:external:backpacks'] = 'Information shared when users submit their badges to an external backpack';
+$string['privacy:metadata:external:backpacks:badge'] = 'The name of the badge';
+$string['privacy:metadata:external:backpacks:description'] = 'The description of the badge';
+$string['privacy:metadata:external:backpacks:image'] = 'The image of the badge';
+$string['privacy:metadata:external:backpacks:issuer'] = 'Some information about the issuer';
+$string['privacy:metadata:external:backpacks:url'] = 'The Moodle URL where the issued badge information can be seen';
+$string['privacy:metadata:issued'] = 'A record of badges awarded';
+$string['privacy:metadata:issued:dateexpire'] = 'The date at which the award expires';
+$string['privacy:metadata:issued:dateissued'] = 'The date of the award';
+$string['privacy:metadata:issued:userid'] = 'The ID of the user who was awarded a badge';
+$string['privacy:metadata:manualaward'] = 'A record of manual awards';
+$string['privacy:metadata:manualaward:datemet'] = 'The date at which the manual award was met';
+$string['privacy:metadata:manualaward:issuerid'] = 'The ID of the user awarding the badge';
+$string['privacy:metadata:manualaward:issuerrole'] = 'The role of the user awarding the badge';
+$string['privacy:metadata:manualaward:recipientid'] = 'The ID of the user who is manually awarded a badge';
 $string['recipients'] = 'Badge recipients';
 $string['recipientdetails'] = 'Recipient details';
 $string['recipientidentificationproblem'] = 'Cannot find a recipient of this badge among the existing users.';
index ad04aef..806aa16 100644 (file)
@@ -56,6 +56,10 @@ $string['myblocks'] = 'My blocks';
 $string['onthispage'] = 'On this page';
 $string['pagetypes'] = 'Page types';
 $string['pagetypewarning'] = 'The previously specified page type is no longer selectable. Please choose the most appropriate page type below.';
+$string['privacy:metadata:userpref:dockedinstance'] = 'Records when the user docked a block';
+$string['privacy:metadata:userpref:hiddenblock'] = 'Records when the user collapsed/hid a block';
+$string['privacy:request:blockisdocked'] = 'Indicates whether the block was docked';
+$string['privacy:request:blockishidden'] = 'Indicates whether the block was hidden/collapsed';
 $string['region'] = 'Region';
 $string['showblock'] = 'Show {$a} block';
 $string['showoncontextandsubs'] = 'Display on \'{$a}\' and any pages within it';
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.';
similarity index 68%
rename from lang/en/register.php
rename to lang/en/fileconverter.php
index 36d0265..add9823 100644 (file)
 //
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
 /**
- *  Strings for core_register.
+ * Language file.
  *
- * @package    core_register
- * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @package    core_fileconverter
+ * @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
  */
-$string['privacy:metadata'] = 'The Register plugin does not store any personal data.';
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['privacy:metadata:plugintypefileconverter'] = 'The fileconverter subsystem acts as a channel, passing requests from plugins to the various fileconverter plugins.';
diff --git a/lang/en/files.php b/lang/en/files.php
new file mode 100644 (file)
index 0000000..7261c19
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Language file.
+ *
+ * @package    core_files
+ * @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();
+
+$string['privacy:metadata:files'] = 'A record of the files uploaded or shared by users';
+$string['privacy:metadata:files:author'] = 'The author of the file\'s content';
+$string['privacy:metadata:files:contenthash'] = 'A hash of the file\'s content';
+$string['privacy:metadata:files:filename'] = 'The name of the file in its file area';
+$string['privacy:metadata:files:filepath'] = 'The path to the file in its file area';
+$string['privacy:metadata:files:filesize'] = 'The size of the file';
+$string['privacy:metadata:files:license'] = 'The license of the file\'s content';
+$string['privacy:metadata:files:mimetype'] = 'The MIME type of the file';
+$string['privacy:metadata:files:source'] = 'The source of the file';
+$string['privacy:metadata:files:timecreated'] = 'The date at which the file was created';
+$string['privacy:metadata:files:timemodified'] = 'The date at which the file was last modified';
+$string['privacy:metadata:files:userid'] = 'The user who created the file';
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 c5028b1..f7debb8 100644 (file)
@@ -472,10 +472,8 @@ $cache = '.var_export($cache, true).';
             'plugin'      => null,
             'portfolio'   => $CFG->dirroot.'/portfolio',
             'privacy'     => $CFG->dirroot . '/privacy',
-            'publish'     => $CFG->dirroot.'/course/publish',
             'question'    => $CFG->dirroot.'/question',
             'rating'      => $CFG->dirroot.'/rating',
-            'register'    => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
             'repository'  => $CFG->dirroot.'/repository',
             'rss'         => $CFG->dirroot.'/rss',
             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
index dfe2ad6..3e3bd96 100644 (file)
@@ -36,7 +36,7 @@ class core_component_testcase extends advanced_testcase {
      * this is defined here to annoy devs that try to add more without any thinking,
      * always verify that it does not collide with any existing add-on modules and subplugins!!!
      */
-    const SUBSYSTEMCOUNT = 68;
+    const SUBSYSTEMCOUNT = 66;
 
     public function setUp() {
         $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
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/assign/classes/privacy/assign_plugin_request_data.php b/mod/assign/classes/privacy/assign_plugin_request_data.php
new file mode 100644 (file)
index 0000000..c935972
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * This file contains the mod_assign assign_plugin_request_data class
+ *
+ * For assign plugin privacy data to fulfill requests.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_assign\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * An object for fulfilling an assign plugin data request.
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assign_plugin_request_data {
+
+    /** @var context The context that we are dealing with. */
+    protected $context;
+
+    /** @var object For submisisons the submission object, for feedback the grade object. */
+    protected $pluginobject;
+
+    /** @var array The path or location that we are exporting data to. */
+    protected $subcontext;
+
+    /** @var object If set then only export data related directly to this user. */
+    protected $user;
+
+    /** @var assign The assign object */
+    protected $assign;
+
+    /**
+     * Object creator for assign plugin request data.
+     *
+     * @param \context $context Context object.
+     * @param \stdClass $pluginobject The grade object.
+     * @param array  $subcontext Directory / file location.
+     * @param \stdClass $user The user object.
+     * @param \assign $assign The assign object.
+     */
+    public function __construct(\context $context, \assign $assign, \stdClass $pluginobject = null, array $subcontext = [],
+            \stdClass $user = null) {
+        $this->context = $context;
+        $this->pluginobject = $pluginobject;
+        $this->subcontext = $subcontext;
+        $this->user = $user;
+        $this->assign = $assign;
+    }
+
+    /**
+     * Getter for this attribute.
+     *
+     * @return context Context
+     */
+    public function get_context() {
+        return $this->context;
+    }
+
+    /**
+     * Getter for this attribute.
+     *
+     * @return object The assign plugin object
+     */
+    public function get_pluginobject() {
+        return $this->pluginobject;
+    }
+
+    /**
+     * Getter for this attribute.
+     *
+     * @return array The location (path) that this data is being writter to.
+     */
+    public function get_subcontext() {
+        return $this->subcontext;
+    }
+
+    /**
+     * Getter for this attribute.
+     *
+     * @return object The user id. If set then only information directly related to this user ID will be returned.
+     */
+    public function get_user() {
+        return $this->user;
+    }
+
+    /**
+     * Getter for this attribute.
+     *
+     * @return assign The assign object.
+     */
+    public function get_assign() {
+        return $this->assign;
+    }
+}
diff --git a/mod/assign/classes/privacy/assignfeedback_provider.php b/mod/assign/classes/privacy/assignfeedback_provider.php
new file mode 100644 (file)
index 0000000..c93852a
--- /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/>.
+
+/**
+ * This file contains the assignfeedback_provider interface.
+ *
+ * Assignment Sub plugins should implement this if they store personal information.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_assign\privacy;
+
+use core_privacy\local\request\contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+interface assignfeedback_provider extends \core_privacy\local\request\plugin\subplugin_provider {
+
+    /**
+     * Retrieves the contextids associated with the provided userid for this subplugin.
+     * NOTE if your subplugin must have an entry in the assign_grade table to work, then this
+     * method can be empty.
+     *
+     * @param int $userid The user ID to get context IDs for.
+     * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs.
+     */
+    public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist);
+
+    /**
+     * Returns student user ids related to the provided teacher ID. If an entry must be present in the assign_grade table for
+     * your plugin to work then there is no need to fill in this method. If you filled in get_context_for_userid_within_feedback()
+     * then you probably have to fill this in as well.
+     *
+     * @param  useridlist $useridlist A list of user IDs of students graded by this user.
+     */
+    public static function get_student_user_ids(useridlist $useridlist);
+
+    /**
+     * Export feedback data with the available grade and userid information provided.
+     * assign_plugin_request_data contains:
+     * - context
+     * - grade object
+     * - current path (subcontext)
+     * - user object
+     *
+     * @param  assign_plugin_request_data $exportdata Contains data to help export the user information.
+     */
+    public static function export_feedback_user_data(assign_plugin_request_data $exportdata);
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     * assign_plugin_request_data contains:
+     * - context
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin.
+     */
+    public static function delete_feedback_for_context(assign_plugin_request_data $requestdata);
+
+    /**
+     * Calling this function should delete all user data associated with this grade.
+     * assign_plugin_request_data contains:
+     * - context
+     * - grade object
+     * - user object
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data.
+     */
+    public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata);
+}
\ No newline at end of file
diff --git a/mod/assign/classes/privacy/assignsubmission_provider.php b/mod/assign/classes/privacy/assignsubmission_provider.php
new file mode 100644 (file)
index 0000000..84c545e
--- /dev/null
@@ -0,0 +1,88 @@
+<?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/>.
+
+/**
+ * This file contains the assignsubmission_provider interface.
+ *
+ * Assignment Sub plugins should implement this if they store personal information.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_assign\privacy;
+
+use core_privacy\local\request\contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+interface assignsubmission_provider extends \core_privacy\local\request\plugin\subplugin_provider {
+
+    /**
+     * Retrieves the contextids associated with the provided userid for this subplugin.
+     * NOTE if your subplugin must have an entry in the assign_submission table to work, then this
+     * method can be empty.
+     *
+     * @param int $userid The user ID to get context IDs for.
+     * @param \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs.
+     */
+    public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist);
+
+    /**
+     * Returns student user ids related to the provided teacher ID. If it is possible that a student ID will not be returned by
+     * the sql query in \mod_assign\privacy\provider::find_grader_info() Then you need to provide some sql to retrive those
+     * student IDs. This is highly likely if you had to fill in get_context_for_userid_within_submission above.
+     *
+     * @param  useridlist $useridlist A user ID list object that you can append your user IDs to.
+     */
+    public static function get_student_user_ids(useridlist $useridlist);
+
+    /**
+     * This method is used to export any user data this sub-plugin has using the assign_plugin_request_data object to get the
+     * context and userid.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - current path (subcontext)
+     * - user object
+     *
+     * @param  assign_plugin_request_data $exportdata Information to use to export user data for this sub-plugin.
+     */
+    public static function export_submission_user_data(assign_plugin_request_data $exportdata);
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     * assign_plugin_request_data contains:
+     * - context
+     * - assign object
+     *
+     * @param assign_plugin_request_data $requestdata Information to use to delete user data for this submission.
+     */
+    public static function delete_submission_for_context(assign_plugin_request_data $requestdata);
+
+    /**
+     * A call to this method should delete user data (where practicle) from the userid and context.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - user object
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $exportdata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $exportdata);
+}
diff --git a/mod/assign/classes/privacy/feedback_legacy_polyfill.php b/mod/assign/classes/privacy/feedback_legacy_polyfill.php
new file mode 100644 (file)
index 0000000..2fd6bdd
--- /dev/null
@@ -0,0 +1,100 @@
+<?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/>.
+
+/**
+ * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_assign\privacy;
+
+use core_privacy\local\request\contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The trait used to provide backwards compatability for third-party plugins.
+ *
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait feedback_legacy_polyfill {
+
+    /**
+     * Retrieves the contextids associated with the provided userid for this subplugin.
+     * NOTE if your subplugin must have an entry in the assign_grade table to work, then this
+     * method can be empty.
+     *
+     * @param  int $userid The user ID to get context IDs for.
+     * @param  \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs.
+     */
+    public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) {
+        return static::_get_context_for_userid_within_feedback($userid, $contextlist);
+    }
+
+    /**
+     * Returns student user ids related to the provided teacher ID. If an entry must be present in the assign_grade table for
+     * your plugin to work then there is no need to fill in this method. If you filled in get_context_for_userid_within_feedback()
+     * then you probably have to fill this in as well.
+     *
+     * @param  useridlist $useridlist A list of user IDs of students graded by this user.
+     */
+    public static function get_student_user_ids(useridlist $useridlist) {
+        return static::_get_student_user_ids($useridlist);
+    }
+
+    /**
+     * Export feedback data with the available grade and userid information provided.
+     * assign_plugin_request_data contains:
+     * - context
+     * - grade object
+     * - current path (subcontext)
+     * - user object
+     *
+     * @param  assign_plugin_request_data $exportdata Contains data to help export the user information.
+     */
+    public static function export_feedback_user_data(assign_plugin_request_data $exportdata) {
+        return static::_export_feedback_user_data($exportdata);
+    }
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     * assign_plugin_request_data contains:
+     * - context
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin.
+     */
+    public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) {
+        return static::_delete_feedback_for_context($requestdata);
+    }
+
+    /**
+     * Calling this function should delete all user data associated with this grade.
+     * assign_plugin_request_data contains:
+     * - context
+     * - grade object
+     * - user object
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data.
+     */
+    public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) {
+        return static::_delete_feedback_for_grade($requestdata);
+    }
+}
diff --git a/mod/assign/classes/privacy/provider.php b/mod/assign/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..bbe0713
--- /dev/null
@@ -0,0 +1,496 @@
+<?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 class for requesting user data.
+ *
+ * @package    mod_assign
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_assign\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\metadata\provider as metadataprovider;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\plugin\provider as pluginprovider;
+use \core_privacy\local\request\user_preference_provider as preference_provider;
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\helper;
+use \core_privacy\manager;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    mod_assign
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements metadataprovider, pluginprovider, preference_provider {
+
+    /** Interface for all assign submission sub-plugins. */
+    const ASSIGNSUBMISSION_INTERFACE = 'mod_assign\privacy\assignsubmission_provider';
+
+    /** Interface for all assign feedback sub-plugins. */
+    const ASSIGNFEEDBACK_INTERFACE = 'mod_assign\privacy\assignfeedback_provider';
+
+    /**
+     * Provides meta data that is stored about a user with mod_assign
+     *
+     * @param  collection $collection A collection of meta data items to be added to.
+     * @return  collection Returns the collection of metadata.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $assigngrades = [
+                'userid' => 'privacy:metadata:userid',
+                'timecreated' => 'privacy:metadata:timecreated',
+                'timemodified' => 'timemodified',
+                'grader' => 'privacy:metadata:grader',
+                'grade' => 'privacy:metadata:grade',
+                'attemptnumber' => 'attemptnumber'
+        ];
+        $assignoverrides = [
+                'groupid' => 'privacy:metadata:groupid',
+                'userid' => 'privacy:metadata:userid',
+                'allowsubmissionsfromdate' => 'allowsubmissionsfromdate',
+                'duedate' => 'duedate',
+                'cutoffdate' => 'cutoffdate'
+        ];
+        $assignsubmission = [
+                'userid' => 'privacy:metadata:userid',
+                'timecreated' => 'privacy:metadata:timecreated',
+                'timemodified' => 'timemodified',
+                'status' => 'gradingstatus',
+                'groupid' => 'privacy:metadata:groupid',
+                'attemptnumber' => 'attemptnumber',
+                'latest' => 'privacy:metadata:latest'
+        ];
+        $assignuserflags = [
+                'userid' => 'privacy:metadata:userid',
+                'assignment' => 'privacy:metadata:assignmentid',
+                'locked' => 'locksubmissions',
+                'mailed' => 'privacy:metadata:mailed',
+                'extensionduedate' => 'extensionduedate',
+                'workflowstate' => 'markingworkflowstate',
+                'allocatedmarker' => 'allocatedmarker'
+        ];
+        $assignusermapping = [
+                'assignment' => 'privacy:metadata:assignmentid',
+                'userid' => 'privacy:metadata:userid'
+        ];
+        $collection->add_database_table('assign_grades', $assigngrades, 'privacy:metadata:assigngrades');
+        $collection->add_database_table('assign_overrides', $assignoverrides, 'privacy:metadata:assignoverrides');
+        $collection->add_database_table('assign_submission', $assignsubmission, 'privacy:metadata:assignsubmissiondetail');
+        $collection->add_database_table('assign_user_flags', $assignuserflags, 'privacy:metadata:assignuserflags');
+        $collection->add_database_table('assign_user_mapping', $assignusermapping, 'privacy:metadata:assignusermapping');
+        $collection->add_user_preference('assign_perpage', 'privacy:metadata:assignperpage');
+        $collection->add_user_preference('assign_filter', 'privacy:metadata:assignfilter');
+        $collection->add_user_preference('assign_markerfilter', 'privacy:metadata:assignmarkerfilter');
+        $collection->add_user_preference('assign_workflowfilter', 'privacy:metadata:assignworkflowfilter');
+        $collection->add_user_preference('assign_quickgrading', 'privacy:metadata:assignquickgrading');
+        $collection->add_user_preference('assign_downloadasfolders', 'privacy:metadata:assigndownloadasfolders');
+
+        // Link to subplugins.
+        $collection->add_plugintype_link('assignsubmission', [],'privacy:metadata:assignsubmissionpluginsummary');
+        $collection->add_plugintype_link('assignfeedback', [], 'privacy:metadata:assignfeedbackpluginsummary');
+        $collection->add_subsystem_link('core_message', [], 'privacy:metadata:assignmessageexplanation');
+
+        return $collection;
+    }
+
+    /**
+     * Returns all of the contexts that has information relating to the userid.
+     *
+     * @param  int $userid The user ID.
+     * @return contextlist an object with the contexts related to a userid.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $params = ['modulename' => 'assign',
+                   'contextlevel' => CONTEXT_MODULE,
+                   'userid' => $userid,
+                   'graderid' => $userid,
+                   'aouserid' => $userid,
+                   'asnuserid' => $userid,
+                   'aufuserid' => $userid,
+                   'aumuserid' => $userid];
+
+        $sql = "SELECT ctx.id
+                  FROM {course_modules} cm
+                  JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
+                  JOIN {assign} a ON cm.instance = a.id
+                  JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
+             LEFT JOIN {assign_grades} ag ON a.id = ag.assignment
+             LEFT JOIN {assign_overrides} ao ON a.id = ao.assignid
+             LEFT JOIN {assign_submission} asn ON a.id = asn.assignment
+             LEFT JOIN {assign_user_flags} auf ON a.id = auf.assignment
+             LEFT JOIN {assign_user_mapping} aum ON a.id = aum.assignment
+                 WHERE ag.userid = :userid OR ag.grader = :graderid OR ao.userid = :aouserid
+                       OR asn.userid = :asnuserid OR auf.userid = :aufuserid OR aum.userid = :aumuserid";
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+        manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
+                'get_context_for_userid_within_feedback', [$userid, $contextlist]);
+        manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
+                'get_context_for_userid_within_submission', [$userid, $contextlist]);
+
+        return $contextlist;
+    }
+
+    /**
+     * Write out the user data filtered by contexts.
+     *
+     * @param approved_contextlist $contextlist contexts that we are writing data out from.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        foreach ($contextlist->get_contexts() as $context) {
+            // Check that the context is a module context.
+            if ($context->contextlevel != CONTEXT_MODULE) {
+                continue;
+            }
+            $user = $contextlist->get_user();
+            $assigndata = helper::get_context_data($context, $user);
+            helper::export_context_files($context, $user);
+
+            writer::with_context($context)->export_data([], $assigndata);
+            $assign = new \assign($context, null, null);
+
+            // I need to find out if I'm a student or a teacher.
+            if ($userids = self::get_graded_users($user->id, $assign)) {
+                // Return teacher info.
+                $currentpath = [get_string('privacy:studentpath', 'mod_assign')];
+                foreach ($userids as $studentuserid) {
+                    $studentpath = array_merge($currentpath, [$studentuserid->id]);
+                    static::export_submission($assign, $studentuserid, $context, $studentpath, true);
+                }
+            }
+
+            static::export_overrides($context, $assign, $user);
+            static::export_submission($assign, $user, $context, []);
+            // Meta data.
+            self::store_assign_user_flags($context, $assign, $user->id);
+            if ($assign->is_blind_marking()) {
+                $uniqueid = $assign->get_uniqueid_for_user_static($assign->get_instance()->id, $contextlist->get_user()->id);
+                if ($uniqueid) {
+                    writer::with_context($context)
+                            ->export_metadata([get_string('blindmarking', 'mod_assign')], 'blindmarkingid', $uniqueid,
+                                    get_string('privacy:blindmarkingidentifier', 'mod_assign'));
+                }
+            }
+        }
+    }
+
+    /**
+     * Delete all use data which matches the specified context.
+     *
+     * @param context $context The module context.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        global $DB;
+
+        if ($context->contextlevel == CONTEXT_MODULE) {
+            // Apparently we can't trust anything that comes via the context.
+            // Go go mega query to find out it we have an assign context that matches an existing assignment.
+            $sql = "SELECT a.id
+                    FROM {assign} a
+                    JOIN {course_modules} cm ON a.id = cm.instance
+                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
+                    JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextmodule
+                    WHERE ctx.id = :contextid";
+            $params = ['modulename' => 'assign', 'contextmodule' => CONTEXT_MODULE, 'contextid' => $context->id];
+            $count = $DB->get_field_sql($sql, $params);
+            // If we have a count over zero then we can proceed.
+            if ($count > 0) {
+                // Get the assignment related to this context.
+                $assign = new \assign($context, null, null);
+                // What to do first... Get sub plugins to delete their stuff.
+                $requestdata = new assign_plugin_request_data($context, $assign);
+                manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
+                    'delete_submission_for_context', [$requestdata]);
+                $requestdata = new assign_plugin_request_data($context, $assign);
+                manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
+                    'delete_feedback_for_context', [$requestdata]);
+                $DB->delete_records('assign_grades', ['assignment' => $assign->get_instance()->id]);
+
+                // Time to roll my own method for deleting overrides.
+                static::delete_user_overrides($assign);
+                $DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]);
+                $DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]);
+                $DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->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) {
+        global $DB;
+
+        $user = $contextlist->get_user();
+
+        foreach ($contextlist as $context) {
+            if ($context->contextlevel != CONTEXT_MODULE) {
+                continue;
+            }
+            // Get the assign object.
+            $assign = new \assign($context, null, null);
+            $assignid = $assign->get_instance()->id;
+
+            $submissions = $DB->get_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
+            foreach ($submissions as $submission) {
+                $requestdata = new assign_plugin_request_data($context, $assign, $submission, [], $user);
+                manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
+                        'delete_submission_for_userid', [$requestdata]);
+            }
+
+            $grades = $DB->get_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
+            foreach ($grades as $grade) {
+                $requestdata = new assign_plugin_request_data($context, $assign, $grade, [], $user);
+                manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
+                        'delete_feedback_for_grade', [$requestdata]);
+            }
+
+            static::delete_user_overrides($assign, $user);
+            $DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]);
+            $DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]);
+            $DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
+            $DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
+        }
+    }
+
+    /**
+     * Deletes assignment overrides.
+     *
+     * @param  \assign $assign The assignment object
+     * @param  \stdClass $user The user object if we are deleting only the overrides for one user.
+     */
+    protected static function delete_user_overrides(\assign $assign, \stdClass $user = null) {
+        global $DB;
+
+        $assignid = $assign->get_instance()->id;
+        $params = (isset($user)) ? ['assignid' => $assignid, 'userid' => $user->id] : ['assignid' => $assignid];
+
+        $overrides = $DB->get_records('assign_overrides', $params);
+        if (!empty($overrides)) {
+            foreach ($overrides as $override) {
+
+                // First delete calendar events associated with this override.
+                $conditions = ['modulename' => 'assign', 'instance' => $assignid];
+                if (isset($user)) {
+                    $conditions['userid'] = $user->id;
+                }
+                $DB->delete_records('event', $conditions);
+
+                // Next delete the overrides.
+                $DB->delete_records('assign_overrides', ['id' => $override->id]);
+            }
+        }
+    }
+
+    /**
+     * Find out if this user has graded any users.
+     *
+     * @param  int $userid The user ID (potential teacher).
+     * @param  assign $assign The assignment object.
+     * @return array If successful an array of objects with userids that this user graded, otherwise false.
+     */
+    protected static function get_graded_users(int $userid, \assign $assign) {
+        $params = ['grader' => $userid, 'assignid' => $assign->get_instance()->id];
+
+        $sql = "SELECT DISTINCT userid AS id
+                  FROM {assign_grades}
+                 WHERE grader = :grader AND assignment = :assignid";
+
+        $useridlist = new useridlist($userid, $assign->get_instance()->id);
+        $useridlist->add_from_sql($sql, $params);
+
+        // Call sub-plugins to see if they have information not already collected.
+        manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE, 'get_student_user_ids',
+                [$useridlist]);
+        manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'get_student_user_ids', [$useridlist]);
+
+        $userids = $useridlist->get_userids();
+        return ($userids) ? $userids : false;
+    }
+
+    /**
+     * Writes out various user meta data about the assignment.
+     *
+     * @param  \context $context The context of this assignment.
+     * @param  \assign $assign The assignment object.
+     * @param  int $userid The user ID
+     */
+    protected static function store_assign_user_flags(\context $context, \assign $assign, int $userid) {
+        $datatypes = ['locked' => get_string('locksubmissions', 'mod_assign'),
+                      'mailed' => get_string('privacy:metadata:mailed', 'mod_assign'),
+                      'extensionduedate' => get_string('extensionduedate', 'mod_assign'),
+                      'workflowstate' => get_string('markingworkflowstate', 'mod_assign'),
+                      'allocatedmarker' => get_string('allocatedmarker_help', 'mod_assign')];
+        $userflags = (array)$assign->get_user_flags($userid, false);
+
+        foreach ($datatypes as $key => $description) {
+            if (isset($userflags[$key]) && !empty($userflags[$key])) {
+                $value = $userflags[$key];
+                if ($key == 'locked' || $key == 'mailed') {
+                    $value = transform::yesno($value);
+                } else if ($key == 'extensionduedate') {
+                    $value = transform::datetime($value);
+                }
+                writer::with_context($context)->export_metadata([], $key, $value, $description);
+            }
+        }
+    }
+
+    /**
+     * Formats and then exports the user's grade data.
+     *
+     * @param  \stdClass $grade The assign grade object
+     * @param  \context $context The context object
+     * @param  array $currentpath Current directory path that we are exporting to.
+     */
+    protected static function export_grade_data(\stdClass $grade, \context $context, array $currentpath) {
+        $gradedata = (object)[
+            'timecreated' => transform::datetime($grade->timecreated),
+            'timemodified' => transform::datetime($grade->timemodified),
+            'grader' => transform::user($grade->grader),
+            'grade' => $grade->grade,
+            'attemptnumber' => $grade->attemptnumber
+        ];
+        writer::with_context($context)
+                ->export_data(array_merge($currentpath, [get_string('privacy:gradepath', 'mod_assign')]), $gradedata);
+    }
+
+    /**
+     * Formats and then exports the user's submission data.
+     *
+     * @param  \stdClass $submission The assign submission object
+     * @param  \context $context The context object
+     * @param  array $currentpath Current directory path that we are exporting to.
+     */
+    protected static function export_submission_data(\stdClass $submission, \context $context, array $currentpath) {
+        $submissiondata = (object)[
+            'timecreated' => transform::datetime($submission->timecreated),
+            'timemodified' => transform::datetime($submission->timemodified),
+            'status' => get_string('submissionstatus_' . $submission->status, 'mod_assign'),
+            'groupid' => $submission->groupid,
+            'attemptnumber' => $submission->attemptnumber,
+            'latest' => transform::yesno($submission->latest)
+        ];
+        writer::with_context($context)
+                ->export_data(array_merge($currentpath, [get_string('privacy:submissionpath', 'mod_assign')]), $submissiondata);
+    }
+
+    /**
+     * Stores the user preferences related to mod_assign.
+     *
+     * @param  int $userid The user ID that we want the preferences for.
+     */
+    public static function export_user_preferences(int $userid) {
+        $context = \context_system::instance();
+        $assignpreferences = [
+            'assign_perpage' => ['string' => get_string('privacy:metadata:assignperpage', 'mod_assign'), 'bool' => false],
+            'assign_filter' => ['string' => get_string('privacy:metadata:assignfilter', 'mod_assign'), 'bool' => false],
+            'assign_markerfilter' => ['string' => get_string('privacy:metadata:assignmarkerfilter', 'mod_assign'), 'bool' => true],
+            'assign_workflowfilter' => ['string' => get_string('privacy:metadata:assignworkflowfilter', 'mod_assign'),
+                    'bool' => true],
+            'assign_quickgrading' => ['string' => get_string('privacy:metadata:assignquickgrading', 'mod_assign'), 'bool' => true],
+            'assign_downloadasfolders' => ['string' => get_string('privacy:metadata:assigndownloadasfolders', 'mod_assign'),
+                    'bool' => true]
+        ];
+        foreach ($assignpreferences as $key => $preference) {
+            $value = get_user_preferences($key, null, $userid);
+            if ($preference['bool']) {
+                $value = transform::yesno($value);
+            }
+            if (isset($value)) {
+                writer::with_context($context)->export_user_preference('mod_assign', $key, $value, $preference['string']);
+            }
+        }
+    }
+
+    /**
+     * Export overrides for this assignment.
+     *
+     * @param  \context $context Context
+     * @param  \assign $assign The assign object.
+     * @param  \stdClass $user The user object.
+     */
+    public static function export_overrides(\context $context, \assign $assign, \stdClass $user) {
+
+        $overrides = $assign->override_exists($user->id);
+        // Overrides returns an array with data in it, but an override with actual data will have the assign ID set.
+        if (isset($overrides->assignid)) {
+            $data = new \stdClass();
+            if (!empty($overrides->duedate)) {
+                $data->duedate = transform::datetime($overrides->duedate);
+            }
+            if (!empty($overrides->cutoffdate)) {
+                $overrides->cutoffdate = transform::datetime($overrides->cutoffdate);
+            }
+            if (!empty($overrides->allowsubmissionsfromdate)) {
+                $overrides->allowsubmissionsfromdate = transform::datetime($overrides->allowsubmissionsfromdate);
+            }
+            if (!empty($data)) {
+                writer::with_context($context)->export_data([get_string('overrides', 'mod_assign')], $data);
+            }
+        }
+    }
+
+    /**
+     * Exports assignment submission data for a user.
+     *
+     * @param  \assign         $assign           The assignment object
+     * @param  \stdClass        $user             The user object
+     * @param  \context_module $context          The context
+     * @param  array           $path             The path for exporting data
+     * @param  bool|boolean    $exportforteacher A flag for if this is exporting data as a teacher.
+     */
+    protected static function export_submission(\assign $assign, \stdClass $user, \context_module $context, array $path,
+            bool $exportforteacher = false) {
+        $submissions = $assign->get_all_submissions($user->id);
+        $teacher = ($exportforteacher) ? $user : null;
+        foreach ($submissions as $submission) {
+            // Attempt numbers start at zero, which is fine for programming, but doesn't make as much sense
+            // for users.
+            $submissionpath = array_merge($path,
+                    [get_string('privacy:attemptpath', 'mod_assign', ($submission->attemptnumber + 1))]);
+
+            $params = new assign_plugin_request_data($context, $assign, $submission, $submissionpath ,$teacher);
+            manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
+                    'export_submission_user_data', [$params]);
+            if (!isset($teacher)) {
+                self::export_submission_data($submission, $context, $submissionpath);
+            }
+            $grade = $assign->get_user_grade($user->id, false, $submission->attemptnumber);
+            if ($grade) {
+                $params = new assign_plugin_request_data($context, $assign, $grade, $submissionpath, $teacher);
+                manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE, 'export_feedback_user_data',
+                        [$params]);
+
+                self::export_grade_data($grade, $context, $submissionpath);
+            }
+        }
+    }
+}
diff --git a/mod/assign/classes/privacy/submission_legacy_polyfill.php b/mod/assign/classes/privacy/submission_legacy_polyfill.php
new file mode 100644 (file)
index 0000000..9b088d9
--- /dev/null
@@ -0,0 +1,101 @@
+<?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/>.
+
+/**
+ * This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up.
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_assign\privacy;
+
+use core_privacy\local\request\contextlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The trait used to provide backwards compatability for third-party plugins.
+ *
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+trait submission_legacy_polyfill {
+
+    /**
+     * Retrieves the contextids associated with the provided userid for this subplugin.
+     * NOTE if your subplugin must have an entry in the assign_submission table to work, then this
+     * method can be empty.
+     *
+     * @param  int $userid The user ID to get context IDs for.
+     * @param  \core_privacy\local\request\contextlist $contextlist Use add_from_sql with this object to add your context IDs.
+     */
+    public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) {
+        return static::_get_context_for_userid_within_submission($userid, $contextlist);
+    }
+
+    /**
+     * Returns student user ids related to the provided teacher ID. If it is possible that a student ID will not be returned by
+     * the sql query in \mod_assign\privacy\provider::find_grader_info() Then you need to provide some sql to retrive those
+     * student IDs. This is highly likely if you had to fill in get_context_for_userid_within_submission above.
+     *
+     * @param  useridlist $useridlist A user ID list object that you can append your user IDs to.
+     */
+    public static function get_student_user_ids(useridlist $useridlist) {
+        return static::_get_student_user_ids($useridlist);
+    }
+
+    /**
+     * This method is used to export any user data this sub-plugin has using the assign_plugin_request_data object to get the
+     * context and userid.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - current path (subcontext)
+     * - user object
+     *
+     * @param  assign_plugin_request_data $exportdata Information to use to export user data for this sub-plugin.
+     */
+    public static function export_submission_user_data(assign_plugin_request_data $exportdata) {
+        return static::_export_submission_user_data($exportdata);
+    }
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     * assign_plugin_request_data contains:
+     * - context
+     * - assign object
+     *
+     * @param assign_plugin_request_data $requestdata Information to use to delete user data for this submission.
+     */
+    public static function delete_submission_for_context(assign_plugin_request_data $requestdata) {
+        return static::_delete_submission_for_context($requestdata);
+    }
+
+    /**
+     * A call to this method should delete user data (where practicle) from the userid and context.
+     * assign_plugin_request_data contains:
+     * - context
+     * - submission object
+     * - user object
+     * - assign object
+     *
+     * @param  assign_plugin_request_data $exportdata Details about the user and context to focus the deletion.
+     */
+    public static function delete_submission_for_userid(assign_plugin_request_data $exportdata) {
+        return static::_delete_submission_for_userid($exportdata);
+    }
+}
diff --git a/mod/assign/classes/privacy/useridlist.php b/mod/assign/classes/privacy/useridlist.php
new file mode 100644 (file)
index 0000000..430d6eb
--- /dev/null
@@ -0,0 +1,99 @@
+<?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/>.
+
+/**
+ * This file contains the mod_assign useridlist
+ *
+ * This is for collecting a list of user IDs
+ *
+ * @package mod_assign
+ * @copyright 2018 Adrian Greeve <adrian@moodle.com>
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_assign\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * An object for collecting user IDs related to a teacher.
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class useridlist {
+
+    /** @var int The ID of the teacher. */
+    protected $teacherid;
+
+    /** @var int The ID of the assignment object. */
+    protected $assignid;
+
+    /** @var array A collection of user IDs (students). */
+    protected $userids = [];
+
+    /**
+     * Create this object.
+     *
+     * @param int $teacherid The teacher ID.
+     * @param int $assignid The assignment ID.
+     */
+    public function __construct($teacherid, $assignid) {
+        $this->teacherid = $teacherid;
+        $this->assignid = $assignid;
+    }
+
+    /**
+     * Returns the teacher ID.
+     *
+     * @return int The teacher ID.
+     */
+    public function get_teacherid() {
+        return $this->teacherid;
+    }
+
+    /**
+     * Returns the assign ID.
+     *
+     * @return int The assign ID.
+     */
+    public function get_assignid() {
+        return $this->assignid;
+    }
+
+    /**
+     * Returns the user IDs.
+     *
+     * @return array User IDs.
+     */
+    public function get_userids() {
+        return $this->userids;
+    }
+
+    /**
+     * Add sql and params to return user IDs.
+     *
+     * @param string $sql The sql string to return user IDs.
+     * @param array $params Parameters for the sql statement.
+     */
+    public function add_from_sql($sql, $params) {
+        global $DB;
+        $userids = $DB->get_records_sql($sql, $params);
+        if (!empty($userids)) {
+            $this->userids = array_merge($this->userids, $userids);
+        }
+    }
+}
diff --git a/mod/assign/feedback/comments/classes/privacy/provider.php b/mod/assign/feedback/comments/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..ac9aa7f
--- /dev/null
@@ -0,0 +1,125 @@
+<?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 class for requesting user data.
+ *
+ * @package    assignfeedback_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_comments\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\metadata\provider as metadataprovider;
+use \mod_assign\privacy\assignfeedback_provider;
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\contextlist;
+use \mod_assign\privacy\assign_plugin_request_data;
+use \mod_assign\privacy\useridlist;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements metadataprovider, assignfeedback_provider {
+
+    /**
+     * Return meta data about this plugin.
+     *
+     * @param  collection $collection A list of information to add to.
+     * @return collection Return the collection after adding to it.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $data = [
+            'assignment' => 'privacy:metadata:assignmentid',
+            'grade' => 'privacy:metadata:gradepurpose',
+            'commenttext' => 'privacy:metadata:commentpurpose'
+        ];
+        $collection->add_database_table('assignfeedback_comments', $data, 'privacy:metadata:tablesummary');
+        return $collection;
+    }
+
+    /**
+     * No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign
+     * provider.
+     *
+     * @param  int $userid The user ID.
+     * @param  contextlist $contextlist The context list.
+     */
+    public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) {
+        // This uses the assign_grades table.
+    }
+
+    /**
+     * This also does not need to be filled in as this is already collected in the mod assign provider.
+     *
+     * @param  useridlist $useridlist A list of user IDs
+     */
+    public static function get_student_user_ids(useridlist $useridlist) {
+        // Not required.
+    }
+
+    /**
+     * Export all user data for this plugin.
+     *
+     * @param  assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful
+     * information to help with exporting.
+     */
+    public static function export_feedback_user_data(assign_plugin_request_data $exportdata) {
+        // Get that comment information and jam it into that exporter.
+        $assign = $exportdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'comments');
+        $comments = $plugin->get_feedback_comments($exportdata->get_pluginobject()->id);
+        if ($comments && !empty($comments->commenttext)) {
+            $data = (object)['commenttext' => format_text($comments->commenttext, $comments->commentformat,
+                    ['context' => $exportdata->get_context()])];
+            writer::with_context($exportdata->get_context())
+                    ->export_data(array_merge($exportdata->get_subcontext(),
+                            [get_string('privacy:commentpath', 'assignfeedback_comments')]), $data);
+        }
+    }
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin.
+     */
+    public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) {
+        $assign = $requestdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'comments');
+        $plugin->delete_instance();
+    }
+
+    /**
+     * Calling this function should delete all user data associated with this grade entry.
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data.
+     */
+    public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) {
+        global $DB;
+        $DB->delete_records('assignfeedback_comments', ['assignment' => $requestdata->get_assign()->get_instance()->id,
+                'grade' => $requestdata->get_pluginobject()->id]);
+    }
+}
index 525eee6..52d8681 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 $string['default'] = 'Enabled by default';
 $string['default_help'] = 'If set, this feedback method will be enabled by default for all new assignments.';
 $string['enabled'] = 'Feedback comments';
 $string['enabled_help'] = 'If enabled, the marker can leave feedback comments for each submission. ';
 $string['pluginname'] = 'Feedback comments';
+$string['privacy:commentpath'] = 'Feedback comments';
+$string['privacy:metadata:assignmentid'] = 'Assignment identifier';
+$string['privacy:metadata:commentpurpose'] = 'The comment text.';
+$string['privacy:metadata:gradepurpose'] = 'The grade ID associated with the comment.';
+$string['privacy:metadata:tablesummary'] = 'This stores comments made by the graders as feedback for the student on their submission.';
 $string['commentinline'] = 'Comment inline';
 $string['commentinline_help'] = 'If enabled, the submission text will be copied into the feedback comment field during grading, making it easier to comment inline (using a different colour, perhaps) or to edit the original text.';
 $string['commentinlinedefault'] = 'Comment inline by default';
 $string['commentinlinedefault_help'] = 'If set, this comment inline functionality will be enabled by default for all new assignments.';
-
diff --git a/mod/assign/feedback/comments/tests/privacy_test.php b/mod/assign/feedback/comments/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..3649452
--- /dev/null
@@ -0,0 +1,203 @@
+<?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 assignfeedback_comments.
+ *
+ * @package    assignfeedback_comments
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+require_once($CFG->dirroot . '/mod/assign/tests/privacy_test.php');
+
+/**
+ * Unit tests for mod/assign/feedback/comments/classes/privacy/
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_comments_privacy_testcase extends \mod_assign\tests\mod_assign_privacy_testcase {
+
+    /**
+     * Convenience function for creating feedback data.
+     *
+     * @param  object   $assign         assign object
+     * @param  stdClass $student        user object
+     * @param  stdClass $teacher        user object
+     * @param  string   $submissiontext Submission text
+     * @param  string   $feedbacktext   Feedback text
+     * @return array   Feedback plugin object and the grade object.
+     */
+    protected function create_feedback($assign, $student, $teacher, $submissiontext, $feedbacktext) {
+        $submission = new \stdClass();
+        $submission->assignment = $assign->get_instance()->id;
+        $submission->userid = $student->id;
+        $submission->timecreated = time();
+        $submission->onlinetext_editor = ['text' => $submissiontext,
+                                         'format' => FORMAT_MOODLE];
+
+        $this->setUser($student);
+        $notices = [];
+        $assign->save_submission($submission, $notices);
+
+        $grade = $assign->get_user_grade($student->id, true);
+
+        $this->setUser($teacher);
+
+        $plugin = $assign->get_feedback_plugin_by_type('comments');
+        $feedbackdata = new \stdClass();
+        $feedbackdata->assignfeedbackcomments_editor = [
+            'text' => $feedbacktext,
+            'format' => 1
+        ];
+
+        $plugin->save($grade, $feedbackdata);
+        return [$plugin, $grade];
+    }
+
+    /**
+     * Quick test to make sure that get_metadata returns something.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('assignfeedback_comments');
+        $collection = \assignfeedback_comments\privacy\provider::get_metadata($collection);
+        $this->assertNotEmpty($collection);
+    }
+
+    /**
+     * Test that feedback comments are exported for a user.
+     */
+    public function test_export_feedback_user_data() {
+        $this->resetAfterTest();
+
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Student.
+        $user1 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user2 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $feedbacktext = '<p>first comment for this test</p>';
+        list($plugin, $grade) = $this->create_feedback($assign, $user1, $user2, 'Submission text', $feedbacktext);
+
+        $writer = \core_privacy\local\request\writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+
+        // The student should be able to see the teachers feedback.
+        $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user1);
+        \assignfeedback_comments\privacy\provider::export_feedback_user_data($exportdata);
+        $this->assertEquals($feedbacktext, $writer->get_data(['Feedback comments'])->commenttext);
+
+        // The teacher should also be able to see the feedback that they provided.
+        $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user2);
+        \assignfeedback_comments\privacy\provider::export_feedback_user_data($exportdata);
+        $this->assertEquals($feedbacktext, $writer->get_data(['Feedback comments'])->commenttext);
+    }
+
+    /**
+     * Test that all feedback is deleted for a context.
+     */
+    public function test_delete_feedback_for_context() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Student.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user3 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $feedbacktext = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second student.</p>';
+        list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext);
+
+        // Check that we have data.
+        $feedbackcomments = $plugin1->get_feedback_comments($grade1->id);
+        $this->assertNotEmpty($feedbackcomments);
+        $feedbackcomments = $plugin1->get_feedback_comments($grade2->id);
+        $this->assertNotEmpty($feedbackcomments);
+
+        // Delete all comments for this context.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign);
+        assignfeedback_comments\privacy\provider::delete_feedback_for_context($requestdata);
+
+        // Check that the data is now gone.
+        $feedbackcomments = $plugin1->get_feedback_comments($grade1->id);
+        $this->assertEmpty($feedbackcomments);
+        $feedbackcomments = $plugin1->get_feedback_comments($grade2->id);
+        $this->assertEmpty($feedbackcomments);
+    }
+
+    /**
+     * Test that a grade item is deleted for a user.
+     */
+    public function test_delete_feedback_for_grade() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Student.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user3 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $feedbacktext = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second student.</p>';
+        list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext);
+
+        // Check that we have data.
+        $feedbackcomments = $plugin1->get_feedback_comments($grade1->id);
+        $this->assertNotEmpty($feedbackcomments);
+        $feedbackcomments = $plugin1->get_feedback_comments($grade2->id);
+        $this->assertNotEmpty($feedbackcomments);
+
+        // Delete all comments for this grade object.
+        $requestdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade1, [], $user1);
+        assignfeedback_comments\privacy\provider::delete_feedback_for_grade($requestdata);
+
+        // These comments should be empty.
+        $feedbackcomments = $plugin1->get_feedback_comments($grade1->id);
+        $this->assertEmpty($feedbackcomments);
+
+        // These comments should not.
+        $feedbackcomments = $plugin1->get_feedback_comments($grade2->id);
+        $this->assertNotEmpty($feedbackcomments);
+    }
+}
diff --git a/mod/assign/feedback/editpdf/classes/privacy/provider.php b/mod/assign/feedback/editpdf/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..e61e781
--- /dev/null
@@ -0,0 +1,147 @@
+<?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 class for requesting user data.
+ *
+ * @package    assignfeedback_editpdf
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_editpdf\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\metadata\provider as metadataprovider;
+use \mod_assign\privacy\assignfeedback_provider;
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\contextlist;
+use \mod_assign\privacy\assign_plugin_request_data;
+use \mod_assign\privacy\useridlist;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_editpdf
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements metadataprovider, assignfeedback_provider {
+
+    /**
+     * Return meta data about this plugin.
+     *
+     * @param  collection $collection A list of information to add to.
+     * @return collection Return the collection after adding to it.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $quickdata = [
+            'userid' => 'privacy:metadata:userid',
+            'rawtext' => 'privacy:metadata:rawtextpurpose',
+            'colour' => 'privacy:metadata:colourpurpose'
+        ];
+        $collection->add_database_table('assignfeedback_editpdf_quick', $quickdata, 'privacy:metadata:tablepurpose');
+        $collection->add_subsystem_link('core_files', [], 'privacy:metadata:filepurpose');
+        $collection->add_subsystem_link('core_fileconverted', [], 'privacy:metadata:conversionpurpose');
+        return $collection;
+    }
+
+    /**
+     * No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign
+     * provider.
+     *
+     * @param  int $userid The user ID.
+     * @param  contextlist $contextlist The context list.
+     */
+    public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) {
+        // This uses the assign_grade table.
+    }
+
+    /**
+     * This also does not need to be filled in as this is already collected in the mod assign provider.
+     *
+     * @param  useridlist $useridlist A list of user IDs
+     */
+    public static function get_student_user_ids(useridlist $useridlist) {
+        // Not required.
+    }
+
+    /**
+     * Export all user data for this plugin.
+     *
+     * @param  assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful
+     * information to help with exporting.
+     */
+    public static function export_feedback_user_data(assign_plugin_request_data $exportdata) {
+        $currentpath = $exportdata->get_subcontext();
+        $currentpath[] = get_string('privacy:path', 'assignfeedback_editpdf');
+        $assign = $exportdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf');
+        $fileareas = $plugin->get_file_areas();
+        $grade = $exportdata->get_pluginobject();
+        foreach ($fileareas as $filearea => $notused) {
+            writer::with_context($exportdata->get_context())
+                    ->export_area_files($currentpath, 'assignfeedback_editpdf', $filearea, $grade->id);
+        }
+    }
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin.
+     */
+    public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) {
+
+        $assign = $requestdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf');
+        $fileareas = $plugin->get_file_areas();
+        $fs = get_file_storage();
+        foreach ($fileareas as $filearea => $notused) {
+            // Delete pdf files.
+            $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_editpdf', $filearea);
+        }
+        // Delete entries from the tables.
+        $plugin->delete_instance();
+    }
+
+    /**
+     * Calling this function should delete all user data associated with this grade.
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data.
+     */
+    public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) {
+        global $DB;
+
+        $assign = $requestdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'editpdf');
+        $fileareas = $plugin->get_file_areas();
+        $fs = get_file_storage();
+        foreach ($fileareas as $filearea => $notused) {
+            // Delete pdf files.
+            $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_editpdf',
+                    $filearea, $requestdata->get_pluginobject()->id);
+        }
+
+        // Remove table entries.
+        $DB->delete_records('assignfeedback_editpdf_annot', ['gradeid' => $requestdata->get_pluginobject()->id]);
+        $DB->delete_records('assignfeedback_editpdf_cmnt', ['gradeid' => $requestdata->get_pluginobject()->id]);
+        // Submission records in assignfeedback_editpdf_queue will be cleaned up in a scheduled task
+    }
+}
index fe6a7ec..1f7fac4 100644 (file)
@@ -73,6 +73,13 @@ $string['pagenumber'] = 'Page {$a}';
 $string['pagexofy'] = 'Page {$a->page} of {$a->total}';
 $string['pen'] = 'Pen';
 $string['pluginname'] = 'Annotate PDF';
+$string['privacy:metadata:colourpurpose'] = 'Colour of the comment or annotation';
+$string['privacy:metadata:conversionpurpose'] = 'Files are converted to PDFs to allow for annotations.';
+$string['privacy:metadata:filepurpose'] = 'Stores an annotated PDF with feedback for the user.';
+$string['privacy:metadata:rawtextpurpose'] = 'Stores raw text for the quick data.';
+$string['privacy:metadata:tablepurpose'] = 'Stores teacher specified quicklist comments';
+$string['privacy:metadata:userid'] = 'An identifier for the user.';
+$string['privacy:path'] = 'PDF Feedback';
 $string['generatingpdf'] = 'Generating the PDF...';
 $string['rectangle'] = 'Rectangle';
 $string['red'] = 'Red';
diff --git a/mod/assign/feedback/editpdf/tests/privacy_test.php b/mod/assign/feedback/editpdf/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..2291d6c
--- /dev/null
@@ -0,0 +1,244 @@
+<?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 assignfeedback_editpdf.
+ *
+ * @package    assignfeedback_editpdf
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+require_once($CFG->dirroot . '/mod/assign/tests/privacy_test.php');
+
+use \assignfeedback_editpdf\page_editor;
+use \mod_assign\privacy\assign_plugin_request_data;
+
+/**
+ * Unit tests for mod/assign/feedback/editpdf/classes/privacy/
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_editpdf_privacy_testcase extends \mod_assign\tests\mod_assign_privacy_testcase {
+
+    public function setUp() {
+        // Skip this test if ghostscript is not supported.
+        $result = \assignfeedback_editpdf\pdf::test_gs_path(false);
+        if ($result->status !== \assignfeedback_editpdf\pdf::GSPATH_OK) {
+            $this->markTestSkipped('Ghostscript not setup');
+            return;
+        }
+        parent::setUp();
+    }
+
+    /**
+     * Convenience function for creating feedback data.
+     *
+     * @param  object   $assign         assign object
+     * @param  stdClass $student        user object
+     * @param  stdClass $teacher        user object
+     * @return array   Feedback plugin object and the grade object.
+     */
+    protected function create_feedback($assign, $student, $teacher) {
+        global $CFG;
+
+        // Create a file submission with the test pdf.
+        $submission = $assign->get_user_submission($student->id, true);
+
+        $this->setUser($student->id);
+
+        $fs = get_file_storage();
+        $pdfsubmission = (object) array(
+            'contextid' => $assign->get_context()->id,
+            'component' => 'assignsubmission_file',
+            'filearea' => ASSIGNSUBMISSION_FILE_FILEAREA,
+            'itemid' => $submission->id,
+            'filepath' => '/',
+            'filename' => 'submission.pdf'
+        );
+        $sourcefile = $CFG->dirroot.'/mod/assign/feedback/editpdf/tests/fixtures/submission.pdf';
+        $fi = $fs->create_file_from_pathname($pdfsubmission, $sourcefile);
+
+        $data = new \stdClass();
+        $plugin = $assign->get_submission_plugin_by_type('file');
+        $plugin->save($submission, $data);
+
+        $this->setUser($teacher->id);
+
+        $plugin = $assign->get_feedback_plugin_by_type('editpdf');
+
+        $grade = $assign->get_user_grade($student->id, true);
+
+        $comment = new \assignfeedback_editpdf\comment();
+
+        $comment->rawtext = 'Comment text';
+        $comment->width = 100;
+        $comment->x = 100;
+        $comment->y = 100;
+        $comment->colour = 'red';
+        page_editor::set_comments($grade->id, 0, [$comment]);
+
+        $annotation = new \assignfeedback_editpdf\annotation();
+
+        $annotation->path = '';
+        $annotation->x = 100;
+        $annotation->y = 100;
+        $annotation->endx = 200;
+        $annotation->endy = 200;
+        $annotation->type = 'line';
+        $annotation->colour = 'red';
+
+        page_editor::set_annotations($grade->id, 0, [$annotation]);
+
+        $comments = page_editor::get_comments($grade->id, 0, true);
+        $annotations = page_editor::get_annotations($grade->id, 0, false);
+        page_editor::release_drafts($grade->id);
+        $storedfile = \assignfeedback_editpdf\document_services::generate_feedback_document($assign->get_instance()->id, $student->id,
+                $grade->attemptnumber);
+
+        return [$plugin, $grade, $storedfile];
+    }
+
+    /**
+     * Quick test to make sure that get_metadata returns something.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('assignfeedback_editpdf');
+        $collection = \assignfeedback_editpdf\privacy\provider::get_metadata($collection);
+        $this->assertNotEmpty($collection);
+    }
+
+    /**
+     * Test that feedback comments are exported for a user.
+     */
+    public function test_export_feedback_user_data() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Student.
+        $user1 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user2 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course,
+                'assignsubmission_file_enabled' => 1,
+                'assignsubmission_file_maxfiles' => 1,
+                'assignfeedback_editpdf_enabled' => 1,
+                'assignsubmission_file_maxsizebytes' => 1000000]);
+
+        $context = $assign->get_context();
+
+        list($plugin, $grade, $storedfile) = $this->create_feedback($assign, $user1, $user2);
+
+        // Check that we have data.
+        $this->assertFalse($plugin->is_empty($grade));
+
+        $writer = \core_privacy\local\request\writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+
+        // The student should be able to see the teachers feedback.
+        $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user1);
+        \assignfeedback_editpdf\privacy\provider::export_feedback_user_data($exportdata);
+        // print_object($writer->get_files([get_string('privacy:path', 'assignfeedback_editpdf')]));
+        // print_object($writer->get_files(['PDF feedback', $storedfile->get_filename()]));
+        $pdffile = $writer->get_files([get_string('privacy:path', 'assignfeedback_editpdf')])[$storedfile->get_filename()];
+        // The writer should have returned a stored file.
+        $this->assertInstanceOf('stored_file', $pdffile);
+    }
+
+    /**
+     * Test that all feedback is deleted for a context.
+     */
+    public function test_delete_feedback_for_context() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Students.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user3 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course,
+                'assignsubmission_file_enabled' => 1,
+                'assignsubmission_file_maxfiles' => 1,
+                'assignfeedback_editpdf_enabled' => 1,
+                'assignsubmission_file_maxsizebytes' => 1000000]);
+
+        $context = $assign->get_context();
+
+        list($plugin1, $grade1, $storedfile1) = $this->create_feedback($assign, $user1, $user3);
+        list($plugin2, $grade2, $storedfile2) = $this->create_feedback($assign, $user2, $user3);
+
+        // Check that we have data.
+        $this->assertFalse($plugin1->is_empty($grade1));
+        $this->assertFalse($plugin2->is_empty($grade2));
+
+        $requestdata = new assign_plugin_request_data($context, $assign);
+        \assignfeedback_editpdf\privacy\provider::delete_feedback_for_context($requestdata);
+
+        // Check that we now have no data.
+        $this->assertTrue($plugin1->is_empty($grade1));
+        $this->assertTrue($plugin2->is_empty($grade2));
+    }
+
+    /**
+     * Test that a grade item is deleted for a user.
+     */
+    public function test_delete_feedback_for_grade() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Students.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user3 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course,
+                'assignsubmission_file_enabled' => 1,
+                'assignsubmission_file_maxfiles' => 1,
+                'assignfeedback_editpdf_enabled' => 1,
+                'assignsubmission_file_maxsizebytes' => 1000000]);
+
+        $context = $assign->get_context();
+
+        list($plugin1, $grade1, $storedfile1) = $this->create_feedback($assign, $user1, $user3);
+        list($plugin2, $grade2, $storedfile2) = $this->create_feedback($assign, $user2, $user3);
+
+        // Check that we have data.
+        $this->assertFalse($plugin1->is_empty($grade1));
+        $this->assertFalse($plugin2->is_empty($grade2));
+
+        $requestdata = new assign_plugin_request_data($context, $assign, $grade1, [], $user1);
+        \assignfeedback_editpdf\privacy\provider::delete_feedback_for_grade($requestdata);
+
+        // Check that we now have no data for user 1.
+        $this->assertTrue($plugin1->is_empty($grade1));
+        // Check that user 2 data is still there.
+        $this->assertFalse($plugin2->is_empty($grade2));
+    }
+}
diff --git a/mod/assign/feedback/file/classes/privacy/provider.php b/mod/assign/feedback/file/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..f2f609f
--- /dev/null
@@ -0,0 +1,140 @@
+<?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 class for requesting user data.
+ *
+ * @package    assignfeedback_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_file\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\metadata\provider as metadataprovider;
+use core_privacy\local\request\contextlist;
+use \mod_assign\privacy\assignfeedback_provider;
+use \mod_assign\privacy\assign_plugin_request_data;
+use mod_assign\privacy\useridlist;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements metadataprovider, assignfeedback_provider {
+
+    /**
+     * Return meta data about this plugin.
+     *
+     * @param  collection $collection A list of information to add to.
+     * @return collection Return the collection after adding to it.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
+        return $collection;
+    }
+
+    /**
+     * No need to fill in this method as all information can be acquired from the assign_grades table in the mod assign
+     * provider.
+     *
+     * @param  int $userid The user ID.
+     * @param  contextlist $contextlist The context list.
+     */
+    public static function get_context_for_userid_within_feedback(int $userid, contextlist $contextlist) {
+        // This uses the assign_grade table.
+    }
+
+    /**
+     * This also does not need to be filled in as this is already collected in the mod assign provider.
+     *
+     * @param  useridlist $useridlist A list of user IDs
+     */
+    public static function get_student_user_ids(useridlist $useridlist) {
+        // Not required.
+    }
+
+    /**
+     * Export all user data for this plugin.
+     *
+     * @param  assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful
+     * information to help with exporting.
+     */
+    public static function export_feedback_user_data(assign_plugin_request_data $exportdata) {
+        $currentpath = $exportdata->get_subcontext();
+        $currentpath[] = get_string('privacy:path', 'assignfeedback_file');
+        $assign = $exportdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'file');
+        $gradeid = $exportdata->get_pluginobject()->id;
+        $filefeedback = $plugin->get_file_feedback($gradeid);
+        if ($filefeedback) {
+            $fileareas = $plugin->get_file_areas();
+            foreach ($fileareas as $filearea => $notused) {
+                \core_privacy\local\request\writer::with_context($exportdata->get_context())
+                        ->export_area_files($currentpath, 'assignfeedback_file', $filearea, $gradeid);
+            }
+        }
+    }
+
+    /**
+     * Any call to this method should delete all user data for the context defined in the deletion_criteria.
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data from this sub-plugin.
+     */
+    public static function delete_feedback_for_context(assign_plugin_request_data $requestdata) {
+
+        $assign = $requestdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'file');
+        $fileareas = $plugin->get_file_areas();
+        $fs = get_file_storage();
+        foreach ($fileareas as $filearea => $notused) {
+            // Delete feedback files.
+            $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_file', $filearea);
+        }
+        $plugin->delete_instance();
+    }
+
+    /**
+     * Calling this function should delete all user data associated with this grade.
+     *
+     * @param  assign_plugin_request_data $requestdata Data useful for deleting user data.
+     */
+    public static function delete_feedback_for_grade(assign_plugin_request_data $requestdata) {
+        global $DB;
+
+        $assign = $requestdata->get_assign();
+        $plugin = $assign->get_plugin_by_type('assignfeedback', 'file');
+        $fileareas = $plugin->get_file_areas();
+        $fs = get_file_storage();
+        foreach ($fileareas as $filearea => $notused) {
+            // Delete feedback files.
+            $fs->delete_area_files($requestdata->get_context()->id, 'assignfeedback_file', $filearea,
+                    $requestdata->get_pluginobject()->id);
+        }
+
+        // Delete table entries.
+        $DB->delete_records('assignfeedback_file', ['assignment' => $requestdata->get_assign()->get_instance()->id,
+                'grade' => $requestdata->get_pluginobject()->id]);
+    }
+}
index 20d4069..3765b00 100644 (file)
@@ -37,6 +37,8 @@ $string['feedbackfileadded'] = 'New feedback file "{$a->filename}" for student "
 $string['feedbackfileupdated'] = 'Modified feedback file "{$a->filename}" for student "{$a->student}"';
 $string['feedbackzip_help'] = 'A zip file containing a list of feedback files for one or more students. Feedback files will be assigned to students based on the participant id which should be the second part of each filename immediately after the users full name. This naming convention is used when downloading submissions so you can download all submissions, add comments to a few files and then rezip and upload all of the files. Files with no changes will be ignored.';
 $string['file'] = 'Feedback files';
+$string['privacy:metadata:filepurpose'] = 'Feedback files from the teacher for the student.';
+$string['privacy:path'] = 'Feedback files';
 $string['filesupdated'] = 'Feedback files updated: {$a}';
 $string['filesadded'] = 'Feedback files added: {$a}';
 $string['importfeedbackfiles'] = 'Import feedback file(s)';
diff --git a/mod/assign/feedback/file/tests/privacy_test.php b/mod/assign/feedback/file/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..5537755
--- /dev/null
@@ -0,0 +1,209 @@
+<?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 assignfeedback_file.
+ *
+ * @package    assignfeedback_file
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/assign/locallib.php');
+require_once($CFG->dirroot . '/mod/assign/tests/privacy_test.php');
+
+use mod_assign\privacy\assign_plugin_request_data;
+
+/**
+ * Unit tests for mod/assign/feedback/file/classes/privacy/
+ *
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assignfeedback_file_privacy_testcase extends \mod_assign\tests\mod_assign_privacy_testcase {
+
+    /**
+     * Convenience function for creating feedback data.
+     *
+     * @param  object   $assign         assign object
+     * @param  stdClass $student        user object
+     * @param  stdClass $teacher        user object
+     * @param  string   $submissiontext Submission text
+     * @param  string   $feedbacktext   Feedback text
+     * @return array   Feedback plugin object and the grade object.
+     */
+    protected function create_feedback($assign, $student, $teacher, $submissiontext, $feedbacktext) {
+
+        $submission = new \stdClass();
+        $submission->assignment = $assign->get_instance()->id;
+        $submission->userid = $student->id;
+        $submission->timecreated = time();
+        $submission->onlinetext_editor = ['text' => $submissiontext,
+                                         'format' => FORMAT_MOODLE];
+
+        $this->setUser($student);
+        $notices = [];
+        $assign->save_submission($submission, $notices);
+
+        $grade = $assign->get_user_grade($student->id, true);
+
+        $this->setUser($teacher);
+
+        $context = context_user::instance($teacher->id);
+
+        $draftitemid = file_get_unused_draft_itemid();
+        file_prepare_draft_area($draftitemid, $context->id, 'assignfeedback_file', 'feedback_files', 1);
+
+        $dummy = array(
+            'contextid' => $context->id,
+            'component' => 'user',
+            'filearea' => 'draft',
+            'itemid' => $draftitemid,
+            'filepath' => '/',
+            'filename' => 'feedback1.txt'
+        );
+
+        $fs = get_file_storage();
+        $file = $fs->create_file_from_string($dummy, $feedbacktext);
+
+        // Create formdata.
+        $data = new stdClass();
+        $data->{'files_' . $teacher->id . '_filemanager'} = $draftitemid;
+
+        $plugin = $assign->get_feedback_plugin_by_type('file');
+        // Save the feedback.
+        $plugin->save($grade, $data);
+
+        return [$plugin, $grade];
+    }
+
+    /**
+     * Quick test to make sure that get_metadata returns something.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('assignfeedback_file');
+        $collection = \assignfeedback_file\privacy\provider::get_metadata($collection);
+        $this->assertNotEmpty($collection);
+    }
+
+    /**
+     * Test that feedback comments are exported for a user.
+     */
+    public function test_export_feedback_user_data() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Student.
+        $user1 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user2 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $feedbacktext = '<p>first comment for this test</p>';
+        list($plugin, $grade) = $this->create_feedback($assign, $user1, $user2, 'Submission text', $feedbacktext);
+
+        $writer = \core_privacy\local\request\writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+
+        // The student should be able to see the teachers feedback.
+        $exportdata = new \mod_assign\privacy\assign_plugin_request_data($context, $assign, $grade, [], $user1);
+        \assignfeedback_file\privacy\provider::export_feedback_user_data($exportdata);
+        $feedbackfile = $writer->get_files([get_string('privacy:path', 'assignfeedback_file')])['feedback1.txt'];
+        // Check that we got a stored file.
+        $this->assertInstanceOf('stored_file', $feedbackfile);
+        $this->assertEquals('feedback1.txt', $feedbackfile->get_filename());
+    }
+
+    /**
+     * Test that all feedback is deleted for a context.
+     */
+    public function test_delete_feedback_for_context() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Students.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user3 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $feedbacktext = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second submission.</p>';
+        list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext);
+
+        // Check that we have data.
+        $this->assertFalse($plugin1->is_empty($grade1));
+        $this->assertFalse($plugin2->is_empty($grade2));
+
+        $requestdata = new assign_plugin_request_data($context, $assign);
+        \assignfeedback_file\privacy\provider::delete_feedback_for_context($requestdata);
+
+        // Check that we now have no data.
+        $this->assertTrue($plugin1->is_empty($grade1));
+        $this->assertTrue($plugin2->is_empty($grade2));
+    }
+
+    /**
+     * Test that a grade item is deleted for a user.
+     */
+    public function test_delete_feedback_for_grade() {
+        $this->resetAfterTest();
+        // Create course, assignment, submission, and then a feedback comment.
+        $course = $this->getDataGenerator()->create_course();
+        // Students.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // Teacher.
+        $user3 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
+        $assign = $this->create_instance(['course' => $course]);
+
+        $context = $assign->get_context();
+
+        $feedbacktext = '<p>first comment for this test</p>';
+        list($plugin1, $grade1) = $this->create_feedback($assign, $user1, $user3, 'Submission text', $feedbacktext);
+        $feedbacktext = '<p>Comment for second submission.</p>';
+        list($plugin2, $grade2) = $this->create_feedback($assign, $user2, $user3, 'Submission text', $feedbacktext);
+
+        // Check that we have data.
+        $this->assertFalse($plugin1->is_empty($grade1));
+        $this->assertFalse($plugin2->is_empty($grade2));
+
+        $requestdata = new assign_plugin_request_data($context, $assign, $grade1, [], $user1);
+        \assignfeedback_file\privacy\provider::delete_feedback_for_grade($requestdata);
+
+        // Check that we now have no data.
+        $this->assertTrue($plugin1->is_empty($grade1));
+        // User 2's data should still be intact.
+        $this->assertFalse($plugin2->is_empty($grade2));
+    }
+}
diff --git a/mod/assign/feedback/offline/classes/privacy/provider.php b/mod/assign/feedback/offline/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..f3793ad
--- /dev/null
@@ -0,0 +1,47 @@
+<?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 class for requesting user data.
+ *
+ * @package    assignfeedback_offline
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace assignfeedback_offline\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @package    assignfeedback_offline
+ * @copyright  2018 Adrian Greeve <adrian@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:nullproviderreason';
+    }
+}
index aefde95..d5722ff 100644 (file)
@@ -38,6 +38,7 @@ $string['importgrades'] = 'Confirm changes in grading worksheet';
 $string['invalidgradeimport'] = 'Moodle could not read the uploaded worksheet. Make sure it is saved in comma separated value format (.csv) and try again.';
 $string['gradesfile'] = 'Grading worksheet (csv format)';
 $string['gradesfile_help'] = 'Grading worksheet with modified grades. This file must be a csv file that has been downloaded from this assignment and must contain columns for the student grade, and identifier. The encoding for the file must be &quot;UTF-8&quot;';
+$string['privacy:nullproviderreason'] = 'This plugin has no database to store user information. It only uses APIs in mod_assign to help with displaying the grading interface.';
 $string['nochanges'] = 'No modified grades found in uploaded worksheet';
 $string['offlinegradingworksheet'] = 'Grades';
 $string['pluginname'] = 'Offline grading worksheet';
index 9ae84d1..8e606c7 100644 (file)
@@ -79,8 +79,8 @@ $string['assignmentsperpage'] = 'Assignments per page';
 $string['assignsubmission'] = 'Submission plugin';
 $string['assignsubmissionpluginname'] = 'Submission plugin';
 $string['attemptheading'] = 'Attempt {$a->attemptnumber}: {$a->submissionsummary}';
-$string['attemptnumber'] = 'Attempt number';
 $string['attempthistory'] = 'Previous attempts';
+$string['attemptnumber'] = 'Attempt number';
 $string['attemptsettings'] = 'Attempt settings';
 $string['attemptreopenmethod'] = 'Attempts reopened';
 $string['attemptreopenmethod_help'] = 'Determines how student submission attempts are reopened. The available options are: <ul><li>Never - The student submission cannot be reopened.</li><li>Manually - The student submission can be reopened by a teacher.</li><li>Automatically until pass - The student submission is automatically reopened until the student achieves the grade to pass value set in the Gradebook (Gradebook setup section) for this assignment.</li></ul>';
@@ -245,8 +245,8 @@ $string['filternone'] = 'No filter';
 $string['filternotsubmitted'] = 'Not submitted';
 $string['filterrequiregrading'] = 'Requires grading';
 $string['filtersubmitted'] = 'Submitted';
-$string['gradedby'] = 'Graded by';
 $string['graded'] = 'Graded';
+$string['gradedby'] = 'Graded by';
 $string['gradedon'] = 'Graded on';
 $string['gradebelowzero'] = 'Grade must be greater than or equal to zero.';
 $string['gradeabovemaximum'] = 'Grade must be less than or equal to {$a}.';
@@ -387,6 +387,32 @@ $string['preventsubmissionnotingroup_help'] = 'If enabled, users who are not mem
 $string['preventsubmissions'] = 'Prevent the user from making any more submissions to this assignment.';
 $string['preventsubmissionsshort'] = 'Prevent submission changes';
 $string['previous'] = 'Previous';
+$string['privacy:attemptpath'] = 'attempt {$a}';
+$string['privacy:blindmarkingidentifier'] = 'The identifier used for blind marking.';
+$string['privacy:gradepath'] = 'grade';
+$string['privacy:metadata:assigndownloadasfolders'] = 'A user preference for whether multiple file submissions should be downloaded into folders';
+$string['privacy:metadata:assignfeedbackpluginsummary'] = 'Feedback data for the assignment.';
+$string['privacy:metadata:assignfilter'] = 'Filter options such as \'Submitted\', \'Not submitted\', \'Requires grading\', and \'Granted extension\'';
+$string['privacy:metadata:assigngrades'] = 'Stores user grades for the assignment';
+$string['privacy:metadata:assignmarkerfilter'] = 'Filter the assign summary by the assigned marker.';
+$string['privacy:metadata:assignmentid'] = 'Assignment identifier.';
+$string['privacy:metadata:assignmessageexplanation'] = 'Messages are sent to students through the messaging system.';
+$string['privacy:metadata:assignoverrides'] = 'Stores override information for the assignment';
+$string['privacy:metadata:assignperpage'] = 'Number of assignments shown per page.';
+$string['privacy:metadata:assignquickgrading'] = 'A preference as to whether quick grading is used or not.';
+$string['privacy:metadata:assignsubmissiondetail'] = 'Stores user submission information';
+$string['privacy:metadata:assignsubmissionpluginsummary'] = 'Submission data for the assignment.';
+$string['privacy:metadata:assignuserflags'] = 'Stores user meta data such as extension dates';
+$string['privacy:metadata:assignusermapping'] = 'The mapping for blind marking';
+$string['privacy:metadata:assignworkflowfilter'] = 'Filter by the different workflow stages.';
+$string['privacy:metadata:grade'] = 'The numerical grade for this assignment submission. Can be determined by scales/advancedgradingforms etc but will always be converted back to a floating point number.';
+$string['privacy:metadata:grader'] = 'The user ID of the person grading.';
+$string['privacy:metadata:groupid'] = 'Group ID that the user is a member of.';
+$string['privacy:metadata:latest'] = 'Greatly simplifies queries wanting to know information about only the latest attempt.';
+$string['privacy:metadata:mailed'] = 'Has this user been mailed yet?';
+$string['privacy:metadata:timecreated'] = 'Time created';
+$string['privacy:metadata:userid'] = 'Identifier for the user.';
+$string['privacy:studentpath'] = 'studentsubmissions';
 $string['quickgrading'] = 'Quick grading';
 $string['quickgradingresult'] = 'Quick grading';
 $string['quickgradingchangessaved'] = 'The grade changes were saved';
@@ -454,6 +480,7 @@ $string['submissionlog'] = 'Student: {$a->fullname}, Status: {$a->status}';
 $string['submissionnotcopiedinvalidstatus'] = 'The submission was not copied because it has been edited since it was reopened.';
 $string['submissionnoteditable'] = 'Student cannot edit this submission';
 $string['submissionnotready'] = 'This assignment is not ready to submit:';
+$string['privacy:submissionpath'] = 'submission';
 $string['submissionplugins'] = 'Submission plugins';
 $string['submissionreceipts'] = 'Send submission receipts';
 $string['submissionreceiptothertext'] = 'Your assignment submission for