--- /dev/null
+<?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_calendar
+ * @copyright 2018 Zig Tan <zig@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_calendar\privacy;
+defined('MOODLE_INTERNAL') || die();
+
+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;
+
+/**
+ * Privacy Subsystem for core_calendar implementing metadata, plugin, and user_preference providers.
+ *
+ * @package core_calendar
+ * @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,
+ \core_privacy\local\request\user_preference_provider
+{
+
+ /**
+ * Provides meta data that is stored about a user with core_calendar.
+ *
+ * @param collection $collection A collection of meta data items to be added to.
+ * @return collection Returns the collection of metadata.
+ */
+ public static function get_metadata(collection $collection) : collection {
+ // The calendar 'event' table contains user data.
+ $collection->add_database_table(
+ 'event',
+ [
+ 'name' => 'privacy:metadata:calendar:event:name',
+ 'description' => 'privacy:metadata:calendar:event:description',
+ 'eventtype' => 'privacy:metadata:calendar:event:eventtype',
+ 'timestart' => 'privacy:metadata:calendar:event:timestart',
+ 'timeduration' => 'privacy:metadata:calendar:event:timeduration',
+ ],
+ 'privacy:metadata:calendar:event'
+ );
+
+ // The calendar 'event_subscriptions' table contains user data.
+ $collection->add_database_table(
+ 'event_subscriptions',
+ [
+ 'name' => 'privacy:metadata:calendar:event_subscriptions:name',
+ 'url' => 'privacy:metadata:calendar:event_subscriptions:url',
+ 'eventtype' => 'privacy:metadata:calendar:event_subscriptions:eventtype',
+ ],
+ 'privacy:metadata:calendar:event_subscriptions'
+ );
+
+ // The calendar user preference setting 'calendar_savedflt'.
+ $collection->add_user_preference(
+ 'calendar_savedflt',
+ 'privacy:metadata:calendar:preferences:calendar_savedflt'
+ );
+
+ return $collection;
+ }
+
+ /**
+ * Get the list of contexts that contain calendar 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();
+
+ // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
+ $params = [
+ 'sitecontext' => CONTEXT_SYSTEM,
+ 'categorycontext' => CONTEXT_COURSECAT,
+ 'coursecontext' => CONTEXT_COURSE,
+ 'groupcontext' => CONTEXT_COURSE,
+ 'usercontext' => CONTEXT_USER,
+ 'cuserid' => $userid,
+ 'modulecontext' => CONTEXT_MODULE,
+ 'muserid' => $userid
+ ];
+
+ // Get contexts of Calendar Events for the owner.
+ $sql = "SELECT ctx.id
+ FROM {context} ctx
+ JOIN {event} e ON
+ (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
+ (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
+ (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
+ (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
+ (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
+ WHERE e.userid = :cuserid
+ UNION
+ SELECT ctx.id
+ FROM {context} ctx
+ JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
+ JOIN {modules} m ON m.id = cm.module
+ JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
+ WHERE e.userid = :muserid";
+ $contextlist->add_from_sql($sql, $params);
+
+ // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
+ $params = [
+ 'sitecontext' => CONTEXT_SYSTEM,
+ 'categorycontext' => CONTEXT_COURSECAT,
+ 'coursecontext' => CONTEXT_COURSE,
+ 'groupcontext' => CONTEXT_COURSE,
+ 'usercontext' => CONTEXT_USER,
+ 'userid' => $userid
+ ];
+
+ // Get contexts for Calendar Subscriptions for the owner.
+ $sql = "SELECT ctx.id
+ FROM {context} ctx
+ JOIN {event_subscriptions} s ON
+ (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
+ (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
+ (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
+ (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
+ (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
+ WHERE s.userid = :userid";
+ $contextlist->add_from_sql($sql, $params);
+
+ // Return combined contextlist for Calendar Events & Calendar Subscriptions.
+ 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 (empty($contextlist)) {
+ return;
+ }
+
+ self::export_user_calendar_event_data($contextlist);
+ self::export_user_calendar_subscription_data($contextlist);
+ }
+
+ /**
+ * 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) {
+ $calendarsavedflt = get_user_preferences('calendar_savedflt', null, $userid);
+
+ if (null !== $calendarsavedflt) {
+ writer::export_user_preference(
+ 'core_calendar',
+ 'calendarsavedflt',
+ $calendarsavedflt,
+ get_string('privacy:metadata:calendar:preferences:calendar_savedflt', 'core_calendar')
+ );
+ }
+ }
+
+ /**
+ * Delete all Calendar Event and Calendar Subscription data for all users in the specified context.
+ *
+ * @param context $context Transform the specific context to delete data for.
+ */
+ public static function delete_data_for_all_users_in_context(\context $context) {
+ if (empty($context)) {
+ return;
+ }
+
+ // Delete all Calendar Events in the specified context in batches.
+ $eventids = array_keys(self::get_calendar_event_ids_by_context($context));
+ self::delete_batch_records('event', 'id', $eventids);
+
+ // Delete all Calendar Subscriptions in the specified context in batches.
+ $subscriptionids = array_keys(self::get_calendar_subscription_ids_by_context($context));
+ self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
+ }
+
+ /**
+ * 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)) {
+ return;
+ }
+
+ // Delete all Calendar Events for the owner and specified contexts in batches.
+ $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);
+ $eventids = [];
+ foreach ($eventdetails as $eventdetail) {
+ $eventids[] = $eventdetail->eventid;
+ }
+ $eventdetails->close();
+ self::delete_batch_records('event', 'id', $eventids);
+
+ // Delete all Calendar Subscriptions for the owner and specified contexts in batches.
+ $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
+ $subscriptionids = [];
+ foreach ($subscriptiondetails as $subscriptiondetail) {
+ $subscriptionids[] = $subscriptiondetail->subscriptionid;
+ }
+ $subscriptiondetails->close();
+ self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
+ }
+
+ /**
+ * Helper function to export Calendar Events data by a User's contextlist.
+ *
+ * @param approved_contextlist $contextlist
+ * @throws \coding_exception
+ */
+ protected static function export_user_calendar_event_data(approved_contextlist $contextlist) {
+ // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
+ $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);
+
+ // Multiple Calendar Events of the same eventtype and time can exist for a context, so collate them for export.
+ $eventrecords = [];
+ foreach ($eventdetails as $eventdetail) {
+ // Create an array key based on the contextid, eventtype, and time.
+ $key = $eventdetail->contextid . $eventdetail->eventtype . $eventdetail->timestart;
+
+ if (array_key_exists($key, $eventrecords) === false) {
+ $eventrecords[$key] = [ $eventdetail ];
+ } else {
+ $eventrecords[$key] = array_merge($eventrecords[$key], [$eventdetail]);
+ }
+ }
+ $eventdetails->close();
+
+ // Export Calendar Event data.
+ foreach ($eventrecords as $eventrecord) {
+ $index = (count($eventrecord) > 1) ? 1 : 0;
+
+ foreach ($eventrecord as $event) {
+ // Export the events using the structure Calendar/Events/{datetime}/{eventtype}-event.json.
+ $subcontexts = [
+ get_string('calendar', 'calendar'),
+ get_string('events', 'calendar'),
+ date('c', $event->timestart)
+ ];
+ $name = $event->eventtype . '-event';
+
+ // Use name {eventtype}-event-{index}.json if multiple eventtypes and time exists at the same context.
+ if ($index != 0) {
+ $name .= '-' . $index;
+ $index++;
+ }
+
+ $eventdetails = (object) [
+ 'name' => $event->name,
+ 'description' => $event->description,
+ 'eventtype' => $event->eventtype,
+ 'timestart' => transform::datetime($event->timestart),
+ 'timeduration' => $event->timeduration
+ ];
+
+ $context = \context::instance_by_id($event->contextid);
+ writer::with_context($context)->export_related_data($subcontexts, $name, $eventdetails);
+ }
+ }
+ }
+
+ /**
+ * Helper function to export Calendar Subscriptions data by a User's contextlist.
+ *
+ * @param approved_contextlist $contextlist
+ * @throws \coding_exception
+ */
+ protected static function export_user_calendar_subscription_data(approved_contextlist $contextlist) {
+ // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
+ $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
+
+ // Multiple Calendar Subscriptions of the same eventtype can exist for a context, so collate them for export.
+ $subscriptionrecords = [];
+ foreach ($subscriptiondetails as $subscriptiondetail) {
+ // Create an array key based on the contextid and eventtype.
+ $key = $subscriptiondetail->contextid . $subscriptiondetail->eventtype;
+
+ if (array_key_exists($key, $subscriptionrecords) === false) {
+ $subscriptionrecords[$key] = [ $subscriptiondetail ];
+ } else {
+ $subscriptionrecords[$key] = array_merge($subscriptionrecords[$key], [$subscriptiondetail]);
+ }
+ }
+ $subscriptiondetails->close();
+
+ // Export Calendar Subscription data.
+ foreach ($subscriptionrecords as $subscriptionrecord) {
+ $index = (count($subscriptionrecord) > 1) ? 1 : 0;
+
+ foreach ($subscriptionrecord as $subscription) {
+ // Export the events using the structure Calendar/Subscriptions/{eventtype}-subscription.json.
+ $subcontexts = [
+ get_string('calendar', 'calendar'),
+ get_string('subscriptions', 'calendar')
+ ];
+ $name = $subscription->eventtype . '-subscription';
+
+ // Use name {eventtype}-subscription-{index}.json if multiple eventtypes exists at the same context.
+ if ($index != 0) {
+ $name .= '-' . $index;
+ $index++;
+ }
+
+ $context = \context::instance_by_id($subscription->contextid);
+ writer::with_context($context)->export_related_data($subcontexts, $name, $subscription);
+ }
+ }
+ }
+
+ /**
+ * Helper function to return all Calendar Event id results for a specified context.
+ *
+ * @param \context $context
+ * @return array|null
+ * @throws \dml_exception
+ */
+ protected static function get_calendar_event_ids_by_context(\context $context) {
+ global $DB;
+
+ // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
+ $events = null;
+
+ if ($context->contextlevel == CONTEXT_MODULE) { // Course Module Contexts.
+ $params = [
+ 'modulecontext' => $context->contextlevel,
+ 'contextid' => $context->id
+ ];
+
+ // Get Calendar Events for the specified Course Module context.
+ $sql = "SELECT DISTINCT
+ e.id AS eventid
+ FROM {context} ctx
+ INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
+ INNER JOIN {modules} m ON m.id = cm.module
+ INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
+ WHERE ctx.id = :contextid";
+ $events = $DB->get_records_sql($sql, $params);
+ } else { // Other Moodle Contexts.
+ $params = [
+ 'sitecontext' => CONTEXT_SYSTEM,
+ 'categorycontext' => CONTEXT_COURSECAT,
+ 'coursecontext' => CONTEXT_COURSE,
+ 'groupcontext' => CONTEXT_COURSE,
+ 'usercontext' => CONTEXT_USER,
+ 'contextid' => $context->id
+ ];
+
+ // Get Calendar Events for the specified Moodle context.
+ $sql = "SELECT DISTINCT
+ e.id AS eventid
+ FROM {context} ctx
+ INNER JOIN {event} e ON
+ (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
+ (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
+ (e.courseid = ctx.instanceid AND (e.eventtype = 'course' OR e.eventtype = 'group' OR e.modulename != '0') AND ctx.contextlevel = :coursecontext) OR
+ (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
+ WHERE ctx.id = :contextid";
+ $events = $DB->get_records_sql($sql, $params);
+ }
+
+ return $events;
+ }
+
+ /**
+ * Helper function to return all Calendar Subscription id results for a specified context.
+ *
+ * @param \context $context
+ * @return array
+ * @throws \dml_exception
+ */
+ protected static function get_calendar_subscription_ids_by_context(\context $context) {
+ global $DB;
+
+ // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
+ $params = [
+ 'sitecontext' => CONTEXT_SYSTEM,
+ 'categorycontext' => CONTEXT_COURSECAT,
+ 'coursecontext' => CONTEXT_COURSE,
+ 'groupcontext' => CONTEXT_COURSE,
+ 'usercontext' => CONTEXT_USER,
+ 'contextid' => $context->id
+ ];
+
+ // Get Calendar Subscriptions for the specified context.
+ $sql = "SELECT DISTINCT
+ s.id AS subscriptionid
+ FROM {context} ctx
+ INNER JOIN {event_subscriptions} s ON
+ (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
+ (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
+ (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
+ (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
+ (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
+ WHERE ctx.id = :contextid";
+
+ return $DB->get_records_sql($sql, $params);
+ }
+
+ /**
+ * Helper function to return the Calendar Events for a given user and context list.
+ *
+ * @param approved_contextlist $contextlist
+ * @return array
+ * @throws \coding_exception
+ * @throws \dml_exception
+ */
+ protected static function get_calendar_event_details_by_contextlist(approved_contextlist $contextlist) {
+ global $DB;
+
+ $userid = $contextlist->get_user()->id;
+
+ list($contextsql1, $contextparams1) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
+ list($contextsql2, $contextparams2) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
+
+ // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
+ $params = [
+ 'sitecontext' => CONTEXT_SYSTEM,
+ 'categorycontext' => CONTEXT_COURSECAT,
+ 'coursecontext' => CONTEXT_COURSE,
+ 'groupcontext' => CONTEXT_COURSE,
+ 'usercontext' => CONTEXT_USER,
+ 'cuserid' => $userid,
+ 'modulecontext' => CONTEXT_MODULE,
+ 'muserid' => $userid
+ ];
+ $params += $contextparams1;
+ $params += $contextparams2;
+
+ // Get Calendar Events for the approved contexts and the owner.
+ $sql = "SELECT ctx.id AS contextid,
+ e.id AS eventid,
+ e.name AS name,
+ e.description AS description,
+ e.eventtype AS eventtype,
+ e.timestart AS timestart,
+ e.timeduration AS timeduration
+ FROM {context} ctx
+ INNER JOIN {event} e ON
+ (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
+ (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
+ (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
+ (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
+ (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
+ WHERE e.userid = :cuserid
+ AND ctx.id {$contextsql1}
+ UNION
+ SELECT ctx.id AS contextid,
+ e.id AS eventid,
+ e.name AS name,
+ e.description AS description,
+ e.eventtype AS eventtype,
+ e.timestart AS timestart,
+ e.timeduration AS timeduration
+ FROM {context} ctx
+ INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
+ INNER JOIN {modules} m ON m.id = cm.module
+ INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
+ WHERE e.userid = :muserid
+ AND ctx.id {$contextsql2}";
+
+ return $DB->get_recordset_sql($sql, $params);
+ }
+
+ /**
+ * Helper function to return the Calendar Subscriptions for a given user and context list.
+ *
+ * @param approved_contextlist $contextlist
+ * @return array
+ * @throws \coding_exception
+ * @throws \dml_exception
+ */
+ protected static function get_calendar_subscription_details_by_contextlist(approved_contextlist $contextlist) {
+ global $DB;
+
+ $user = $contextlist->get_user();
+
+ list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
+
+ $params = [
+ 'sitecontext' => CONTEXT_SYSTEM,
+ 'categorycontext' => CONTEXT_COURSECAT,
+ 'coursecontext' => CONTEXT_COURSE,
+ 'groupcontext' => CONTEXT_COURSE,
+ 'usercontext' => CONTEXT_USER,
+ 'userid' => $user->id
+ ];
+ $params += $contextparams;
+
+ // Get Calendar Subscriptions for the approved contexts and the owner.
+ $sql = "SELECT DISTINCT
+ c.id as contextid,
+ s.id as subscriptionid,
+ s.name as name,
+ s.url as url,
+ s.eventtype as eventtype
+ FROM {context} c
+ INNER JOIN {event_subscriptions} s ON
+ (s.eventtype = 'site' AND c.contextlevel = :sitecontext) OR
+ (s.categoryid = c.instanceid AND s.eventtype = 'category' AND c.contextlevel = :categorycontext) OR
+ (s.courseid = c.instanceid AND s.eventtype = 'course' AND c.contextlevel = :coursecontext) OR
+ (s.courseid = c.instanceid AND s.eventtype = 'group' AND c.contextlevel = :groupcontext) OR
+ (s.userid = c.instanceid AND s.eventtype = 'user' AND c.contextlevel = :usercontext)
+ WHERE s.userid = :userid
+ AND c.id {$contextsql}";
+
+ return $DB->get_recordset_sql($sql, $params);
+ }
+
+ /**
+ * Helper function to delete records in batches in order to minimise amount of deletion queries.
+ *
+ * @param string $tablename The table name to delete from.
+ * @param string $field The table column field name to delete records by.
+ * @param array $values The table column field values to delete records by.
+ * @throws \dml_exception
+ */
+ protected static function delete_batch_records($tablename, $field, $values) {
+ global $DB;
+
+ // Batch deletion with an upper limit of 2000 records to minimise the number of deletion queries.
+ $batchrecords = array_chunk($values, 2000);
+
+ foreach ($batchrecords as $batchrecord) {
+ $DB->delete_records_list($tablename, $field, $batchrecord);
+ }
+ }
+
+}
--- /dev/null
+<?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 core_calendar.
+ *
+ * @package core_calendar
+ * @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;
+
+require_once($CFG->dirroot . '/calendar/lib.php');
+require_once($CFG->dirroot . '/calendar/tests/externallib_test.php');
+
+use \core_calendar\privacy\provider;
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\writer;
+use \core_privacy\tests\provider_testcase;
+
+/**
+ * Unit tests for calendar/classes/privacy/provider
+ *
+ * @copyright 2018 Zig Tan <zig@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_calendar_privacy_testcase extends provider_testcase {
+
+ /**
+ * Overriding setUp() function to always reset after tests.
+ */
+ public function setUp() {
+ $this->resetAfterTest(true);
+ }
+
+ /**
+ * Test for provider::get_contexts_for_userid().
+ *
+ * @throws coding_exception
+ */
+ public function test_get_contexts_for_userid() {
+ // Create test user to create Calendar Events and Subscriptions.
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ // Create a Category and Courses to assign Calendar Events and Subscriptions.
+ $category = $this->getDataGenerator()->create_category();
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+ $course3 = $this->getDataGenerator()->create_course();
+ $grouprecord = (object)[
+ 'courseid' => $course3->id,
+ 'name' => 'test_group'
+ ];
+ $course3group = $this->getDataGenerator()->create_group($grouprecord);
+
+ // Get contexts.
+ $usercontext = context_user::instance($user->id);
+ $categorycontext = context_coursecat::instance($category->id);
+ $course1context = context_course::instance($course1->id);
+ $course2context = context_course::instance($course2->id);
+ $course3context = context_course::instance($course3->id);
+
+ // Add Category Calendar Events for Category.
+ $this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id);
+ $this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id);
+
+ // Add User Calendar Events for User.
+ $this->create_test_standard_calendar_event('user', $user->id, time(), '');
+ $this->create_test_standard_calendar_event('user', $user->id, time(), '', 0, $course1->id);
+ $this->create_test_standard_calendar_event('user', $user->id, time(), '', 0, $course2->id);
+
+ // Add a Course Calendar Event for Course 1.
+ $this->create_test_standard_calendar_event('course', $user->id, time(), '', 0, $course1->id);
+
+ // Add a Course Assignment Action Calendar Event for Course 2.
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $params['course'] = $course2->id;
+ $params['assignsubmission_onlinetext_enabled'] = 1;
+ $instance = $generator->create_instance($params);
+ $cm = get_coursemodule_from_instance('assign', $instance->id);
+ $modulecontext = context_module::instance($cm->id);
+ $assign = new assign($modulecontext, $cm, $course2);
+ $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user->id, time());
+ $this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user->id, time());
+
+ // Add a Calendar Subscription and Group Calendar Event to Course 3.
+ $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user->id, 0, $course3->id);
+ $this->create_test_standard_calendar_event('group', $user->id, time(), '', 0, $course1->id, $course3group->id);
+
+ // The user will be in these contexts.
+ $usercontextids = [
+ $usercontext->id,
+ $categorycontext->id,
+ $course1context->id,
+ $modulecontext->id,
+ $course3context->id
+ ];
+ // Retrieve the user's context ids.
+ $contextids = provider::get_contexts_for_userid($user->id);
+
+ // Check the user context list and retrieved user context lists contains the same number of records.
+ $this->assertEquals(count($usercontextids), count($contextids->get_contextids()));
+ // There should be no difference between the contexts.
+ $this->assertEmpty(array_diff($usercontextids, $contextids->get_contextids()));
+ }
+
+ /**
+ * Test for provider::export_user_data().
+ *
+ * @throws coding_exception
+ */
+ public function test_export_user_data() {
+ global $DB;
+
+ // Create test user to create Calendar Events and Subscriptions with.
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ // Create a Category to test creating a Category Calendar Event.
+ $category = $this->getDataGenerator()->create_category();
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+ $course3 = $this->getDataGenerator()->create_course();
+ $grouprecord = (object)[
+ 'courseid' => $course3->id,
+ 'name' => 'test_group'
+ ];
+ $course3group = $this->getDataGenerator()->create_group($grouprecord);
+
+ // Add User Calendar Events for User.
+ $event1 = $this->create_test_standard_calendar_event('user', $user->id, time(), '');
+
+ // Add Category Calendar Events for Category.
+ $event2 = $this->create_test_standard_calendar_event('category', $user->id, time(), '', $category->id);
+
+ // Add two Course Calendar Event for Course 1 and set the same time (1 day a head).
+ $time = strtotime('+1 day', time());
+ $event3 = $this->create_test_standard_calendar_event('course', $user->id, $time, 'ABC', 0, $course1->id);
+ $event4 = $this->create_test_standard_calendar_event('course', $user->id, $time, 'DEF', 0, $course1->id);
+
+ // Add a Course Assignment Action Calendar Event for Course 2.
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $params['course'] = $course2->id;
+ $params['assignsubmission_onlinetext_enabled'] = 1;
+ $instance = $generator->create_instance($params);
+ $cm = get_coursemodule_from_instance('assign', $instance->id);
+ $modulecontext = context_module::instance($cm->id);
+ $assign = new assign($modulecontext, $cm, $course2);
+ $event5 = $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user->id, time());
+
+ // Add a Calendar Subscription and Group Calendar Event to Course 3.
+ $subscription1 = $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user->id, 0, $course3->id);
+ $event6 = $this->create_test_standard_calendar_event('group', $user->id, time(), '', 0, $course3->id, $course3group->id);
+
+ // Retrieve the user's context ids.
+ $contextlist = provider::get_contexts_for_userid($user->id);
+ $approvedcontextlist = new approved_contextlist($user, 'core_calendar', $contextlist->get_contextids());
+
+ // Retrieve Calendar Event and Subscriptions data only for this user.
+ provider::export_user_data($approvedcontextlist);
+
+ foreach ($contextlist as $context) {
+ $writer = writer::with_context($context);
+ $this->assertTrue($writer->has_any_data());
+
+ // Test event 1 that was created for the test User.
+ if ($context->instanceid == $user->id && $context->contextlevel == CONTEXT_USER) {
+ // Test the content contains Calendar Event user data.
+ $subcontexts = [
+ get_string('calendar', 'calendar'),
+ get_string('events', 'calendar'),
+ date('c', $event1->timestart)
+ ];
+ $name = "user-event";
+ $data = $writer->get_related_data($subcontexts, $name);
+ $this->assertEquals('Standard Calendar Event user', $data->name);
+ }
+
+ // Test event 2 that was created for the test Category.
+ if ($context->instanceid == $category->id && $context->contextlevel == CONTEXT_COURSECAT) {
+ // Test the content contains Calendar Event category data.
+ $subcontexts = [
+ get_string('calendar', 'calendar'),
+ get_string('events', 'calendar'),
+ date('c', $event2->timestart)
+ ];
+ $name = "category-event";
+ $data = $writer->get_related_data($subcontexts, $name);
+ $this->assertEquals('Standard Calendar Event category', $data->name);
+ }
+
+ // Test events 3, 4, and 5 that were created for the test Course 1.
+ if ($context->instanceid == $course1->id && $context->contextlevel == CONTEXT_COURSE) {
+ // Test the content contains Calendar Event course data set with the same time, and the exported files are uniquely identified.
+ $subcontext1 = [
+ get_string('calendar', 'calendar'),
+ get_string('events', 'calendar'),
+ date('c', $event3->timestart)
+ ];
+ $name1 = "course-event-1";
+ $data1 = $writer->get_related_data($subcontext1, $name1);
+ $this->assertEquals('Standard Calendar Event course -- ABC', $data1->name);
+
+ $subcontext2 = [
+ get_string('calendar', 'calendar'),
+ get_string('events', 'calendar'),
+ date('c', $event4->timestart)
+ ];
+ $name2 = "course-event-2";
+ $data2 = $writer->get_related_data($subcontext2, $name2);
+ $this->assertEquals('Standard Calendar Event course -- DEF', $data2->name);
+ }
+
+ // Test action event that were created for the test Course 2.
+ if ($context->instanceid == $cm->id && $context->contextlevel == CONTEXT_MODULE) {
+ // Test the content contains Calendar Action Event course data.
+ $subcontexts = [
+ get_string('calendar', 'calendar'),
+ get_string('events', 'calendar'),
+ date('c', $event5->timestart)
+ ];
+ $name = "duedate-event";
+ $data = $writer->get_related_data($subcontexts, $name);
+ $this->assertEquals('Action Calendar Event duedate -- assign', $data->name);
+ }
+
+ // Test Calendar Subscription and Event that were created for the test Course 3.
+ if ($context->instanceid == $course3->id && $context->contextlevel == CONTEXT_COURSE) {
+ // Test the content contains Calendar Subscription data also created for the test Course 3.
+ $subcontexts = [
+ get_string('calendar', 'calendar'),
+ get_string('subscriptions', 'calendar')
+ ];
+ $name = "course-subscription";
+ $data = $writer->get_related_data($subcontexts, $name);
+ $this->assertEquals('Calendar Subscription course', $data->name);
+
+ // Test the content contains Calendar Event group data also created for the test Course 3.
+ $subcontexts = [
+ get_string('calendar', 'calendar'),
+ get_string('events', 'calendar'),
+ date('c', $event6->timestart)
+ ];
+ $name = "group-event";
+ $data = $writer->get_related_data($subcontexts, $name);
+ $this->assertEquals('Standard Calendar Event group', $data->name);
+ }
+ }
+
+ }
+
+ /**
+ * Test for provider::test_export_user_preferences().
+ */
+ public function test_export_user_preferences() {
+ global $DB;
+
+ // Test setup.
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ // Add a user home page preference for the User.
+ set_user_preference('calendar_savedflt', 'true', $user);
+
+ // Test the user preference exists.
+ $params = [
+ 'userid' => $user->id,
+ 'name' => 'calendar_savedflt'
+ ];
+
+ // 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('core_calendar');
+ $this->assertCount(1, (array) $exportedpreferences);
+ $this->assertEquals('true', $exportedpreferences->calendarsavedflt->value);
+ }
+
+ /**
+ * Test for provider::delete_data_for_all_users_in_context().
+ *
+ * @throws dml_exception
+ */
+ public function test_delete_data_for_all_users_in_context() {
+ global $DB;
+
+ // Create test user to create Calendar Events and Subscriptions with.
+ $user1 = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+
+ // Create a Course to test creating a Category Calendar Event.
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+
+ // Get contexts.
+ $course1context = context_course::instance($course1->id);
+ $course2context = context_course::instance($course2->id);
+
+ // Add a Course Calendar Event by User 1 for Course 1 and Course 2.
+ $this->setUser($user1);
+ $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id);
+ $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course2->id);
+
+ // Add a Calendar Subscription by User 1 for Course 1.
+ $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user1->id, 0, $course1->id);
+
+ // Add a Course Calendar Event by User 2 for Course 1 and Course 2.
+ $this->setUser($user2);
+ $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id);
+ $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course2->id);
+
+ // Add a Calendar Subscription by User 2 for Course 2.
+ $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user2->id, 0, $course2->id);
+
+ // Add a Course Assignment Action Calendar Event by User 2 for Course 2.
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+ $params['course'] = $course2->id;
+ $params['assignsubmission_onlinetext_enabled'] = 1;
+ $instance = $generator->create_instance($params);
+ $cm = get_coursemodule_from_instance('assign', $instance->id);
+ $modulecontext = context_module::instance($cm->id);
+ $assign = new assign($modulecontext, $cm, $course2);
+ $this->create_test_action_calendar_event('duedate', $course2->id, $instance->id, 'assign', $user2->id, time());
+ $this->create_test_action_calendar_event('gradingduedate', $course2->id, $instance->id, 'assign', $user2->id, time());
+
+ // Delete all Calendar Events for all Users by Context for Course 1.
+ provider::delete_data_for_all_users_in_context($course1context);
+
+ // Verify all Calendar Events for Course 1 were deleted.
+ $events = $DB->get_records('event', array('courseid' => $course1->id));
+ $this->assertCount(0, $events);
+ // Verify all Calendar Subscriptions for Course 1 were deleted.
+ $subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course1->id));
+ $this->assertCount(0, $subscriptions);
+
+ // Verify all Calendar Events for Course 2 exists still.
+ $events = $DB->get_records('event', array('courseid' => $course2->id));
+ $this->assertCount(4, $events);
+ // Verify all Calendar Subscriptions for Course 2 exists still.
+ $subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course2->id));
+ $this->assertCount(1, $subscriptions);
+
+ // Delete all Calendar Events for all Users by Context for Course 2.
+ provider::delete_data_for_all_users_in_context($course2context);
+
+ // Verify all Calendar Events for Course 2 were deleted.
+ $events = $DB->get_records('event', array('courseid' => $course2->id));
+ $this->assertCount(0, $events);
+ // Verify all Calendar Subscriptions for Course 2 were deleted.
+ $subscriptions = $DB->get_records('event_subscriptions', array('courseid' => $course2->id));
+ $this->assertCount(0, $subscriptions);
+ }
+
+ /**
+ * Test for provider::delete_data_for_user().
+ *
+ * @throws dml_exception
+ */
+ public function test_delete_data_for_user() {
+ global $DB;
+
+ // Create test user to create Calendar Events and Subscriptions with.
+ $user1 = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+
+ // Create a Category and Courses to test creating a Category Calendar Event.
+ $category = $this->getDataGenerator()->create_category();
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+
+ // Add 5 Calendar Events for User 1 for various contexts.
+ $this->setUser($user1);
+ $this->create_test_standard_calendar_event('user', $user1->id, time(), '');
+ $this->create_test_standard_calendar_event('site', $user1->id, time(), '', 0, 1);
+ $this->create_test_standard_calendar_event('category', $user1->id, time(), '', $category->id);
+ $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course1->id);
+ $this->create_test_standard_calendar_event('course', $user1->id, time(), '', 0, $course2->id);
+
+ // Add 1 Calendar Subscription for User 1 at course context.
+ $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user1->id, 0, $course2->id);
+
+ // Add 3 Calendar Events for User 2 for various contexts.
+ $this->setUser($user2);
+ $this->create_test_standard_calendar_event('user', $user2->id, time(), '');
+ $this->create_test_standard_calendar_event('category', $user2->id, time(), '', $category->id);
+ $this->create_test_standard_calendar_event('course', $user2->id, time(), '', 0, $course1->id);
+
+ // Add 1 Calendar Subscription for User 2 at course context.
+ $this->create_test_calendar_subscription('course', 'https://calendar.google.com/', $user2->id, 0, $course2->id);
+
+ // Retrieve the user's context ids.
+ $contextlist = provider::get_contexts_for_userid($user1->id);
+ $approvedcontextlist = new approved_contextlist($user1, 'core_calendar', $contextlist->get_contextids());
+
+ // Delete all Calendar data for User 1.
+ provider::delete_data_for_user($approvedcontextlist);
+
+ // Test all Calendar Events and Subscriptions for User 1 equals zero.
+ $events = $DB->get_records('event', ['userid' => $user1->id]);
+ $this->assertCount(0, $events);
+ $eventsubscriptions = $DB->get_records('event_subscriptions', ['userid' => $user1->id]);
+ $this->assertCount(0, $eventsubscriptions);
+
+ // Test all Calendar Events and Subscriptions for User 2 still exists and matches the same number created.
+ $events = $DB->get_records('event', ['userid' => $user2->id]);
+ $this->assertCount(3, $events);
+ $eventsubscriptions = $DB->get_records('event_subscriptions', ['userid' => $user2->id]);
+ $this->assertCount(1, $eventsubscriptions);
+ }
+
+ // Start of helper functions.
+
+ /**
+ * Helper function to create a Standard Calendar Event.
+ *
+ * @param string $eventtype Calendar event type
+ * @param int $userid User Id
+ * @param int $time Timestamp value
+ * @param string $customname Custom name
+ * @param int $categoryid Course Category Id
+ * @param int $courseid Course Id
+ * @param int $groupid Group Id
+ * @return bool|calendar_event Standard Calendar Event created.
+ * @throws coding_exception
+ */
+ protected function create_test_standard_calendar_event($eventtype, $userid, $time, $customname = '', $categoryid = 0, $courseid = 0, $groupid = 0) {
+ // Create a standard calendar event.
+ $name = "Standard Calendar Event $eventtype";
+ if ($customname != '') {
+ $name .= " -- $customname";
+ }
+
+ $event = (object)[
+ 'name' => $name,
+ 'categoryid' => $categoryid,
+ 'courseid' => $courseid,
+ 'groupid' => $groupid,
+ 'userid' => $userid,
+ 'modulename' => 0,
+ 'instance' => 0,
+ 'eventtype' => $eventtype,
+ 'type' => CALENDAR_EVENT_TYPE_STANDARD,
+ 'timestart' => $time,
+ 'visible' => 1
+ ];
+ return calendar_event::create($event, false);
+ }
+
+ /**
+ * Helper function to create an Action Calendar Event.
+ *
+ * @param string $eventtype Calendar event type
+ * @param int $courseid Course Id
+ * @param int $instanceid Activity Module instance id
+ * @param string $modulename Activity Module name
+ * @param int $userid User Id
+ * @param int $time Timestamp value
+ * @return bool|calendar_event Action Calendar Event created.
+ * @throws coding_exception
+ */
+ protected function create_test_action_calendar_event($eventtype, $courseid, $instanceid, $modulename, $userid, $time) {
+ // Create an action calendar event.
+ $event = (object)[
+ 'name' => "Action Calendar Event $eventtype -- $modulename",
+ 'categoryid' => 0,
+ 'courseid' => $courseid,
+ 'groupid' => 0,
+ 'userid' => $userid,
+ 'modulename' => $modulename,
+ 'instance' => $instanceid,
+ 'eventtype' => $eventtype,
+ 'type' => CALENDAR_EVENT_TYPE_ACTION,
+ 'timestart' => $time,
+ 'visible' => 1
+ ];
+ return calendar_event::create($event, false);
+ }
+
+ /**
+ * Helper function to create a Calendar Subscription.
+ *
+ * @param string $eventtype Calendar Subscription event type
+ * @param string $url Calendar Subscription URL
+ * @param int $userid User Id
+ * @param int $categoryid Category Id
+ * @param int $courseid Course Id
+ * @param int $groupid Group Id
+ * @return int Calendar Subscription Id
+ */
+ protected function create_test_calendar_subscription($eventtype, $url, $userid, $categoryid = 0, $courseid = 0, $groupid = 0) {
+ // Create a subscription calendar event.
+ $subscription = (object)[
+ 'name' => "Calendar Subscription " . $eventtype,
+ 'url' => $url,
+ 'categoryid' => $categoryid,
+ 'courseid' => $courseid,
+ 'groupid' => $groupid,
+ 'userid' => $userid,
+ 'eventtype' => $eventtype
+ ];
+
+ return calendar_add_subscription($subscription);
+ }
+
+}