022e4c046f4b463cc7a89d1eeeacfc30e261bf10
[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_database_table(
53             'lti_submission',
54             [
55                 'userid' => 'privacy:metadata:lti_submission:userid',
56                 'datesubmitted' => 'privacy:metadata:lti_submission:datesubmitted',
57                 'dateupdated' => 'privacy:metadata:lti_submission:dateupdated',
58                 'gradepercent' => 'privacy:metadata:lti_submission:gradepercent',
59                 'originalgrade' => 'privacy:metadata:lti_submission:originalgrade',
60             ],
61             'privacy:metadata:lti_submission'
62         );
64         $items->add_database_table(
65             'lti_tool_proxies',
66             [
67                 'name' => 'privacy:metadata:lti_tool_proxies:name',
68                 'createdby' => 'privacy:metadata:createdby',
69                 'timecreated' => 'privacy:metadata:timecreated',
70                 'timemodified' => 'privacy:metadata:timemodified'
71             ],
72             'privacy:metadata:lti_tool_proxies'
73         );
75         $items->add_database_table(
76             'lti_types',
77             [
78                 'name' => 'privacy:metadata:lti_types:name',
79                 'createdby' => 'privacy:metadata:createdby',
80                 'timecreated' => 'privacy:metadata:timecreated',
81                 'timemodified' => 'privacy:metadata:timemodified'
82             ],
83             'privacy:metadata:lti_types'
84         );
86         return $items;
87     }
89     /**
90      * Get the list of contexts that contain user information for the specified user.
91      *
92      * @param int $userid the userid.
93      * @return contextlist the list of contexts containing user info for the user.
94      */
95     public static function get_contexts_for_userid(int $userid) : contextlist {
96         // Fetch all LTI submissions.
97         $sql = "SELECT c.id
98                   FROM {context} c
99             INNER JOIN {course_modules} cm
100                     ON cm.id = c.instanceid
101                    AND c.contextlevel = :contextlevel
102             INNER JOIN {modules} m
103                     ON m.id = cm.module
104                    AND m.name = :modname
105             INNER JOIN {lti} lti
106                     ON lti.id = cm.instance
107             INNER JOIN {lti_submission} ltisub
108                     ON ltisub.ltiid = lti.id
109                  WHERE ltisub.userid = :userid";
111         $params = [
112             'modname' => 'lti',
113             'contextlevel' => CONTEXT_MODULE,
114             'userid' => $userid,
115         ];
116         $contextlist = new contextlist();
117         $contextlist->add_from_sql($sql, $params);
119         // Fetch all LTI types.
120         $sql = "SELECT c.id
121                  FROM {context} c
122                  JOIN {course} course
123                    ON c.contextlevel = :contextlevel
124                   AND c.instanceid = course.id
125                  JOIN {lti_types} ltit
126                    ON ltit.course = course.id
127                 WHERE ltit.createdby = :userid";
129         $params = [
130             'contextlevel' => CONTEXT_COURSE,
131             'userid' => $userid
132         ];
133         $contextlist->add_from_sql($sql, $params);
135         // The LTI tool proxies sit in the system context.
136         $contextlist->add_system_context();
138         return $contextlist;
139     }
141     /**
142      * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
143      *
144      * @param approved_contextlist $contextlist a list of contexts approved for export.
145      */
146     public static function export_user_data(approved_contextlist $contextlist) {
147         self::export_user_data_lti_submissions($contextlist);
149         self::export_user_data_lti_types($contextlist);
151         self::export_user_data_lti_tool_proxies($contextlist);
152     }
154     /**
155      * Delete all data for all users in the specified context.
156      *
157      * @param \context $context the context to delete in.
158      */
159     public static function delete_data_for_all_users_in_context(\context $context) {
160         global $DB;
162         if (!$context instanceof \context_module) {
163             return;
164         }
166         if ($cm = get_coursemodule_from_id('lti', $context->instanceid)) {
167             $DB->delete_records('lti_submission', ['ltiid' => $cm->instance]);
168         }
169     }
171     /**
172      * Delete all user data for the specified user, in the specified contexts.
173      *
174      * @param approved_contextlist $contextlist a list of contexts approved for deletion.
175      */
176     public static function delete_data_for_user(approved_contextlist $contextlist) {
177         global $DB;
179         if (empty($contextlist->count())) {
180             return;
181         }
183         $userid = $contextlist->get_user()->id;
184         foreach ($contextlist->get_contexts() as $context) {
185             if (!$context instanceof \context_module) {
186                 continue;
187             }
188             $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
189             $DB->delete_records('lti_submission', ['ltiid' => $instanceid, 'userid' => $userid]);
190         }
191     }
193     /**
194      * Export personal data for the given approved_contextlist related to LTI submissions.
195      *
196      * @param approved_contextlist $contextlist a list of contexts approved for export.
197      */
198     protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) {
199         global $DB;
201         // Filter out any contexts that are not related to modules.
202         $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
203             if ($context->contextlevel == CONTEXT_MODULE) {
204                 $carry[] = $context->instanceid;
205             }
206             return $carry;
207         }, []);
209         if (empty($cmids)) {
210             return;
211         }
213         $user = $contextlist->get_user();
215         // Get all the LTI activities associated with the above course modules.
216         $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids);
217         $ltiids = array_keys($ltiidstocmids);
219         list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED);
220         $params = array_merge($inparams, ['userid' => $user->id]);
221         $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id');
222         self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) {
223             $carry[] = [
224                 'gradepercent' => $record->gradepercent,
225                 'originalgrade' => $record->originalgrade,
226                 'datesubmitted' => transform::datetime($record->datesubmitted),
227                 'dateupdated' => transform::datetime($record->dateupdated)
228             ];
229             return $carry;
230         }, function($ltiid, $data) use ($user, $ltiidstocmids) {
231             $context = \context_module::instance($ltiidstocmids[$ltiid]);
232             $contextdata = helper::get_context_data($context, $user);
233             $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]);
234             helper::export_context_files($context, $user);
235             writer::with_context($context)->export_data([], $finaldata);
236         });
237     }
239     /**
240      * Export personal data for the given approved_contextlist related to LTI types.
241      *
242      * @param approved_contextlist $contextlist a list of contexts approved for export.
243      */
244     protected static function export_user_data_lti_types(approved_contextlist $contextlist) {
245         global $DB;
247         // Filter out any contexts that are not related to courses.
248         $courseids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
249             if ($context->contextlevel == CONTEXT_COURSE) {
250                 $carry[] = $context->instanceid;
251             }
252             return $carry;
253         }, []);
255         if (empty($courseids)) {
256             return;
257         }
259         $user = $contextlist->get_user();
261         list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
262         $params = array_merge($inparams, ['userid' => $user->id]);
263         $ltitypes = $DB->get_recordset_select('lti_types', "course $insql AND createdby = :userid", $params, 'timecreated ASC');
264         self::recordset_loop_and_export($ltitypes, 'course', [], function($carry, $record) {
265             $context = \context_course::instance($record->course);
266             $options = ['context' => $context];
267             $carry[] = [
268                 'name' => format_string($record->name, true, $options),
269                 'createdby' => transform::user($record->createdby),
270                 'timecreated' => transform::datetime($record->timecreated),
271                 'timemodified' => transform::datetime($record->timemodified)
272             ];
273             return $carry;
274         }, function($courseid, $data) {
275             $context = \context_course::instance($courseid);
276             $finaldata = (object) ['lti_types' => $data];
277             writer::with_context($context)->export_data([], $finaldata);
278         });
279     }
281     /**
282      * Export personal data for the given approved_contextlist related to LTI tool proxies.
283      *
284      * @param approved_contextlist $contextlist a list of contexts approved for export.
285      */
286     protected static function export_user_data_lti_tool_proxies(approved_contextlist $contextlist) {
287         global $DB;
289         // Filter out any contexts that are not related to system context.
290         $systemcontexts = array_filter($contextlist->get_contexts(), function($context) {
291             return $context->contextlevel == CONTEXT_SYSTEM;
292         });
294         if (empty($systemcontexts)) {
295             return;
296         }
298         $user = $contextlist->get_user();
300         $systemcontext = \context_system::instance();
302         $data = [];
303         $ltiproxies = $DB->get_recordset('lti_tool_proxies', ['createdby' => $user->id], 'timecreated ASC');
304         foreach ($ltiproxies as $ltiproxy) {
305             $data[] = [
306                 'name' => format_string($ltiproxy->name, true, $systemcontext),
307                 'createdby' => transform::user($ltiproxy->createdby),
308                 'timecreated' => transform::datetime($ltiproxy->timecreated),
309                 'timemodified' => transform::datetime($ltiproxy->timemodified)
310             ];
311         }
312         $ltiproxies->close();
314         $finaldata = (object) ['lti_tool_proxies' => $data];
315         writer::with_context($systemcontext)->export_data([], $finaldata);
316     }
318     /**
319      * Return a dict of LTI IDs mapped to their course module ID.
320      *
321      * @param array $cmids The course module IDs.
322      * @return array In the form of [$ltiid => $cmid].
323      */
324     protected static function get_lti_ids_to_cmids_from_cmids(array $cmids) {
325         global $DB;
327         list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
328         $sql = "SELECT lti.id, cm.id AS cmid
329                  FROM {lti} lti
330                  JOIN {modules} m
331                    ON m.name = :lti
332                  JOIN {course_modules} cm
333                    ON cm.instance = lti.id
334                   AND cm.module = m.id
335                 WHERE cm.id $insql";
336         $params = array_merge($inparams, ['lti' => 'lti']);
338         return $DB->get_records_sql_menu($sql, $params);
339     }
341     /**
342      * Loop and export from a recordset.
343      *
344      * @param \moodle_recordset $recordset The recordset.
345      * @param string $splitkey The record key to determine when to export.
346      * @param mixed $initial The initial data to reduce from.
347      * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
348      * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
349      * @return void
350      */
351     protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
352                                                         callable $reducer, callable $export) {
353         $data = $initial;
354         $lastid = null;
356         foreach ($recordset as $record) {
357             if ($lastid && $record->{$splitkey} != $lastid) {
358                 $export($lastid, $data);
359                 $data = $initial;
360             }
361             $data = $reducer($data, $record);
362             $lastid = $record->{$splitkey};
363         }
364         $recordset->close();
366         if (!empty($lastid)) {
367             $export($lastid, $data);
368         }
369     }