MDL-61693 core_calendar: make results deterministic for better testing
[moodle.git] / calendar / classes / privacy / provider.php
blobaa543a4aaf12a40f11874dc41345bebf2fcfacb4
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16 /**
17 * Privacy class for requesting user data.
19 * @package core_calendar
20 * @copyright 2018 Zig Tan <zig@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 namespace core_calendar\privacy;
24 defined('MOODLE_INTERNAL') || die();
26 use \core_privacy\local\metadata\collection;
27 use \core_privacy\local\request\approved_contextlist;
28 use \core_privacy\local\request\context;
29 use \core_privacy\local\request\contextlist;
30 use \core_privacy\local\request\transform;
31 use \core_privacy\local\request\writer;
33 /**
34 * Privacy Subsystem for core_calendar implementing metadata, plugin, and user_preference providers.
36 * @package core_calendar
37 * @copyright 2018 Zig Tan <zig@moodle.com>
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class provider implements
41 \core_privacy\local\metadata\provider,
42 \core_privacy\local\request\plugin\provider,
43 \core_privacy\local\request\user_preference_provider
46 /**
47 * Provides meta data that is stored about a user with core_calendar.
49 * @param collection $collection A collection of meta data items to be added to.
50 * @return collection Returns the collection of metadata.
52 public static function get_metadata(collection $collection) : collection {
53 // The calendar 'event' table contains user data.
54 $collection->add_database_table(
55 'event',
57 'name' => 'privacy:metadata:calendar:event:name',
58 'description' => 'privacy:metadata:calendar:event:description',
59 'eventtype' => 'privacy:metadata:calendar:event:eventtype',
60 'timestart' => 'privacy:metadata:calendar:event:timestart',
61 'timeduration' => 'privacy:metadata:calendar:event:timeduration',
63 'privacy:metadata:calendar:event'
66 // The calendar 'event_subscriptions' table contains user data.
67 $collection->add_database_table(
68 'event_subscriptions',
70 'name' => 'privacy:metadata:calendar:event_subscriptions:name',
71 'url' => 'privacy:metadata:calendar:event_subscriptions:url',
72 'eventtype' => 'privacy:metadata:calendar:event_subscriptions:eventtype',
74 'privacy:metadata:calendar:event_subscriptions'
77 // The calendar user preference setting 'calendar_savedflt'.
78 $collection->add_user_preference(
79 'calendar_savedflt',
80 'privacy:metadata:calendar:preferences:calendar_savedflt'
83 return $collection;
86 /**
87 * Get the list of contexts that contain calendar user information for the specified user.
89 * @param int $userid The user to search.
90 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
92 public static function get_contexts_for_userid(int $userid) : contextlist {
93 $contextlist = new contextlist();
95 // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
96 $params = [
97 'sitecontext' => CONTEXT_SYSTEM,
98 'categorycontext' => CONTEXT_COURSECAT,
99 'coursecontext' => CONTEXT_COURSE,
100 'groupcontext' => CONTEXT_COURSE,
101 'usercontext' => CONTEXT_USER,
102 'cuserid' => $userid,
103 'modulecontext' => CONTEXT_MODULE,
104 'muserid' => $userid
107 // Get contexts of Calendar Events for the owner.
108 $sql = "SELECT ctx.id
109 FROM {context} ctx
110 JOIN {event} e ON
111 (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
112 (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
113 (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
114 (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
115 (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
116 WHERE e.userid = :cuserid
117 UNION
118 SELECT ctx.id
119 FROM {context} ctx
120 JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
121 JOIN {modules} m ON m.id = cm.module
122 JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
123 WHERE e.userid = :muserid";
124 $contextlist->add_from_sql($sql, $params);
126 // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
127 $params = [
128 'sitecontext' => CONTEXT_SYSTEM,
129 'categorycontext' => CONTEXT_COURSECAT,
130 'coursecontext' => CONTEXT_COURSE,
131 'groupcontext' => CONTEXT_COURSE,
132 'usercontext' => CONTEXT_USER,
133 'userid' => $userid
136 // Get contexts for Calendar Subscriptions for the owner.
137 $sql = "SELECT ctx.id
138 FROM {context} ctx
139 JOIN {event_subscriptions} s ON
140 (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
141 (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
142 (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
143 (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
144 (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
145 WHERE s.userid = :userid";
146 $contextlist->add_from_sql($sql, $params);
148 // Return combined contextlist for Calendar Events & Calendar Subscriptions.
149 return $contextlist;
153 * Export all user data for the specified user, in the specified contexts.
155 * @param approved_contextlist $contextlist The approved contexts to export information for.
157 public static function export_user_data(approved_contextlist $contextlist) {
158 if (empty($contextlist)) {
159 return;
162 self::export_user_calendar_event_data($contextlist);
163 self::export_user_calendar_subscription_data($contextlist);
167 * Export all user preferences for the plugin.
169 * @param int $userid The userid of the user whose data is to be exported.
171 public static function export_user_preferences(int $userid) {
172 $calendarsavedflt = get_user_preferences('calendar_savedflt', null, $userid);
174 if (null !== $calendarsavedflt) {
175 writer::export_user_preference(
176 'core_calendar',
177 'calendarsavedflt',
178 $calendarsavedflt,
179 get_string('privacy:metadata:calendar:preferences:calendar_savedflt', 'core_calendar')
185 * Delete all Calendar Event and Calendar Subscription data for all users in the specified context.
187 * @param context $context Transform the specific context to delete data for.
189 public static function delete_data_for_all_users_in_context(\context $context) {
190 if (empty($context)) {
191 return;
194 // Delete all Calendar Events in the specified context in batches.
195 $eventids = array_keys(self::get_calendar_event_ids_by_context($context));
196 self::delete_batch_records('event', 'id', $eventids);
198 // Delete all Calendar Subscriptions in the specified context in batches.
199 $subscriptionids = array_keys(self::get_calendar_subscription_ids_by_context($context));
200 self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
204 * Delete all user data for the specified user, in the specified contexts.
206 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
208 public static function delete_data_for_user(approved_contextlist $contextlist) {
209 if (empty($contextlist)) {
210 return;
213 // Delete all Calendar Events for the owner and specified contexts in batches.
214 $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);
215 $eventids = [];
216 foreach ($eventdetails as $eventdetail) {
217 $eventids[] = $eventdetail->eventid;
219 $eventdetails->close();
220 self::delete_batch_records('event', 'id', $eventids);
222 // Delete all Calendar Subscriptions for the owner and specified contexts in batches.
223 $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
224 $subscriptionids = [];
225 foreach ($subscriptiondetails as $subscriptiondetail) {
226 $subscriptionids[] = $subscriptiondetail->subscriptionid;
228 $subscriptiondetails->close();
229 self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
233 * Helper function to export Calendar Events data by a User's contextlist.
235 * @param approved_contextlist $contextlist
236 * @throws \coding_exception
238 protected static function export_user_calendar_event_data(approved_contextlist $contextlist) {
239 // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
240 $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);
242 // Multiple Calendar Events of the same eventtype and time can exist for a context, so collate them for export.
243 $eventrecords = [];
244 foreach ($eventdetails as $eventdetail) {
245 // Create an array key based on the contextid, eventtype, and time.
246 $key = $eventdetail->contextid . $eventdetail->eventtype . $eventdetail->timestart;
248 if (array_key_exists($key, $eventrecords) === false) {
249 $eventrecords[$key] = [ $eventdetail ];
250 } else {
251 $eventrecords[$key] = array_merge($eventrecords[$key], [$eventdetail]);
254 $eventdetails->close();
256 // Export Calendar Event data.
257 foreach ($eventrecords as $eventrecord) {
258 $index = (count($eventrecord) > 1) ? 1 : 0;
260 foreach ($eventrecord as $event) {
261 // Export the events using the structure Calendar/Events/{datetime}/{eventtype}-event.json.
262 $subcontexts = [
263 get_string('calendar', 'calendar'),
264 get_string('events', 'calendar'),
265 date('c', $event->timestart)
267 $name = $event->eventtype . '-event';
269 // Use name {eventtype}-event-{index}.json if multiple eventtypes and time exists at the same context.
270 if ($index != 0) {
271 $name .= '-' . $index;
272 $index++;
275 $eventdetails = (object) [
276 'name' => $event->name,
277 'description' => $event->description,
278 'eventtype' => $event->eventtype,
279 'timestart' => transform::datetime($event->timestart),
280 'timeduration' => $event->timeduration
283 $context = \context::instance_by_id($event->contextid);
284 writer::with_context($context)->export_related_data($subcontexts, $name, $eventdetails);
290 * Helper function to export Calendar Subscriptions data by a User's contextlist.
292 * @param approved_contextlist $contextlist
293 * @throws \coding_exception
295 protected static function export_user_calendar_subscription_data(approved_contextlist $contextlist) {
296 // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
297 $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
299 // Multiple Calendar Subscriptions of the same eventtype can exist for a context, so collate them for export.
300 $subscriptionrecords = [];
301 foreach ($subscriptiondetails as $subscriptiondetail) {
302 // Create an array key based on the contextid and eventtype.
303 $key = $subscriptiondetail->contextid . $subscriptiondetail->eventtype;
305 if (array_key_exists($key, $subscriptionrecords) === false) {
306 $subscriptionrecords[$key] = [ $subscriptiondetail ];
307 } else {
308 $subscriptionrecords[$key] = array_merge($subscriptionrecords[$key], [$subscriptiondetail]);
311 $subscriptiondetails->close();
313 // Export Calendar Subscription data.
314 foreach ($subscriptionrecords as $subscriptionrecord) {
315 $index = (count($subscriptionrecord) > 1) ? 1 : 0;
317 foreach ($subscriptionrecord as $subscription) {
318 // Export the events using the structure Calendar/Subscriptions/{eventtype}-subscription.json.
319 $subcontexts = [
320 get_string('calendar', 'calendar'),
321 get_string('subscriptions', 'calendar')
323 $name = $subscription->eventtype . '-subscription';
325 // Use name {eventtype}-subscription-{index}.json if multiple eventtypes exists at the same context.
326 if ($index != 0) {
327 $name .= '-' . $index;
328 $index++;
331 $context = \context::instance_by_id($subscription->contextid);
332 writer::with_context($context)->export_related_data($subcontexts, $name, $subscription);
338 * Helper function to return all Calendar Event id results for a specified context.
340 * @param \context $context
341 * @return array|null
342 * @throws \dml_exception
344 protected static function get_calendar_event_ids_by_context(\context $context) {
345 global $DB;
347 // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
348 $events = null;
350 if ($context->contextlevel == CONTEXT_MODULE) { // Course Module Contexts.
351 $params = [
352 'modulecontext' => $context->contextlevel,
353 'contextid' => $context->id
356 // Get Calendar Events for the specified Course Module context.
357 $sql = "SELECT DISTINCT
358 e.id AS eventid
359 FROM {context} ctx
360 INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
361 INNER JOIN {modules} m ON m.id = cm.module
362 INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
363 WHERE ctx.id = :contextid";
364 $events = $DB->get_records_sql($sql, $params);
365 } else { // Other Moodle Contexts.
366 $params = [
367 'sitecontext' => CONTEXT_SYSTEM,
368 'categorycontext' => CONTEXT_COURSECAT,
369 'coursecontext' => CONTEXT_COURSE,
370 'groupcontext' => CONTEXT_COURSE,
371 'usercontext' => CONTEXT_USER,
372 'contextid' => $context->id
375 // Get Calendar Events for the specified Moodle context.
376 $sql = "SELECT DISTINCT
377 e.id AS eventid
378 FROM {context} ctx
379 INNER JOIN {event} e ON
380 (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
381 (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
382 (e.courseid = ctx.instanceid AND (e.eventtype = 'course' OR e.eventtype = 'group' OR e.modulename != '0') AND ctx.contextlevel = :coursecontext) OR
383 (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
384 WHERE ctx.id = :contextid";
385 $events = $DB->get_records_sql($sql, $params);
388 return $events;
392 * Helper function to return all Calendar Subscription id results for a specified context.
394 * @param \context $context
395 * @return array
396 * @throws \dml_exception
398 protected static function get_calendar_subscription_ids_by_context(\context $context) {
399 global $DB;
401 // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
402 $params = [
403 'sitecontext' => CONTEXT_SYSTEM,
404 'categorycontext' => CONTEXT_COURSECAT,
405 'coursecontext' => CONTEXT_COURSE,
406 'groupcontext' => CONTEXT_COURSE,
407 'usercontext' => CONTEXT_USER,
408 'contextid' => $context->id
411 // Get Calendar Subscriptions for the specified context.
412 $sql = "SELECT DISTINCT
413 s.id AS subscriptionid
414 FROM {context} ctx
415 INNER JOIN {event_subscriptions} s ON
416 (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
417 (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
418 (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
419 (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
420 (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
421 WHERE ctx.id = :contextid";
423 return $DB->get_records_sql($sql, $params);
427 * Helper function to return the Calendar Events for a given user and context list.
429 * @param approved_contextlist $contextlist
430 * @return array
431 * @throws \coding_exception
432 * @throws \dml_exception
434 protected static function get_calendar_event_details_by_contextlist(approved_contextlist $contextlist) {
435 global $DB;
437 $userid = $contextlist->get_user()->id;
439 list($contextsql1, $contextparams1) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
440 list($contextsql2, $contextparams2) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
442 // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
443 $params = [
444 'sitecontext' => CONTEXT_SYSTEM,
445 'categorycontext' => CONTEXT_COURSECAT,
446 'coursecontext' => CONTEXT_COURSE,
447 'groupcontext' => CONTEXT_COURSE,
448 'usercontext' => CONTEXT_USER,
449 'cuserid' => $userid,
450 'modulecontext' => CONTEXT_MODULE,
451 'muserid' => $userid
453 $params += $contextparams1;
454 $params += $contextparams2;
456 // Get Calendar Events details for the approved contexts and the owner.
457 $sql = "SELECT ctxid as contextid,
458 details.id as eventid,
459 details.name as name,
460 details.description as description,
461 details.eventtype as eventtype,
462 details.timestart as timestart,
463 details.timeduration as timeduration
464 FROM (
465 SELECT e.id AS id,
466 ctx.id AS ctxid
467 FROM {context} ctx
468 INNER JOIN {event} e ON
469 (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
470 (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
471 (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
472 (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
473 (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
474 WHERE e.userid = :cuserid
475 AND ctx.id {$contextsql1}
476 UNION
477 SELECT e.id AS id,
478 ctx.id AS ctxid
479 FROM {context} ctx
480 INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
481 INNER JOIN {modules} m ON m.id = cm.module
482 INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
483 WHERE e.userid = :muserid
484 AND ctx.id {$contextsql2}
485 ) ids
486 JOIN {event} details ON details.id = ids.id
487 ORDER BY ids.id";
489 return $DB->get_recordset_sql($sql, $params);
493 * Helper function to return the Calendar Subscriptions for a given user and context list.
495 * @param approved_contextlist $contextlist
496 * @return array
497 * @throws \coding_exception
498 * @throws \dml_exception
500 protected static function get_calendar_subscription_details_by_contextlist(approved_contextlist $contextlist) {
501 global $DB;
503 $user = $contextlist->get_user();
505 list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
507 $params = [
508 'sitecontext' => CONTEXT_SYSTEM,
509 'categorycontext' => CONTEXT_COURSECAT,
510 'coursecontext' => CONTEXT_COURSE,
511 'groupcontext' => CONTEXT_COURSE,
512 'usercontext' => CONTEXT_USER,
513 'userid' => $user->id
515 $params += $contextparams;
517 // Get Calendar Subscriptions for the approved contexts and the owner.
518 $sql = "SELECT DISTINCT
519 c.id as contextid,
520 s.id as subscriptionid,
521 s.name as name,
522 s.url as url,
523 s.eventtype as eventtype
524 FROM {context} c
525 INNER JOIN {event_subscriptions} s ON
526 (s.eventtype = 'site' AND c.contextlevel = :sitecontext) OR
527 (s.categoryid = c.instanceid AND s.eventtype = 'category' AND c.contextlevel = :categorycontext) OR
528 (s.courseid = c.instanceid AND s.eventtype = 'course' AND c.contextlevel = :coursecontext) OR
529 (s.courseid = c.instanceid AND s.eventtype = 'group' AND c.contextlevel = :groupcontext) OR
530 (s.userid = c.instanceid AND s.eventtype = 'user' AND c.contextlevel = :usercontext)
531 WHERE s.userid = :userid
532 AND c.id {$contextsql}";
534 return $DB->get_recordset_sql($sql, $params);
538 * Helper function to delete records in batches in order to minimise amount of deletion queries.
540 * @param string $tablename The table name to delete from.
541 * @param string $field The table column field name to delete records by.
542 * @param array $values The table column field values to delete records by.
543 * @throws \dml_exception
545 protected static function delete_batch_records($tablename, $field, $values) {
546 global $DB;
548 // Batch deletion with an upper limit of 2000 records to minimise the number of deletion queries.
549 $batchrecords = array_chunk($values, 2000);
551 foreach ($batchrecords as $batchrecord) {
552 $DB->delete_records_list($tablename, $field, $batchrecord);