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