diff --git a/calendar/classes/privacy/provider.php b/calendar/classes/privacy/provider.php new file mode 100644 index 0000000000000..0065137ccb173 --- /dev/null +++ b/calendar/classes/privacy/provider.php @@ -0,0 +1,555 @@ +. +/** + * Privacy class for requesting user data. + * + * @package core_calendar + * @copyright 2018 Zig Tan + * @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 + * @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); + } + } + +} diff --git a/calendar/tests/privacy_test.php b/calendar/tests/privacy_test.php new file mode 100644 index 0000000000000..4a47348c6237e --- /dev/null +++ b/calendar/tests/privacy_test.php @@ -0,0 +1,526 @@ +. + +/** + * Privacy tests for core_calendar. + * + * @package core_calendar + * @category test + * @copyright 2018 Zig Tan + * @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 + * @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); + } + +} diff --git a/lang/en/calendar.php b/lang/en/calendar.php index 206ebb217f63b..d93fb9c2b545b 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -105,6 +105,7 @@ $string['eventnameandcourse'] = '{$a->course}: {$a->name}'; $string['eventnone'] = 'No events'; $string['eventrepeat'] = 'Repeats'; +$string['events'] = 'Events'; $string['eventsall'] = 'All events'; $string['eventsdeleted'] = 'Events deleted'; $string['eventsimported'] = 'Events imported: {$a}'; @@ -198,6 +199,17 @@ $string['pref_startwday_help'] = 'Calendar weeks will be shown as starting on the day that you select here.'; $string['pref_timeformat'] = 'Time display format'; $string['pref_timeformat_help'] = 'You can choose to see times in either 12 or 24 hour format. If you choose "default", then the format will be automatically chosen according to the language you use in the site.'; +$string['privacy:metadata:calendar:event'] = 'The Calendar component can store user calendar event details within the core subsystem.'; +$string['privacy:metadata:calendar:event:name'] = 'The name of the calendar event.'; +$string['privacy:metadata:calendar:event:description'] = 'The description of the calendar event.'; +$string['privacy:metadata:calendar:event:eventtype'] = 'The event type of the calendar event.'; +$string['privacy:metadata:calendar:event:timestart'] = 'The start time of the calendar event.'; +$string['privacy:metadata:calendar:event:timeduration'] = 'The duration of the calendar event.'; +$string['privacy:metadata:calendar:event_subscriptions'] = 'The Calendar component can stores user calendar subscriptions details within the core subsystem.'; +$string['privacy:metadata:calendar:event_subscriptions:name'] = 'The name of the calendar subscription.'; +$string['privacy:metadata:calendar:event_subscriptions:url'] = 'The url of the calendar subscription.'; +$string['privacy:metadata:calendar:event_subscriptions:eventtype'] = 'The event type of the calendar subscription.'; +$string['privacy:metadata:calendar:preferences:calendar_savedflt'] = 'The configured calendar event type display user preference.'; $string['recentupcoming'] = 'Recent and next 60 days'; $string['repeatedevents'] = 'Repeated events'; $string['repeateditall'] = 'Also apply changes to the other {$a} events in this repeat series';