See Release Notes
Long Term Support Release
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_data. 19 * 20 * @package mod_data 21 * @copyright 2018 Marina Glancy 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace mod_data\privacy; 26 27 use core_privacy\local\metadata\collection; 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\helper; 32 use core_privacy\local\request\transform; 33 use core_privacy\local\request\userlist; 34 use core_privacy\local\request\writer; 35 use core_privacy\manager; 36 37 defined('MOODLE_INTERNAL') || die(); 38 39 /** 40 * Implementation of the privacy subsystem plugin provider for the database activity module. 41 * 42 * @package mod_data 43 * @copyright 2018 Marina Glancy 44 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 45 */ 46 class provider implements 47 // This plugin stores personal data. 48 \core_privacy\local\metadata\provider, 49 50 // This plugin is capable of determining which users have data within it. 51 \core_privacy\local\request\core_userlist_provider, 52 53 // This plugin is a core_user_data_provider. 54 \core_privacy\local\request\plugin\provider { 55 56 /** 57 * Return the fields which contain personal data. 58 * 59 * @param collection $collection a reference to the collection to use to store the metadata. 60 * @return collection the updated collection of metadata items. 61 */ 62 public static function get_metadata(collection $collection) : collection { 63 $collection->add_database_table( 64 'data_records', 65 [ 66 'userid' => 'privacy:metadata:data_records:userid', 67 'groupid' => 'privacy:metadata:data_records:groupid', 68 'timecreated' => 'privacy:metadata:data_records:timecreated', 69 'timemodified' => 'privacy:metadata:data_records:timemodified', 70 'approved' => 'privacy:metadata:data_records:approved', 71 ], 72 'privacy:metadata:data_records' 73 ); 74 $collection->add_database_table( 75 'data_content', 76 [ 77 'fieldid' => 'privacy:metadata:data_content:fieldid', 78 'content' => 'privacy:metadata:data_content:content', 79 'content1' => 'privacy:metadata:data_content:content1', 80 'content2' => 'privacy:metadata:data_content:content2', 81 'content3' => 'privacy:metadata:data_content:content3', 82 'content4' => 'privacy:metadata:data_content:content4', 83 ], 84 'privacy:metadata:data_content' 85 ); 86 87 // Link to subplugins. 88 $collection->add_plugintype_link('datafield', [], 'privacy:metadata:datafieldnpluginsummary'); 89 90 // Subsystems used. 91 $collection->link_subsystem('core_comment', 'privacy:metadata:commentpurpose'); 92 $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose'); 93 $collection->link_subsystem('core_tag', 'privacy:metadata:tagpurpose'); 94 $collection->link_subsystem('core_rating', 'privacy:metadata:ratingpurpose'); 95 96 return $collection; 97 } 98 99 /** 100 * Get the list of contexts that contain user information for the specified user. 101 * 102 * @param int $userid the userid. 103 * @return contextlist the list of contexts containing user info for the user. 104 */ 105 public static function get_contexts_for_userid(int $userid) : contextlist { 106 $contextlist = new contextlist(); 107 108 // Fetch all data records that the user rote. 109 $sql = "SELECT c.id 110 FROM {context} c 111 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 112 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 113 JOIN {data} d ON d.id = cm.instance 114 JOIN {data_records} dr ON dr.dataid = d.id 115 WHERE dr.userid = :userid"; 116 117 $params = [ 118 'contextlevel' => CONTEXT_MODULE, 119 'modname' => 'data', 120 'userid' => $userid, 121 ]; 122 $contextlist->add_from_sql($sql, $params); 123 124 // Fetch contexts where the user commented. 125 $sql = "SELECT c.id 126 FROM {context} c 127 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 128 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 129 JOIN {data} d ON d.id = cm.instance 130 JOIN {data_records} dr ON dr.dataid = d.id 131 JOIN {comments} com ON com.commentarea = :commentarea and com.itemid = dr.id 132 WHERE com.userid = :userid"; 133 134 $params = [ 135 'contextlevel' => CONTEXT_MODULE, 136 'modname' => 'data', 137 'commentarea' => 'database_entry', 138 'userid' => $userid, 139 ]; 140 $contextlist->add_from_sql($sql, $params); 141 142 // Fetch all data records. 143 $ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_data', 'entry', 'dr.id', $userid, true); 144 $sql = "SELECT c.id 145 FROM {context} c 146 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 147 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 148 JOIN {data} d ON d.id = cm.instance 149 JOIN {data_records} dr ON dr.dataid = d.id 150 {$ratingquery->join} 151 WHERE {$ratingquery->userwhere}"; 152 153 $params = [ 154 'contextlevel' => CONTEXT_MODULE, 155 'modname' => 'data', 156 ] + $ratingquery->params; 157 $contextlist->add_from_sql($sql, $params); 158 159 return $contextlist; 160 } 161 162 /** 163 * Get the list of users who have data within a context. 164 * 165 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. 166 * 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 // Find users with data records. 176 $sql = "SELECT dr.userid 177 FROM {context} c 178 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 179 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 180 JOIN {data} d ON d.id = cm.instance 181 JOIN {data_records} dr ON dr.dataid = d.id 182 WHERE c.id = :contextid"; 183 184 $params = [ 185 'modname' => 'data', 186 'contextid' => $context->id, 187 'contextlevel' => CONTEXT_MODULE, 188 ]; 189 190 $userlist->add_from_sql('userid', $sql, $params); 191 192 // Find users with comments. 193 \core_comment\privacy\provider::get_users_in_context_from_sql($userlist, 'com', 'mod_data', 'database_entry', $context->id); 194 195 // Find users with ratings. 196 $sql = "SELECT dr.id 197 FROM {context} c 198 JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel 199 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 200 JOIN {data} d ON d.id = cm.instance 201 JOIN {data_records} dr ON dr.dataid = d.id 202 WHERE c.id = :contextid"; 203 204 $params = [ 205 'modname' => 'data', 206 'contextid' => $context->id, 207 'contextlevel' => CONTEXT_MODULE, 208 ]; 209 210 \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_data', 'entry', $sql, $params); 211 } 212 213 /** 214 * Creates an object from all fields in the $record where key starts with $prefix 215 * 216 * @param \stdClass $record 217 * @param string $prefix 218 * @param array $additionalfields 219 * @return \stdClass 220 */ 221 protected static function extract_object_from_record($record, $prefix, $additionalfields = []) { 222 $object = new \stdClass(); 223 foreach ($record as $key => $value) { 224 if (preg_match('/^'.preg_quote($prefix, '/').'(.*)/', $key, $matches)) { 225 $object->{$matches[1]} = $value; 226 } 227 } 228 if ($additionalfields) { 229 foreach ($additionalfields as $key => $value) { 230 $object->$key = $value; 231 } 232 } 233 return $object; 234 } 235 236 /** 237 * Export one field answer in a record in database activity module 238 * 239 * @param \context $context 240 * @param \stdClass $recordobj record from DB table {data_records} 241 * @param \stdClass $fieldobj record from DB table {data_fields} 242 * @param \stdClass $contentobj record from DB table {data_content} 243 */ 244 protected static function export_data_content($context, $recordobj, $fieldobj, $contentobj) { 245 $value = (object)[ 246 'field' => [ 247 // Name and description are displayed in mod_data without applying format_string(). 248 'name' => $fieldobj->name, 249 'description' => $fieldobj->description, 250 'type' => $fieldobj->type, 251 'required' => transform::yesno($fieldobj->required), 252 ], 253 'content' => $contentobj->content 254 ]; 255 foreach (['content1', 'content2', 'content3', 'content4'] as $key) { 256 if ($contentobj->$key !== null) { 257 $value->$key = $contentobj->$key; 258 } 259 } 260 $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type); 261 if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) { 262 component_class_callback($classname, 'export_data_content', 263 [$context, $recordobj, $fieldobj, $contentobj, $value]); 264 } else { 265 // Data field plugin does not implement datafield_provider, just export default value. 266 writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $value); 267 } 268 writer::with_context($context)->export_area_files([$recordobj->id, $contentobj->id], 'mod_data', 269 'content', $contentobj->id); 270 } 271 272 /** 273 * SQL query that returns all fields from {data_content}, {data_fields} and {data_records} tables 274 * 275 * @return string 276 */ 277 protected static function sql_fields() { 278 return 'd.id AS dataid, dc.id AS contentid, dc.fieldid, df.type AS fieldtype, df.name AS fieldname, 279 df.description AS fielddescription, df.required AS fieldrequired, 280 df.param1 AS fieldparam1, df.param2 AS fieldparam2, df.param3 AS fieldparam3, df.param4 AS fieldparam4, 281 df.param5 AS fieldparam5, df.param6 AS fieldparam6, df.param7 AS fieldparam7, df.param8 AS fieldparam8, 282 df.param9 AS fieldparam9, df.param10 AS fieldparam10, 283 dc.content AS contentcontent, dc.content1 AS contentcontent1, dc.content2 AS contentcontent2, 284 dc.content3 AS contentcontent3, dc.content4 AS contentcontent4, 285 dc.recordid, dr.timecreated AS recordtimecreated, dr.timemodified AS recordtimemodified, 286 dr.approved AS recordapproved, dr.groupid AS recordgroupid, dr.userid AS recorduserid'; 287 } 288 289 /** 290 * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. 291 * 292 * @param approved_contextlist $contextlist a list of contexts approved for export. 293 */ 294 public static function export_user_data(approved_contextlist $contextlist) { 295 global $DB; 296 297 if (!$contextlist->count()) { 298 return; 299 } 300 301 $user = $contextlist->get_user(); 302 303 list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); 304 $sql = "SELECT cm.id AS cmid, d.name AS dataname, cm.course AS courseid, " . self::sql_fields() . " 305 FROM {context} ctx 306 JOIN {course_modules} cm ON cm.id = ctx.instanceid 307 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 308 JOIN {data} d ON d.id = cm.instance 309 JOIN {data_records} dr ON dr.dataid = d.id 310 JOIN {data_content} dc ON dc.recordid = dr.id 311 JOIN {data_fields} df ON df.id = dc.fieldid 312 WHERE ctx.id {$contextsql} AND ctx.contextlevel = :contextlevel 313 AND dr.userid = :userid OR 314 EXISTS (SELECT 1 FROM {comments} com WHERE com.commentarea=:commentarea 315 AND com.itemid = dr.id AND com.userid = :userid1) OR 316 EXISTS (SELECT 1 FROM {rating} r WHERE r.contextid = ctx.id AND r.itemid = dr.id AND r.component = :moddata 317 AND r.ratingarea = :ratingarea AND r.userid = :userid2) 318 ORDER BY cm.id, dr.id, dc.fieldid"; 319 $rs = $DB->get_recordset_sql($sql, $contextparams + ['contextlevel' => CONTEXT_MODULE, 320 'modname' => 'data', 'userid' => $user->id, 'userid1' => $user->id, 'commentarea' => 'database_entry', 321 'userid2' => $user->id, 'ratingarea' => 'entry', 'moddata' => 'mod_data']); 322 323 $context = null; 324 $recordobj = null; 325 foreach ($rs as $row) { 326 if (!$context || $context->instanceid != $row->cmid) { 327 // This row belongs to the different data module than the previous row. 328 // Export the data for the previous module. 329 self::export_data($context, $user); 330 // Start new data module. 331 $context = \context_module::instance($row->cmid); 332 } 333 334 if (!$recordobj || $row->recordid != $recordobj->id) { 335 // Export previous data record. 336 self::export_data_record($context, $user, $recordobj); 337 // Prepare for exporting new data record. 338 $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]); 339 } 340 $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]); 341 $contentobj = self::extract_object_from_record($row, 'content', 342 ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]); 343 self::export_data_content($context, $recordobj, $fieldobj, $contentobj); 344 } 345 $rs->close(); 346 self::export_data_record($context, $user, $recordobj); 347 self::export_data($context, $user); 348 } 349 350 /** 351 * Export one entry in the database activity module (one record in {data_records} table) 352 * 353 * @param \context $context 354 * @param \stdClass $user 355 * @param \stdClass $recordobj 356 */ 357 protected static function export_data_record($context, $user, $recordobj) { 358 if (!$recordobj) { 359 return; 360 } 361 $data = [ 362 'userid' => transform::user($user->id), 363 'groupid' => $recordobj->groupid, 364 'timecreated' => transform::datetime($recordobj->timecreated), 365 'timemodified' => transform::datetime($recordobj->timemodified), 366 'approved' => transform::yesno($recordobj->approved), 367 ]; 368 // Data about the record. 369 writer::with_context($context)->export_data([$recordobj->id], (object)$data); 370 // Related tags. 371 \core_tag\privacy\provider::export_item_tags($user->id, $context, [$recordobj->id], 372 'mod_data', 'data_records', $recordobj->id); 373 // Export comments. For records that were not made by this user export only this user's comments, for own records 374 // export comments made by everybody. 375 \core_comment\privacy\provider::export_comments($context, 'mod_data', 'database_entry', $recordobj->id, 376 [$recordobj->id], $recordobj->userid != $user->id); 377 // Export ratings. For records that were not made by this user export only this user's ratings, for own records 378 // export ratings from everybody. 379 \core_rating\privacy\provider::export_area_ratings($user->id, $context, [$recordobj->id], 'mod_data', 'entry', 380 $recordobj->id, $recordobj->userid != $user->id); 381 } 382 383 /** 384 * Export basic info about database activity module 385 * 386 * @param \context $context 387 * @param \stdClass $user 388 */ 389 protected static function export_data($context, $user) { 390 if (!$context) { 391 return; 392 } 393 $contextdata = helper::get_context_data($context, $user); 394 helper::export_context_files($context, $user); 395 writer::with_context($context)->export_data([], $contextdata); 396 } 397 398 /** 399 * Delete all data for all users in the specified context. 400 * 401 * @param \context $context the context to delete in. 402 */ 403 public static function delete_data_for_all_users_in_context(\context $context) { 404 global $DB; 405 406 if (!$context instanceof \context_module) { 407 return; 408 } 409 $recordstobedeleted = []; 410 411 $sql = "SELECT " . self::sql_fields() . " 412 FROM {course_modules} cm 413 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 414 JOIN {data} d ON d.id = cm.instance 415 JOIN {data_records} dr ON dr.dataid = d.id 416 LEFT JOIN {data_content} dc ON dc.recordid = dr.id 417 LEFT JOIN {data_fields} df ON df.id = dc.fieldid 418 WHERE cm.id = :cmid 419 ORDER BY dr.id"; 420 $rs = $DB->get_recordset_sql($sql, ['cmid' => $context->instanceid, 'modname' => 'data']); 421 foreach ($rs as $row) { 422 self::mark_data_content_for_deletion($context, $row); 423 $recordstobedeleted[$row->recordid] = $row->recordid; 424 } 425 $rs->close(); 426 427 self::delete_data_records($context, $recordstobedeleted); 428 } 429 430 /** 431 * Delete all user data for the specified user, in the specified contexts. 432 * 433 * @param approved_contextlist $contextlist a list of contexts approved for deletion. 434 */ 435 public static function delete_data_for_user(approved_contextlist $contextlist) { 436 global $DB; 437 438 if (empty($contextlist->count())) { 439 return; 440 } 441 442 $user = $contextlist->get_user(); 443 $recordstobedeleted = []; 444 445 foreach ($contextlist->get_contexts() as $context) { 446 $sql = "SELECT " . self::sql_fields() . " 447 FROM {context} ctx 448 JOIN {course_modules} cm ON cm.id = ctx.instanceid 449 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 450 JOIN {data} d ON d.id = cm.instance 451 JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid = :userid 452 LEFT JOIN {data_content} dc ON dc.recordid = dr.id 453 LEFT JOIN {data_fields} df ON df.id = dc.fieldid 454 WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel 455 ORDER BY dr.id"; 456 $rs = $DB->get_recordset_sql($sql, ['ctxid' => $context->id, 'contextlevel' => CONTEXT_MODULE, 457 'modname' => 'data', 'userid' => $user->id]); 458 foreach ($rs as $row) { 459 self::mark_data_content_for_deletion($context, $row); 460 $recordstobedeleted[$row->recordid] = $row->recordid; 461 } 462 $rs->close(); 463 self::delete_data_records($context, $recordstobedeleted); 464 } 465 466 // Additionally remove comments this user made on other entries. 467 \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_data', 'database_entry'); 468 469 // We do not delete ratings made by this user on other records because it may change grades. 470 } 471 472 /** 473 * Delete multiple users within a single context. 474 * 475 * @param approved_userlist $userlist The approved context and user information to delete information for. 476 */ 477 public static function delete_data_for_users(approved_userlist $userlist) { 478 global $DB; 479 480 $context = $userlist->get_context(); 481 $recordstobedeleted = []; 482 list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); 483 484 $sql = "SELECT " . self::sql_fields() . " 485 FROM {context} ctx 486 JOIN {course_modules} cm ON cm.id = ctx.instanceid 487 JOIN {modules} m ON m.id = cm.module AND m.name = :modname 488 JOIN {data} d ON d.id = cm.instance 489 JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid {$userinsql} 490 LEFT JOIN {data_content} dc ON dc.recordid = dr.id 491 LEFT JOIN {data_fields} df ON df.id = dc.fieldid 492 WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel 493 ORDER BY dr.id"; 494 495 $params = [ 496 'ctxid' => $context->id, 497 'contextlevel' => CONTEXT_MODULE, 498 'modname' => 'data', 499 ]; 500 $params += $userinparams; 501 502 $rs = $DB->get_recordset_sql($sql, $params); 503 foreach ($rs as $row) { 504 self::mark_data_content_for_deletion($context, $row); 505 $recordstobedeleted[$row->recordid] = $row->recordid; 506 } 507 $rs->close(); 508 509 self::delete_data_records($context, $recordstobedeleted); 510 511 // Additionally remove comments these users made on other entries. 512 \core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_data', 'database_entry'); 513 514 // We do not delete ratings made by users on other records because it may change grades. 515 } 516 517 /** 518 * Marks a data_record/data_content for deletion 519 * 520 * Also invokes callback from datafield plugin in case it stores additional data that needs to be deleted 521 * 522 * @param \context $context 523 * @param \stdClass $row result of SQL query - tables data_content, data_record, data_fields join together 524 */ 525 protected static function mark_data_content_for_deletion($context, $row) { 526 $recordobj = self::extract_object_from_record($row, 'record', ['dataid' => $row->dataid]); 527 if ($row->contentid && $row->fieldid) { 528 $fieldobj = self::extract_object_from_record($row, 'field', ['dataid' => $row->dataid]); 529 $contentobj = self::extract_object_from_record($row, 'content', 530 ['fieldid' => $fieldobj->id, 'recordid' => $recordobj->id]); 531 532 // Allow datafield plugin to implement their own deletion. 533 $classname = manager::get_provider_classname_for_component('datafield_' . $fieldobj->type); 534 if (class_exists($classname) && is_subclass_of($classname, datafield_provider::class)) { 535 component_class_callback($classname, 'delete_data_content', 536 [$context, $recordobj, $fieldobj, $contentobj]); 537 } 538 } 539 } 540 541 /** 542 * Deletes records marked for deletion and all associated data 543 * 544 * Should be executed after all records were marked by {@link mark_data_content_for_deletion()} 545 * 546 * Deletes records from data_content and data_records tables, associated files, tags, comments and ratings. 547 * 548 * @param \context $context 549 * @param array $recordstobedeleted list of ids of the data records that need to be deleted 550 */ 551 protected static function delete_data_records($context, $recordstobedeleted) { 552 global $DB; 553 if (empty($recordstobedeleted)) { 554 return; 555 } 556 557 list($sql, $params) = $DB->get_in_or_equal($recordstobedeleted, SQL_PARAMS_NAMED); 558 559 // Delete files. 560 get_file_storage()->delete_area_files_select($context->id, 'mod_data', 'data_records', 561 "IN (SELECT dc.id FROM {data_content} dc WHERE dc.recordid $sql)", $params); 562 // Delete from data_content. 563 $DB->delete_records_select('data_content', 'recordid ' . $sql, $params); 564 // Delete from data_records. 565 $DB->delete_records_select('data_records', 'id ' . $sql, $params); 566 // Delete tags. 567 \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_data', 'data_records', $sql, $params); 568 // Delete comments. 569 \core_comment\privacy\provider::delete_comments_for_all_users_select($context, 'mod_data', 'database_entry', $sql, $params); 570 // Delete ratings. 571 \core_rating\privacy\provider::delete_ratings_select($context, 'mod_data', 'entry', $sql, $params); 572 } 573 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body