MDL-62456 mod_lti: add missing add_external_location_link call
[moodle.git] / mod / lti / 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/>.
17 /**
18  * Privacy Subsystem implementation for mod_lti.
19  *
20  * @package    mod_lti
21  * @copyright  2018 Mark Nelson <markn@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace mod_lti\privacy;
26 use core_privacy\local\metadata\collection;
27 use core_privacy\local\request\approved_contextlist;
28 use core_privacy\local\request\contextlist;
29 use core_privacy\local\request\helper;
30 use core_privacy\local\request\transform;
31 use core_privacy\local\request\writer;
33 defined('MOODLE_INTERNAL') || die();
35 /**
36  * Privacy Subsystem implementation for mod_lti.
37  *
38  * @copyright  2018 Mark Nelson <markn@moodle.com>
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class provider implements
42     \core_privacy\local\metadata\provider,
43     \core_privacy\local\request\plugin\provider {
45     /**
46      * Return the fields which contain personal data.
47      *
48      * @param collection $items a reference to the collection to use to store the metadata.
49      * @return collection the updated collection of metadata items.
50      */
51     public static function get_metadata(collection $items) : collection {
52         $items->add_external_location_link(
53             'lti_provider',
54             [
55                 'userid' => 'privacy:metadata:userid',
56                 'username' => 'privacy:metadata:username',
57                 'useridnumber' => 'privacy:metadata:useridnumber',
58                 'firstname' => 'privacy:metadata:firstname',
59                 'lastname' => 'privacy:metadata:lastname',
60                 'fullname' => 'privacy:metadata:fullname',
61                 'email' => 'privacy:metadata:email',
62                 'role' => 'privacy:metadata:role',
63                 'courseid' => 'privacy:metadata:courseid',
64                 'courseidnumber' => 'privacy:metadata:courseidnumber',
65                 'courseshortname' => 'privacy:metadata:courseshortname',
66                 'coursefullname' => 'privacy:metadata:coursefullname',
67             ],
68             'privacy:metadata:externalpurpose'
69         );
71         $items->add_database_table(
72             'lti_submission',
73             [
74                 'userid' => 'privacy:metadata:lti_submission:userid',
75                 'datesubmitted' => 'privacy:metadata:lti_submission:datesubmitted',
76                 'dateupdated' => 'privacy:metadata:lti_submission:dateupdated',
77                 'gradepercent' => 'privacy:metadata:lti_submission:gradepercent',
78                 'originalgrade' => 'privacy:metadata:lti_submission:originalgrade',
79             ],
80             'privacy:metadata:lti_submission'
81         );
83         $items->add_database_table(
84             'lti_tool_proxies',
85             [
86                 'name' => 'privacy:metadata:lti_tool_proxies:name',
87                 'createdby' => 'privacy:metadata:createdby',
88                 'timecreated' => 'privacy:metadata:timecreated',
89                 'timemodified' => 'privacy:metadata:timemodified'
90             ],
91             'privacy:metadata:lti_tool_proxies'
92         );
94         $items->add_database_table(
95             'lti_types',
96             [
97                 'name' => 'privacy:metadata:lti_types:name',
98                 'createdby' => 'privacy:metadata:createdby',
99                 'timecreated' => 'privacy:metadata:timecreated',
100                 'timemodified' => 'privacy:metadata:timemodified'
101             ],
102             'privacy:metadata:lti_types'
103         );
105         return $items;
106     }
108     /**
109      * Get the list of contexts that contain user information for the specified user.
110      *
111      * @param int $userid the userid.
112      * @return contextlist the list of contexts containing user info for the user.
113      */
114     public static function get_contexts_for_userid(int $userid) : contextlist {
115         // Fetch all LTI submissions.
116         $sql = "SELECT c.id
117                   FROM {context} c
118             INNER JOIN {course_modules} cm
119                     ON cm.id = c.instanceid
120                    AND c.contextlevel = :contextlevel
121             INNER JOIN {modules} m
122                     ON m.id = cm.module
123                    AND m.name = :modname
124             INNER JOIN {lti} lti
125                     ON lti.id = cm.instance
126             INNER JOIN {lti_submission} ltisub
127                     ON ltisub.ltiid = lti.id
128                  WHERE ltisub.userid = :userid";
130         $params = [
131             'modname' => 'lti',
132             'contextlevel' => CONTEXT_MODULE,
133             'userid' => $userid,
134         ];
135         $contextlist = new contextlist();
136         $contextlist->add_from_sql($sql, $params);
138         // Fetch all LTI types.
139         $sql = "SELECT c.id
140                  FROM {context} c
141                  JOIN {course} course
142                    ON c.contextlevel = :contextlevel
143                   AND c.instanceid = course.id
144                  JOIN {lti_types} ltit
145                    ON ltit.course = course.id
146                 WHERE ltit.createdby = :userid";
148         $params = [
149             'contextlevel' => CONTEXT_COURSE,
150             'userid' => $userid
151         ];
152         $contextlist->add_from_sql($sql, $params);
154         // The LTI tool proxies sit in the system context.
155         $contextlist->add_system_context();
157         return $contextlist;
158     }
160     /**
161      * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
162      *
163      * @param approved_contextlist $contextlist a list of contexts approved for export.
164      */
165     public static function export_user_data(approved_contextlist $contextlist) {
166         self::export_user_data_lti_submissions($contextlist);
168         self::export_user_data_lti_types($contextlist);
170         self::export_user_data_lti_tool_proxies($contextlist);
171     }
173     /**
174      * Delete all data for all users in the specified context.
175      *
176      * @param \context $context the context to delete in.
177      */
178     public static function delete_data_for_all_users_in_context(\context $context) {
179         global $DB;
181         if (!$context instanceof \context_module) {
182             return;
183         }
185         if ($cm = get_coursemodule_from_id('lti', $context->instanceid)) {
186             $DB->delete_records('lti_submission', ['ltiid' => $cm->instance]);
187         }
188     }
190     /**
191      * Delete all user data for the specified user, in the specified contexts.
192      *
193      * @param approved_contextlist $contextlist a list of contexts approved for deletion.
194      */
195     public static function delete_data_for_user(approved_contextlist $contextlist) {
196         global $DB;
198         if (empty($contextlist->count())) {
199             return;
200         }
202         $userid = $contextlist->get_user()->id;
203         foreach ($contextlist->get_contexts() as $context) {
204             if (!$context instanceof \context_module) {
205                 continue;
206             }
207             $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
208             $DB->delete_records('lti_submission', ['ltiid' => $instanceid, 'userid' => $userid]);
209         }
210     }
212     /**
213      * Export personal data for the given approved_contextlist related to LTI submissions.
214      *
215      * @param approved_contextlist $contextlist a list of contexts approved for export.
216      */
217     protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) {
218         global $DB;
220         // Filter out any contexts that are not related to modules.
221         $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
222             if ($context->contextlevel == CONTEXT_MODULE) {
223                 $carry[] = $context->instanceid;
224             }
225             return $carry;
226         }, []);
228         if (empty($cmids)) {
229             return;
230         }
232         $user = $contextlist->get_user();
234         // Get all the LTI activities associated with the above course modules.
235         $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids);
236         $ltiids = array_keys($ltiidstocmids);
238         list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED);
239         $params = array_merge($inparams, ['userid' => $user->id]);
240         $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id');
241         self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) {
242             $carry[] = [
243                 'gradepercent' => $record->gradepercent,
244                 'originalgrade' => $record->originalgrade,
245                 'datesubmitted' => transform::datetime($record->datesubmitted),
246                 'dateupdated' => transform::datetime($record->dateupdated)
247             ];
248             return $carry;
249         }, function($ltiid, $data) use ($user, $ltiidstocmids) {
250             $context = \context_module::instance($ltiidstocmids[$ltiid]);
251             $contextdata = helper::get_context_data($context, $user);
252             $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]);
253             helper::export_context_files($context, $user);
254             writer::with_context($context)->export_data([], $finaldata);
255         });
256     }
258     /**
259      * Export personal data for the given approved_contextlist related to LTI types.
260      *
261      * @param approved_contextlist $contextlist a list of contexts approved for export.
262      */
263     protected static function export_user_data_lti_types(approved_contextlist $contextlist) {
264         global $DB;
266         // Filter out any contexts that are not related to courses.
267         $courseids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
268             if ($context->contextlevel == CONTEXT_COURSE) {
269                 $carry[] = $context->instanceid;
270             }
271             return $carry;
272         }, []);
274         if (empty($courseids)) {
275             return;
276         }
278         $user = $contextlist->get_user();
280         list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
281         $params = array_merge($inparams, ['userid' => $user->id]);
282         $ltitypes = $DB->get_recordset_select('lti_types', "course $insql AND createdby = :userid", $params, 'timecreated ASC');
283         self::recordset_loop_and_export($ltitypes, 'course', [], function($carry, $record) {
284             $context = \context_course::instance($record->course);
285             $options = ['context' => $context];
286             $carry[] = [
287                 'name' => format_string($record->name, true, $options),
288                 'createdby' => transform::user($record->createdby),
289                 'timecreated' => transform::datetime($record->timecreated),
290                 'timemodified' => transform::datetime($record->timemodified)
291             ];
292             return $carry;
293         }, function($courseid, $data) {
294             $context = \context_course::instance($courseid);
295             $finaldata = (object) ['lti_types' => $data];
296             writer::with_context($context)->export_data([], $finaldata);
297         });
298     }
300     /**
301      * Export personal data for the given approved_contextlist related to LTI tool proxies.
302      *
303      * @param approved_contextlist $contextlist a list of contexts approved for export.
304      */
305     protected static function export_user_data_lti_tool_proxies(approved_contextlist $contextlist) {
306         global $DB;
308         // Filter out any contexts that are not related to system context.
309         $systemcontexts = array_filter($contextlist->get_contexts(), function($context) {
310             return $context->contextlevel == CONTEXT_SYSTEM;
311         });
313         if (empty($systemcontexts)) {
314             return;
315         }
317         $user = $contextlist->get_user();
319         $systemcontext = \context_system::instance();
321         $data = [];
322         $ltiproxies = $DB->get_recordset('lti_tool_proxies', ['createdby' => $user->id], 'timecreated ASC');
323         foreach ($ltiproxies as $ltiproxy) {
324             $data[] = [
325                 'name' => format_string($ltiproxy->name, true, $systemcontext),
326                 'createdby' => transform::user($ltiproxy->createdby),
327                 'timecreated' => transform::datetime($ltiproxy->timecreated),
328                 'timemodified' => transform::datetime($ltiproxy->timemodified)
329             ];
330         }
331         $ltiproxies->close();
333         $finaldata = (object) ['lti_tool_proxies' => $data];
334         writer::with_context($systemcontext)->export_data([], $finaldata);
335     }
337     /**
338      * Return a dict of LTI IDs mapped to their course module ID.
339      *
340      * @param array $cmids The course module IDs.
341      * @return array In the form of [$ltiid => $cmid].
342      */
343     protected static function get_lti_ids_to_cmids_from_cmids(array $cmids) {
344         global $DB;
346         list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
347         $sql = "SELECT lti.id, cm.id AS cmid
348                  FROM {lti} lti
349                  JOIN {modules} m
350                    ON m.name = :lti
351                  JOIN {course_modules} cm
352                    ON cm.instance = lti.id
353                   AND cm.module = m.id
354                 WHERE cm.id $insql";
355         $params = array_merge($inparams, ['lti' => 'lti']);
357         return $DB->get_records_sql_menu($sql, $params);
358     }
360     /**
361      * Loop and export from a recordset.
362      *
363      * @param \moodle_recordset $recordset The recordset.
364      * @param string $splitkey The record key to determine when to export.
365      * @param mixed $initial The initial data to reduce from.
366      * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
367      * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
368      * @return void
369      */
370     protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
371                                                         callable $reducer, callable $export) {
372         $data = $initial;
373         $lastid = null;
375         foreach ($recordset as $record) {
376             if ($lastid && $record->{$splitkey} != $lastid) {
377                 $export($lastid, $data);
378                 $data = $initial;
379             }
380             $data = $reducer($data, $record);
381             $lastid = $record->{$splitkey};
382         }
383         $recordset->close();
385         if (!empty($lastid)) {
386             $export($lastid, $data);
387         }
388     }