Merge branch 'wip-MDL-62138-master' of git://github.com/marinaglancy/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 1 May 2018 03:46:27 +0000 (11:46 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 1 May 2018 03:46:27 +0000 (11:46 +0800)
283 files changed:
.eslintignore
.stylelintignore
admin/tool/assignmentupgrade/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/assignmentupgrade/lang/en/tool_assignmentupgrade.php
admin/tool/assignmentupgrade/tests/privacy_test.php [new file with mode: 0644]
admin/tool/cohortroles/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/cohortroles/lang/en/tool_cohortroles.php
admin/tool/cohortroles/tests/privacy_test.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/tests/privacy_provider_test.php [new file with mode: 0644]
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/policy/classes/api.php
admin/tool/policy/classes/form/accept_policy.php
admin/tool/policy/classes/output/page_agreedocs.php
admin/tool/policy/classes/output/page_nopermission.php
admin/tool/policy/classes/output/page_viewalldoc.php
admin/tool/policy/classes/output/user_agreement.php
admin/tool/policy/index.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/tests/behat/acceptances.feature
admin/tool/policy/tests/behat/consent.feature
auth/email/classes/external.php
auth/oauth2/classes/privacy/provider.php [new file with mode: 0644]
auth/oauth2/lang/en/auth_oauth2.php
auth/oauth2/tests/privacy_provider_test.php [new file with mode: 0644]
badges/classes/privacy/provider.php [new file with mode: 0644]
badges/tests/privacy_test.php [new file with mode: 0644]
blocks/activity_modules/lang/en/block_activity_modules.php
blocks/activity_results/lang/en/block_activity_results.php
blocks/badges/lang/en/block_badges.php
blocks/calendar_upcoming/lang/en/block_calendar_upcoming.php
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/course_summary/lang/en/block_course_summary.php
blocks/login/lang/en/block_login.php
blocks/myoverview/classes/output/courses_view.php
blocks/myoverview/classes/privacy/provider.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/course-paging-content-item.mustache
blocks/myoverview/templates/courses-view-course-item.mustache
blocks/myoverview/tests/privacy_test.php [new file with mode: 0644]
blocks/myprofile/lang/en/block_myprofile.php
blocks/private_files/lang/en/block_private_files.php
blocks/tag_youtube/lang/en/block_tag_youtube.php
blocks/tests/privacy_test.php [new file with mode: 0644]
calendar/type/gregorian/lang/en/calendartype_gregorian.php
cohort/classes/privacy/provider.php [new file with mode: 0644]
cohort/tests/privacy_test.php [new file with mode: 0644]
course/lib.php
course/moodleform_mod.php
dataformat/csv/lang/en/dataformat_csv.php
dataformat/excel/lang/en/dataformat_excel.php
dataformat/html/lang/en/dataformat_html.php
dataformat/json/lang/en/dataformat_json.php
dataformat/ods/lang/en/dataformat_ods.php
enrol/category/classes/privacy/provider.php [new file with mode: 0644]
enrol/category/lang/en/enrol_category.php
enrol/cohort/classes/privacy/provider.php [new file with mode: 0644]
enrol/cohort/lang/en/enrol_cohort.php
enrol/database/classes/privacy/provider.php [new file with mode: 0644]
enrol/database/lang/en/enrol_database.php
enrol/flatfile/classes/privacy/provider.php [new file with mode: 0644]
enrol/flatfile/lang/en/enrol_flatfile.php
enrol/guest/classes/privacy/provider.php [new file with mode: 0644]
enrol/guest/lang/en/enrol_guest.php
enrol/imsenterprise/classes/privacy/provider.php [new file with mode: 0644]
enrol/imsenterprise/lang/en/enrol_imsenterprise.php
enrol/ldap/classes/privacy/provider.php [new file with mode: 0644]
enrol/ldap/lang/en/enrol_ldap.php
enrol/manual/classes/enrol_users_form.php
enrol/manual/classes/privacy/provider.php [new file with mode: 0644]
enrol/manual/lang/en/enrol_manual.php
enrol/meta/classes/privacy/provider.php [new file with mode: 0644]
enrol/meta/lang/en/enrol_meta.php
enrol/mnet/classes/privacy/provider.php [new file with mode: 0644]
enrol/mnet/lang/en/enrol_mnet.php
enrol/paypal/lang/en/enrol_paypal.php
enrol/self/classes/privacy/provider.php [new file with mode: 0644]
enrol/self/lang/en/enrol_self.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
filter/classes/privacy/provider.php [new file with mode: 0644]
lang/en/admin.php
lang/en/analytics.php
lang/en/badges.php
lang/en/block.php
lang/en/blog.php
lang/en/cohort.php
lang/en/editor.php
lang/en/fileconverter.php [new file with mode: 0644]
lang/en/files.php [new file with mode: 0644]
lang/en/filters.php
lang/en/moodle.php
lang/en/my.php
lang/en/notes.php
lang/en/rss.php
lang/en/search.php
lang/en/tag.php
lang/en/userkey.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/classes/component.php
lib/classes/geopattern.php [new file with mode: 0644]
lib/classes/text.php
lib/classes/user.php
lib/db/services.php
lib/editor/atto/lang/en/editor_atto.php
lib/editor/textarea/lang/en/editor_textarea.php
lib/editor/tinymce/lang/en/editor_tinymce.php
lib/editor/tinymce/plugins/pdw/lang/en/tinymce_pdw.php
lib/form/autocomplete.php
lib/form/templates/filetypes-descriptions.mustache
lib/geopattern-php/GeoPattern/GeoPattern.php [new file with mode: 0644]
lib/geopattern-php/GeoPattern/SVG.php [new file with mode: 0644]
lib/geopattern-php/GeoPattern/SVGElements/Base.php [new file with mode: 0644]
lib/geopattern-php/GeoPattern/SVGElements/Circle.php [new file with mode: 0644]
lib/geopattern-php/GeoPattern/SVGElements/Group.php [new file with mode: 0644]
lib/geopattern-php/GeoPattern/SVGElements/Path.php [new file with mode: 0644]
lib/geopattern-php/GeoPattern/SVGElements/Polyline.php [new file with mode: 0644]
lib/geopattern-php/GeoPattern/SVGElements/Rectangle.php [new file with mode: 0644]
lib/geopattern-php/LICENSE.md [new file with mode: 0644]
lib/geopattern-php/moodle_readme.txt [new file with mode: 0644]
lib/moodlelib.php
lib/tests/component_test.php
lib/tests/text_test.php
lib/tests/user_test.php
lib/thirdpartylibs.xml
lib/userkey/classes/privacy/provider.php [new file with mode: 0644]
lib/userkey/tests/privacy_provider.php [new file with mode: 0644]
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js
lib/yui/src/notification/js/confirm.js
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/jabber/lang/en/message_jabber.php
message/output/popup/classes/privacy/provider.php [new file with mode: 0644]
message/output/popup/lang/en/message_popup.php
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/chat/classes/privacy/provider.php [new file with mode: 0644]
mod/chat/lang/en/chat.php
mod/chat/tests/privacy_test.php [new file with mode: 0644]
mod/choice/classes/external.php
mod/choice/lang/en/choice.php
mod/choice/tests/externallib_test.php
mod/feedback/lang/en/feedback.php
mod/folder/lang/en/folder.php
mod/forum/tests/behat/edit_post_student.feature
mod/imscp/classes/privacy/provider.php [new file with mode: 0644]
mod/imscp/lang/en/imscp.php
mod/label/lang/en/label.php
mod/lesson/classes/privacy/provider.php [new file with mode: 0644]
mod/lesson/lang/en/lesson.php
mod/lesson/tests/privacy_test.php [new file with mode: 0644]
mod/lti/amd/build/contentitem.min.js
mod/lti/amd/src/contentitem.js
mod/lti/locallib.php
mod/lti/mod_form.js
mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php
mod/page/lang/en/page.php
mod/resource/lang/en/resource.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]
mod/survey/lang/en/survey.php
mod/url/lang/en/url.php
my/classes/privacy/provider.php [new file with mode: 0644]
my/tests/privacy_test.php [new file with mode: 0644]
notes/classes/privacy/provider.php [new file with mode: 0644]
notes/tests/privacy_test.php [new file with mode: 0644]
phpunit.xml.dist
plagiarism/classes/privacy/provider.php
portfolio/classes/privacy/provider.php
privacy/classes/local/request/contextlist.php
privacy/classes/local/request/moodle_content_writer.php
privacy/classes/local/request/transform.php
privacy/classes/local/sitepolicy/default_handler.php
privacy/classes/local/sitepolicy/manager.php
privacy/classes/manager.php
privacy/classes/tests/request/content_writer.php
privacy/tests/contextlist_test.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
privacy/tests/request_transform_test.php
privacy/tests/sitepolicy_test.php
privacy/tests/tests_content_writer_test.php
question/classes/output/qbank_chooser.php
repository/flickr_public/lang/en/repository_flickr_public.php
repository/youtube/lang/en/repository_youtube.php
rss/classes/privacy/provider.php
rss/tests/privacy_test.php
search/amd/build/form-search-user-selector.min.js [new file with mode: 0644]
search/amd/src/form-search-user-selector.js [new file with mode: 0644]
search/classes/document.php
search/classes/engine.php
search/classes/external.php [new file with mode: 0644]
search/classes/manager.php
search/classes/output/form/search.php
search/engine/solr/classes/engine.php
search/engine/solr/tests/engine_test.php
search/templates/form-user-selector-suggestion.mustache [new file with mode: 0644]
search/tests/behat/search_by_user.feature [new file with mode: 0644]
search/tests/external_test.php [new file with mode: 0644]
search/upgrade.txt
theme/boost/amd/build/drawer.min.js
theme/boost/amd/src/drawer.js
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/bootstrap-rtl.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/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/preset/default.scss
theme/boost/templates/core/action_menu.mustache
theme/boost/templates/core_form/element-autocomplete.mustache
theme/bootstrapbase/less/moodle/backup-restore.less
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-paging-content-item.mustache
theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache
theme/bootstrapbase/templates/block_myoverview/paging-content-item.mustache [new file with mode: 0644]
user/profile/field/checkbox/classes/privacy/provider.php [new file with mode: 0644]
user/profile/field/checkbox/lang/en/profilefield_checkbox.php
user/profile/field/checkbox/tests/privacy_test.php [new file with mode: 0644]
version.php

index 296343b..6345306 100644 (file)
@@ -60,6 +60,7 @@ lib/fonts/
 lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
+lib/geopattern-php/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index bdd33f8..6368112 100644 (file)
@@ -61,6 +61,7 @@ lib/fonts/
 lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
+lib/geopattern-php/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
diff --git a/admin/tool/assignmentupgrade/classes/privacy/provider.php b/admin/tool/assignmentupgrade/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..452cc15
--- /dev/null
@@ -0,0 +1,73 @@
+<?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 tool_assignmentupgrade.
+ *
+ * @package    tool_assignmentupgrade
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_assignmentupgrade\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\writer;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tool_assignmentupgrade implementing metadata, plugin, and user_preference providers.
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\user_preference_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_user_preference(
+            'tool_assignmentupgrade_perpage',
+            'privacy:metadata:preference:perpage'
+        );
+        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) {
+        $perpage = get_user_preferences('tool_assignmentupgrade_perpage', null, $userid);
+        if ($perpage !== null) {
+            writer::export_user_preference(
+                'tool_assignmentupgrade',
+                'perpage',
+                $perpage,
+                get_string('privacy:metadata:preference:perpage', 'tool_assignmentupgrade')
+            );
+        }
+    }
+
+}
index 9dc8c6e..c7e7d26 100644 (file)
@@ -58,3 +58,4 @@ $string['upgradeallconfirm'] = 'Upgrade all assignments?';
 $string['upgradeprogress'] = 'Upgrade assignment {$a->current} of {$a->total}';
 $string['upgradesingle'] = 'Upgrade single assignment';
 $string['viewcourse'] = 'View the course with the converted assignment';
