Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402]
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 /** 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; 25 26 use core_privacy\local\metadata\collection; 27 use core_privacy\local\request\approved_contextlist; 28 use core_privacy\local\request\approved_userlist; 29 use core_privacy\local\request\contextlist; 30 use core_privacy\local\request\helper; 31 use core_privacy\local\request\transform; 32 use core_privacy\local\request\userlist; 33 use core_privacy\local\request\writer; 34 35 defined('MOODLE_INTERNAL') || die(); 36 37 /** 38 * Privacy Subsystem implementation for mod_lti. 39 * 40 * @copyright 2018 Mark Nelson <markn@moodle.com> 41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 42 */ 43 class provider implements 44 \core_privacy\local\metadata\provider, 45 \core_privacy\local\request\core_userlist_provider, 46 \core_privacy\local\request\plugin\provider { 47 48 /** 49 * Return the fields which contain personal data. 50 * 51 * @param collection $items a reference to the collection to use to store the metadata. 52 * @return collection the updated collection of metadata items. 53 */ 54 public static function get_metadata(collection $items) : collection { 55 $items->add_external_location_link( 56 'lti_provider', 57 [ 58 'userid' => 'privacy:metadata:userid', 59 'username' => 'privacy:metadata:username', 60 'useridnumber' => 'privacy:metadata:useridnumber', 61 'firstname' => 'privacy:metadata:firstname', 62 'lastname' => 'privacy:metadata:lastname', 63 'fullname' => 'privacy:metadata:fullname', 64 'email' => 'privacy:metadata:email', 65 'role' => 'privacy:metadata:role', 66 'courseid' => 'privacy:metadata:courseid', 67 'courseidnumber' => 'privacy:metadata:courseidnumber', 68 'courseshortname' => 'privacy:metadata:courseshortname', 69 'coursefullname' => 'privacy:metadata:coursefullname', 70 ], 71 'privacy:metadata:externalpurpose' 72 ); 73 74 $items->add_database_table( 75 'lti_submission', 76 [ 77 'userid' => 'privacy:metadata:lti_submission:userid', 78 'datesubmitted' => 'privacy:metadata:lti_submission:datesubmitted', 79 'dateupdated' => 'privacy:metadata:lti_submission:dateupdated', 80 'gradepercent' => 'privacy:metadata:lti_submission:gradepercent', 81 'originalgrade' => 'privacy:metadata:lti_submission:originalgrade', 82 ], 83 'privacy:metadata:lti_submission' 84 ); 85 86 $items->add_database_table( 87 'lti_tool_proxies', 88 [ 89 'name' => 'privacy:metadata:lti_tool_proxies:name', 90 'createdby' => 'privacy:metadata:createdby', 91 'timecreated' => 'privacy:metadata:timecreated', 92 'timemodified' => 'privacy:metadata:timemodified' 93 ], 94 'privacy:metadata:lti_tool_proxies' 95 ); 96 97 $items->add_database_table( 98 'lti_types', 99 [ 100 'name' => 'privacy:metadata:lti_types:name', 101 'createdby' => 'privacy:metadata:createdby', 102 'timecreated' => 'privacy:metadata:timecreated', 103 'timemodified' => 'privacy:metadata:timemodified' 104 ], 105 'privacy:metadata:lti_types' 106 ); 107 108 return $items; 109 } 110 111 /** 112 * Get the list of contexts that contain user information for the specified user. 113 * 114 * @param int $userid the userid. 115 * @return contextlist the list of contexts containing user info for the user. 116 */ 117 public static function get_contexts_for_userid(int $userid) : contextlist { 118 // Fetch all LTI submissions. 119 $sql = "SELECT c.id 120 FROM {context} c 121 INNER JOIN {course_modules} cm 122 ON cm.id = c.instanceid 123 AND c.contextlevel = :contextlevel 124 INNER JOIN {modules} m 125 ON m.id = cm.module 126 AND m.name = :modname 127 INNER JOIN {lti} lti 128 ON lti.id = cm.instance 129 INNER JOIN {lti_submission} ltisub 130 ON ltisub.ltiid = lti.id 131 WHERE ltisub.userid = :userid"; 132 133 $params = [ 134 'modname' => 'lti', 135 'contextlevel' => CONTEXT_MODULE, 136 'userid' => $userid, 137 ]; 138 $contextlist = new contextlist(); 139 $contextlist->add_from_sql($sql, $params); 140 141 // Fetch all LTI types. 142 $sql = "SELECT c.id 143 FROM {context} c 144 JOIN {course} course 145 ON c.contextlevel = :contextlevel 146 AND c.instanceid = course.id 147 JOIN {lti_types} ltit 148 ON ltit.course = course.id 149 WHERE ltit.createdby = :userid"; 150 151 $params = [ 152 'contextlevel' => CONTEXT_COURSE, 153 'userid' => $userid 154 ]; 155 $contextlist->add_from_sql($sql, $params); 156 157 // The LTI tool proxies sit in the system context. 158 $contextlist->add_system_context(); 159 160 return $contextlist; 161 } 162 163 /** 164 * Get the list of users who have data within a context. 165 * 166 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. 167 */ 168 public static function get_users_in_context(userlist $userlist) { 169 $context = $userlist->get_context(); 170 171 if (!is_a($context, \context_module::class)) { 172 return; 173 } 174 175 // Fetch all LTI submissions. 176 $sql = "SELECT ltisub.userid 177 FROM {context} c 178 INNER JOIN {course_modules} cm 179 ON cm.id = c.instanceid 180 AND c.contextlevel = :contextlevel 181 INNER JOIN {modules} m 182 ON m.id = cm.module 183 AND m.name = :modname 184 INNER JOIN {lti} lti 185 ON lti.id = cm.instance 186 INNER JOIN {lti_submission} ltisub 187 ON ltisub.ltiid = lti.id 188 WHERE c.id = :contextid"; 189 190 $params = [ 191 'modname' => 'lti', 192 'contextlevel' => CONTEXT_MODULE, 193 'contextid' => $context->id, 194 ]; 195 196 $userlist->add_from_sql('userid', $sql, $params); 197 198 // Fetch all LTI types. 199 $sql = "SELECT ltit.createdby AS userid 200 FROM {context} c 201 JOIN {course} course 202 ON c.contextlevel = :contextlevel 203 AND c.instanceid = course.id 204 JOIN {lti_types} ltit 205 ON ltit.course = course.id 206 WHERE c.id = :contextid"; 207 208 $params = [ 209 'contextlevel' => CONTEXT_COURSE, 210 'contextid' => $context->id, 211 ]; 212 $userlist->add_from_sql('userid', $sql, $params); 213 } 214 215 /** 216 * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. 217 * 218 * @param approved_contextlist $contextlist a list of contexts approved for export. 219 */ 220 public static function export_user_data(approved_contextlist $contextlist) { 221 self::export_user_data_lti_submissions($contextlist); 222 223 self::export_user_data_lti_types($contextlist); 224 225 self::export_user_data_lti_tool_proxies($contextlist); 226 } 227 228 /** 229 * Delete all data for all users in the specified context. 230 * 231 * @param \context $context the context to delete in. 232 */ 233 public static function delete_data_for_all_users_in_context(\context $context) { 234 global $DB; 235 236 if (!$context instanceof \context_module) { 237 return; 238 } 239 240 if ($cm = get_coursemodule_from_id('lti', $context->instanceid)) { 241 $DB->delete_records('lti_submission', ['ltiid' => $cm->instance]); 242 } 243 } 244 245 /** 246 * Delete all user data for the specified user, in the specified contexts. 247 * 248 * @param approved_contextlist $contextlist a list of contexts approved for deletion. 249 */ 250 public static function delete_data_for_user(approved_contextlist $contextlist) { 251 global $DB; 252 253 if (empty($contextlist->count())) { 254 return; 255 } 256 257 $userid = $contextlist->get_user()->id; 258 foreach ($contextlist->get_contexts() as $context) { 259 if (!$context instanceof \context_module) { 260 continue; 261 } 262 $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); 263 $DB->delete_records('lti_submission', ['ltiid' => $instanceid, 'userid' => $userid]); 264 } 265 } 266 267 /** 268 * Delete multiple users within a single context. 269 * 270 * @param approved_userlist $userlist The approved context and user information to delete information for. 271 */ 272 public static function delete_data_for_users(approved_userlist $userlist) { 273 global $DB; 274 275 $context = $userlist->get_context(); 276 277 if ($context instanceof \context_module) { 278 $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); 279 280 list($insql, $inparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); 281 $sql = "ltiid = :instanceid AND userid {$insql}"; 282 $params = array_merge(['instanceid' => $instanceid], $inparams); 283 284 $DB->delete_records_select('lti_submission', $sql, $params); 285 } 286 } 287 288 /** 289 * Export personal data for the given approved_contextlist related to LTI submissions. 290 * 291 * @param approved_contextlist $contextlist a list of contexts approved for export. 292 */ 293 protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) { 294 global $DB; 295 296 // Filter out any contexts that are not related to modules. 297 $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { 298 if ($context->contextlevel == CONTEXT_MODULE) { 299 $carry[] = $context->instanceid; 300 } 301 return $carry; 302 }, []); 303 304 if (empty($cmids)) { 305 return; 306 } 307 308 $user = $contextlist->get_user(); 309 310 // Get all the LTI activities associated with the above course modules. 311 $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids); 312 $ltiids = array_keys($ltiidstocmids); 313 314 list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED); 315 $params = array_merge($inparams, ['userid' => $user->id]); 316 $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id'); 317 self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) { 318 $carry[] = [ 319 'gradepercent' => $record->gradepercent, 320 'originalgrade' => $record->originalgrade, 321 'datesubmitted' => transform::datetime($record->datesubmitted), 322 'dateupdated' => transform::datetime($record->dateupdated) 323 ]; 324 return $carry; 325 }, function($ltiid, $data) use ($user, $ltiidstocmids) { 326 $context = \context_module::instance($ltiidstocmids[$ltiid]); 327 $contextdata = helper::get_context_data($context, $user); 328 $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]); 329 helper::export_context_files($context, $user); 330 writer::with_context($context)->export_data([], $finaldata); 331 }); 332 } 333 334 /** 335 * Export personal data for the given approved_contextlist related to LTI types. 336 * 337 * @param approved_contextlist $contextlist a list of contexts approved for export. 338 */ 339 protected static function export_user_data_lti_types(approved_contextlist $contextlist) { 340 global $DB; 341 342 // Filter out any contexts that are not related to courses. 343 $courseids = array_reduce($contextlist->get_contexts(), function($carry, $context) { 344 if ($context->contextlevel == CONTEXT_COURSE) { 345 $carry[] = $context->instanceid; 346 } 347 return $carry; 348 }, []); 349 350 if (empty($courseids)) { 351 return; 352 } 353 354 $user = $contextlist->get_user(); 355 356 list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); 357 $params = array_merge($inparams, ['userid' => $user->id]); 358 $ltitypes = $DB->get_recordset_select('lti_types', "course $insql AND createdby = :userid", $params, 'timecreated ASC'); 359 self::recordset_loop_and_export($ltitypes, 'course', [], function($carry, $record) { 360 $context = \context_course::instance($record->course); 361 $options = ['context' => $context]; 362 $carry[] = [ 363 'name' => format_string($record->name, true, $options), 364 'createdby' => transform::user($record->createdby), 365 'timecreated' => transform::datetime($record->timecreated), 366 'timemodified' => transform::datetime($record->timemodified) 367 ]; 368 return $carry; 369 }, function($courseid, $data) { 370 $context = \context_course::instance($courseid); 371 $finaldata = (object) ['lti_types' => $data]; 372 writer::with_context($context)->export_data([], $finaldata); 373 }); 374 } 375 376 /** 377 * Export personal data for the given approved_contextlist related to LTI tool proxies. 378 * 379 * @param approved_contextlist $contextlist a list of contexts approved for export. 380 */ 381 protected static function export_user_data_lti_tool_proxies(approved_contextlist $contextlist) { 382 global $DB; 383 384 // Filter out any contexts that are not related to system context. 385 $systemcontexts = array_filter($contextlist->get_contexts(), function($context) { 386 return $context->contextlevel == CONTEXT_SYSTEM; 387 }); 388 389 if (empty($systemcontexts)) { 390 return; 391 } 392 393 $user = $contextlist->get_user(); 394 395 $systemcontext = \context_system::instance(); 396 397 $data = []; 398 $ltiproxies = $DB->get_recordset('lti_tool_proxies', ['createdby' => $user->id], 'timecreated ASC'); 399 foreach ($ltiproxies as $ltiproxy) { 400 $data[] = [ 401 'name' => format_string($ltiproxy->name, true, ['context' => $systemcontext]), 402 'createdby' => transform::user($ltiproxy->createdby), 403 'timecreated' => transform::datetime($ltiproxy->timecreated), 404 'timemodified' => transform::datetime($ltiproxy->timemodified) 405 ]; 406 } 407 $ltiproxies->close(); 408 409 $finaldata = (object) ['lti_tool_proxies' => $data]; 410 writer::with_context($systemcontext)->export_data([], $finaldata); 411 } 412 413 /** 414 * Return a dict of LTI IDs mapped to their course module ID. 415 * 416 * @param array $cmids The course module IDs. 417 * @return array In the form of [$ltiid => $cmid]. 418 */ 419 protected static function get_lti_ids_to_cmids_from_cmids(array $cmids) { 420 global $DB; 421 422 list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); 423 $sql = "SELECT lti.id, cm.id AS cmid 424 FROM {lti} lti 425 JOIN {modules} m 426 ON m.name = :lti 427 JOIN {course_modules} cm 428 ON cm.instance = lti.id 429 AND cm.module = m.id 430 WHERE cm.id $insql"; 431 $params = array_merge($inparams, ['lti' => 'lti']); 432 433 return $DB->get_records_sql_menu($sql, $params); 434 } 435 436 /** 437 * Loop and export from a recordset. 438 * 439 * @param \moodle_recordset $recordset The recordset. 440 * @param string $splitkey The record key to determine when to export. 441 * @param mixed $initial The initial data to reduce from. 442 * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. 443 * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. 444 * @return void 445 */ 446 protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, 447 callable $reducer, callable $export) { 448 $data = $initial; 449 $lastid = null; 450 451 foreach ($recordset as $record) { 452 if ($lastid && $record->{$splitkey} != $lastid) { 453 $export($lastid, $data); 454 $data = $initial; 455 } 456 $data = $reducer($data, $record); 457 $lastid = $record->{$splitkey}; 458 } 459 $recordset->close(); 460 461 if (!empty($lastid)) { 462 $export($lastid, $data); 463 } 464 } 465 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body