MDL-37624 calendar: Added location support
[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                     'location' => $event->location,
277                     'eventtype' => $event->eventtype,
278                     'timestart' => transform::datetime($event->timestart),
279                     'timeduration' => $event->timeduration
280                 ];
282                 $context = \context::instance_by_id($event->contextid);
283                 writer::with_context($context)->export_related_data($subcontexts, $name, $eventdetails);
284             }
285         }
286     }
288     /**
289      * Helper function to export Calendar Subscriptions data by a User's contextlist.
290      *
291      * @param approved_contextlist $contextlist
292      * @throws \coding_exception
293      */
294     protected static function export_user_calendar_subscription_data(approved_contextlist $contextlist) {
295         // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
296         $subscriptiondetails = self::get_calendar_subscription_details_by_contextlist($contextlist);
298         // Multiple Calendar Subscriptions of the same eventtype can exist for a context, so collate them for export.
299         $subscriptionrecords = [];
300         foreach ($subscriptiondetails as $subscriptiondetail) {
301             // Create an array key based on the contextid and eventtype.
302             $key = $subscriptiondetail->contextid . $subscriptiondetail->eventtype;
304             if (array_key_exists($key, $subscriptionrecords) === false) {
305                 $subscriptionrecords[$key] = [ $subscriptiondetail ];
306             } else {
307                 $subscriptionrecords[$key] = array_merge($subscriptionrecords[$key], [$subscriptiondetail]);
308             }
309         }
310         $subscriptiondetails->close();
312         // Export Calendar Subscription data.
313         foreach ($subscriptionrecords as $subscriptionrecord) {
314             $index = (count($subscriptionrecord) > 1) ? 1 : 0;
316             foreach ($subscriptionrecord as $subscription) {
317                 // Export the events using the structure Calendar/Subscriptions/{eventtype}-subscription.json.
318                 $subcontexts = [
319                     get_string('calendar', 'calendar'),
320                     get_string('subscriptions', 'calendar')
321                 ];
322                 $name = $subscription->eventtype . '-subscription';
324                 // Use name {eventtype}-subscription-{index}.json if multiple eventtypes exists at the same context.
325                 if ($index != 0) {
326                     $name .= '-' . $index;
327                     $index++;
328                 }
330                 $context = \context::instance_by_id($subscription->contextid);
331                 writer::with_context($context)->export_related_data($subcontexts, $name, $subscription);
332             }
333         }
334     }
336     /**
337      * Helper function to return all Calendar Event id results for a specified context.
338      *
339      * @param \context $context
340      * @return array|null
341      * @throws \dml_exception
342      */
343     protected static function get_calendar_event_ids_by_context(\context $context) {
344         global $DB;
346         // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
347         $events = null;
349         if ($context->contextlevel == CONTEXT_MODULE) { // Course Module Contexts.
350             $params = [
351                 'modulecontext'     => $context->contextlevel,
352                 'contextid'         => $context->id
353             ];
355             // Get Calendar Events for the specified Course Module context.
356             $sql = "SELECT DISTINCT
357                            e.id AS eventid
358                       FROM {context} ctx
359                 INNER JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
360                 INNER JOIN {modules} m ON m.id = cm.module
361                 INNER JOIN {event} e ON e.modulename = m.name AND e.courseid = cm.course AND e.instance = cm.instance
362                      WHERE ctx.id = :contextid";
363             $events = $DB->get_records_sql($sql, $params);
364         } else {                                        // Other Moodle Contexts.
365             $params = [
366                 'sitecontext'       => CONTEXT_SYSTEM,
367                 'categorycontext'   => CONTEXT_COURSECAT,
368                 'coursecontext'     => CONTEXT_COURSE,
369                 'groupcontext'      => CONTEXT_COURSE,
370                 'usercontext'       => CONTEXT_USER,
371                 'contextid'         => $context->id
372             ];
374             // Get Calendar Events for the specified Moodle context.
375             $sql = "SELECT DISTINCT
376                            e.id AS eventid
377                       FROM {context} ctx
378                 INNER JOIN {event} e ON
379                            (e.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
380                            (e.categoryid = ctx.instanceid AND e.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
381                            (e.courseid = ctx.instanceid AND (e.eventtype = 'course' OR e.eventtype = 'group' OR e.modulename != '0') AND ctx.contextlevel = :coursecontext) OR
382                            (e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
383                      WHERE ctx.id = :contextid";
384             $events = $DB->get_records_sql($sql, $params);
385         }
387         return $events;
388     }
390     /**
391      * Helper function to return all Calendar Subscription id results for a specified context.
392      *
393      * @param \context $context
394      * @return array
395      * @throws \dml_exception
396      */
397     protected static function get_calendar_subscription_ids_by_context(\context $context) {
398         global $DB;
400         // Calendar Subscriptions can exist at Site, Course Category, Course, Course Group, or User contexts.
401         $params = [
402             'sitecontext'       => CONTEXT_SYSTEM,
403             'categorycontext'   => CONTEXT_COURSECAT,
404             'coursecontext'     => CONTEXT_COURSE,
405             'groupcontext'      => CONTEXT_COURSE,
406             'usercontext'       => CONTEXT_USER,
407             'contextid'         => $context->id
408         ];
410         // Get Calendar Subscriptions for the specified context.
411         $sql = "SELECT DISTINCT
412                        s.id AS subscriptionid
413                   FROM {context} ctx
414             INNER JOIN {event_subscriptions} s ON
415                        (s.eventtype = 'site' AND ctx.contextlevel = :sitecontext) OR
416                        (s.categoryid = ctx.instanceid AND s.eventtype = 'category' AND ctx.contextlevel = :categorycontext) OR
417                        (s.courseid = ctx.instanceid AND s.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
418                        (s.courseid = ctx.instanceid AND s.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
419                        (s.userid = ctx.instanceid AND s.eventtype = 'user' AND ctx.contextlevel = :usercontext)
420                  WHERE ctx.id = :contextid";
422         return $DB->get_records_sql($sql, $params);
423     }
425     /**
426      * Helper function to return the Calendar Events for a given user and context list.
427      *
428      * @param approved_contextlist $contextlist
429      * @return array
430      * @throws \coding_exception
431      * @throws \dml_exception
432      */
433     protected static function get_calendar_event_details_by_contextlist(approved_contextlist $contextlist) {
434         global $DB;
436         $userid = $contextlist->get_user()->id;
438         list($contextsql1, $contextparams1) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
439         list($contextsql2, $contextparams2) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
441         // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
442         $params = [
443             'sitecontext'       => CONTEXT_SYSTEM,
444             'categorycontext'   => CONTEXT_COURSECAT,
445             'coursecontext'     => CONTEXT_COURSE,
446             'groupcontext'      => CONTEXT_COURSE,
447             'usercontext'       => CONTEXT_USER,
448             'cuserid'           => $userid,
449             'modulecontext'     => CONTEXT_MODULE,
450             'muserid'           => $userid
451         ];
452         $params += $contextparams1;
453         $params += $contextparams2;
455         // Get Calendar Events details for the approved contexts and the owner.
456         $sql = "SELECT ctxid as contextid,
457                        details.id as eventid,
458                        details.name as name,
459                        details.description as description,
460                        details.location as location,
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);
490     }
492     /**
493      * Helper function to return the Calendar Subscriptions for a given user and context list.
494      *
495      * @param approved_contextlist $contextlist
496      * @return array
497      * @throws \coding_exception
498      * @throws \dml_exception
499      */
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
514         ];
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);
535     }
537     /**
538      * Helper function to delete records in batches in order to minimise amount of deletion queries.
539      *
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
544      */
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);
553         }
554     }