+$string['privacy:metadata:preference:perpage'] = 'The assignment upgrade records per page preference set for the user.';
diff --git a/admin/tool/assignmentupgrade/tests/privacy_test.php b/admin/tool/assignmentupgrade/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..d22770c
--- /dev/null
@@ -0,0 +1,75 @@
+<?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 tests for tool_assignmentupgrade.
+ *
+ * @package    tool_assignmentupgrade
+ * @category   test
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\tests\provider_testcase;
+use \core_privacy\local\request\writer;
+use \tool_assignmentupgrade\privacy\provider;
+
+/**
+ * Unit tests for tool_assignmentupgrade/classes/privacy/policy
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_assignmentupgrade_privacy_testcase extends provider_testcase {
+
+    /**
+     * Overriding setUp() function to always reset after tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test for provider::test_export_user_preferences().
+     */
+    public function test_export_user_preferences() {
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Add a user home page preference for the User.
+        set_user_preference('tool_assignmentupgrade_perpage', '100', $user);
+
+        // Test the user preference exists.
+        $params = [
+            'userid' => $user->id,
+            'name' => 'tool_assignmentupgrade_perpage'
+        ];
+
+        // Test the user preferences export contains 1 user preference record for the User.
+        provider::export_user_preferences($user->id);
+        $contextuser = context_user::instance($user->id);
+        $writer = writer::with_context($contextuser);
+        $this->assertTrue($writer->has_any_data());
+
+        $exportedpreferences = $writer->get_user_preferences('tool_assignmentupgrade');
+        $this->assertCount(1, (array) $exportedpreferences);
+        $this->assertEquals('100', $exportedpreferences->perpage->value);
+    }
+
+}
diff --git a/admin/tool/cohortroles/classes/privacy/provider.php b/admin/tool/cohortroles/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..bc38e7c
--- /dev/null
@@ -0,0 +1,200 @@
+<?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 tool_cohortroles.
+ *
+ * @package    tool_cohortroles
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_cohortroles\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\context;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tool_cohortroles implementing metadata and plugin providers.
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @param   collection $collection The initialised collection to add items to.
+     * @return  collection     A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        // The tool_cohortroles plugin utilises the mdl_tool_cohortroles table.
+        $collection->add_database_table(
+            'tool_cohortroles',
+            [
+                'id' => 'privacy:metadata:tool_cohortroles:id',
+                'cohortid' => 'privacy:metadata:tool_cohortroles:cohortid',
+                'roleid' => 'privacy:metadata:tool_cohortroles:roleid',
+                'userid' => 'privacy:metadata:tool_cohortroles:userid',
+                'timecreated' => 'privacy:metadata:tool_cohortroles:timecreated',
+                'timemodified' => 'privacy:metadata:tool_cohortroles:timemodified',
+                'usermodified' => 'privacy:metadata:tool_cohortroles:usermodified'
+            ],
+            'privacy:metadata:tool_cohortroles'
+        );
+
+        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 {
+        $contextlist = new contextlist();
+
+        // Retrieve the User context associated with tool_cohortroles records.
+        $sql = "SELECT DISTINCT c.id
+                  FROM {context} c
+                  JOIN {tool_cohortroles} cr ON cr.userid = c.instanceid AND c.contextlevel = :contextuser
+                 WHERE cr.userid = :userid";
+
+        $params = [
+            'contextuser' => 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;
+
+        // If the user has tool_cohortroles data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        // Retrieve the tool_cohortroles records created for the user.
+        $sql = 'SELECT cr.id as cohortroleid,
+                       c.name as cohortname,
+                       c.idnumber as cohortidnumber,
+                       c.description as cohortdescription,
+                       r.shortname as roleshortname,
+                       cr.userid as userid,
+                       cr.timecreated as timecreated,
+                       cr.timemodified as timemodified
+                  FROM {tool_cohortroles} cr
+                  JOIN {cohort} c ON c.id = cr.cohortid
+                  JOIN {role} r ON r.id = cr.roleid
+                 WHERE cr.userid = :userid';
+
+        $params = [
+            'userid' => $userid
+        ];
+
+        $cohortroles = $DB->get_records_sql($sql, $params);
+        foreach ($cohortroles as $cohortrole) {
+            // The tool_cohortroles data export is organised in:
+            // {User Context}/Cohort roles management/{cohort name}/{role shortname}/data.json.
+            $subcontext = [
+                get_string('pluginname', 'tool_cohortroles'),
+                $cohortrole->cohortname,
+                $cohortrole->roleshortname
+            ];
+
+            $data = (object) [
+                'cohortname' => $cohortrole->cohortname,
+                'cohortidnumber' => $cohortrole->cohortidnumber,
+                'cohortdescription' => $cohortrole->cohortdescription,
+                'roleshortname' => $cohortrole->roleshortname,
+                'userid' => transform::user($cohortrole->userid),
+                'timecreated' => transform::datetime($cohortrole->timecreated),
+                'timemodified' => transform::datetime($cohortrole->timemodified)
+            ];
+
+            writer::with_context($context)->export_data($subcontext, $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;
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        // Delete the tool_cohortroles records created for the userid.
+        $DB->delete_records('tool_cohortroles', ['userid' => $userid]);
+    }
+
+    /**
+     * 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 the user has tool_cohortroles data, then only the User context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+
+        // Sanity check that context is at the User context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+
+        // Delete the tool_cohortroles records created for the userid.
+        $DB->delete_records('tool_cohortroles', ['userid' => $userid]);
+    }
+
+}
index 08f00c9..a8afe83 100644 (file)
@@ -42,3 +42,11 @@ $string['selectrole'] = 'Select role';
 $string['selectusers'] = 'Select users to assign role';
 $string['taskname'] = 'Sync cohort role assignments';
 $string['thisuserroles'] = 'Roles assigned relative to this user';
+$string['privacy:metadata:tool_cohortroles'] = 'The Cohort roles management plugin stores user cohort role mappings.';
+$string['privacy:metadata:tool_cohortroles:id'] = 'The ID of the cohort role mapping record.';
+$string['privacy:metadata:tool_cohortroles:cohortid'] = 'The ID of the cohort.';
+$string['privacy:metadata:tool_cohortroles:roleid'] = 'The ID of the role.';
+$string['privacy:metadata:tool_cohortroles:userid'] = 'The ID of the user.';
+$string['privacy:metadata:tool_cohortroles:timecreated'] = 'The date/time of when the cohort  role mapping was created.';
+$string['privacy:metadata:tool_cohortroles:timemodified'] = 'The date/time of when the cohort role mapping was modified.';
+$string['privacy:metadata:tool_cohortroles:usermodified'] = 'The ID of the user who last modified the cohort role mapping.';
diff --git a/admin/tool/cohortroles/tests/privacy_test.php b/admin/tool/cohortroles/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..f118b74
--- /dev/null
@@ -0,0 +1,206 @@
+<?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 the tool_cohortroles implementation of the privacy API.
+ *
+ * @package    tool_cohortroles
+ * @category   test
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\approved_contextlist;
+use \tool_cohortroles\api;
+use \tool_cohortroles\privacy\provider;
+
+/**
+ * Unit tests for the tool_cohortroles implementation of the privacy API.
+ *
+ * @copyright  2018 Zig Tan <zig@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_cohortroles_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Overriding setUp() function to always reset after tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test for provider::get_contexts_for_userid().
+     */
+    public function test_get_contexts_for_userid() {
+        global $DB;
+
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 3;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's assigned cohortroles matches 3.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount($nocohortroles, $cohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+    }
+
+    /**
+     * Test for provider::export_user_data().
+     */
+    public function test_export_user_data() {
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 3;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+
+        // Retrieve the User's tool_cohortroles data.
+        $approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
+        provider::export_user_data($approvedcontextlist);
+
+        // Test the tool_cohortroles data is exported at the User context level.
+        $writer = writer::with_context($context);
+        $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;
+
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 4;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's assigned cohortroles matches 4.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount($nocohortroles, $cohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+
+        // Delete all the User's records in mdl_tool_cohortroles table by the specified User context.
+        provider::delete_data_for_all_users_in_context($context);
+
+        // Test the cohort roles records in mdl_tool_cohortroles table is equals zero.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount(0, $cohortroles);
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->setAdminUser();
+
+        $nocohortroles = 4;
+        $this->setup_test_scenario_data($user->id, $nocohortroles);
+
+        // Test the User's assigned cohortroles matches 4.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount($nocohortroles, $cohortroles);
+
+        // Test the User's retrieved contextlist contains only one context.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $contexts = $contextlist->get_contexts();
+        $this->assertCount(1, $contexts);
+
+        // Test the User's contexts equal the User's own context.
+        $context = reset($contexts);
+        $this->assertEquals(CONTEXT_USER, $context->contextlevel);
+        $this->assertEquals($user->id, $context->instanceid);
+
+        // Delete all the User's records in mdl_tool_cohortroles table by the specified User approved context list.
+        $approvedcontextlist = new approved_contextlist($user, 'tool_cohortroles', $contextlist->get_contextids());
+        provider::delete_data_for_user($approvedcontextlist);
+
+        // Test the records in mdl_tool_cohortroles table is equals zero.
+        $cohortroles = $DB->get_records('tool_cohortroles', ['userid' => $user->id]);
+        $this->assertCount(0, $cohortroles);
+    }
+
+    /**
+     * Helper function to setup tool_cohortroles records for testing a specific user.
+     *
+     * @param int $userid           The ID of the user used for testing.
+     * @param int $nocohortroles    The number of tool_cohortroles to create for the user.
+     * @throws \core_competency\invalid_persistent_exception
+     * @throws coding_exception
+     */
+    protected function setup_test_scenario_data($userid, $nocohortroles) {
+        $roleid = create_role('Sausage Roll', 'sausageroll', 'mmmm');
+
+        for ($c = 0; $c < $nocohortroles; $c++) {
+            $cohort = $this->getDataGenerator()->create_cohort();
+
+            $params = (object)array(
+                'userid' => $userid,
+                'roleid' => $roleid,
+                'cohortid' => $cohort->id
+            );
+
+            api::create_cohort_role_assignment($params);
+        }
+    }
+
+}
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 3132033..999ba5e 100644 (file)
@@ -100,7 +100,7 @@ $string['expiry'] = 'Expiry';
 $string['expandplugin'] = 'Expand and collapse plugin.';
 $string['expandplugintype'] = 'Expand and collapse plugin type.';
 $string['explanationtitle'] = 'Icons used on this page and what they mean.';
-$string['external'] = 'External';
+$string['external'] = 'Additional';
 $string['externalexplanation'] = 'An additional plugin installed on this site.';
 $string['frontpagecourse'] = 'Front page course';
 $string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes';
@@ -168,7 +168,7 @@ $string['privacy:metadata:request'] = 'Information from personal data requests (
 $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.';
 $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs';
 $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.';
-$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s respective privacy officer regarding the request.';
+$string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s Data Protection Officer regarding the request.';
 $string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.';
 $string['protected'] = 'Protected';
 $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.';
index c8271f3..b074060 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['classname'] = 'Class name';
 $string['component'] = 'Component';
-$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, you can use the [server]:[port], for example mail.example.com:587 format. If you leave this field blank, Moodle will use the default port for the type of mail server you specify.';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
 $string['defaultexpiration'] = 'Default address expiry period';
 $string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
 $string['description'] = 'Description';
@@ -45,7 +45,7 @@ $string['invalid_recipient_handler'] = 'If a valid message is received but the s
 This handler processes those replies.
 
 It is not possible to disable sender verification of this handler because the user may reply from an incorrect email address if their email client configuration is incorrect.';
-$string['invalid_recipient_handler_name'] = 'Invalid recipient handler';
+$string['invalid_recipient_handler_name'] = 'Invalid sender handler';
 $string['invalidrecipientdescription'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
 $string['invalidrecipientdescriptionhtml'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
 $string['invalidrecipientfinal'] = 'The message "{$a->subject}" could not be authenticated. Please check that you are sending your message from the same email address as in your profile.';
index 9ef98e1..43c8464 100644 (file)
@@ -277,6 +277,7 @@ class api {
             $settings->tool_mobile_customlangstrings = get_config('tool_mobile', 'customlangstrings');
             $settings->tool_mobile_disabledfeatures = get_config('tool_mobile', 'disabledfeatures');
             $settings->tool_mobile_custommenuitems = get_config('tool_mobile', 'custommenuitems');
+            $settings->tool_mobile_apppolicy = get_config('tool_mobile', 'apppolicy');
         }
 
         return $settings;
diff --git a/admin/tool/mobile/classes/privacy/provider.php b/admin/tool/mobile/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..c29c0a1
--- /dev/null
@@ -0,0 +1,142 @@
+<?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 tool_mobile.
+ *
+ * @package    tool_mobile
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_mobile\privacy;
+defined('MOODLE_INTERNAL') || die();
+use \core_privacy\local\request\writer;
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\transform;
+
+/**
+ * Privacy provider for tool_mobile.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\user_preference_provider,
+    \core_privacy\local\request\plugin\provider {
+    /**
+     * Returns meta data about this system.
+     *
+     * @param   collection     $collection The initialised item collection to add items to.
+     * @return  collection     A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        // There is a one user preference.
+        $collection->add_user_preference('tool_mobile_autologin_request_last',
+            'privacy:metadata:preference:tool_mobile_autologin_request_last');
+        $collection->add_subsystem_link('core_userkey', [], 'privacy:metadata:core_userkey');
+
+        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 {
+        $sql = "SELECT ctx.id
+                  FROM {user_private_key} k
+                  JOIN {user} u ON k.userid = u.id
+                  JOIN {context} ctx ON ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel
+                 WHERE k.userid = :userid AND k.script = 'tool_mobile'";
+        $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
+        $contextlist = new contextlist();
+        $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) {
+        // If the user has data, then only the CONTEXT_USER should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+        // Sanity check that context is at the user context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        // Export associated userkeys.
+        \core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile');
+    }
+    /**
+     * 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) {
+        $autologinrequestlast = get_user_preferences('tool_mobile_autologin_request_last', null, $userid);
+        if ($autologinrequestlast !== null) {
+            $time = transform::datetime($autologinrequestlast);
+            writer::export_user_preference('tool_mobile',
+                'tool_mobile_autologin_request_last',
+                $time,
+                get_string('privacy:metadata:preference:tool_mobile_autologin_request_last', 'tool_mobile')
+            );
+        }
+    }
+    /**
+     * 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) {
+        // Sanity check that context is at the user context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+        // Delete all the userkeys.
+        \core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
+    }
+    /**
+     * 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) {
+        // If the user has data, then only the user context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+        $context = reset($contexts);
+        // Sanity check that context is at the user context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+        $userid = $context->instanceid;
+        // Delete all the userkeys.
+        \core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
+    }
+}
\ No newline at end of file
index 1693ba5..195af12 100644 (file)
@@ -25,6 +25,8 @@
 $string['adodbdebugwarning'] = 'ADOdb debugging is enabled. It should be disabled in the external database authentication or external database enrolment plugin settings.';
 $string['androidappid'] = 'Android app\'s unique identifier';
 $string['androidappid_desc'] = 'This setting may be left as default unless you have a custom Android app.';
+$string['apppolicy'] = 'App policy URL';
+$string['apppolicy_help'] = 'The URL of a policy for app users which is listed on the About page in the app. If the field is left empty, the site policy URL will be used instead.';
 $string['autologinkeygenerationlockout'] = 'Auto-login key generation is blocked. You need to wait 6 minutes between requests.';
 $string['autologinnotallowedtoadmins'] = 'Auto-login is not allowed for site admins.';
 $string['cachedef_plugininfo'] = 'This stores the list of plugins with mobile addons';
@@ -65,7 +67,7 @@ $string['invalidcertificatechainwarning'] = 'It seems that the certificate chain
 $string['invalidcertificateexpiredatewarning'] = 'It seems that the HTTPS certificate for the site has expired.';
 $string['invalidcertificatestartdatewarning'] = 'It seems that the HTTPS certificate for the site is not yet valid (with a start date in the future).';
 $string['invalidprivatetoken'] = 'Invalid private token. Token should not be empty or passed via GET parameter.';
-$string['invaliduserquotawarning'] = 'The user quota (userquota) is set to an invalid number. It should be set to a valid number (an integer value) in Site policies.';
+$string['invaliduserquotawarning'] = 'The user quota (userquota) is set to an invalid number. It should be set to a valid number (an integer value) in Site security settings.';
 $string['iosappid'] = 'iOS app\'s unique identifier';
 $string['iosappid_desc'] = 'This setting may be left as default unless you have a custom iOS app.';
 $string['loginintheapp'] = 'Via the app';
@@ -92,3 +94,5 @@ $string['smartappbanners'] = 'App Banners';
 $string['typeoflogin'] = 'Type of login';
 $string['typeoflogin_desc'] = 'If the site uses a SSO authentication method, then select via a browser window or via an embedded browser. An embedded browser provides a better user experience, though it doesn\'t work with all SSO plugins.';
 $string['getmoodleonyourmobile'] = 'Get the mobile app';
+$string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The date of the last auto-login key request. Between each request 6 minutes are required.';
+$string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
index 8af64c6..125a72a 100644 (file)
@@ -43,6 +43,9 @@ if ($hassiteconfig) {
                 new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), $default));
     }
 
+    $temp->add(new admin_setting_configtext('tool_mobile/apppolicy', new lang_string('apppolicy', 'tool_mobile'),
+        new lang_string('apppolicy_help', 'tool_mobile'), '', PARAM_URL));
+
     $ADMIN->add('mobileapp', $temp);
 
     // Show only mobile settings if the mobile service is enabled.
index 0b8873b..d5a8a34 100644 (file)
@@ -173,6 +173,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             array('name' => 'tool_mobile_customlangstrings', 'value' => ''),
             array('name' => 'tool_mobile_disabledfeatures', 'value' => ''),
             array('name' => 'tool_mobile_custommenuitems', 'value' => ''),
+            array('name' => 'tool_mobile_apppolicy', 'value' => ''),
         );
         $this->assertCount(0, $result['warnings']);
         $this->assertEquals($expected, $result['settings']);
diff --git a/admin/tool/mobile/tests/privacy_provider_test.php b/admin/tool/mobile/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..d553a10
--- /dev/null
@@ -0,0 +1,132 @@
+<?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/>.
+/**
+ * Base class for unit tests for tool_mobile.
+ *
+ * @package    tool_mobile
+ * @category   test
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\request\writer;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\approved_contextlist;
+use \tool_mobile\privacy\provider;
+
+/**
+ * Unit tests for the tool_mobile implementation of the privacy API.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_mobile_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Basic setup for these tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test to check export_user_preferences.
+     * returns user preferences data.
+     */
+    public function test_export_user_preferences() {
+        $user = $this->getDataGenerator()->create_user();
+        $expectedtime = time();
+        set_user_preference('tool_mobile_autologin_request_last', time(), $user);
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $prefs = $writer->get_user_preferences('tool_mobile');
+        $time = transform::datetime($expectedtime);
+        $this->assertEquals($time, $prefs->tool_mobile_autologin_request_last->value);
+        $this->assertEquals(get_string('privacy:metadata:preference:tool_mobile_autologin_request_last', 'tool_mobile'),
+            $prefs->tool_mobile_autologin_request_last->description);
+    }
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $key = get_user_key('tool_mobile', $user->id);
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $this->assertEquals($context->id, $contextlist->current()->id);
+    }
+    /**
+     * Test that data is exported correctly for this plugin.
+     */
+    public function test_export_user_data() {
+        global $DB;
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $keyvalue = get_user_key('tool_mobile', $user->id);
+        $key = $DB->get_record('user_private_key', ['value' => $keyvalue]);
+        // Validate exported data.
+        $this->setUser($user);
+        $writer = writer::with_context($context);
+        $this->assertFalse($writer->has_any_data());
+        $this->export_context_data_for_user($user->id, $context, 'tool_mobile');
+        $userkeydata = $writer->get_related_data([], 'userkeys');
+        $this->assertCount(1, $userkeydata->keys);
+        $this->assertEquals($key->script, reset($userkeydata->keys)->script);
+    }
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $keyvalue = get_user_key('tool_mobile', $user->id);
+        $key = $DB->get_record('user_private_key', ['value' => $keyvalue]);
+        // Before deletion, we should have 1 user_private_key.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(1, $count);
+        // Delete data.
+        provider::delete_data_for_all_users_in_context($context);
+        // After deletion, the user_private_key entries should have been deleted.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(0, $count);
+    }
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+        // Create user and Mobile user keys.
+        $user = $this->getDataGenerator()->create_user();
+        $context = \context_user::instance($user->id);
+        $keyvalue = get_user_key('tool_mobile', $user->id);
+        $key = $DB->get_record('user_private_key', ['value' => $keyvalue]);
+        // Before deletion, we should have 1 user_private_key.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(1, $count);
+        // Delete data.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $approvedcontextlist = new approved_contextlist($user, 'tool_mobile', $contextlist->get_contextids());
+        provider::delete_data_for_user($approvedcontextlist);
+        // After deletion, the user_private_key entries should have been deleted.
+        $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+        $this->assertEquals(0, $count);
+    }
+}
\ No newline at end of file
index 81d20bb..01b18fd 100644 (file)
@@ -97,9 +97,9 @@ $string['privacy:metadata:timecreatedsub'] = 'When this subscription was created
 $string['privacy:metadata:timemodifiedrule'] = 'When this rule was last modified';
 $string['privacy:metadata:timesent'] = 'When the message was sent';
 $string['privacy:metadata:timewindow'] = 'Time window in seconds';
-$string['privacy:metadata:userid'] = 'Id of user who created the rule';
-$string['privacy:metadata:useridhistory'] = 'User to whom this notification was sent';
-$string['privacy:metadata:useridsub'] = 'User id of the subscriber';
+$string['privacy:metadata:userid'] = 'The ID of user who created the rule.';
+$string['privacy:metadata:useridhistory'] = 'The ID of the user to whom this notification was sent';
+$string['privacy:metadata:useridsub'] = 'The ID of the subscriber.';
 $string['privacy:subscriptions'] = 'My event monitor subscriptions';
 $string['processevents'] = 'Process events';
 $string['rulename'] = 'Rule name';
index fab6d03..0b668d6 100644 (file)
@@ -29,6 +29,7 @@ use coding_exception;
 use context_helper;
 use context_system;
 use context_user;
+use core\session\manager;
 use stdClass;
 use tool_policy\event\acceptance_created;
 use tool_policy\event\acceptance_updated;
@@ -773,33 +774,67 @@ class api {
     }
 
     /**
-     * Accepts the current revisions of all policies that the user has not yet accepted
+     * Checks if user can accept policies for themselves or on behalf of another user
      *
-     * @param array|int $policyversionid
-     * @param int|null $userid
-     * @param string|null $note
-     * @param string|null $lang
+     * @param int $userid
+     * @param bool $throwexception
+     * @return bool
      */
-    public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) {
-        global $DB, $USER;
+    public static function can_accept_policies($userid = null, $throwexception = false) {
+        global $USER;
         if (!isloggedin() || isguestuser()) {
-            throw new \moodle_exception('noguest');
+            if ($throwexception) {
+                throw new \moodle_exception('noguest');
+            } else {
+                return false;
+            }
         }
         if (!$userid) {
             $userid = $USER->id;
         }
+
+        if ($userid == $USER->id && !manager::is_loggedinas()) {
+            if ($throwexception) {
+                require_capability('tool/policy:accept', context_system::instance());
+                return;
+            } else {
+                return has_capability('tool/policy:accept', context_system::instance());
+            }
+        }
+
+        // Check capability to accept on behalf as the real user.
+        $realuser = manager::get_realuser();
         $usercontext = \context_user::instance($userid);
-        if ($userid == $USER->id) {
-            require_capability('tool/policy:accept', context_system::instance());
+        if ($throwexception) {
+            require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
+            return;
         } else {
-            require_capability('tool/policy:acceptbehalf', $usercontext);
+            return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
         }
+    }
 
+    /**
+     * Accepts the current revisions of all policies that the user has not yet accepted
+     *
+     * @param array|int $policyversionid
+     * @param int|null $userid
+     * @param string|null $note
+     * @param string|null $lang
+     */
+    public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) {
+        global $DB, $USER;
+        // Validate arguments and capabilities.
         if (empty($policyversionid)) {
             return;
         } else if (!is_array($policyversionid)) {
             $policyversionid = [$policyversionid];
         }
+        if (!$userid) {
+            $userid = $USER->id;
+        }
+        self::can_accept_policies($userid, true);
+
+        // Retrieve the list of policy versions that need agreement (do not update existing agreements).
         list($sql, $params) = $DB->get_in_or_equal($policyversionid, SQL_PARAMS_NAMED);
         $sql = "SELECT v.id AS versionid, a.*
                   FROM {tool_policy_versions} v
@@ -807,8 +842,9 @@ class api {
                   WHERE (a.id IS NULL or a.status <> 1) AND v.id " . $sql;
         $needacceptance = $DB->get_records_sql($sql, ['userid' => $userid] + $params);
 
+        $realuser = manager::get_realuser();
         $updatedata = ['status' => 1, 'lang' => $lang ?: current_language(),
-            'timemodified' => time(), 'usermodified' => $USER->id, 'note' => $note];
+            'timemodified' => time(), 'usermodified' => $realuser->id, 'note' => $note];
         foreach ($needacceptance as $versionid => $currentacceptance) {
             unset($currentacceptance->versionid);
             if ($currentacceptance->id) {
@@ -871,23 +907,16 @@ class api {
      */
     public static function revoke_acceptance($policyversionid, $userid, $note = null) {
         global $DB, $USER;
-        if (!isloggedin() || isguestuser()) {
-            throw new \moodle_exception('noguest');
-        }
         if (!$userid) {
             $userid = $USER->id;
         }
-        $usercontext = \context_user::instance($userid);
-        if ($userid == $USER->id) {
-            require_capability('tool/policy:accept', context_system::instance());
-        } else {
-            require_capability('tool/policy:acceptbehalf', $usercontext);
-        }
+        self::can_accept_policies($userid, true);
 
         if ($currentacceptance = $DB->get_record('tool_policy_acceptances',
                 ['policyversionid' => $policyversionid, 'userid' => $userid])) {
+            $realuser = manager::get_realuser();
             $updatedata = ['id' => $currentacceptance->id, 'status' => 0, 'timemodified' => time(),
-                'usermodified' => $USER->id, 'note' => $note];
+                'usermodified' => $realuser->id, 'note' => $note];
             $DB->update_record('tool_policy_acceptances', $updatedata);
             acceptance_updated::create_from_record((object)($updatedata + (array)$currentacceptance))->trigger();
         }
index 5b54e2c..8fcc474 100644 (file)
@@ -94,7 +94,7 @@ class accept_policy extends \moodleform {
      * @return array (userid=>username)
      */
     protected function validate_and_get_users($userids) {
-        global $DB, $USER;
+        global $DB;
         $usernames = [];
         list($sql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
         $params['usercontextlevel'] = CONTEXT_USER;
@@ -103,7 +103,6 @@ class accept_policy extends \moodleform {
             " FROM {user} u JOIN {context} ctx ON ctx.contextlevel=:usercontextlevel AND ctx.instanceid = u.id
             WHERE u.id " . $sql, $params);
 
-        $acceptany = has_capability('tool/policy:acceptbehalf', \context_system::instance());
         foreach ($userids as $userid) {
             if (!isset($users[$userid])) {
                 throw new \dml_missing_record_exception('user', 'id=?', [$userid]);
@@ -112,12 +111,8 @@ class accept_policy extends \moodleform {
             if (isguestuser($user)) {
                 throw new \moodle_exception('noguest');
             }
-            if ($userid == $USER->id) {
-                require_capability('tool/policy:accept', \context_system::instance());
-            } else if (!$acceptany) {
-                \context_helper::preload_from_record($user);
-                require_capability('tool/policy:acceptbehalf', \context_user::instance($userid));
-            }
+            \context_helper::preload_from_record($user);
+            api::can_accept_policies($userid, true);
             $usernames[$userid] = fullname($user);
         }
         return $usernames;
index 171a62a..278e97b 100644 (file)
@@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 
 use context_system;
 use core\output\notification;
+use core\session\manager;
 use core_user;
 use html_writer;
 use moodle_url;
@@ -77,21 +78,19 @@ class page_agreedocs implements renderable, templatable {
      */
     public function __construct($agreedocs = null, $behalfid = 0, $action = null) {
         global $USER;
+        $realuser = manager::get_realuser();
 
         $this->agreedocs = $agreedocs;
         if (empty($this->agreedocs)) {
             $this->agreedocs = [];
         }
 
-        $this->behalfid = $behalfid;
         $this->action = $action;
 
-        if (!empty($this->behalfid) && $USER->id != $this->behalfid) {
-            $this->behalfuser = core_user::get_user($this->behalfid, '*');
-            // If behalf user doesn't exist, behalfid parameter will be ignored.
-            if ($this->behalfuser === false) {
-                $this->behalfid = 0;
-            }
+        $behalfid = $behalfid ?: $USER->id;
+        if ($realuser->id != $behalfid) {
+            $this->behalfuser = core_user::get_user($behalfid, '*', MUST_EXIST);
+            $this->behalfid = $this->behalfuser->id;
         }
 
         $this->policies = api::list_current_versions(policy_version::AUDIENCE_LOGGEDIN);
@@ -267,12 +266,7 @@ class page_agreedocs implements renderable, templatable {
         // Check for correct user capabilities.
         if (!empty($USER->id)) {
             // For existing users, it's needed to check if they have the capability for accepting policies.
-            if (empty($this->behalfid) || $this->behalfid == $USER->id) {
-                require_capability('tool/policy:accept', context_system::instance());
-            } else {
-                $usercontext = \context_user::instance($this->behalfid);
-                require_capability('tool/policy:acceptbehalf', $usercontext);
-            }
+            api::can_accept_policies($this->behalfid, true);
         } else {
             // For new users, the behalfid parameter is ignored.
             if ($this->behalfid != $USER->id) {
index 94c0ec8..0e56487 100644 (file)
@@ -25,6 +25,7 @@
 
 namespace tool_policy\output;
 
+use core\session\manager;
 use moodle_exception;
 
 defined('MOODLE_INTERNAL') || die();
@@ -69,23 +70,16 @@ class page_nopermission implements renderable, templatable {
     public function __construct($behalfid) {
         global $USER;
 
-        $this->behalfid = $behalfid;
-        if (!empty($this->behalfid) && $USER->id != $this->behalfid) {
-            $this->behalfuser = core_user::get_user($this->behalfid, '*');
-            // If behalf user doesn't exist, behalfid parameter will be ignored.
-            if ($this->behalfuser === false) {
-                $this->behalfid = 0;
-            }
+        $behalfid = $behalfid ?: $USER->id;
+        $realuser = manager::get_realuser();
+        if ($realuser->id != $behalfid) {
+            $this->behalfuser = core_user::get_user($behalfid, '*', MUST_EXIST);
+            $this->behalfid = $this->behalfuser->id;
         }
 
         if (!empty($USER->id)) {
             // For existing users, it's needed to check if they have the capability for accepting policies.
-            if (empty($this->behalfid) || $this->behalfid == $USER->id) {
-                $this->haspermissionagreedocs = has_capability('tool/policy:accept', context_system::instance());
-            } else {
-                $usercontext = \context_user::instance($this->behalfid);
-                $this->haspermissionagreedocs = has_capability('tool/policy:acceptbehalf', $usercontext);
-            }
+            $this->haspermissionagreedocs = api::can_accept_policies($this->behalfid);
         }
 
         $this->policies = api::list_current_versions(policy_version::AUDIENCE_LOGGEDIN);
@@ -128,7 +122,7 @@ class page_nopermission implements renderable, templatable {
      * Export the page data for the mustache template.
      *
      * @param renderer_base $output renderer to be used to render the page elements.
-     * @return stdClass
+     * @return \stdClass
      */
     public function export_for_template(renderer_base $output) {
         global $CFG;
index d6db0b8..232547a 100644 (file)
@@ -54,8 +54,8 @@ class page_viewalldoc implements renderable, templatable {
      */
     public function __construct() {
 
-        $this->prepare_policies();
         $this->prepare_global_page_access();
+        $this->prepare_policies();
     }
 
     /**
index 04db65e..b1d96c8 100644 (file)
@@ -77,8 +77,7 @@ class user_agreement implements \templatable, \renderable {
         $this->accepted = $accepted;
         $this->canaccept = $canaccept;
         if (count($this->accepted) < count($this->versions) && $canaccept === null) {
-            $this->canaccept = (has_capability('tool/policy:acceptbehalf', \context_system::instance()) ||
-                has_capability('tool/policy:acceptbehalf', \context_user::instance($this->userid)));
+            $this->canaccept = \tool_policy\api::can_accept_policies($this->userid);
         }
     }
 
index 38073ac..c420c5f 100644 (file)
@@ -44,15 +44,9 @@ $PAGE->set_context(context_system::instance());
 $PAGE->set_url('/admin/tool/policy/index.php');
 $PAGE->set_popup_notification_allowed(false);
 
-$haspermissionagreedocs = false;
 if (!empty($USER->id)) {
     // Existing user.
-    if (empty($behalfid) || $behalfid == $USER->id) {
-        $haspermissionagreedocs = has_capability('tool/policy:accept', context_system::instance());
-    } else {
-        $usercontext = \context_user::instance($behalfid);
-        $haspermissionagreedocs = has_capability('tool/policy:acceptbehalf', $usercontext);
-    }
+    $haspermissionagreedocs = api::can_accept_policies($behalfid);
 } else {
     // New user.
     $haspermissionagreedocs = true;
@@ -63,6 +57,9 @@ if (!$haspermissionagreedocs) {
 } else if ($cancel) {
     redirect(new moodle_url('/'));
 } else {
+    if (!$behalfid && \core\session\manager::is_loggedinas()) {
+        $behalfid = $USER->id;
+    }
     $outputpage = new \tool_policy\output\page_agreedocs($agreedocs, $behalfid, $submit);
 }
 
index d218563..5febbbe 100644 (file)
@@ -118,7 +118,7 @@ $string['policyversionacceptedinotherlang'] = 'This policy version has been agre
 $string['previousversions'] = '{$a} previous versions';
 $string['privacy:metadata:acceptances'] = 'Information from policies agreements made by the users of this site.';
 $string['privacy:metadata:acceptances:policyversionid'] = 'The ID of the accepted version policy.';
-$string['privacy:metadata:acceptances:userid'] = 'The ID of the user who has agreed to the policy.';
+$string['privacy:metadata:acceptances:userid'] = 'The ID of the user who agreed to the policy.';
 $string['privacy:metadata:acceptances:status'] = 'The status of the agreement: 0 if not accepted; 1 otherwise.';
 $string['privacy:metadata:acceptances:lang'] = 'The current language displayed when the policy is accepted.';
 $string['privacy:metadata:acceptances:usermodified'] = 'The ID of the user accepting the policy, if made on behalf of another user.';
index cfca937..5bdd926 100644 (file)
@@ -228,3 +228,34 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
     And I navigate to course participants
     And I follow "User Two"
     Then I should see "Policies and agreements"
+
+  Scenario: Agree on behalf of another user as an admin who is logged in as a manager
+    Given I log in as "admin"
+    And I set the following system permissions of "Manager" role:
+      | capability | permission |
+      | tool/policy:acceptbehalf | Allow |
+    And I log out
+    When I log in as "manager"
+    And I press "Next"
+    And I set the field "I agree to the This site policy" to "1"
+    And I press "Next"
+    And I log out
+    And I log in as "admin"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I follow "Manager"
+    And I follow "Log in as"
+    And I press "Continue"
+    And I navigate to "Privacy and policies > Manage policies" in site administration
+    And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
+    And I click on "Not agreed" "link" in the "User One" "table_row"
+    Then I should see "Consent details"
+    And I should see "User One"
+    And I should see "This site policy"
+    And I should see "I acknowledge that consents to these policies have been acquired"
+    And I set the field "Remarks" to "Consent received from a parent"
+    And I press "I agree to the policy"
+    And "Agreed on behalf of" "icon" should exist in the "User One" "table_row"
+    And "Max Manager" "link" should not exist in the "User One" "table_row"
+    And "Admin User" "link" should exist in the "User One" "table_row"
+    And "Consent received from a parent" "text" should exist in the "User One" "table_row"
+    And "Not agreed" "icon" should exist in the "User Two" "table_row"
index 4ceaff9..ec9aae6 100644 (file)
@@ -551,3 +551,63 @@ Feature: User must accept policy managed by this plugin when logging in and sign
     When I press "Next"
     Then I should not see "I understand and agree"
     And I should see "New account"
+
+  Scenario: Accept policy while being logged in as another user
+    Given the following config values are set as admin:
+      | sitepolicyhandler | tool_policy |
+    And the following policies exist:
+      | Name                | Type | Revision | Content    | Summary     | Status   | Audience |
+      | This site policy    | 0    |          | full text2 | short text2 | active   | all      |
+      | This privacy policy | 1    |          | full text3 | short text3 | active   | loggedin |
+    And the following "users" exist:
+      | username | firstname | lastname | email            |
+      | user1    | User      | 1        | user1@example.com    |
+    When I log in as "admin"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I follow "User 1"
+    And I follow "Log in as"
+    Then I should see "You are logged in as User 1"
+    And I press "Continue"
+    And I should see "Please read our This site policy"
+    And I press "Next"
+    And I should see "Please read our This privacy policy"
+    And I press "Next"
+    And I should see "Viewing this page on behalf of User 1"
+    And I set the field "I agree to the This site policy" to "1"
+    And I set the field "I agree to the This privacy policy" to "1"
+    And I press "Next"
+    And I log out
+    And I log in as "user1"
+    And I follow "Profile" in the user menu
+    And I follow "Policies and agreements"
+    And "Admin User" "link" should exist in the "This site policy" "table_row"
+    And "Admin User" "link" should exist in the "This privacy policy" "table_row"
+
+  Scenario: Log in as another user without capability to accept policies on their behalf
+    Given the following config values are set as admin:
+      | sitepolicyhandler | tool_policy |
+    And the following policies exist:
+      | Name                | Type | Revision | Content    | Summary     | Status   | Audience |
+      | This site policy    | 0    |          | full text2 | short text2 | active   | all      |
+      | This privacy policy | 1    |          | full text3 | short text3 | active   | loggedin |
+    And the following "users" exist:
+      | username | firstname | lastname | email            |
+      | user1    | User      | 1        | user1@example.com    |
+      | manager  | Max       | Manager  | man@example.com |
+    And the following "role assigns" exist:
+      | user    | role           | contextlevel | reference |
+      | manager | manager        | System       |           |
+    When I log in as "manager"
+    And I press "Next"
+    And I press "Next"
+    And I set the field "I agree to the This site policy" to "1"
+    And I set the field "I agree to the This privacy policy" to "1"
+    And I press "Next"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I follow "User 1"
+    And I follow "Log in as"
+    Then I should see "You are logged in as User 1"
+    And I press "Continue"
+    And I should see "Policies and agreements"
+    And I should see "No permission to agree to the policies on behalf of this user"
+    And I should see "Sorry, you do not have the required permission to agree to the following policies on behalf of User 1"
index a51aa69..a7b9bf2 100644 (file)
@@ -293,7 +293,7 @@ class auth_email_external extends external_api {
         $data['email2'] = $data['email'];
         // Force policy agreed if a site policy is set. The client is responsible of implementing the interface check.
         $manager = new \core_privacy\local\sitepolicy\manager();
-        if (!$manager->is_defined()) {
+        if ($manager->is_defined()) {
             $data['policyagreed'] = 1;
         }
         unset($data['recaptcharesponse']);
diff --git a/auth/oauth2/classes/privacy/provider.php b/auth/oauth2/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..2ab7ff0
--- /dev/null
@@ -0,0 +1,163 @@
+<?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 for auth_oauth2.
+ *
+ * @package    auth_oauth2
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace auth_oauth2\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\transform;
+use \core_privacy\local\request\writer;
+
+/**
+ * Privacy provider for auth_oauth2
+ *
+ * @package    auth_oauth2
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Get information about the user data stored by this plugin.
+     *
+     * @param  collection $collection An object for storing metadata.
+     * @return collection The metadata.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $authfields = [
+            'timecreated' => 'privacy:metadata:auth_oauth2:timecreated',
+            'timemodified' => 'privacy:metadata:auth_oauth2:timemodified',
+            'usermodified' => 'privacy:metadata:auth_oauth2:usermodified',
+            'userid' => 'privacy:metadata:auth_oauth2:userid',
+            'issuerid' => 'privacy:metadata:auth_oauth2:issuerid',
+            'username' => 'privacy:metadata:auth_oauth2:username',
+            'email' => 'privacy:metadata:auth_oauth2:email',
+            'confirmtoken' => 'privacy:metadata:auth_oauth2:confirmtoken',
+            'confirmtokenexpires' => 'privacy:metadata:auth_oauth2:confirmtokenexpires'
+        ];
+
+        $collection->add_database_table('auth_oauth2_linked_login', $authfields, 'privacy:metadata:auth_oauth2:tableexplanation');
+        $collection->link_subsystem('core_auth', 'privacy:metadata:auth_oauth2:authsubsystem');
+
+        return $collection;
+    }
+
+    /**
+     * Return all contexts for this userid. In this situation the user context.
+     *
+     * @param  int $userid The user ID.
+     * @return contextlist The list of context IDs.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $sql = "SELECT ctx.id
+                  FROM {auth_oauth2_linked_login} ao
+                  JOIN {user} u ON ao.userid = u.id
+                  JOIN {context} ctx ON ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel
+                 WHERE ao.userid = :userid";
+        $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all oauth2 information for the list of contexts and this user.
+     *
+     * @param  approved_contextlist $contextlist The list of approved contexts for a user.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        global $DB;
+
+        // Export oauth2 linked accounts.
+        $context = \context_user::instance($contextlist->get_user()->id);
+        $sql = "SELECT ll.id, ll.username, ll.email, ll.timecreated, ll.timemodified, oi.name as issuername
+                FROM {auth_oauth2_linked_login} ll JOIN {oauth2_issuer} oi ON oi.id = ll.issuerid
+                WHERE ll.userid = :userid";
+        if ($oauth2accounts = $DB->get_records_sql($sql, ['userid' => $contextlist->get_user()->id])) {
+            foreach ($oauth2accounts as $oauth2account) {
+                $data = (object)[
+                    'timecreated' => transform::datetime($oauth2account->timecreated),
+                    'timemodified' => transform::datetime($oauth2account->timemodified),
+                    'issuerid' => $oauth2account->issuername,
+                    'username' => $oauth2account->username,
+                    'email' => $oauth2account->email
+                ];
+                writer::with_context($context)->export_data([
+                        get_string('privacy:metadata:auth_oauth2', 'auth_oauth2'),
+                        $oauth2account->issuername
+                    ], $data);
+            }
+        }
+    }
+
+    /**
+     * Delete all user data for this context.
+     *
+     * @param  \context $context The context to delete data for.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        if (empty($context)) {
+            return;
+        }
+
+        if ($context->contextlevel != CONTEXT_USER) {
+            return;
+        }
+        static::delete_user_data($context->instanceid);
+    }
+
+    /**
+     * Delete all user data for this user only.
+     *
+     * @param  approved_contextlist $contextlist The list of approved contexts for a user.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        if (empty($contextlist->count())) {
+            return;
+        }
+        foreach ($contextlist->get_contexts() as $context) {
+            if ($context->contextlevel != CONTEXT_USER) {
+                return;
+            }
+            // Because we only use user contexts the instance ID is the user ID.
+            static::delete_user_data($context->instanceid);
+        }
+    }
+
+    /**
+     * This does the deletion of user data for the auth_oauth2.
+     *
+     * @param  int $userid The user ID
+     */
+    protected static function delete_user_data(int $userid) {
+        global $DB;
+
+        // Because we only use user contexts the instance ID is the user ID.
+        $DB->delete_records('auth_oauth2_linked_login', ['userid' => $userid]);
+    }
+}
index 75201f2..124ce27 100644 (file)
@@ -88,3 +88,15 @@ $string['notenabled'] = 'Sorry, OAuth 2 authentication plugin is not enabled';
 $string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.';
 $string['pluginname'] = 'OAuth 2';
 $string['alreadylinked'] = 'This external account is already linked to an account on this site';
+$string['privacy:metadata:auth_oauth2'] = 'OAuth2 authentication';
+$string['privacy:metadata:auth_oauth2:authsubsystem'] = 'This plugin is connected to the authentication subsystem.';
+$string['privacy:metadata:auth_oauth2:confirmtoken'] = 'The confirmation token.';
+$string['privacy:metadata:auth_oauth2:confirmtokenexpires'] = 'Indicates the timestamp the confirmation token expires.';
+$string['privacy:metadata:auth_oauth2:email'] = 'The external email that maps to this account.';
+$string['privacy:metadata:auth_oauth2:issuerid'] = 'The identifier of the OAuth2 issuer for this OAuth2 login.';
+$string['privacy:metadata:auth_oauth2:tableexplanation'] = 'OAuth2 accounts linked to a user\'s Moodle account are being stored here.';
+$string['privacy:metadata:auth_oauth2:timecreated'] = 'Indicates the timestamp when the user account was linked to the OAuth2 login.';
+$string['privacy:metadata:auth_oauth2:timemodified'] = 'Indicates the timestamp when this record was modified.';
+$string['privacy:metadata:auth_oauth2:userid'] = 'The user ID of the  user account this OAuth2 login is linked to.';
+$string['privacy:metadata:auth_oauth2:usermodified'] = 'The ID of the user who modified this account.';
+$string['privacy:metadata:auth_oauth2:username'] = 'The external username that maps to this account.';
diff --git a/auth/oauth2/tests/privacy_provider_test.php b/auth/oauth2/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..ed1b785
--- /dev/null
@@ -0,0 +1,172 @@
+<?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 test for the authentication oauth2
+ *
+ * @package    auth_oauth2
+ * @category   test
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \auth_oauth2\privacy\provider;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\writer;
+use \core_privacy\tests\provider_testcase;
+
+/**
+ * Privacy test for the authentication oauth2
+ *
+ * @package    auth_oauth2
+ * @category   test
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class auth_oauth2_privacy_testcase extends provider_testcase {
+    /**
+     * Set up method.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+    }
+
+    /**
+     * Check that a user context is returned if there is any user data for this user.
+     */
+    public function test_get_contexts_for_userid() {
+        $user = $this->getDataGenerator()->create_user();
+        $this->assertEmpty(provider::get_contexts_for_userid($user->id));
+
+        $issuer = \core\oauth2\api::create_standard_issuer('google');
+        $info = [];
+        $info['username'] = 'gina';
+        $info['email'] = 'gina@example.com';
+        \auth_oauth2\api::link_login($info, $issuer, $user->id, false);
+
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        // Check that we only get back one context.
+        $this->assertCount(1, $contextlist);
+
+        // Check that a context is returned is the expected.
+        $usercontext = \context_user::instance($user->id);
+        $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]);
+    }
+
+    /**
+     * Test that user data is exported correctly.
+     */
+    public function test_export_user_data() {
+        $user = $this->getDataGenerator()->create_user();
+        $issuer = \core\oauth2\api::create_standard_issuer('google');
+        $info = [];
+        $info['username'] = 'gina';
+        $info['email'] = 'gina@example.com';
+        \auth_oauth2\api::link_login($info, $issuer, $user->id, false);
+        $usercontext = \context_user::instance($user->id);
+
+        $writer = writer::with_context($usercontext);
+        $this->assertFalse($writer->has_any_data());
+        $approvedlist = new approved_contextlist($user, 'auth_oauth2', [$usercontext->id]);
+        provider::export_user_data($approvedlist);
+        $data = $writer->get_data([get_string('privacy:metadata:auth_oauth2', 'auth_oauth2'), $issuer->get('name')]);
+        $this->assertEquals($info['username'], $data->username);
+        $this->assertEquals($info['email'], $data->email);
+    }
+
+    /**
+     * Test deleting all user data for a specific context.
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $issuer1 = \core\oauth2\api::create_standard_issuer('google');
+        $info = [];
+        $info['username'] = 'gina';
+        $info['email'] = 'gina@example.com';
+        \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false);
+        $user1context = \context_user::instance($user1->id);
+
+        $user2 = $this->getDataGenerator()->create_user();
+        $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft');
+        $info = [];
+        $info['username'] = 'jerry';
+        $info['email'] = 'jerry@example.com';
+        \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false);
+        $user2context = \context_user::instance($user2->id);
+
+        // Get all oauth2 accounts.
+        $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array());
+        // There should be two.
+        $this->assertCount(2, $oauth2accounts);
+
+        // Delete everything for the first user context.
+        provider::delete_data_for_all_users_in_context($user1context);
+
+        // Get all oauth2 accounts match with user1.
+        $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', ['userid' => $user1->id]);
+        $this->assertCount(0, $oauth2accounts);
+
+        // Get all oauth2 accounts.
+        $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array());
+        // There should be one.
+        $this->assertCount(1, $oauth2accounts);
+    }
+
+    /**
+     * This should work identical to the above test.
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $issuer1 = \core\oauth2\api::create_standard_issuer('google');
+        $info = [];
+        $info['username'] = 'gina';
+        $info['email'] = 'gina@example.com';
+        \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false);
+        $user1context = \context_user::instance($user1->id);
+
+        $user2 = $this->getDataGenerator()->create_user();
+        $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft');
+        $info = [];
+        $info['username'] = 'jerry';
+        $info['email'] = 'jerry@example.com';
+        \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false);
+        $user2context = \context_user::instance($user2->id);
+
+        // Get all oauth2 accounts.
+        $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array());
+        // There should be two.
+        $this->assertCount(2, $oauth2accounts);
+
+        // Delete everything for the first user.
+        $approvedlist = new approved_contextlist($user1, 'auth_oauth2', [$user1context->id]);
+        provider::delete_data_for_user($approvedlist);
+
+        // Get all oauth2 accounts match with user1.
+        $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', ['userid' => $user1->id]);
+        $this->assertCount(0, $oauth2accounts);
+
+        // Get all oauth2 accounts.
+        $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array());
+        // There should be one user.
+        $this->assertCount(1, $oauth2accounts);
+    }
+}
diff --git a/badges/classes/privacy/provider.php b/badges/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..29939af
--- /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[] = "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[] = "(type = :typecourse AND courseid $insql)";
+                $params = array_merge($params, ['typecourse' => BADGE_TYPE_COURSE], $inparams);
+            }
+
+            $sqlwhere = '(' . implode(' OR ', $sqls) . ')';
+            $sql = "
+                SELECT *, COALESCE(courseid, 0) AS normalisedcourseid
+                  FROM {badge}
+                 WHERE (usermodified = :userid1 OR usercreated = :userid2)
+                   AND $sqlwhere
+              ORDER BY courseid, 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.
+            $uid = $DB->sql_concat_join("'-'", ['b.id', 'COALESCE(bc.id, 0)', 'COALESCE(bi.id, 0)',
+                'COALESCE(bma.id, 0)', 'COALESCE(bcm.id, 0)']);
+            $sql = "
+                SELECT $uid AS uid, 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;
+    }
+
+}
index 38f388a..1bb9de5 100644 (file)
@@ -24,4 +24,4 @@
 
 $string['activity_modules:addinstance'] = 'Add a new activities block';
 $string['pluginname'] = 'Activities';
-$string['privacy:metadata'] = 'The Activites block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The Activities block only shows data stored in other locations.';
index 23a2b10..97dec82 100644 (file)
@@ -65,4 +65,4 @@ $string['worstgrade'] = 'The lowest grade:';
 $string['worstgrades'] = 'The {$a} lowest grades:';
 $string['worstgroupgrade'] = 'The group with the lowest average:';
 $string['worstgroupgrades'] = 'The {$a} groups with the lowest average:';
-$string['privacy:metadata'] = 'The Activites results block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The Activity results block only shows data stored in other locations.';
index df44819..d1cb144 100644 (file)
@@ -28,4 +28,4 @@ $string['numbadgestodisplay'] = 'Number of latest badges to display';
 $string['nothingtodisplay'] = 'You have no badges to display';
 $string['badges:addinstance'] = 'Add a new My latest badges block';
 $string['badges:myaddinstance'] = 'Add a new My latest badges block to Dashboard';
-$string['privacy:metadata'] = 'The Badges block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The Latest badges block only shows data stored in other locations.';
index f565520..0b951f3 100644 (file)
@@ -26,4 +26,4 @@ $string['calendar_upcoming:addinstance'] = 'Add a new upcoming events block';
 $string['calendar_upcoming:myaddinstance'] = 'Add a new upcoming events block to Dashboard';
 $string['gotocalendar'] = 'Go to calendar...';
 $string['pluginname'] = 'Upcoming events';
-$string['privacy:metadata'] = 'The Upcoming calendar events block only displays existing calendar data.';
+$string['privacy:metadata'] = 'The Upcoming events block only displays existing calendar data.';
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])
+        );
+    }
+}
index 4534d40..902dd5f 100644 (file)
@@ -26,4 +26,4 @@
 $string['coursesummary'] = 'Course summary';
 $string['course_summary:addinstance'] = 'Add a new course/site summary block';
 $string['pluginname'] = 'Course/site summary';
-$string['privacy:metadata'] = 'The Course and site summaryblock only shows information about courses and does not store data itself.';
+$string['privacy:metadata'] = 'The Course/site summary block only shows information about courses and does not store data itself.';
index bf0f6d5..29b53ad 100644 (file)
@@ -24,4 +24,4 @@
 
 $string['login:addinstance'] = 'Add a new login block';
 $string['pluginname'] = 'Login';
-$string['privacy:metadata'] = 'The Login block only provides a way to login and does not store any data itself.';
+$string['privacy:metadata'] = 'The Login block only provides a way to log in and does not store any data itself.';
index 2f8cb9a..b00741d 100644 (file)
@@ -65,6 +65,7 @@ class courses_view implements renderable, templatable {
     public function export_for_template(renderer_base $output) {
         global $CFG;
         require_once($CFG->dirroot.'/course/lib.php');
+        require_once($CFG->dirroot.'/lib/coursecatlib.php');
 
         // Build courses view data structure.
         $coursesview = [
@@ -83,6 +84,29 @@ class courses_view implements renderable, templatable {
             // Convert summary to plain text.
             $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat);
 
+            $course = new \course_in_list($course);
+            foreach ($course->get_course_overviewfiles() as $file) {
+                $isimage = $file->is_valid_image();
+                if ($isimage) {
+                    $url = file_encode_url("$CFG->wwwroot/pluginfile.php",
+                        '/'. $file->get_contextid(). '/'. $file->get_component(). '/'.
+                        $file->get_filearea(). $file->get_filepath(). $file->get_filename(), !$isimage);
+                    $exportedcourse->courseimage = $url;
+                    $exportedcourse->classes = 'courseimage';
+                    break;
+                }
+            }
+
+            $exportedcourse->color = $this->coursecolor($course->id);
+
+            if (!isset($exportedcourse->courseimage)) {
+                $pattern = new \core_geopattern();
+                $pattern->setColor($exportedcourse->color);
+                $pattern->patternbyid($courseid);
+                $exportedcourse->classes = 'coursepattern';
+                $exportedcourse->courseimage = $pattern->datauri();
+            }
+
             // Include course visibility.
             $exportedcourse->visible = (bool)$course->visible;
 
@@ -148,4 +172,19 @@ class courses_view implements renderable, templatable {
 
         return $coursesview;
     }
+
+    /**
+     * Generate a semi-random color based on the courseid number (so it will always return
+     * the same color for a course)
+     *
+     * @param int $courseid
+     * @return string $color, hexvalue color code.
+     */
+    protected function coursecolor($courseid) {
+        // The colour palette is hardcoded for now. It would make sense to combine it with theme settings.
+        $basecolors = ['#81ecec', '#74b9ff', '#a29bfe', '#dfe6e9', '#00b894', '#0984e3', '#b2bec3', '#fdcb6e', '#fd79a8', '#6c5ce7'];
+
+        $color = $basecolors[$courseid % 10];
+        return $color;
+    }
 }
index 9f46f60..d0ee9e8 100644 (file)
@@ -27,20 +27,35 @@ namespace block_myoverview\privacy;
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Privacy Subsystem for block_myoverview implementing null_provider.
+ * Privacy Subsystem for block_myoverview.
  *
  * @copyright  2018 Zig Tan <zig@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider {
 
     /**
-     * Get the language string identifier with the component's language
-     * file to explain why this plugin stores no data.
+     * Returns meta-data information about the myoverview block.
      *
-     * @return  string
+     * @param  \core_privacy\local\metadata\collection $collection A collection of meta-data.
+     * @return \core_privacy\local\metadata\collection Return the collection of meta-data.
      */
-    public static function get_reason() : string {
-        return 'privacy:metadata';
+    public static function get_metadata(\core_privacy\local\metadata\collection $collection) :
+            \core_privacy\local\metadata\collection {
+        $collection->add_user_preference('block_myoverview_last_tab', 'privacy:metadata:overviewlasttab');
+        return $collection;
+    }
+
+    /**
+     * Export all user preferences for the myoverview block
+     *
+     * @param int $userid The userid of the user whose data is to be exported.
+     */
+    public static function export_user_preferences(int $userid) {
+        $preference = get_user_preferences('block_myoverview_last_tab', null, $userid);
+        if (isset($preference)) {
+            \core_privacy\local\request\writer::export_user_preference('block_myoverview', 'block_myoverview_last_tab',
+                    $preference, get_string('privacy:metadata:overviewlasttab', 'block_myoverview'));
+        }
     }
 }
index c762db0..a3ca64e 100644 (file)
@@ -44,4 +44,4 @@ $string['sortbydates'] = 'Sort by dates';
 $string['timeline'] = 'Timeline';
 $string['viewcourse'] = 'View course';
 $string['viewcoursename'] = 'View course {$a}';
-$string['privacy:metadata'] = 'The Course overview block only shows data stored in other locations.';
+$string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
index e9c99f7..bbaa637 100644 (file)
@@ -38,7 +38,7 @@
     }
 }}
 {{< block_myoverview/paging-content-item }}
-    {{$classes}}row{{/classes}}
+    {{$classes}}row card-deck{{/classes}}
     {{$content}}
         {{#courses}}
             {{> block_myoverview/courses-view-course-item }}
index afe5f2f..db2034d 100644 (file)
         "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
     }
 }}
-<div class="col-lg-6">
-    <div class="card mb-3 courses-view-course-item">
-        <div class="card-body course-info-container" id="course-info-container-{{id}}">
+<div class="card mb-3 courses-view-course-item">
+    <a href="{{viewurl}}">
+        <div class="card-img-top myoverviewimg {{classes}}" style='background-image: url("{{{courseimage}}}");'>
+        </div>
+    </a>
+    <div class="card-body course-info-container" id="course-info-container-{{id}}">
 
-            <div class="media">
-                <div class="mr-2">
-                    {{> block_myoverview/progress-chart}}
-                </div>
-                <div class="media-body">
-                    <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4>
-                </div>
+        <div class="media">
+            <div class="mr-2">
+                {{> block_myoverview/progress-chart}}
+            </div>
+            <div class="media-body">
+                <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4>
             </div>
-
-            <p class="text-muted">
-                {{#shortentext}} 140, {{summary}}{{/shortentext}}
-            </p>
         </div>
+
+        <p class="text-muted">
+            {{#shortentext}} 140, {{summary}}{{/shortentext}}
+        </p>
     </div>
 </div>
\ No newline at end of file
diff --git a/blocks/myoverview/tests/privacy_test.php b/blocks/myoverview/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..875dd03
--- /dev/null
@@ -0,0 +1,80 @@
+<?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 the block_myoverview implementation of the privacy API.
+ *
+ * @package    block_myoverview
+ * @category   test
+ * @copyright  2018 Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\request\writer;
+use \block_myoverview\privacy\provider;
+
+/**
+ * Unit tests for the block_myoverview implementation of the privacy API.
+ *
+ * @copyright  2018 Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Ensure that export_user_preferences returns no data if the user has not visited the myoverview block.
+     */
+    public function test_export_user_preferences_no_pref() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $this->assertFalse($writer->has_any_data());
+    }
+
+    /**
+     * Test that the preference courses is exported properly.
+     */
+    public function test_export_user_preferences_course_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_myoverview_last_tab', 'courses', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_myoverview');
+        $this->assertEquals('courses', $blockpreferences->block_myoverview_last_tab->value);
+    }
+
+    /**
+     * Test that the preference timeline is exported properly.
+     */
+    public function test_export_user_preferences_timeline_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_myoverview_last_tab', 'timeline', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_myoverview');
+        $this->assertEquals('timeline', $blockpreferences->block_myoverview_last_tab->value);
+    }
+}
index 24a7767..b7de027 100644 (file)
@@ -45,7 +45,7 @@ $string['myprofile:addinstance'] = 'Add a new logged in user block';
 $string['myprofile:myaddinstance'] = 'Add a new logged in user block to Dashboard';
 $string['myprofile_settings'] = 'Visible user information';
 $string['pluginname'] = 'Logged in user';
-$string['privacy:metadata'] = 'The Logged in users block only shows information about logged in users and does not store data itself.';
+$string['privacy:metadata'] = 'The Logged in user block only shows information about the logged in user and does not store data itself.';
 
 // Deprecated since Moodle 3.2.
 $string['display_un'] = 'Display name';
index 31a0066..f0791fc 100644 (file)
@@ -26,4 +26,4 @@ $string['pluginname'] = 'Private files';
 $string['privatefiles'] = 'Private files';
 $string['private_files:addinstance'] = 'Add a new private files block';
 $string['private_files:myaddinstance'] = 'Add a new private files block to Dashboard';
-$string['privacy:metadata'] = 'The Private files block only provides a view of, and link to, the user private files.';
+$string['privacy:metadata'] = 'The Private files block only provides a view of, and a link to, the user\'s private files.';
index 3b169ae..05ecc00 100644 (file)
@@ -47,4 +47,4 @@ $string['scienceandtech'] = 'Science &amp; Tech';
 $string['sports'] = 'Sports';
 $string['tag_youtube:addinstance'] = 'Add a new YouTube block';
 $string['travel'] = 'Travel &amp; Places';
-$string['privacy:metadata'] = 'The Youtube block only shows data stored in other locations.';
+$string['privacy:metadata'] = 'The YouTube block only shows data stored in other locations.';
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);
+    }
+
+}
index 8471cf8..683d570 100644 (file)
@@ -24,4 +24,4 @@
 
 $string['name'] = 'Gregorian';
 $string['pluginname'] = 'Gregorian calendar type';
-$string['privacy:metadata'] = 'The Gregorian Calendar format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The Gregorian calendar format plugin does not store any personal data.';
diff --git a/cohort/classes/privacy/provider.php b/cohort/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..4e625e2
--- /dev/null
@@ -0,0 +1,194 @@
+<?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    core_cohort
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_cohort\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+
+/**
+ * Privacy class for requesting user data.
+ *
+ * @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\provider,
+        \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Return the fields which contain personal data.
+     *
+     * @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('cohort_members', [
+                'cohortid' => 'privacy:metadata:cohort_members:cohortid',
+                'userid' => 'privacy:metadata:cohort_members:userid',
+                'timeadded' => 'privacy:metadata:cohort_members:timeadded'
+            ], 'privacy:metadata:cohort_members');
+        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 {
+        $sql = "SELECT ctx.id
+                  FROM {context} ctx
+            INNER JOIN {cohort} c ON c.contextid = ctx.id
+            INNER JOIN {cohort_members} cm ON cm.cohortid = c.id
+                 WHERE cm.userid = :userid AND (ctx.contextlevel = :contextlevel1 OR ctx.contextlevel = :contextlevel2)";
+        $params = [
+            'userid'        => $userid,
+            'contextlevel1' => CONTEXT_SYSTEM,
+            'contextlevel2' => CONTEXT_COURSECAT,
+        ];
+        $contextlist = new contextlist();
+        $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;
+
+        // Remove contexts different from SYSTEM or COURSECAT.
+        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+            if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
+                $carry[] = $context->id;
+            }
+            return $carry;
+        }, []);
+
+        if (empty($contexts)) {
+            return;
+        }
+
+        // Get cohort data.
+        $userid = $contextlist->get_user()->id;
+        list($contextsql, $contextparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
+        $sql = "SELECT c.name,
+                       c.idnumber,
+                       c.description,
+                       c.visible,
+                       cm.timeadded,
+                       ctx.id as contextid
+                  FROM {context} ctx
+            INNER JOIN {cohort} c ON c.contextid = ctx.id
+            INNER JOIN {cohort_members} cm ON cm.cohortid = c.id
+                 WHERE ctx.id {$contextsql}
+                       AND cm.userid = :userid";
+        $params = [
+                'userid'        => $userid
+            ] + $contextparams;
+
+        $cohorts = $DB->get_recordset_sql($sql, $params);
+        foreach ($cohorts as $cohort) {
+            $alldata[$cohort->contextid][] = (object)[
+                    'name' => $cohort->name,
+                    'idnumber' => $cohort->idnumber,
+                    'visible' => transform::yesno($cohort->visible),
+                    'timeadded' => transform::datetime($cohort->timeadded),
+                ];
+        }
+        $cohorts->close();
+
+        // Export cohort data.
+        array_walk($alldata, function($data, $contextid) {
+            $context = \context::instance_by_id($contextid);
+            writer::with_context($context)->export_related_data([], 'cohort', $data);
+        });
+
+    }
+
+    /**
+     * Delete all use data which matches the specified context.
+     *
+     * @param context $context A user context.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        if (empty($context)) {
+            return;
+        }
+
+        if (!$context instanceof \context_system && !$context instanceof \context_coursecat) {
+            return;
+        }
+
+        static::delete_data($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) {
+        if (empty($contextlist->count())) {
+            return;
+        }
+
+        $userid = $contextlist->get_user()->id;
+        foreach ($contextlist->get_contexts() as $context) {
+            if (!$context instanceof \context_system && !$context instanceof \context_coursecat) {
+                return;
+            }
+            static::delete_data($context, $userid);
+        }
+    }
+
+    /**
+     * Delete data related to a context and user (if defined).
+     *
+     * @param context $context A context.
+     * @param int $userid The user ID.
+     */
+    protected static function delete_data(\context $context, int $userid = null) {
+        global $DB;
+
+        $cohortids = $DB->get_fieldset_select('cohort', 'id', 'contextid = :contextid', ['contextid' => $context->id]);
+        foreach ($cohortids as $cohortid) {
+            $params = ['cohortid' => $cohortid];
+            if (!empty($userid)) {
+                $params['userid'] = $userid;
+            }
+            $DB->delete_records('cohort_members', $params);
+        }
+    }
+}
diff --git a/cohort/tests/privacy_test.php b/cohort/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..eba3d65
--- /dev/null
@@ -0,0 +1,218 @@
+<?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/>.
+
+/**
+ * Base class for unit tests for core_cohort.
+ *
+ * @package    core_cohort
+ * @category   test
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_cohort\privacy\provider;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\writer;
+use core_privacy\tests\provider_testcase;
+
+/**
+ * Unit tests for cohort\classes\privacy\provider.php
+ *
+ * @copyright  2018 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_cohort_testcase extends provider_testcase {
+
+    /**
+     * Basic setup for these tests.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+            ]);
+        $systemcohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user->id);
+        cohort_add_member($systemcohort->id, $user->id);
+
+        // User is member of 2 cohorts.
+        $contextlist = provider::get_contexts_for_userid($user->id);
+        $this->assertCount(2, (array) $contextlist->get_contextids());
+        $this->assertContains($coursecategoryctx->id, $contextlist->get_contextids());
+        $this->assertContains($systemctx->id, $contextlist->get_contextids());
+    }
+
+    /**
+     * Test that data is exported correctly for this plugin.
+     */
+    public function test_export_user_data() {
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+            ]);
+        $systemcohort1 = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+        $systemcohort2 = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 2'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user->id);
+        cohort_add_member($systemcohort1->id, $user->id);
+        cohort_add_member($systemcohort2->id, $user->id);
+
+        // Validate system cohort exported data.
+        $writer = writer::with_context($systemctx);
+        $this->assertFalse($writer->has_any_data());
+        $this->export_context_data_for_user($user->id, $systemctx, 'core_cohort');
+        $data = $writer->get_related_data([], 'cohort');
+        $this->assertCount(2, $data);
+
+        // Validate category cohort exported data.
+        $writer = writer::with_context($coursecategoryctx);
+        $this->assertFalse($writer->has_any_data());
+        $this->export_context_data_for_user($user->id, $coursecategoryctx, 'core_cohort');
+        $data = $writer->get_related_data([], 'cohort');
+        $this->assertCount(1, $data);
+        $this->assertEquals($categorycohort->name, reset($data)->name);
+    }
+
+    /**
+     * Test for provider::delete_data_for_all_users_in_context().
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+                'idnumber' => '',
+                'description' => ''
+            ]);
+        $systemcohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user->id);
+        cohort_add_member($systemcohort->id, $user->id);
+
+        // Before deletion, we should have 2 entries in the cohort_members table.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(2, $count);
+
+        // Delete data based on system context.
+        provider::delete_data_for_all_users_in_context($systemctx);
+
+        // After deletion, the cohort_members entries should have been deleted.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(1, $count);
+
+        // Delete data based on category context.
+        provider::delete_data_for_all_users_in_context($coursecategoryctx);
+
+        // After deletion, the cohort_members entries should have been deleted.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(0, $count);
+    }
+
+    /**
+     * Test for provider::delete_data_for_user().
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        // Create system cohort and category cohort.
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $coursecategoryctx = \context_coursecat::instance($coursecategory->id);
+        $systemctx = \context_system::instance();
+        $categorycohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $coursecategoryctx->id,
+                'name' => 'Category cohort 1',
+                'idnumber' => '',
+                'description' => ''
+            ]);
+        $systemcohort = $this->getDataGenerator()->create_cohort([
+                'contextid' => $systemctx->id,
+                'name' => 'System cohort 1'
+            ]);
+
+        // Create user and add to the system and category cohorts.
+        $user1 = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user1->id);
+        cohort_add_member($systemcohort->id, $user1->id);
+
+        // Create another user and add to the system and category cohorts.
+        $user2 = $this->getDataGenerator()->create_user();
+        cohort_add_member($categorycohort->id, $user2->id);
+        cohort_add_member($systemcohort->id, $user2->id);
+
+        // Create another user and add to the system cohort.
+        $user3 = $this->getDataGenerator()->create_user();
+        cohort_add_member($systemcohort->id, $user3->id);
+
+        // Before deletion, we should have 5 entries in the cohort_members table.
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(5, $count);
+
+        $contextlist = provider::get_contexts_for_userid($user1->id);
+        $approvedcontextlist = new approved_contextlist($user1, 'cohort', $contextlist->get_contextids());
+        provider::delete_data_for_user($approvedcontextlist);
+
+        // After deletion, the cohort_members entries for the first student should have been deleted.
+        $count = $DB->count_records('cohort_members', ['userid' => $user1->id]);
+        $this->assertEquals(0, $count);
+        $count = $DB->count_records('cohort_members');
+        $this->assertEquals(3, $count);
+
+        // Confirm that the cohorts hasn't been removed.
+        $cohortscount = $DB->get_records('cohort');
+        $this->assertCount(2, (array) $cohortscount);
+    }
+}
index b2d0b1d..2062044 100644 (file)
@@ -2988,6 +2988,8 @@ class course_request {
     public function approve() {
         global $CFG, $DB, $USER;
 
+        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
         $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST);
 
         $courseconfig = get_config('moodlecourse');
@@ -3022,6 +3024,8 @@ class course_request {
             $data->enddate        = usergetmidnight(time()) + $courseconfig->courseduration;
         }
 
+        list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
+
         $course = create_course($data);
         $context = context_course::instance($course->id, MUST_EXIST);
 
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 cf1176b..c7517f9 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Comma separated values (.csv)';
-$string['privacy:metadata'] = 'The CSV Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The CSV data format plugin does not store any personal data.';
 $string['shortname'] = 'CSV';
 
index cd90da3..b09f1f8 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Microsoft Excel (.xlsx)';
-$string['privacy:metadata'] = 'The Excel Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The Excel data format plugin does not store any personal data.';
 $string['shortname'] = 'Excel';
 
index e5efa77..0d62cfb 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'HTML table';
-$string['privacy:metadata'] = 'The HTML Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The HTML table data format plugin does not store any personal data.';
 $string['shortname'] = 'HTML';
 
index 43540ab..a8028a4 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'Javascript Object Notation (.json)';
-$string['privacy:metadata'] = 'The JSON Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The JavaScript Object Notation data format plugin does not store any personal data.';
 $string['shortname'] = 'JSON';
 
index edf5a27..1d2773c 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 $string['dataformat'] = 'OpenDocument (.ods)';
-$string['privacy:metadata'] = 'The OpenDoc Data format plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The OpenDocument data format plugin does not store any personal data.';
 $string['shortname'] = 'OpenDoc';
 
diff --git a/enrol/category/classes/privacy/provider.php b/enrol/category/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..47ced3e
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_category.
+ *
+ * @package    enrol_category
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_category\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_category implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 1c36a0f..5e139c4 100644 (file)
@@ -27,3 +27,4 @@ $string['category:synchronised'] = 'Role assignments synchronised to course enro
 $string['enrolcategorysynctask'] = 'Category enrolment sync task';
 $string['pluginname'] = 'Category enrolments';
 $string['pluginname_desc'] = 'The category enrolments plugin synchronises any role assignments in the category context for roles with the capability enrol/category:synchronised allowed.';
+$string['privacy:metadata'] = 'The Category enrolments plugin does not store any personal data.';
diff --git a/enrol/cohort/classes/privacy/provider.php b/enrol/cohort/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..7f0b932
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_cohort.
+ *
+ * @package    enrol_cohort
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_cohort\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_cohort implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 50fdd26..a1e23cf 100644 (file)
@@ -33,3 +33,4 @@ $string['pluginname'] = 'Cohort sync';
 $string['pluginname_desc'] = 'Cohort enrolment plugin synchronises cohort members with course participants.';
 $string['status'] = 'Active';
 $string['creategroup'] = 'Create new group';
+$string['privacy:metadata'] = 'The Cohort sync enrolments plugin does not store any personal data.';
diff --git a/enrol/database/classes/privacy/provider.php b/enrol/database/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..eb0c3cd
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_database.
+ *
+ * @package    enrol_database
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_database\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_database implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 8fc2e39..be21f84 100644 (file)
@@ -73,3 +73,4 @@ $string['settingsheadernewcourses'] = 'Creation of new courses';
 $string['remoteuserfield_desc'] = 'The name of the field in the remote table that we are using to match entries in the user table.';
 $string['templatecourse'] = 'New course template';
 $string['templatecourse_desc'] = 'Optional: auto-created courses can copy their settings from a template course. Type here the shortname of the template course.';
+$string['privacy:metadata'] = 'The External database enrolments plugin does not store any personal data.';
diff --git a/enrol/flatfile/classes/privacy/provider.php b/enrol/flatfile/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..5e403cd
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_flatfile.
+ *
+ * @package    enrol_flatfile
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_flatfile\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_flatfile implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 8861928..12043ae 100644 (file)
@@ -61,3 +61,4 @@ It could look something like this:
    del, student, 17, CF101
    add, student, 21, CF101, 1091115000, 1091215000
 </pre>';
+$string['privacy:metadata'] = 'The Flat file (CSV) enrolments plugin does not store any personal data.';
diff --git a/enrol/guest/classes/privacy/provider.php b/enrol/guest/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..cf03919
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_guest.
+ *
+ * @package    enrol_guest
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_guest\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_guest implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index f536bb2..fa97e43 100644 (file)
@@ -43,3 +43,4 @@ $string['status_help'] = 'This setting determines whether a user can access the
 $string['status_link'] = 'enrol/guest';
 $string['usepasswordpolicy'] = 'Use password policy';
 $string['usepasswordpolicy_desc'] = 'Use standard password policy for guest access passwords.';
+$string['privacy:metadata'] = 'The Guest access enrolments plugin does not store any personal data.';
diff --git a/enrol/imsenterprise/classes/privacy/provider.php b/enrol/imsenterprise/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..ad24aac
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_imsenterprise.
+ *
+ * @package    enrol_imsenterprise
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_imsenterprise\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_imsenterprise implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 01caa10..7e4a444 100644 (file)
@@ -92,3 +92,4 @@ $string['zeroisnotruncation'] = '0 indicates no truncation';
 $string['roles'] = 'Roles';
 $string['ignore'] = 'Ignore';
 $string['importimsfile'] = 'Import IMS Enterprise file';
+$string['privacy:metadata'] = 'The IMS Enterprise file enrolments plugin does not store any personal data.';
diff --git a/enrol/ldap/classes/privacy/provider.php b/enrol/ldap/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..2354006
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_ldap.
+ *
+ * @package    enrol_ldap
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_ldap\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_ldap implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index e0834e2..7b4db43 100644 (file)
@@ -128,3 +128,4 @@ $string['user_type'] = 'If the group membership contains distinguished names, sp
 $string['user_type_key'] = 'User type';
 $string['version'] = 'The version of the LDAP protocol your server is using';
 $string['version_key'] = 'Version';
+$string['privacy:metadata'] = 'The LDAP enrolments plugin does not store any personal data.';
index dcb5f29..da4cd68 100644 (file)
@@ -129,6 +129,7 @@ class enrol_manual_enrol_users_form extends moodleform {
 
         $mform->addElement('checkbox', 'recovergrades', get_string('recovergrades', 'enrol'));
         $mform->setAdvanced('recovergrades');
+        $mform->setDefault('recovergrades', $CFG->recovergradesdefault);
         $mform->addElement('select', 'duration', get_string('defaultperiod', 'enrol_manual'), $periodmenu);
         $mform->setDefault('duration', $defaultperiod);
         $mform->setAdvanced('duration');
diff --git a/enrol/manual/classes/privacy/provider.php b/enrol/manual/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..20d6f56
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_manual.
+ *
+ * @package    enrol_manual
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_manual\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_manual implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 1d68899..a28d07d 100644 (file)
@@ -30,7 +30,7 @@ $string['assignrole'] = 'Assign role';
 $string['assignroles'] = 'Assign roles';
 $string['browseusers'] = 'Browse users';
 $string['browsecohorts'] = 'Browse cohorts';
-$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
+$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these user enrolments?';
 $string['defaultstart'] = 'Default enrolment start';
 $string['defaultperiod'] = 'Default enrolment duration';
 $string['defaultperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.';
@@ -82,3 +82,4 @@ $string['wscannotenrol'] = 'Plugin instance cannot manually enrol a user in the
 $string['wsnoinstance'] = 'Manual enrolment plugin instance doesn\'t exist or is disabled for the course (id = {$a->courseid})';
 $string['wsusercannotassign'] = 'You don\'t have the permission to assign this role ({$a->roleid}) to this user ({$a->userid}) in this course({$a->courseid}).';
 $string['manualpluginnotinstalled'] = 'The "Manual" plugin has not yet been installed';
+$string['privacy:metadata'] = 'The Manual enrolments plugin does not store any personal data.';
diff --git a/enrol/meta/classes/privacy/provider.php b/enrol/meta/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..04db729
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_meta.
+ *
+ * @package    enrol_meta
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_meta\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_meta implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index e2b43d3..f2a90d0 100644 (file)
@@ -38,3 +38,4 @@ $string['pluginname'] = 'Course meta link';
 $string['pluginname_desc'] = 'Course meta link enrolment plugin synchronises enrolments and roles in two different courses.';
 $string['syncall'] = 'Synchronise all enrolled users';
 $string['syncall_desc'] = 'If enabled all enrolled users are synchronised even if they have no role in parent course, if disabled only users that have at least one synchronised role are enrolled in child course.';
+$string['privacy:metadata'] = 'The Course meta link enrolments plugin does not store any personal data.';
diff --git a/enrol/mnet/classes/privacy/provider.php b/enrol/mnet/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..7af9084
--- /dev/null
@@ -0,0 +1,41 @@
+<?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 enrol_mnet.
+ *
+ * @package    enrol_mnet
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace enrol_mnet\privacy;
+defined('MOODLE_INTERNAL') || die();
+/**
+ * Privacy Subsystem for enrol_mnet implementing null_provider.
+ *
+ * @copyright  2018 Carlos Escobedo <carlos@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 2fb3ed0..4741841 100644 (file)
@@ -35,3 +35,4 @@ $string['remotesubscriber_help'] = 'Select \'All hosts\' to open this course for
 $string['remotesubscribersall'] = 'All hosts';
 $string['roleforremoteusers'] = 'Role for their users';
 $string['roleforremoteusers_help'] = 'What role will the remote users from the selected host get.';
+$string['privacy:metadata'] = 'The MNet remote enrolments plugin does not store any personal data.';
index 500e693..6aa0de2 100644 (file)
@@ -39,7 +39,7 @@ $string['enrolperiod_desc'] = 'Default length of time that the enrolment is vali
 $string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user is enrolled. If disabled, the enrolment duration will be unlimited.';
 $string['enrolstartdate'] = 'Start date';
 $string['enrolstartdate_help'] = 'If enabled, users can be enrolled from this date onward only.';
-$string['errdisabled'] = 'PayPal plugin is disabled and does not handle payment notifications.';
+$string['errdisabled'] = 'The PayPal enrolment plugin is disabled and does not handle payment notifications.';
 $string['erripninvalid&#