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