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 * Data provider. 19 * 20 * @package mod_feedback 21 * @copyright 2018 Frédéric Massart 22 * @author Frédéric Massart <fred@branchup.tech> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace mod_feedback\privacy; 27 defined('MOODLE_INTERNAL') || die(); 28 29 use context; 30 use context_helper; 31 use stdClass; 32 use core_privacy\local\metadata\collection; 33 use core_privacy\local\request\approved_contextlist; 34 use core_privacy\local\request\approved_userlist; 35 use core_privacy\local\request\contextlist; 36 use core_privacy\local\request\helper; 37 use core_privacy\local\request\transform; 38 use core_privacy\local\request\userlist; 39 use core_privacy\local\request\writer; 40 41 require_once($CFG->dirroot . '/mod/feedback/lib.php'); 42 43 /** 44 * Data provider class. 45 * 46 * @package mod_feedback 47 * @copyright 2018 Frédéric Massart 48 * @author Frédéric Massart <fred@branchup.tech> 49 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 50 */ 51 class provider implements 52 \core_privacy\local\metadata\provider, 53 \core_privacy\local\request\core_userlist_provider, 54 \core_privacy\local\request\plugin\provider { 55 56 /** 57 * Returns metadata. 58 * 59 * @param collection $collection The initialised collection to add items to. 60 * @return collection A listing of user data stored through this system. 61 */ 62 public static function get_metadata(collection $collection) : collection { 63 $completedfields = [ 64 'userid' => 'privacy:metadata:completed:userid', 65 'timemodified' => 'privacy:metadata:completed:timemodified', 66 'anonymous_response' => 'privacy:metadata:completed:anonymousresponse', 67 ]; 68 69 $collection->add_database_table('feedback_completed', $completedfields, 'privacy:metadata:completed'); 70 $collection->add_database_table('feedback_completedtmp', $completedfields, 'privacy:metadata:completedtmp'); 71 72 $valuefields = [ 73 'value' => 'privacy:metadata:value:value' 74 ]; 75 76 $collection->add_database_table('feedback_value', $valuefields, 'privacy:metadata:value'); 77 $collection->add_database_table('feedback_valuetmp', $valuefields, 'privacy:metadata:valuetmp'); 78 79 return $collection; 80 } 81 82 /** 83 * Get the list of contexts that contain user information for the specified user. 84 * 85 * @param int $userid The user to search. 86 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. 87 */ 88 public static function get_contexts_for_userid(int $userid) : contextlist { 89 $sql = " 90 SELECT DISTINCT ctx.id 91 FROM {%s} fc 92 JOIN {modules} m 93 ON m.name = :feedback 94 JOIN {course_modules} cm 95 ON cm.instance = fc.feedback 96 AND cm.module = m.id 97 JOIN {context} ctx 98 ON ctx.instanceid = cm.id 99 AND ctx.contextlevel = :modlevel 100 WHERE fc.userid = :userid"; 101 $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'userid' => $userid]; 102 $contextlist = new contextlist(); 103 $contextlist->add_from_sql(sprintf($sql, 'feedback_completed'), $params); 104 $contextlist->add_from_sql(sprintf($sql, 'feedback_completedtmp'), $params); 105 return $contextlist; 106 } 107 108 /** 109 * Get the list of users who have data within a context. 110 * 111 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. 112 * 113 */ 114 public static function get_users_in_context(userlist $userlist) { 115 $context = $userlist->get_context(); 116 117 if (!is_a($context, \context_module::class)) { 118 return; 119 } 120 121 // Find users with feedback entries. 122 $sql = " 123 SELECT fc.userid 124 FROM {%s} fc 125 JOIN {modules} m 126 ON m.name = :feedback 127 JOIN {course_modules} cm 128 ON cm.instance = fc.feedback 129 AND cm.module = m.id 130 JOIN {context} ctx 131 ON ctx.instanceid = cm.id 132 AND ctx.contextlevel = :modlevel 133 WHERE ctx.id = :contextid"; 134 $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'contextid' => $context->id]; 135 136 $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completed'), $params); 137 $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completedtmp'), $params); 138 } 139 140 /** 141 * Export all user data for the specified user, in the specified contexts. 142 * 143 * @param approved_contextlist $contextlist The approved contexts to export information for. 144 */ 145 public static function export_user_data(approved_contextlist $contextlist) { 146 global $DB; 147 148 $user = $contextlist->get_user(); 149 $userid = $user->id; 150 $contextids = array_map(function($context) { 151 return $context->id; 152 }, array_filter($contextlist->get_contexts(), function($context) { 153 return $context->contextlevel == CONTEXT_MODULE; 154 })); 155 156 if (empty($contextids)) { 157 return; 158 } 159 160 $flushdata = function($context, $data) use ($user) { 161 $contextdata = helper::get_context_data($context, $user); 162 helper::export_context_files($context, $user); 163 $mergeddata = array_merge((array) $contextdata, (array) $data); 164 165 // Drop the temporary keys. 166 if (array_key_exists('submissions', $mergeddata)) { 167 $mergeddata['submissions'] = array_values($mergeddata['submissions']); 168 } 169 170 writer::with_context($context)->export_data([], (object) $mergeddata); 171 }; 172 173 $lastctxid = null; 174 $data = (object) []; 175 list($sql, $params) = static::prepare_export_query($contextids, $userid); 176 $recordset = $DB->get_recordset_sql($sql, $params); 177 foreach ($recordset as $record) { 178 if ($lastctxid && $lastctxid != $record->contextid) { 179 $flushdata(context::instance_by_id($lastctxid), $data); 180 $data = (object) []; 181 } 182 183 context_helper::preload_from_record($record); 184 $id = ($record->istmp ? 'tmp' : 'notmp') . $record->submissionid; 185 186 if (!isset($data->submissions)) { 187 $data->submissions = []; 188 } 189 190 if (!isset($data->submissions[$id])) { 191 $data->submissions[$id] = [ 192 'inprogress' => transform::yesno($record->istmp), 193 'anonymousresponse' => transform::yesno($record->anonymousresponse == FEEDBACK_ANONYMOUS_YES), 194 'timemodified' => transform::datetime($record->timemodified), 195 'answers' => [] 196 ]; 197 } 198 $item = static::extract_item_record_from_record($record); 199 $value = static::extract_value_record_from_record($record); 200 $itemobj = feedback_get_item_class($record->itemtyp); 201 $data->submissions[$id]['answers'][] = [ 202 'question' => format_text($record->itemname, FORMAT_HTML, [ 203 'context' => context::instance_by_id($record->contextid), 204 'para' => false, 205 'noclean' => true, 206 ]), 207 'answer' => $itemobj->get_printval($item, $value) 208 ]; 209 210 $lastctxid = $record->contextid; 211 } 212 213 if (!empty($lastctxid)) { 214 $flushdata(context::instance_by_id($lastctxid), $data); 215 } 216 217 $recordset->close(); 218 } 219 220 /** 221 * Delete all data for all users in the specified context. 222 * 223 * @param context $context The specific context to delete data for. 224 */ 225 public static function delete_data_for_all_users_in_context(\context $context) { 226 global $DB; 227 228 // This should not happen, but just in case. 229 if ($context->contextlevel != CONTEXT_MODULE) { 230 return; 231 } 232 233 // Prepare SQL to gather all completed IDs. 234 235 $completedsql = " 236 SELECT fc.id 237 FROM {%s} fc 238 JOIN {modules} m 239 ON m.name = :feedback 240 JOIN {course_modules} cm 241 ON cm.instance = fc.feedback 242 AND cm.module = m.id 243 WHERE cm.id = :cmid"; 244 $completedparams = ['cmid' => $context->instanceid, 'feedback' => 'feedback']; 245 246 // Delete temp answers and submissions. 247 $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams); 248 if (!empty($completedtmpids)) { 249 list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED); 250 $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams); 251 $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams); 252 } 253 254 // Delete answers and submissions. 255 $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams); 256 if (!empty($completedids)) { 257 list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED); 258 $DB->delete_records_select('feedback_value', "completed $insql", $inparams); 259 $DB->delete_records_select('feedback_completed', "id $insql", $inparams); 260 } 261 } 262 263 /** 264 * Delete all user data for the specified user, in the specified contexts. 265 * 266 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. 267 */ 268 public static function delete_data_for_user(approved_contextlist $contextlist) { 269 global $DB; 270 $userid = $contextlist->get_user()->id; 271 272 // Ensure that we only act on module contexts. 273 $contextids = array_map(function($context) { 274 return $context->instanceid; 275 }, array_filter($contextlist->get_contexts(), function($context) { 276 return $context->contextlevel == CONTEXT_MODULE; 277 })); 278 279 // Prepare SQL to gather all completed IDs. 280 list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); 281 $completedsql = " 282 SELECT fc.id 283 FROM {%s} fc 284 JOIN {modules} m 285 ON m.name = :feedback 286 JOIN {course_modules} cm 287 ON cm.instance = fc.feedback 288 AND cm.module = m.id 289 WHERE fc.userid = :userid 290 AND cm.id $insql"; 291 $completedparams = array_merge($inparams, ['userid' => $userid, 'feedback' => 'feedback']); 292 293 // Delete all submissions in progress. 294 $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams); 295 if (!empty($completedtmpids)) { 296 list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED); 297 $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams); 298 $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams); 299 } 300 301 // Delete all final submissions. 302 $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams); 303 if (!empty($completedids)) { 304 list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED); 305 $DB->delete_records_select('feedback_value', "completed $insql", $inparams); 306 $DB->delete_records_select('feedback_completed', "id $insql", $inparams); 307 } 308 } 309 310 /** 311 * Delete multiple users within a single context. 312 * 313 * @param approved_userlist $userlist The approved context and user information to delete information for. 314 */ 315 public static function delete_data_for_users(approved_userlist $userlist) { 316 global $DB; 317 318 $context = $userlist->get_context(); 319 $userids = $userlist->get_userids(); 320 321 // Prepare SQL to gather all completed IDs. 322 list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 323 $completedsql = " 324 SELECT fc.id 325 FROM {%s} fc 326 JOIN {modules} m 327 ON m.name = :feedback 328 JOIN {course_modules} cm 329 ON cm.instance = fc.feedback 330 AND cm.module = m.id 331 WHERE cm.id = :instanceid 332 AND fc.userid $insql"; 333 $completedparams = array_merge($inparams, ['instanceid' => $context->instanceid, 'feedback' => 'feedback']); 334 335 // Delete all submissions in progress. 336 $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams); 337 if (!empty($completedtmpids)) { 338 list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED); 339 $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams); 340 $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams); 341 } 342 343 // Delete all final submissions. 344 $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams); 345 if (!empty($completedids)) { 346 list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED); 347 $DB->delete_records_select('feedback_value', "completed $insql", $inparams); 348 $DB->delete_records_select('feedback_completed', "id $insql", $inparams); 349 } 350 } 351 352 /** 353 * Extract an item record from a database record. 354 * 355 * @param stdClass $record The record. 356 * @return The item record. 357 */ 358 protected static function extract_item_record_from_record(stdClass $record) { 359 $newrec = new stdClass(); 360 foreach ($record as $key => $value) { 361 if (strpos($key, 'item') !== 0) { 362 continue; 363 } 364 $key = substr($key, 4); 365 $newrec->{$key} = $value; 366 } 367 return $newrec; 368 } 369 370 /** 371 * Extract a value record from a database record. 372 * 373 * @param stdClass $record The record. 374 * @return The value record. 375 */ 376 protected static function extract_value_record_from_record(stdClass $record) { 377 $newrec = new stdClass(); 378 foreach ($record as $key => $value) { 379 if (strpos($key, 'value') !== 0) { 380 continue; 381 } 382 $key = substr($key, 5); 383 $newrec->{$key} = $value; 384 } 385 return $newrec; 386 } 387 388 /** 389 * Prepare the query to export all data. 390 * 391 * Doing it this way allows for merging all records from both the temporary and final tables 392 * as most of their columns are shared. It is a lot easier to deal with the records when 393 * exporting as we do not need to try to manually group the two types of submissions in the 394 * same reported dataset. 395 * 396 * The ordering may affect performance on large datasets. 397 * 398 * @param array $contextids The context IDs. 399 * @param int $userid The user ID. 400 * @return array With SQL and params. 401 */ 402 protected static function prepare_export_query(array $contextids, $userid) { 403 global $DB; 404 405 $makefetchsql = function($istmp) use ($DB, $contextids, $userid) { 406 $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); 407 list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); 408 409 $i = $istmp ? 0 : 1; 410 $istmpsqlval = $istmp ? 1 : 0; 411 $prefix = $istmp ? 'idtmp' : 'id'; 412 $uniqid = $DB->sql_concat("'$prefix'", 'fc.id'); 413 414 $sql = " 415 SELECT $uniqid AS uniqid, 416 f.id AS feedbackid, 417 ctx.id AS contextid, 418 419 $istmpsqlval AS istmp, 420 fc.id AS submissionid, 421 fc.anonymous_response AS anonymousresponse, 422 fc.timemodified AS timemodified, 423 424 fv.id AS valueid, 425 fv.course_id AS valuecourse_id, 426 fv.item AS valueitem, 427 fv.completed AS valuecompleted, 428 fv.tmp_completed AS valuetmp_completed, 429 430 $ctxfields 431 FROM {context} ctx 432 JOIN {course_modules} cm 433 ON cm.id = ctx.instanceid 434 JOIN {feedback} f 435 ON f.id = cm.instance 436 JOIN {%s} fc 437 ON fc.feedback = f.id 438 JOIN {%s} fv 439 ON fv.completed = fc.id 440 WHERE ctx.id $insql 441 AND fc.userid = :userid{$i}"; 442 443 $params = array_merge($inparams, [ 444 'userid' . $i => $userid, 445 ]); 446 447 $completedtbl = $istmp ? 'feedback_completedtmp' : 'feedback_completed'; 448 $valuetbl = $istmp ? 'feedback_valuetmp' : 'feedback_value'; 449 return [sprintf($sql, $completedtbl, $valuetbl), $params]; 450 }; 451 452 list($nontmpsql, $nontmpparams) = $makefetchsql(false); 453 list($tmpsql, $tmpparams) = $makefetchsql(true); 454 455 // Oracle does not support UNION on text fields, therefore we must get the itemdescription 456 // and valuevalue after doing the union by joining on the result. 457 $sql = " 458 SELECT q.*, 459 460 COALESCE(fv.value, fvt.value) AS valuevalue, 461 462 fi.id AS itemid, 463 fi.feedback AS itemfeedback, 464 fi.template AS itemtemplate, 465 fi.name AS itemname, 466 fi.label AS itemlabel, 467 fi.presentation AS itempresentation, 468 fi.typ AS itemtyp, 469 fi.hasvalue AS itemhasvalue, 470 fi.position AS itemposition, 471 fi.required AS itemrequired, 472 fi.dependitem AS itemdependitem, 473 fi.dependvalue AS itemdependvalue, 474 fi.options AS itemoptions 475 476 FROM ($nontmpsql UNION $tmpsql) q 477 LEFT JOIN {feedback_value} fv 478 ON fv.id = q.valueid AND q.istmp = 0 479 LEFT JOIN {feedback_valuetmp} fvt 480 ON fvt.id = q.valueid AND q.istmp = 1 481 JOIN {feedback_item} fi 482 ON (fi.id = fv.item OR fi.id = fvt.item) 483 ORDER BY q.contextid, q.istmp, q.submissionid, q.valueid"; 484 $params = array_merge($nontmpparams, $tmpparams); 485 486 return [$sql, $params]; 487 } 488 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body