Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]
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_quiz. 19 * 20 * @package mod_quiz 21 * @category privacy 22 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace mod_quiz\privacy; 27 28 use core_privacy\local\request\approved_contextlist; 29 use core_privacy\local\request\approved_userlist; 30 use core_privacy\local\request\contextlist; 31 use core_privacy\local\request\deletion_criteria; 32 use core_privacy\local\request\transform; 33 use core_privacy\local\metadata\collection; 34 use core_privacy\local\request\userlist; 35 use core_privacy\local\request\writer; 36 use core_privacy\manager; 37 use mod_quiz\quiz_attempt; 38 39 defined('MOODLE_INTERNAL') || die(); 40 41 require_once($CFG->dirroot . '/mod/quiz/lib.php'); 42 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 43 44 /** 45 * Privacy Subsystem implementation for mod_quiz. 46 * 47 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> 48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 49 */ 50 class provider implements 51 // This plugin has data. 52 \core_privacy\local\metadata\provider, 53 54 // This plugin currently implements the original plugin_provider interface. 55 \core_privacy\local\request\plugin\provider, 56 57 // This plugin is capable of determining which users have data within it. 58 \core_privacy\local\request\core_userlist_provider { 59 60 /** 61 * Get the list of contexts that contain user information for the specified user. 62 * 63 * @param collection $items The collection to add metadata to. 64 * @return collection The array of metadata 65 */ 66 public static function get_metadata(collection $items) : collection { 67 // The table 'quiz' stores a record for each quiz. 68 // It does not contain user personal data, but data is returned from it for contextual requirements. 69 70 // The table 'quiz_attempts' stores a record of each quiz attempt. 71 // It contains a userid which links to the user making the attempt and contains information about that attempt. 72 $items->add_database_table('quiz_attempts', [ 73 'attempt' => 'privacy:metadata:quiz_attempts:attempt', 74 'currentpage' => 'privacy:metadata:quiz_attempts:currentpage', 75 'preview' => 'privacy:metadata:quiz_attempts:preview', 76 'state' => 'privacy:metadata:quiz_attempts:state', 77 'timestart' => 'privacy:metadata:quiz_attempts:timestart', 78 'timefinish' => 'privacy:metadata:quiz_attempts:timefinish', 79 'timemodified' => 'privacy:metadata:quiz_attempts:timemodified', 80 'timemodifiedoffline' => 'privacy:metadata:quiz_attempts:timemodifiedoffline', 81 'timecheckstate' => 'privacy:metadata:quiz_attempts:timecheckstate', 82 'sumgrades' => 'privacy:metadata:quiz_attempts:sumgrades', 83 'gradednotificationsenttime' => 'privacy:metadata:quiz_attempts:gradednotificationsenttime', 84 ], 'privacy:metadata:quiz_attempts'); 85 86 // The table 'quiz_feedback' contains the feedback responses which will be shown to users depending upon the 87 // grade they achieve in the quiz. 88 // It does not identify the user who wrote the feedback item so cannot be returned directly and is not 89 // described, but relevant feedback items will be included with the quiz export for a user who has a grade. 90 91 // The table 'quiz_grades' contains the current grade for each quiz/user combination. 92 $items->add_database_table('quiz_grades', [ 93 'quiz' => 'privacy:metadata:quiz_grades:quiz', 94 'userid' => 'privacy:metadata:quiz_grades:userid', 95 'grade' => 'privacy:metadata:quiz_grades:grade', 96 'timemodified' => 'privacy:metadata:quiz_grades:timemodified', 97 ], 'privacy:metadata:quiz_grades'); 98 99 // The table 'quiz_overrides' contains any user or group overrides for users. 100 // It should be included where data exists for a user. 101 $items->add_database_table('quiz_overrides', [ 102 'quiz' => 'privacy:metadata:quiz_overrides:quiz', 103 'userid' => 'privacy:metadata:quiz_overrides:userid', 104 'timeopen' => 'privacy:metadata:quiz_overrides:timeopen', 105 'timeclose' => 'privacy:metadata:quiz_overrides:timeclose', 106 'timelimit' => 'privacy:metadata:quiz_overrides:timelimit', 107 ], 'privacy:metadata:quiz_overrides'); 108 109 // These define the structure of the quiz. 110 111 // The table 'quiz_sections' contains data about the structure of a quiz. 112 // It does not contain any user identifying data and does not need a mapping. 113 114 // The table 'quiz_slots' contains data about the structure of a quiz. 115 // It does not contain any user identifying data and does not need a mapping. 116 117 // The table 'quiz_reports' does not contain any user identifying data and does not need a mapping. 118 119 // The table 'quiz_statistics' contains abstract statistics about question usage and cannot be mapped to any 120 // specific user. 121 // It does not contain any user identifying data and does not need a mapping. 122 123 // The quiz links to the 'core_question' subsystem for all question functionality. 124 $items->add_subsystem_link('core_question', [], 'privacy:metadata:core_question'); 125 126 // The quiz has two subplugins.. 127 $items->add_plugintype_link('quiz', [], 'privacy:metadata:quiz'); 128 $items->add_plugintype_link('quizaccess', [], 'privacy:metadata:quizaccess'); 129 130 // Although the quiz supports the core_completion API and defines custom completion items, these will be 131 // noted by the manager as all activity modules are capable of supporting this functionality. 132 133 return $items; 134 } 135 136 /** 137 * Get the list of contexts where the specified user has attempted a quiz, or been involved with manual marking 138 * and/or grading of a quiz. 139 * 140 * @param int $userid The user to search. 141 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. 142 */ 143 public static function get_contexts_for_userid(int $userid) : contextlist { 144 $resultset = new contextlist(); 145 146 // Users who attempted the quiz. 147 $sql = "SELECT c.id 148 FROM {context} c 149 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 150 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 151 JOIN {quiz} q ON q.id = cm.instance 152 JOIN {quiz_attempts} qa ON qa.quiz = q.id 153 WHERE qa.userid = :userid AND qa.preview = 0"; 154 $params = ['contextlevel' => CONTEXT_MODULE, 'modname' => 'quiz', 'userid' => $userid]; 155 $resultset->add_from_sql($sql, $params); 156 157 // Users with quiz overrides. 158 $sql = "SELECT c.id 159 FROM {context} c 160 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 161 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 162 JOIN {quiz} q ON q.id = cm.instance 163 JOIN {quiz_overrides} qo ON qo.quiz = q.id 164 WHERE qo.userid = :userid"; 165 $params = ['contextlevel' => CONTEXT_MODULE, 'modname' => 'quiz', 'userid' => $userid]; 166 $resultset->add_from_sql($sql, $params); 167 168 // Get the SQL used to link indirect question usages for the user. 169 // This includes where a user is the manual marker on a question attempt. 170 $qubaid = \core_question\privacy\provider::get_related_question_usages_for_user('rel', 'mod_quiz', 'qa.uniqueid', $userid); 171 172 // Select the context of any quiz attempt where a user has an attempt, plus the related usages. 173 $sql = "SELECT c.id 174 FROM {context} c 175 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 176 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 177 JOIN {quiz} q ON q.id = cm.instance 178 JOIN {quiz_attempts} qa ON qa.quiz = q.id 179 " . $qubaid->from . " 180 WHERE " . $qubaid->where() . " AND qa.preview = 0"; 181 $params = ['contextlevel' => CONTEXT_MODULE, 'modname' => 'quiz'] + $qubaid->from_where_params(); 182 $resultset->add_from_sql($sql, $params); 183 184 return $resultset; 185 } 186 187 /** 188 * Get the list of users who have data within a context. 189 * 190 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. 191 */ 192 public static function get_users_in_context(userlist $userlist) { 193 $context = $userlist->get_context(); 194 195 if (!$context instanceof \context_module) { 196 return; 197 } 198 199 $params = [ 200 'cmid' => $context->instanceid, 201 'modname' => 'quiz', 202 ]; 203 204 // Users who attempted the quiz. 205 $sql = "SELECT qa.userid 206 FROM {course_modules} cm 207 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 208 JOIN {quiz} q ON q.id = cm.instance 209 JOIN {quiz_attempts} qa ON qa.quiz = q.id 210 WHERE cm.id = :cmid AND qa.preview = 0"; 211 $userlist->add_from_sql('userid', $sql, $params); 212 213 // Users with quiz overrides. 214 $sql = "SELECT qo.userid 215 FROM {course_modules} cm 216 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 217 JOIN {quiz} q ON q.id = cm.instance 218 JOIN {quiz_overrides} qo ON qo.quiz = q.id 219 WHERE cm.id = :cmid"; 220 $userlist->add_from_sql('userid', $sql, $params); 221 222 // Question usages in context. 223 // This includes where a user is the manual marker on a question attempt. 224 $sql = "SELECT qa.uniqueid 225 FROM {course_modules} cm 226 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 227 JOIN {quiz} q ON q.id = cm.instance 228 JOIN {quiz_attempts} qa ON qa.quiz = q.id 229 WHERE cm.id = :cmid AND qa.preview = 0"; 230 \core_question\privacy\provider::get_users_in_context_from_sql($userlist, 'qn', $sql, $params); 231 } 232 233 /** 234 * Export all user data for the specified user, in the specified contexts. 235 * 236 * @param approved_contextlist $contextlist The approved contexts to export information for. 237 */ 238 public static function export_user_data(approved_contextlist $contextlist) { 239 global $DB; 240 241 if (!count($contextlist)) { 242 return; 243 } 244 245 $user = $contextlist->get_user(); 246 $userid = $user->id; 247 list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); 248 249 $sql = "SELECT 250 q.*, 251 qg.id AS hasgrade, 252 qg.grade AS bestgrade, 253 qg.timemodified AS grademodified, 254 qo.id AS hasoverride, 255 qo.timeopen AS override_timeopen, 256 qo.timeclose AS override_timeclose, 257 qo.timelimit AS override_timelimit, 258 c.id AS contextid, 259 cm.id AS cmid 260 FROM {context} c 261 INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 262 INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname 263 INNER JOIN {quiz} q ON q.id = cm.instance 264 LEFT JOIN {quiz_overrides} qo ON qo.quiz = q.id AND qo.userid = :qouserid 265 LEFT JOIN {quiz_grades} qg ON qg.quiz = q.id AND qg.userid = :qguserid 266 WHERE c.id {$contextsql}"; 267 268 $params = [ 269 'contextlevel' => CONTEXT_MODULE, 270 'modname' => 'quiz', 271 'qguserid' => $userid, 272 'qouserid' => $userid, 273 ]; 274 $params += $contextparams; 275 276 // Fetch the individual quizzes. 277 $quizzes = $DB->get_recordset_sql($sql, $params); 278 foreach ($quizzes as $quiz) { 279 list($course, $cm) = get_course_and_cm_from_cmid($quiz->cmid, 'quiz'); 280 $quizobj = new \mod_quiz\quiz_settings($quiz, $cm, $course); 281 $context = $quizobj->get_context(); 282 283 $quizdata = \core_privacy\local\request\helper::get_context_data($context, $contextlist->get_user()); 284 \core_privacy\local\request\helper::export_context_files($context, $contextlist->get_user()); 285 286 if (!empty($quizdata->timeopen)) { 287 $quizdata->timeopen = transform::datetime($quiz->timeopen); 288 } 289 if (!empty($quizdata->timeclose)) { 290 $quizdata->timeclose = transform::datetime($quiz->timeclose); 291 } 292 if (!empty($quizdata->timelimit)) { 293 $quizdata->timelimit = $quiz->timelimit; 294 } 295 296 if (!empty($quiz->hasoverride)) { 297 $quizdata->override = (object) []; 298 299 if (!empty($quizdata->override_override_timeopen)) { 300 $quizdata->override->timeopen = transform::datetime($quiz->override_timeopen); 301 } 302 if (!empty($quizdata->override_timeclose)) { 303 $quizdata->override->timeclose = transform::datetime($quiz->override_timeclose); 304 } 305 if (!empty($quizdata->override_timelimit)) { 306 $quizdata->override->timelimit = $quiz->override_timelimit; 307 } 308 } 309 310 $quizdata->accessdata = (object) []; 311 312 $components = \core_component::get_plugin_list('quizaccess'); 313 $exportparams = [ 314 $quizobj, 315 $user, 316 ]; 317 foreach (array_keys($components) as $component) { 318 $classname = manager::get_provider_classname_for_component("quizaccess_$component"); 319 if (class_exists($classname) && is_subclass_of($classname, quizaccess_provider::class)) { 320 $result = component_class_callback($classname, 'export_quizaccess_user_data', $exportparams); 321 if (count((array) $result)) { 322 $quizdata->accessdata->$component = $result; 323 } 324 } 325 } 326 327 if (empty((array) $quizdata->accessdata)) { 328 unset($quizdata->accessdata); 329 } 330 331 writer::with_context($context) 332 ->export_data([], $quizdata); 333 } 334 $quizzes->close(); 335 336 // Store all quiz attempt data. 337 static::export_quiz_attempts($contextlist); 338 } 339 340 /** 341 * Delete all data for all users in the specified context. 342 * 343 * @param context $context The specific context to delete data for. 344 */ 345 public static function delete_data_for_all_users_in_context(\context $context) { 346 if ($context->contextlevel != CONTEXT_MODULE) { 347 // Only quiz module will be handled. 348 return; 349 } 350 351 $cm = get_coursemodule_from_id('quiz', $context->instanceid); 352 if (!$cm) { 353 // Only quiz module will be handled. 354 return; 355 } 356 357 $quizobj = \mod_quiz\quiz_settings::create($cm->instance); 358 $quiz = $quizobj->get_quiz(); 359 360 // Handle the 'quizaccess' subplugin. 361 manager::plugintype_class_callback( 362 'quizaccess', 363 quizaccess_provider::class, 364 'delete_subplugin_data_for_all_users_in_context', 365 [$quizobj] 366 ); 367 368 // Delete all overrides - do not log. 369 quiz_delete_all_overrides($quiz, false); 370 371 // This will delete all question attempts, quiz attempts, and quiz grades for this quiz. 372 quiz_delete_all_attempts($quiz); 373 } 374 375 /** 376 * Delete all user data for the specified user, in the specified contexts. 377 * 378 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. 379 */ 380 public static function delete_data_for_user(approved_contextlist $contextlist) { 381 global $DB; 382 383 foreach ($contextlist as $context) { 384 if ($context->contextlevel != CONTEXT_MODULE) { 385 // Only quiz module will be handled. 386 continue; 387 } 388 389 $cm = get_coursemodule_from_id('quiz', $context->instanceid); 390 if (!$cm) { 391 // Only quiz module will be handled. 392 continue; 393 } 394 395 // Fetch the details of the data to be removed. 396 $quizobj = \mod_quiz\quiz_settings::create($cm->instance); 397 $quiz = $quizobj->get_quiz(); 398 $user = $contextlist->get_user(); 399 400 // Handle the 'quizaccess' quizaccess. 401 manager::plugintype_class_callback( 402 'quizaccess', 403 quizaccess_provider::class, 404 'delete_quizaccess_data_for_user', 405 [$quizobj, $user] 406 ); 407 408 // Remove overrides for this user. 409 $overrides = $DB->get_records('quiz_overrides' , [ 410 'quiz' => $quizobj->get_quizid(), 411 'userid' => $user->id, 412 ]); 413 414 foreach ($overrides as $override) { 415 quiz_delete_override($quiz, $override->id, false); 416 } 417 418 // This will delete all question attempts, quiz attempts, and quiz grades for this quiz. 419 quiz_delete_user_attempts($quizobj, $user); 420 } 421 } 422 423 /** 424 * Delete multiple users within a single context. 425 * 426 * @param approved_userlist $userlist The approved context and user information to delete information for. 427 */ 428 public static function delete_data_for_users(approved_userlist $userlist) { 429 global $DB; 430 431 $context = $userlist->get_context(); 432 433 if ($context->contextlevel != CONTEXT_MODULE) { 434 // Only quiz module will be handled. 435 return; 436 } 437 438 $cm = get_coursemodule_from_id('quiz', $context->instanceid); 439 if (!$cm) { 440 // Only quiz module will be handled. 441 return; 442 } 443 444 $quizobj = \mod_quiz\quiz_settings::create($cm->instance); 445 $quiz = $quizobj->get_quiz(); 446 447 $userids = $userlist->get_userids(); 448 449 // Handle the 'quizaccess' quizaccess. 450 manager::plugintype_class_callback( 451 'quizaccess', 452 quizaccess_user_provider::class, 453 'delete_quizaccess_data_for_users', 454 [$userlist] 455 ); 456 457 foreach ($userids as $userid) { 458 // Remove overrides for this user. 459 $overrides = $DB->get_records('quiz_overrides' , [ 460 'quiz' => $quizobj->get_quizid(), 461 'userid' => $userid, 462 ]); 463 464 foreach ($overrides as $override) { 465 quiz_delete_override($quiz, $override->id, false); 466 } 467 468 // This will delete all question attempts, quiz attempts, and quiz grades for this user in the given quiz. 469 quiz_delete_user_attempts($quizobj, (object)['id' => $userid]); 470 } 471 } 472 473 /** 474 * Store all quiz attempts for the contextlist. 475 * 476 * @param approved_contextlist $contextlist 477 */ 478 protected static function export_quiz_attempts(approved_contextlist $contextlist) { 479 global $DB; 480 481 $userid = $contextlist->get_user()->id; 482 list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); 483 $qubaid = \core_question\privacy\provider::get_related_question_usages_for_user('rel', 'mod_quiz', 'qa.uniqueid', $userid); 484 485 $sql = "SELECT 486 c.id AS contextid, 487 cm.id AS cmid, 488 qa.* 489 FROM {context} c 490 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 491 JOIN {modules} m ON m.id = cm.module AND m.name = 'quiz' 492 JOIN {quiz} q ON q.id = cm.instance 493 JOIN {quiz_attempts} qa ON qa.quiz = q.id 494 " . $qubaid->from. " 495 WHERE ( 496 qa.userid = :qauserid OR 497 " . $qubaid->where() . " 498 ) AND qa.preview = 0 499 "; 500 501 $params = array_merge( 502 [ 503 'contextlevel' => CONTEXT_MODULE, 504 'qauserid' => $userid, 505 ], 506 $qubaid->from_where_params() 507 ); 508 509 $attempts = $DB->get_recordset_sql($sql, $params); 510 foreach ($attempts as $attempt) { 511 $quiz = $DB->get_record('quiz', ['id' => $attempt->quiz]); 512 $context = \context_module::instance($attempt->cmid); 513 $attemptsubcontext = helper::get_quiz_attempt_subcontext($attempt, $contextlist->get_user()); 514 $options = quiz_get_review_options($quiz, $attempt, $context); 515 516 if ($attempt->userid == $userid) { 517 // This attempt was made by the user. 518 // They 'own' all data on it. 519 // Store the question usage data. 520 \core_question\privacy\provider::export_question_usage($userid, 521 $context, 522 $attemptsubcontext, 523 $attempt->uniqueid, 524 $options, 525 true 526 ); 527 528 // Store the quiz attempt data. 529 $data = (object) [ 530 'state' => quiz_attempt::state_name($attempt->state), 531 ]; 532 533 if (!empty($attempt->timestart)) { 534 $data->timestart = transform::datetime($attempt->timestart); 535 } 536 if (!empty($attempt->timefinish)) { 537 $data->timefinish = transform::datetime($attempt->timefinish); 538 } 539 if (!empty($attempt->timemodified)) { 540 $data->timemodified = transform::datetime($attempt->timemodified); 541 } 542 if (!empty($attempt->timemodifiedoffline)) { 543 $data->timemodifiedoffline = transform::datetime($attempt->timemodifiedoffline); 544 } 545 if (!empty($attempt->timecheckstate)) { 546 $data->timecheckstate = transform::datetime($attempt->timecheckstate); 547 } 548 if (!empty($attempt->gradednotificationsenttime)) { 549 $data->gradednotificationsenttime = transform::datetime($attempt->gradednotificationsenttime); 550 } 551 552 if ($options->marks == \question_display_options::MARK_AND_MAX) { 553 $grade = quiz_rescale_grade($attempt->sumgrades, $quiz, false); 554 $data->grade = (object) [ 555 'grade' => quiz_format_grade($quiz, $grade), 556 'feedback' => quiz_feedback_for_grade($grade, $quiz, $context), 557 ]; 558 } 559 560 writer::with_context($context) 561 ->export_data($attemptsubcontext, $data); 562 } else { 563 // This attempt was made by another user. 564 // The current user may have marked part of the quiz attempt. 565 \core_question\privacy\provider::export_question_usage( 566 $userid, 567 $context, 568 $attemptsubcontext, 569 $attempt->uniqueid, 570 $options, 571 false 572 ); 573 } 574 } 575 $attempts->close(); 576 } 577 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body