4fc58deccee9930c09e561cecf8b11a9b034b7ef
[moodle.git] / calendar / classes / privacy / provider.php
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.
13 //
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.
18  *
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
22  */
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.
35  *
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
39  */
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
44 {
46     /**
47      * Provides meta data that is stored about a user with core_calendar.
48      *
49      * @param  collection $collection A collection of meta data items to be added to.
50      * @return  collection Returns the collection of metadata.
51      */
52     public static function get_metadata(collection $collection) : collection {
53         // The calendar 'event' table contains user data.
54         $collection->add_database_table(
55             'event',
56             [
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',
62             ],
63             'privacy:metadata:calendar:event'
64         );
66         // The calendar 'event_subscriptions' table contains user data.
67         $collection->add_database_table(
68             'event_subscriptions',
69             [
70                 'name' => 'privacy:metadata:calendar:event_subscriptions:name',
71                 'url' => 'privacy:metadata:calendar:event_subscriptions:url',
72                 'eventtype' => 'privacy:metadata:calendar:event_subscriptions:eventtype',
73             ],
74             'privacy:metadata:calendar:event_subscriptions'
75         );
77         // The calendar user preference setting 'calendar_savedflt'.
78         $collection->add_user_preference(
79             'calendar_savedflt',
80             'privacy:metadata:calendar:preferences:calendar_savedflt'
81         );
83         return $collection;
84     }
86     /**
87      * Get the list of contexts that contain calendar user information for the specified user.
88      *
89      * @param   int $userid The user to search.
90      * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
91      */
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
105         ];
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
134         ];
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;
150     }
152     /**
153      * Export all user data for the specified user, in the specified contexts.
154      *
155      * @param   approved_contextlist $contextlist The approved contexts to export information for.
156      */
157     public static function export_user_data(approved_contextlist $contextlist) {
158         if (empty($contextlist)) {
159             return;
160         }
162         self::export_user_calendar_event_data($contextlist);
163         self::export_user_calendar_subscription_data($contextlist);
164     }
166     /**
167      * Export all user preferences for the plugin.
168      *
169      * @param   int $userid The userid of the user whose data is to be exported.
170      */
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')
180             );
181         }
182     }
184     /**
185      * Delete all Calendar Event and Calendar Subscription data for all users in the specified context.
186      *
187      * @param   context $context Transform the specific context to delete data for.
188      */
189     public static function delete_data_for_all_users_in_context(\context $context) {
190         // Delete all Calendar Events in the specified context in batches.
191         if ($eventids = array_keys(self::get_calendar_event_ids_by_context($context))) {
192             self::delete_batch_records('event', 'id', $eventids);
193         }
195         // Delete all Calendar Subscriptions in the specified context in batches.
196         if ($subscriptionids = array_keys(self::get_calendar_subscription_ids_by_context($context))) {
197             self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
198         }
199     }
201     /**
202      * Delete all user data for the specified user, in the specified contexts.
203      *
204      * @param   approved_contextlist $contextlist The approved contexts and user information to delete information for.
205      */
206     public static function delete_data_for_user(approved_contextlist $contextlist) {
207         if (empty($contextlist)) {
208             return;
209         }
211         // Delete all Calendar Events for the owner and specified contexts in batches.
212         $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);
213         $eventids = [];
214         foreach ($eventdetails as $eventdetail) {
215             $eventids[] = $eventdetail->eventid;
216         }
217         $eventdetails->close();
218         self::delete_batch_records('event', 'id', $eventids);
220         // Delete all Calendar Subscriptions for the owner and specified contexts in batches.
221         $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
222         $subscriptionids = [];
223         foreach ($subscriptiondetails as $subscriptiondetail) {
224             $subscriptionids[] = $subscriptiondetail->subscriptionid;
225         }
226         $subscriptiondetails->close();
227         self::delete_batch_records('event_subscriptions', 'id', $subscriptionids);
228     }
230     /**
231      * Helper function to export Calendar Events data by a User's contextlist.
232      *
233      * @param approved_contextlist $contextlist
234      * @throws \coding_exception
235      */
236     protected static function export_user_calendar_event_data(approved_contextlist $contextlist) {
237         // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
238         $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);
240         // Multiple Calendar Events of the same eventtype and time can exist for a context, so collate them for export.
241         $eventrecords = [];
242         foreach ($eventdetails as $eventdetail) {
243             // Create an array key based on the contextid, eventtype, and time.
244             $key = $eventdetail->contextid . $eventdetail->eventtype . $eventdetail->timestart;
246             if (array_key_exists($key, $eventrecords) === false) {
247                 $eventrecords[$key] = [ $eventdetail ];
248             } else {
249                 $eventrecords[$key] = array_merge($eventrecords[$key], [$eventdetail]);
250             }
251         }
252         $eventdetails->close();
254         // Export Calendar Event data.
255         foreach ($eventrecords as $eventrecord) {
256             $index = (count($eventrecord) > 1) ? 1 : 0;
258             foreach ($eventrecord as $event) {
259                 // Export the events using the structure Calendar/Events/{datetime}/{eventtype}-event.json.
260                 $subcontexts = [
261                     get_string('calendar', 'calendar'),
262                     get_string('events', 'calendar'),
263                     date('c', $event->timestart)
264                 ];
265                 $name = $event->eventtype . '-event';
267                 // Use name {eventtype}-event-{index}.json if multiple eventtypes and time exists at the same context.
268                 if ($index != 0) {
269                     $name .= '-' . $index;
270                     $index++;
271                 }
273                 $eventdetails = (object) [
274                     'name' => $event->name,
275                     'description' => $event->description,
276                     'eventtype' => $event->eventtype,
277                     'timestart' => transform::datetime($event->timestart),
278                     'timeduration' => $event->timeduration
279                 ];
281                 $context = \context::instance_by_id($event->contextid);
282                 writer::with_context($context)->export_related_data($subcontexts, $name, $eventdetails);
283             }
284         }
285     }
287     /**
288      * Helper function to export Calendar Subscriptions data by a User's contextlist.
289      *
290      * @param approved_contextlist $contextlist
291      * @throws \coding_exception
292      */
293     protected static function export_user_calendar_subscription_data(approved_contextlist $contextlist) {
294         // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
295         $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
297         // Multiple Calendar Subscriptions of the same eventtype can exist for a context, so collate them for export.
298         $subscriptionrecords = [];
299         foreach ($subscriptiondetails as $subscriptiondetail) {
300             // Create an array key based on the contextid and eventtype.
301             $key = $subscriptiondetail->contextid . $subscriptiondetail->eventtype;
303             if (array_key_exists($key, $subscriptionrecords) === false) {
304                 $subscriptionrecords[$key] = [ $subscriptiondetail ];
305             } else {
306                 $subscriptionrecords[$key] = array_merge($subscriptionrecords[$key], [$subscriptiondetail]);
307             }
308         }
309         $subscriptiondetails->close();
311         // Export Calendar Subscription data.
312         foreach ($subscriptionrecords as $subscriptionrecord) {
313             $index = (count($subscriptionrecord) > 1) ? 1 : 0;
315             foreach ($subscriptionrecord as $subscription) {
316                 // Export the events using the structure Calendar/Subscriptions/{eventtype}-subscription.json.
317                 $subcontexts = [
318                     get_string('calendar', 'calendar'),
319                     get_string('subscriptions', 'calendar')
320                 ];
321                 $name = $subscription->eventtype . '-subscription';
323                 // Use name {eventtype}-subscription-{index}.json if multiple eventtypes exists at the same context.
324                 if ($index != 0) {
325                     $name .= '-' . $index;
326                     $index++;
327                 }
329                 $context = \context::instance_by_id($subscription->contextid);
330                 writer::with_context($context)->export_related_data($subcontexts, $name, $subscription);
331             }
332         }
333     }
335     /**
336      * Helper function to return all Calendar Event id results for a specified context.
337      *
338      * @param \context $context
339      * @return array|null
340      * @throws \dml_exception
341      */
342     protected static function get_calendar_event_ids_by_context(\context $context) {
343         global $DB;
345         // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
346         $events = null;
348         if ($context->contextlevel == CONTEXT_MODULE) { // Course Module Contexts.
349             $params = [
350                 'modulecontext'     => $context->contextlevel,
351                 'contextid'         => $context->id
352             ];
354             // Get Calendar Events for the specified Course Module context.
355             $sql = "SELECT DISTINCT
356                            e.id AS eventid
357                       FROM {context} ctx
358                 INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
359                 INNER JOIN {modules} m ON m.id = cm.module
360                 INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
361                      WHERE ctx.id = :contextid";
362             $events = $DB->get_records_sql($sql, $params);
363         } else {                                        // Other Moodle Contexts.
364             $params = [
365                 'sitecontext'       => CONTEXT_SYSTEM,
366                 'categorycontext'   => CONTEXT_COURSECAT,
367                 'coursecontext'     => CONTEXT_COURSE,
368                 'groupcontext'      => CONTEXT_COURSE,
369                 'usercontext'       => CONTEXT_USER,
370                 'contextid'         => $context->id
371             ];
373             // Get Calendar Events for the specified Moodle context.
374             $sql = "SELECT DISTINCT
375                            e.id AS eventid
376                       FROM {context} ctx
377                 INNER JOIN {event} e ON
378                            (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
379                            (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
380                            (e.courseid = ctx.instanceid AND (e.eventtype = 'course' OR e.eventtype = 'group' OR e.modulename != '0') AND ctx.contextlevel = :coursecontext) OR
381                            (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
382                      WHERE ctx.id = :contextid";
383             $events = $DB->get_records_sql($sql, $params);
384         }
386         return $events;
387     }
389     /**
390      * Helper function to return all Calendar Subscription id results for a specified context.
391      *
392      * @param \context $context
393      * @return array
394      * @throws \dml_exception
395      */
396     protected static function get_calendar_subscription_ids_by_context(\context $context) {
397         global $DB;
399         // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
400         $params = [
401             'sitecontext'       => CONTEXT_SYSTEM,
402             'categorycontext'   => CONTEXT_COURSECAT,
403             'coursecontext'     => CONTEXT_COURSE,
404             'groupcontext'      => CONTEXT_COURSE,
405             'usercontext'       => CONTEXT_USER,
406             'contextid'         => $context->id
407         ];
409         // Get Calendar Subscriptions for the specified context.
410         $sql = "SELECT DISTINCT
411                        s.id AS subscriptionid
412                   FROM {context} ctx
413             INNER JOIN {event_subscriptions} s ON
414                        (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
415                        (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
416                        (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
417                        (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
418                        (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
419                  WHERE ctx.id = :contextid";
421         return $DB->get_records_sql($sql, $params);
422     }
424     /**
425      * Helper function to return the Calendar Events for a given user and context list.
426      *
427      * @param approved_contextlist $contextlist
428      * @return array
429      * @throws \coding_exception
430      * @throws \dml_exception
431      */
432     protected static function get_calendar_event_details_by_contextlist(approved_contextlist $contextlist) {
433         global $DB;
435         $userid = $contextlist->get_user()->id;
437         list($contextsql1, $contextparams1) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
438         list($contextsql2, $contextparams2) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
440         // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
441         $params = [
442             'sitecontext'       => CONTEXT_SYSTEM,
443             'categorycontext'   => CONTEXT_COURSECAT,
444             'coursecontext'     => CONTEXT_COURSE,
445             'groupcontext'      => CONTEXT_COURSE,
446             'usercontext'       => CONTEXT_USER,
447             'cuserid'           => $userid,
448             'modulecontext'     => CONTEXT_MODULE,
449             'muserid'           => $userid
450         ];
451         $params += $contextparams1;
452         $params += $contextparams2;
454         // Get Calendar Events details for the approved contexts and the owner.
455         $sql = "SELECT ctxid as contextid,
456                        details.id as eventid,
457                        details.name as name,
458                        details.description as description,
459                        details.eventtype as eventtype,
460                        details.timestart as timestart,
461                        details.timeduration as timeduration
462                   FROM (
463                           SELECT e.id AS id,
464                                  ctx.id AS ctxid
465                             FROM {context} ctx
466                       INNER JOIN {event} e ON
467                                  (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
468                                  (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
469                                  (e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
470                                  (e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
471                                  (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
472                            WHERE e.userid = :cuserid
473                              AND ctx.id {$contextsql1}
474                            UNION
475                           SELECT e.id AS id,
476                                  ctx.id AS ctxid
477                             FROM {context} ctx
478                       INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
479                       INNER JOIN {modules} m ON m.id = cm.module
480                       INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
481                            WHERE e.userid = :muserid
482                              AND ctx.id {$contextsql2}
483                   ) ids
484                   JOIN {event} details ON details.id = ids.id
485               ORDER BY ids.id";
487         return $DB->get_recordset_sql($sql, $params);
488     }
490     /**
491      * Helper function to return the Calendar Subscriptions for a given user and context list.
492      *
493      * @param approved_contextlist $contextlist
494      * @return array
495      * @throws \coding_exception
496      * @throws \dml_exception
497      */
498     protected static function get_calendar_subscription_details_by_contextlist(approved_contextlist $contextlist) {
499         global $DB;
501         $user = $contextlist->get_user();
503         list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
505         $params = [
506             'sitecontext' => CONTEXT_SYSTEM,
507             'categorycontext' => CONTEXT_COURSECAT,
508             'coursecontext' => CONTEXT_COURSE,
509             'groupcontext' => CONTEXT_COURSE,
510             'usercontext' => CONTEXT_USER,
511             'userid' => $user->id
512         ];
513         $params += $contextparams;
515         // Get Calendar Subscriptions for the approved contexts and the owner.
516         $sql = "SELECT DISTINCT
517                        c.id as contextid,
518                        s.id as subscriptionid,
519                        s.name as name,
520                        s.url as url,
521                        s.eventtype as eventtype
522                   FROM {context} c
523             INNER JOIN {event_subscriptions} s ON
524                        (s.eventtype = 'site' AND c.contextlevel = :sitecontext) OR
525                        (s.categoryid = c.instanceid AND s.eventtype = 'category' AND c.contextlevel = :categorycontext) OR
526                        (s.courseid = c.instanceid AND s.eventtype = 'course' AND c.contextlevel = :coursecontext) OR
527                        (s.courseid = c.instanceid AND s.eventtype = 'group' AND c.contextlevel = :groupcontext) OR
528                        (s.userid = c.instanceid AND s.eventtype = 'user' AND c.contextlevel = :usercontext)
529                  WHERE s.userid = :userid
530                    AND c.id {$contextsql}";
532         return $DB->get_recordset_sql($sql, $params);
533     }
535     /**
536      * Helper function to delete records in batches in order to minimise amount of deletion queries.
537      *
538      * @param string    $tablename  The table name to delete from.
539      * @param string    $field      The table column field name to delete records by.
540      * @param array     $values     The table column field values to delete records by.
541      * @throws \dml_exception
542      */
543     protected static function delete_batch_records($tablename, $field, $values) {
544         global $DB;
546         // Batch deletion with an upper limit of 2000 records to minimise the number of deletion queries.
547         $batchrecords = array_chunk($values, 2000);
549         foreach ($batchrecords as $batchrecord) {
550             $DB->delete_records_list($tablename, $field, $batchrecord);
551         }
552     }