See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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 * Class containing helper methods for processing data requests. 19 * 20 * @package tool_dataprivacy 21 * @copyright 2018 Jun Pataleta 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace tool_dataprivacy; 25 26 use coding_exception; 27 use context_helper; 28 use context_system; 29 use core\invalid_persistent_exception; 30 use core\message\message; 31 use core\task\manager; 32 use core_privacy\local\request\approved_contextlist; 33 use core_privacy\local\request\contextlist_collection; 34 use core_user; 35 use dml_exception; 36 use moodle_exception; 37 use moodle_url; 38 use required_capability_exception; 39 use stdClass; 40 use tool_dataprivacy\external\data_request_exporter; 41 use tool_dataprivacy\local\helper; 42 use tool_dataprivacy\task\process_data_request_task; 43 use tool_dataprivacy\data_request; 44 45 defined('MOODLE_INTERNAL') || die(); 46 47 /** 48 * Class containing helper methods for processing data requests. 49 * 50 * @copyright 2018 Jun Pataleta 51 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 52 */ 53 class api { 54 55 /** Data export request type. */ 56 const DATAREQUEST_TYPE_EXPORT = 1; 57 58 /** Data deletion request type. */ 59 const DATAREQUEST_TYPE_DELETE = 2; 60 61 /** Other request type. Usually of enquiries to the DPO. */ 62 const DATAREQUEST_TYPE_OTHERS = 3; 63 64 /** Newly submitted and we haven't yet started finding out where they have data. */ 65 const DATAREQUEST_STATUS_PENDING = 0; 66 67 /** Metadata ready and awaiting review and approval by the Data Protection officer. */ 68 const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2; 69 70 /** Request approved and will be processed soon. */ 71 const DATAREQUEST_STATUS_APPROVED = 3; 72 73 /** The request is now being processed. */ 74 const DATAREQUEST_STATUS_PROCESSING = 4; 75 76 /** Information/other request completed. */ 77 const DATAREQUEST_STATUS_COMPLETE = 5; 78 79 /** Data request cancelled by the user. */ 80 const DATAREQUEST_STATUS_CANCELLED = 6; 81 82 /** Data request rejected by the DPO. */ 83 const DATAREQUEST_STATUS_REJECTED = 7; 84 85 /** Data request download ready. */ 86 const DATAREQUEST_STATUS_DOWNLOAD_READY = 8; 87 88 /** Data request expired. */ 89 const DATAREQUEST_STATUS_EXPIRED = 9; 90 91 /** Data delete request completed, account is removed. */ 92 const DATAREQUEST_STATUS_DELETED = 10; 93 94 /** Approve data request. */ 95 const DATAREQUEST_ACTION_APPROVE = 1; 96 97 /** Reject data request. */ 98 const DATAREQUEST_ACTION_REJECT = 2; 99 100 /** 101 * Determines whether the user can contact the site's Data Protection Officer via Moodle. 102 * 103 * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled. 104 * @throws dml_exception 105 */ 106 public static function can_contact_dpo() { 107 return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1; 108 } 109 110 /** 111 * Checks whether the current user has the capability to manage data requests. 112 * 113 * @param int $userid The user ID. 114 * @return bool 115 */ 116 public static function can_manage_data_requests($userid) { 117 // Privacy officers can manage data requests. 118 return self::is_site_dpo($userid); 119 } 120 121 /** 122 * Checks if the current user can manage the data registry at the provided id. 123 * 124 * @param int $contextid Fallback to system context id. 125 * @throws \required_capability_exception 126 * @return null 127 */ 128 public static function check_can_manage_data_registry($contextid = false) { 129 if ($contextid) { 130 $context = \context_helper::instance_by_id($contextid); 131 } else { 132 $context = \context_system::instance(); 133 } 134 135 require_capability('tool/dataprivacy:managedataregistry', $context); 136 } 137 138 /** 139 * Fetches the list of configured privacy officer roles. 140 * 141 * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes 142 * any role that doesn't have the required capability anymore. 143 * 144 * @return int[] 145 * @throws dml_exception 146 */ 147 public static function get_assigned_privacy_officer_roles() { 148 $roleids = []; 149 150 // Get roles from config. 151 $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles'))); 152 if (!empty($configroleids)) { 153 // Fetch roles that have the capability to manage data requests. 154 $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests')); 155 156 // Extract the configured roles that have the capability from the list of capable roles. 157 $roleids = array_intersect($capableroles, $configroleids); 158 } 159 160 return $roleids; 161 } 162 163 /** 164 * Fetches the role shortnames of Data Protection Officer roles. 165 * 166 * @return array An array of the DPO role shortnames 167 */ 168 public static function get_dpo_role_names() : array { 169 global $DB; 170 171 $dporoleids = self::get_assigned_privacy_officer_roles(); 172 $dponames = array(); 173 174 if (!empty($dporoleids)) { 175 list($insql, $inparams) = $DB->get_in_or_equal($dporoleids); 176 $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams); 177 } 178 179 return $dponames; 180 } 181 182 /** 183 * Fetches the list of users with the Privacy Officer role. 184 */ 185 public static function get_site_dpos() { 186 // Get role(s) that can manage data requests. 187 $dporoles = self::get_assigned_privacy_officer_roles(); 188 189 $dpos = []; 190 $context = context_system::instance(); 191 foreach ($dporoles as $roleid) { 192 $allnames = get_all_user_name_fields(true, 'u'); 193 $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' . 194 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '. 195 'u.country, u.picture, u.idnumber, u.department, u.institution, '. 196 'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' . 197 'r.name AS rolename, r.sortorder, '. 198 'r.shortname AS roleshortname, rn.name AS rolecoursealias'; 199 // Fetch users that can manage data requests. 200 $dpos += get_role_users($roleid, $context, false, $fields); 201 } 202 203 // If the site has no data protection officer, defer to site admin(s). 204 if (empty($dpos)) { 205 $dpos = get_admins(); 206 } 207 return $dpos; 208 } 209 210 /** 211 * Checks whether a given user is a site Privacy Officer. 212 * 213 * @param int $userid The user ID. 214 * @return bool 215 */ 216 public static function is_site_dpo($userid) { 217 $dpos = self::get_site_dpos(); 218 return array_key_exists($userid, $dpos) || is_siteadmin(); 219 } 220 221 /** 222 * Lodges a data request and sends the request details to the site Data Protection Officer(s). 223 * 224 * @param int $foruser The user whom the request is being made for. 225 * @param int $type The request type. 226 * @param string $comments Request comments. 227 * @param int $creationmethod The creation method of the data request. 228 * @param bool $notify Notify DPOs of this pending request. 229 * @return data_request 230 * @throws invalid_persistent_exception 231 * @throws coding_exception 232 */ 233 public static function create_data_request($foruser, $type, $comments = '', 234 $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL, 235 $notify = null 236 ) { 237 global $USER; 238 239 if (null === $notify) { 240 // Only if notifications have not been decided by caller. 241 if ( data_request::DATAREQUEST_CREATION_AUTO == $creationmethod) { 242 // If the request was automatically created, then do not notify unless explicitly set. 243 $notify = false; 244 } else { 245 $notify = true; 246 } 247 } 248 249 $datarequest = new data_request(); 250 // The user the request is being made for. 251 $datarequest->set('userid', $foruser); 252 253 // The cron is considered to be a guest user when it creates a data request. 254 // NOTE: This should probably be changed. We should leave the default value for $requestinguser if 255 // the request is not explicitly created by a specific user. 256 $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ? 257 get_admin()->id : $USER->id; 258 // The user making the request. 259 $datarequest->set('requestedby', $requestinguser); 260 // Set status. 261 $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL; 262 if (self::is_automatic_request_approval_on($type)) { 263 // Set status to approved if automatic data request approval is enabled. 264 $status = self::DATAREQUEST_STATUS_APPROVED; 265 // Set the privacy officer field if the one making the data request is a privacy officer. 266 if (self::is_site_dpo($requestinguser)) { 267 $datarequest->set('dpo', $requestinguser); 268 } 269 // Mark this request as system approved. 270 $datarequest->set('systemapproved', true); 271 // No need to notify privacy officer(s) about automatically approved data requests. 272 $notify = false; 273 } 274 $datarequest->set('status', $status); 275 // Set request type. 276 $datarequest->set('type', $type); 277 // Set request comments. 278 $datarequest->set('comments', $comments); 279 // Set the creation method. 280 $datarequest->set('creationmethod', $creationmethod); 281 282 // Store subject access request. 283 $datarequest->create(); 284 285 // Queue the ad-hoc task for automatically approved data requests. 286 if ($status == self::DATAREQUEST_STATUS_APPROVED) { 287 $userid = null; 288 if ($type == self::DATAREQUEST_TYPE_EXPORT) { 289 $userid = $foruser; 290 } 291 self::queue_data_request_task($datarequest->get('id'), $userid); 292 } 293 294 if ($notify) { 295 // Get the list of the site Data Protection Officers. 296 $dpos = self::get_site_dpos(); 297 298 // Email the data request to the Data Protection Officer(s)/Admin(s). 299 foreach ($dpos as $dpo) { 300 self::notify_dpo($dpo, $datarequest); 301 } 302 } 303 304 return $datarequest; 305 } 306 307 /** 308 * Fetches the list of the data requests. 309 * 310 * If user ID is provided, it fetches the data requests for the user. 311 * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests. 312 * (e.g. Users with the Data Protection Officer roles) 313 * 314 * @param int $userid The User ID. 315 * @param int[] $statuses The status filters. 316 * @param int[] $types The request type filters. 317 * @param int[] $creationmethods The request creation method filters. 318 * @param string $sort The order by clause. 319 * @param int $offset Amount of records to skip. 320 * @param int $limit Amount of records to fetch. 321 * @return data_request[] 322 * @throws coding_exception 323 * @throws dml_exception 324 */ 325 public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [], 326 $sort = '', $offset = 0, $limit = 0) { 327 global $DB, $USER; 328 $results = []; 329 $sqlparams = []; 330 $sqlconditions = []; 331 332 // Set default sort. 333 if (empty($sort)) { 334 $sort = 'status ASC, timemodified ASC'; 335 } 336 337 // Set status filters. 338 if (!empty($statuses)) { 339 list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED); 340 $sqlconditions[] = "status $statusinsql"; 341 } 342 343 // Set request type filter. 344 if (!empty($types)) { 345 list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED); 346 $sqlconditions[] = "type $typeinsql"; 347 $sqlparams = array_merge($sqlparams, $typeparams); 348 } 349 350 // Set request creation method filter. 351 if (!empty($creationmethods)) { 352 list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED); 353 $sqlconditions[] = "creationmethod $typeinsql"; 354 $sqlparams = array_merge($sqlparams, $typeparams); 355 } 356 357 if ($userid) { 358 // Get the data requests for the user or data requests made by the user. 359 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)"; 360 $params = [ 361 'userid' => $userid, 362 'requestedby' => $userid 363 ]; 364 365 // Build a list of user IDs that the user is allowed to make data requests for. 366 // Of course, the user should be included in this list. 367 $alloweduserids = [$userid]; 368 // Get any users that the user can make data requests for. 369 if ($children = helper::get_children_of_user($userid)) { 370 // Get the list of user IDs of the children and merge to the allowed user IDs. 371 $alloweduserids = array_merge($alloweduserids, array_keys($children)); 372 } 373 list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED); 374 $sqlconditions[] .= "userid $insql"; 375 $select = implode(' AND ', $sqlconditions); 376 $params = array_merge($params, $inparams, $sqlparams); 377 378 $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit); 379 } else { 380 // If the current user is one of the site's Data Protection Officers, then fetch all data requests. 381 if (self::is_site_dpo($USER->id)) { 382 if (!empty($sqlconditions)) { 383 $select = implode(' AND ', $sqlconditions); 384 $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit); 385 } else { 386 $results = data_request::get_records(null, $sort, '', $offset, $limit); 387 } 388 } 389 } 390 391 // If any are due to expire, expire them and re-fetch updated data. 392 if (empty($statuses) 393 || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses) 394 || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) { 395 $expiredrequests = data_request::get_expired_requests($userid); 396 397 if (!empty($expiredrequests)) { 398 data_request::expire($expiredrequests); 399 $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit); 400 } 401 } 402 403 return $results; 404 } 405 406 /** 407 * Fetches the count of data request records based on the given parameters. 408 * 409 * @param int $userid The User ID. 410 * @param int[] $statuses The status filters. 411 * @param int[] $types The request type filters. 412 * @param int[] $creationmethods The request creation method filters. 413 * @return int 414 * @throws coding_exception 415 * @throws dml_exception 416 */ 417 public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) { 418 global $DB, $USER; 419 $count = 0; 420 $sqlparams = []; 421 $sqlconditions = []; 422 if (!empty($statuses)) { 423 list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED); 424 $sqlconditions[] = "status $statusinsql"; 425 } 426 if (!empty($types)) { 427 list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED); 428 $sqlconditions[] = "type $typeinsql"; 429 $sqlparams = array_merge($sqlparams, $typeparams); 430 } 431 if (!empty($creationmethods)) { 432 list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED); 433 $sqlconditions[] = "creationmethod $typeinsql"; 434 $sqlparams = array_merge($sqlparams, $typeparams); 435 } 436 if ($userid) { 437 // Get the data requests for the user or data requests made by the user. 438 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)"; 439 $params = [ 440 'userid' => $userid, 441 'requestedby' => $userid 442 ]; 443 444 // Build a list of user IDs that the user is allowed to make data requests for. 445 // Of course, the user should be included in this list. 446 $alloweduserids = [$userid]; 447 // Get any users that the user can make data requests for. 448 if ($children = helper::get_children_of_user($userid)) { 449 // Get the list of user IDs of the children and merge to the allowed user IDs. 450 $alloweduserids = array_merge($alloweduserids, array_keys($children)); 451 } 452 list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED); 453 $sqlconditions[] .= "userid $insql"; 454 $select = implode(' AND ', $sqlconditions); 455 $params = array_merge($params, $inparams, $sqlparams); 456 457 $count = data_request::count_records_select($select, $params); 458 } else { 459 // If the current user is one of the site's Data Protection Officers, then fetch all data requests. 460 if (self::is_site_dpo($USER->id)) { 461 if (!empty($sqlconditions)) { 462 $select = implode(' AND ', $sqlconditions); 463 $count = data_request::count_records_select($select, $sqlparams); 464 } else { 465 $count = data_request::count_records(); 466 } 467 } 468 } 469 470 return $count; 471 } 472 473 /** 474 * Checks whether there is already an existing pending/in-progress data request for a user for a given request type. 475 * 476 * @param int $userid The user ID. 477 * @param int $type The request type. 478 * @return bool 479 * @throws coding_exception 480 * @throws dml_exception 481 */ 482 public static function has_ongoing_request($userid, $type) { 483 global $DB; 484 485 // Check if the user already has an incomplete data request of the same type. 486 $nonpendingstatuses = [ 487 self::DATAREQUEST_STATUS_COMPLETE, 488 self::DATAREQUEST_STATUS_CANCELLED, 489 self::DATAREQUEST_STATUS_REJECTED, 490 self::DATAREQUEST_STATUS_DOWNLOAD_READY, 491 self::DATAREQUEST_STATUS_EXPIRED, 492 self::DATAREQUEST_STATUS_DELETED, 493 ]; 494 list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false); 495 $select = "type = :type AND userid = :userid AND status {$insql}"; 496 $params = array_merge([ 497 'type' => $type, 498 'userid' => $userid 499 ], $inparams); 500 501 return data_request::record_exists_select($select, $params); 502 } 503 504 /** 505 * Find whether any ongoing requests exist for a set of users. 506 * 507 * @param array $userids 508 * @return array 509 */ 510 public static function find_ongoing_request_types_for_users(array $userids) : array { 511 global $DB; 512 513 if (empty($userids)) { 514 return []; 515 } 516 517 // Check if the user already has an incomplete data request of the same type. 518 $nonpendingstatuses = [ 519 self::DATAREQUEST_STATUS_COMPLETE, 520 self::DATAREQUEST_STATUS_CANCELLED, 521 self::DATAREQUEST_STATUS_REJECTED, 522 self::DATAREQUEST_STATUS_DOWNLOAD_READY, 523 self::DATAREQUEST_STATUS_EXPIRED, 524 self::DATAREQUEST_STATUS_DELETED, 525 ]; 526 list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false); 527 list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us'); 528 529 $select = "userid {$userinsql} AND status {$statusinsql}"; 530 $params = array_merge($statusparams, $userparams); 531 532 $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type'); 533 534 $returnval = []; 535 foreach ($userids as $userid) { 536 $returnval[$userid] = (object) []; 537 } 538 539 foreach ($requests as $request) { 540 $returnval[$request->userid]->{$request->type} = true; 541 } 542 543 return $returnval; 544 } 545 546 /** 547 * Determines whether a request is active or not based on its status. 548 * 549 * @param int $status The request status. 550 * @return bool 551 */ 552 public static function is_active($status) { 553 // List of statuses which doesn't require any further processing. 554 $finalstatuses = [ 555 self::DATAREQUEST_STATUS_COMPLETE, 556 self::DATAREQUEST_STATUS_CANCELLED, 557 self::DATAREQUEST_STATUS_REJECTED, 558 self::DATAREQUEST_STATUS_DOWNLOAD_READY, 559 self::DATAREQUEST_STATUS_EXPIRED, 560 self::DATAREQUEST_STATUS_DELETED, 561 ]; 562 563 return !in_array($status, $finalstatuses); 564 } 565 566 /** 567 * Cancels the data request for a given request ID. 568 * 569 * @param int $requestid The request identifier. 570 * @param int $status The request status. 571 * @param int $dpoid The user ID of the Data Protection Officer 572 * @param string $comment The comment about the status update. 573 * @return bool 574 * @throws invalid_persistent_exception 575 * @throws coding_exception 576 */ 577 public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') { 578 // Update the request. 579 $datarequest = new data_request($requestid); 580 $datarequest->set('status', $status); 581 if ($dpoid) { 582 $datarequest->set('dpo', $dpoid); 583 } 584 // Update the comment if necessary. 585 if (!empty(trim($comment))) { 586 $params = [ 587 'date' => userdate(time()), 588 'comment' => $comment 589 ]; 590 $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params); 591 // Check if there's an existing DPO comment. 592 $currentcomment = trim($datarequest->get('dpocomment')); 593 if ($currentcomment) { 594 // Append the new comment to the current comment and give them 1 line space in between. 595 $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave; 596 } 597 $datarequest->set('dpocomment', $commenttosave); 598 } 599 600 return $datarequest->update(); 601 } 602 603 /** 604 * Fetches a request based on the request ID. 605 * 606 * @param int $requestid The request identifier 607 * @return data_request 608 */ 609 public static function get_request($requestid) { 610 return new data_request($requestid); 611 } 612 613 /** 614 * Approves a data request based on the request ID. 615 * 616 * @param int $requestid The request identifier 617 * @return bool 618 * @throws coding_exception 619 * @throws dml_exception 620 * @throws invalid_persistent_exception 621 * @throws required_capability_exception 622 * @throws moodle_exception 623 */ 624 public static function approve_data_request($requestid) { 625 global $USER; 626 627 // Check first whether the user can manage data requests. 628 if (!self::can_manage_data_requests($USER->id)) { 629 $context = context_system::instance(); 630 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); 631 } 632 633 // Check if request is already awaiting for approval. 634 $request = new data_request($requestid); 635 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { 636 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); 637 } 638 639 // Check if current user has permission to approve delete data request. 640 if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) { 641 throw new required_capability_exception(context_system::instance(), 642 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', ''); 643 } 644 645 // Update the status and the DPO. 646 $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id); 647 648 // Fire an ad hoc task to initiate the data request process. 649 $userid = null; 650 if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) { 651 $userid = $request->get('userid'); 652 } 653 self::queue_data_request_task($requestid, $userid); 654 655 return $result; 656 } 657 658 /** 659 * Rejects a data request based on the request ID. 660 * 661 * @param int $requestid The request identifier 662 * @return bool 663 * @throws coding_exception 664 * @throws dml_exception 665 * @throws invalid_persistent_exception 666 * @throws required_capability_exception 667 * @throws moodle_exception 668 */ 669 public static function deny_data_request($requestid) { 670 global $USER; 671 672 if (!self::can_manage_data_requests($USER->id)) { 673 $context = context_system::instance(); 674 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); 675 } 676 677 // Check if request is already awaiting for approval. 678 $request = new data_request($requestid); 679 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { 680 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); 681 } 682 683 // Check if current user has permission to reject delete data request. 684 if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) { 685 throw new required_capability_exception(context_system::instance(), 686 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', ''); 687 } 688 689 // Update the status and the DPO. 690 return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id); 691 } 692 693 /** 694 * Sends a message to the site's Data Protection Officer about a request. 695 * 696 * @param stdClass $dpo The DPO user record 697 * @param data_request $request The data request 698 * @return int|false 699 * @throws coding_exception 700 * @throws moodle_exception 701 */ 702 public static function notify_dpo($dpo, data_request $request) { 703 global $PAGE, $SITE; 704 705 $output = $PAGE->get_renderer('tool_dataprivacy'); 706 707 $usercontext = \context_user::instance($request->get('requestedby')); 708 $requestexporter = new data_request_exporter($request, ['context' => $usercontext]); 709 $requestdata = $requestexporter->export($output); 710 711 // Create message to send to the Data Protection Officer(s). 712 $typetext = null; 713 $typetext = $requestdata->typename; 714 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext); 715 716 $requestedby = $requestdata->requestedbyuser; 717 $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); 718 $message = new message(); 719 $message->courseid = $SITE->id; 720 $message->component = 'tool_dataprivacy'; 721 $message->name = 'contactdataprotectionofficer'; 722 $message->userfrom = $requestedby->id; 723 $message->replyto = $requestedby->email; 724 $message->replytoname = $requestedby->fullname; 725 $message->subject = $subject; 726 $message->fullmessageformat = FORMAT_HTML; 727 $message->notification = 1; 728 $message->contexturl = $datarequestsurl; 729 $message->contexturlname = get_string('datarequests', 'tool_dataprivacy'); 730 731 // Prepare the context data for the email message body. 732 $messagetextdata = [ 733 'requestedby' => $requestedby->fullname, 734 'requesttype' => $typetext, 735 'requestdate' => userdate($requestdata->timecreated), 736 'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]), 737 'requestoriginurl' => new moodle_url('/'), 738 'requestcomments' => $requestdata->messagehtml, 739 'datarequestsurl' => $datarequestsurl 740 ]; 741 $requestingfor = $requestdata->foruser; 742 if ($requestedby->id == $requestingfor->id) { 743 $messagetextdata['requestfor'] = $messagetextdata['requestedby']; 744 } else { 745 $messagetextdata['requestfor'] = $requestingfor->fullname; 746 } 747 748 // Email the data request to the Data Protection Officer(s)/Admin(s). 749 $messagetextdata['dponame'] = fullname($dpo); 750 // Render message email body. 751 $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata); 752 $message->userto = $dpo; 753 $message->fullmessage = html_to_text($messagehtml); 754 $message->fullmessagehtml = $messagehtml; 755 756 // Send message. 757 return message_send($message); 758 } 759 760 /** 761 * Checks whether a non-DPO user can make a data request for another user. 762 * 763 * @param int $user The user ID of the target user. 764 * @param int $requester The user ID of the user making the request. 765 * @return bool 766 */ 767 public static function can_create_data_request_for_user($user, $requester = null) { 768 $usercontext = \context_user::instance($user); 769 770 return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester); 771 } 772 773 /** 774 * Require that the current user can make a data request for the specified other user. 775 * 776 * @param int $user The user ID of the target user. 777 * @param int $requester The user ID of the user making the request. 778 * @return bool 779 */ 780 public static function require_can_create_data_request_for_user($user, $requester = null) { 781 $usercontext = \context_user::instance($user); 782 783 require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester); 784 785 return true; 786 } 787 788 /** 789 * Check if user has permisson to create data deletion request for themselves. 790 * 791 * @param int|null $userid ID of the user. 792 * @return bool 793 * @throws coding_exception 794 */ 795 public static function can_create_data_deletion_request_for_self(int $userid = null): bool { 796 global $USER; 797 $userid = $userid ?: $USER->id; 798 return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid) 799 && !is_primary_admin($userid); 800 } 801 802 /** 803 * Check if user has permission to create data deletion request for another user. 804 * 805 * @param int|null $userid ID of the user. 806 * @return bool 807 * @throws coding_exception 808 * @throws dml_exception 809 */ 810 public static function can_create_data_deletion_request_for_other(int $userid = null): bool { 811 global $USER; 812 $userid = $userid ?: $USER->id; 813 return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid); 814 } 815 816 /** 817 * Check if parent can create data deletion request for their children. 818 * 819 * @param int $userid ID of a user being requested. 820 * @param int|null $requesterid ID of a user making request. 821 * @return bool 822 * @throws coding_exception 823 */ 824 public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool { 825 global $USER; 826 $requesterid = $requesterid ?: $USER->id; 827 return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid), 828 $requesterid) && !is_primary_admin($userid); 829 } 830 831 /** 832 * Checks whether a user can download a data request. 833 * 834 * @param int $userid Target user id (subject of data request) 835 * @param int $requesterid Requester user id (person who requsted it) 836 * @param int|null $downloaderid Person who wants to download user id (default current) 837 * @return bool 838 * @throws coding_exception 839 */ 840 public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) { 841 global $USER; 842 843 if (!$downloaderid) { 844 $downloaderid = $USER->id; 845 } 846 847 $usercontext = \context_user::instance($userid); 848 // If it's your own and you have the right capability, you can download it. 849 if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) { 850 return true; 851 } 852 // If you can download anyone's in that context, you can download it. 853 if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) { 854 return true; 855 } 856 // If you can have the 'child access' ability to request in that context, and you are the one 857 // who requested it, then you can download it. 858 if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) { 859 return true; 860 } 861 return false; 862 } 863 864 /** 865 * Gets an action menu link to download a data request. 866 * 867 * @param \context_user $usercontext User context (of user who the data is for) 868 * @param int $requestid Request id 869 * @return \action_menu_link_secondary Action menu link 870 * @throws coding_exception 871 */ 872 public static function get_download_link(\context_user $usercontext, $requestid) { 873 $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 874 'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true); 875 $downloadtext = get_string('download', 'tool_dataprivacy'); 876 return new \action_menu_link_secondary($downloadurl, null, $downloadtext); 877 } 878 879 /** 880 * Creates a new data purpose. 881 * 882 * @param stdClass $record 883 * @return \tool_dataprivacy\purpose. 884 */ 885 public static function create_purpose(stdClass $record) { 886 $purpose = new purpose(0, $record); 887 $purpose->create(); 888 889 return $purpose; 890 } 891 892 /** 893 * Updates an existing data purpose. 894 * 895 * @param stdClass $record 896 * @return \tool_dataprivacy\purpose. 897 */ 898 public static function update_purpose(stdClass $record) { 899 if (!isset($record->sensitivedatareasons)) { 900 $record->sensitivedatareasons = ''; 901 } 902 903 $purpose = new purpose($record->id); 904 $purpose->from_record($record); 905 906 $result = $purpose->update(); 907 908 return $purpose; 909 } 910 911 /** 912 * Deletes a data purpose. 913 * 914 * @param int $id 915 * @return bool 916 */ 917 public static function delete_purpose($id) { 918 $purpose = new purpose($id); 919 if ($purpose->is_used()) { 920 throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.'); 921 } 922 return $purpose->delete(); 923 } 924 925 /** 926 * Get all system data purposes. 927 * 928 * @return \tool_dataprivacy\purpose[] 929 */ 930 public static function get_purposes() { 931 return purpose::get_records([], 'name', 'ASC'); 932 } 933 934 /** 935 * Creates a new data category. 936 * 937 * @param stdClass $record 938 * @return \tool_dataprivacy\category. 939 */ 940 public static function create_category(stdClass $record) { 941 $category = new category(0, $record); 942 $category->create(); 943 944 return $category; 945 } 946 947 /** 948 * Updates an existing data category. 949 * 950 * @param stdClass $record 951 * @return \tool_dataprivacy\category. 952 */ 953 public static function update_category(stdClass $record) { 954 $category = new category($record->id); 955 $category->from_record($record); 956 957 $result = $category->update(); 958 959 return $category; 960 } 961 962 /** 963 * Deletes a data category. 964 * 965 * @param int $id 966 * @return bool 967 */ 968 public static function delete_category($id) { 969 $category = new category($id); 970 if ($category->is_used()) { 971 throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.'); 972 } 973 return $category->delete(); 974 } 975 976 /** 977 * Get all system data categories. 978 * 979 * @return \tool_dataprivacy\category[] 980 */ 981 public static function get_categories() { 982 return category::get_records([], 'name', 'ASC'); 983 } 984 985 /** 986 * Sets the context instance purpose and category. 987 * 988 * @param \stdClass $record 989 * @return \tool_dataprivacy\context_instance 990 */ 991 public static function set_context_instance($record) { 992 if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) { 993 // Update. 994 $instance->from_record($record); 995 996 if (empty($record->purposeid) && empty($record->categoryid)) { 997 // We accept one of them to be null but we delete it if both are null. 998 self::unset_context_instance($instance); 999 return; 1000 } 1001 1002 } else { 1003 // Add. 1004 $instance = new context_instance(0, $record); 1005 } 1006 $instance->save(); 1007 1008 return $instance; 1009 } 1010 1011 /** 1012 * Unsets the context instance record. 1013 * 1014 * @param \tool_dataprivacy\context_instance $instance 1015 * @return null 1016 */ 1017 public static function unset_context_instance(context_instance $instance) { 1018 $instance->delete(); 1019 } 1020 1021 /** 1022 * Sets the context level purpose and category. 1023 * 1024 * @throws \coding_exception 1025 * @param \stdClass $record 1026 * @return contextlevel 1027 */ 1028 public static function set_contextlevel($record) { 1029 global $DB; 1030 1031 if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) { 1032 throw new \coding_exception('Only context system and context user can set a contextlevel ' . 1033 'purpose and retention'); 1034 } 1035 1036 if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) { 1037 // Update. 1038 $contextlevel->from_record($record); 1039 } else { 1040 // Add. 1041 $contextlevel = new contextlevel(0, $record); 1042 } 1043 $contextlevel->save(); 1044 1045 // We sync with their defaults as we removed these options from the defaults page. 1046 $classname = \context_helper::get_class_for_level($record->contextlevel); 1047 list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname); 1048 set_config($purposevar, $record->purposeid, 'tool_dataprivacy'); 1049 set_config($categoryvar, $record->categoryid, 'tool_dataprivacy'); 1050 1051 return $contextlevel; 1052 } 1053 1054 /** 1055 * Returns the effective category given a context instance. 1056 * 1057 * @param \context $context 1058 * @param int $forcedvalue Use this categoryid value as if this was this context instance category. 1059 * @return category|false 1060 */ 1061 public static function get_effective_context_category(\context $context, $forcedvalue = false) { 1062 if (!data_registry::defaults_set()) { 1063 return false; 1064 } 1065 1066 return data_registry::get_effective_context_value($context, 'category', $forcedvalue); 1067 } 1068 1069 /** 1070 * Returns the effective purpose given a context instance. 1071 * 1072 * @param \context $context 1073 * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose. 1074 * @return purpose|false 1075 */ 1076 public static function get_effective_context_purpose(\context $context, $forcedvalue = false) { 1077 if (!data_registry::defaults_set()) { 1078 return false; 1079 } 1080 1081 return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue); 1082 } 1083 1084 /** 1085 * Returns the effective category given a context level. 1086 * 1087 * @param int $contextlevel 1088 * @return category|false 1089 */ 1090 public static function get_effective_contextlevel_category($contextlevel) { 1091 if (!data_registry::defaults_set()) { 1092 return false; 1093 } 1094 1095 return data_registry::get_effective_contextlevel_value($contextlevel, 'category'); 1096 } 1097 1098 /** 1099 * Returns the effective purpose given a context level. 1100 * 1101 * @param int $contextlevel 1102 * @param int $forcedvalue Use this purposeid value as if this was this context level purpose. 1103 * @return purpose|false 1104 */ 1105 public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) { 1106 if (!data_registry::defaults_set()) { 1107 return false; 1108 } 1109 1110 return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue); 1111 } 1112 1113 /** 1114 * Creates an expired context record for the provided context id. 1115 * 1116 * @param int $contextid 1117 * @return \tool_dataprivacy\expired_context 1118 */ 1119 public static function create_expired_context($contextid) { 1120 $record = (object)[ 1121 'contextid' => $contextid, 1122 'status' => expired_context::STATUS_EXPIRED, 1123 ]; 1124 $expiredctx = new expired_context(0, $record); 1125 $expiredctx->save(); 1126 1127 return $expiredctx; 1128 } 1129 1130 /** 1131 * Deletes an expired context record. 1132 * 1133 * @param int $id The tool_dataprivacy_ctxexpire id. 1134 * @return bool True on success. 1135 */ 1136 public static function delete_expired_context($id) { 1137 $expiredcontext = new expired_context($id); 1138 return $expiredcontext->delete(); 1139 } 1140 1141 /** 1142 * Updates the status of an expired context. 1143 * 1144 * @param \tool_dataprivacy\expired_context $expiredctx 1145 * @param int $status 1146 * @return null 1147 */ 1148 public static function set_expired_context_status(expired_context $expiredctx, $status) { 1149 $expiredctx->set('status', $status); 1150 $expiredctx->save(); 1151 } 1152 1153 /** 1154 * Finds all contextlists having at least one approved context, and returns them as in a contextlist_collection. 1155 * 1156 * @param contextlist_collection $collection The collection of unapproved contextlist objects. 1157 * @param \stdClass $foruser The target user 1158 * @param int $type The purpose of the collection 1159 * @return contextlist_collection The collection of approved_contextlist objects. 1160 */ 1161 public static function get_approved_contextlist_collection_for_collection(contextlist_collection $collection, 1162 \stdClass $foruser, int $type) : contextlist_collection { 1163 1164 // Create the approved contextlist collection object. 1165 $approvedcollection = new contextlist_collection($collection->get_userid()); 1166 $isconfigured = data_registry::defaults_set(); 1167 1168 foreach ($collection as $contextlist) { 1169 $contextids = []; 1170 foreach ($contextlist as $context) { 1171 if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $type) { 1172 // Data can only be deleted from it if the context is either expired, or unprotected. 1173 // Note: We can only check whether a context is expired or unprotected if the site is configured and 1174 // defaults are set appropriately. If they are not, we treat all contexts as though they are 1175 // unprotected. 1176 $purpose = static::get_effective_context_purpose($context); 1177 if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) { 1178 continue; 1179 } 1180 } 1181 1182 $contextids[] = $context->id; 1183 } 1184 1185 // The data for the last component contextlist won't have been written yet, so write it now. 1186 if (!empty($contextids)) { 1187 $approvedcollection->add_contextlist( 1188 new approved_contextlist($foruser, $contextlist->get_component(), $contextids) 1189 ); 1190 } 1191 } 1192 1193 return $approvedcollection; 1194 } 1195 1196 /** 1197 * Updates the default category and purpose for a given context level (and optionally, a plugin). 1198 * 1199 * @param int $contextlevel The context level. 1200 * @param int $categoryid The ID matching the category. 1201 * @param int $purposeid The ID matching the purpose record. 1202 * @param int $activity The name of the activity that we're making a defaults configuration for. 1203 * @param bool $override Whether to override the purpose/categories of existing instances to these defaults. 1204 * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception. 1205 */ 1206 public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) { 1207 global $DB; 1208 1209 // Get the class name associated with this context level. 1210 $classname = context_helper::get_class_for_level($contextlevel); 1211 list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity); 1212 1213 // Check the default category to be set. 1214 if ($categoryid == context_instance::INHERIT) { 1215 unset_config($categoryvar, 'tool_dataprivacy'); 1216 1217 } else { 1218 // Make sure the given category ID exists first. 1219 $categorypersistent = new category($categoryid); 1220 $categorypersistent->read(); 1221 1222 // Then set the new default value. 1223 set_config($categoryvar, $categoryid, 'tool_dataprivacy'); 1224 } 1225 1226 // Check the default purpose to be set. 1227 if ($purposeid == context_instance::INHERIT) { 1228 // If the defaults is set to inherit, just unset the config value. 1229 unset_config($purposevar, 'tool_dataprivacy'); 1230 1231 } else { 1232 // Make sure the given purpose ID exists first. 1233 $purposepersistent = new purpose($purposeid); 1234 $purposepersistent->read(); 1235 1236 // Then set the new default value. 1237 set_config($purposevar, $purposeid, 'tool_dataprivacy'); 1238 } 1239 1240 // Unset instances that have been assigned with custom purpose and category, if override was specified. 1241 if ($override) { 1242 // We'd like to find context IDs that we want to unset. 1243 $statements = ["SELECT c.id as contextid FROM {context} c"]; 1244 // Based on this context level. 1245 $params = ['contextlevel' => $contextlevel]; 1246 1247 if ($contextlevel == CONTEXT_MODULE) { 1248 // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table. 1249 $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid"; 1250 // And that the module is listed on the modules table. 1251 $statements[] = "JOIN {modules} m ON m.id = cm.module"; 1252 1253 if ($activity) { 1254 // If we're overriding for an activity module, make sure that the context instance matches that activity. 1255 $statements[] = "AND m.name = :modname"; 1256 $params['modname'] = $activity; 1257 } 1258 } 1259 // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table. 1260 $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id"; 1261 // And that the context level of this instance matches the given context level. 1262 $statements[] = "WHERE c.contextlevel = :contextlevel"; 1263 1264 // Build our SQL query by gluing the statements. 1265 $sql = implode("\n", $statements); 1266 1267 // Get the context records matching our query. 1268 $contextids = $DB->get_fieldset_sql($sql, $params); 1269 1270 // Delete the matching context instances. 1271 foreach ($contextids as $contextid) { 1272 if ($instance = context_instance::get_record_by_contextid($contextid, false)) { 1273 self::unset_context_instance($instance); 1274 } 1275 } 1276 } 1277 1278 return true; 1279 } 1280 1281 /** 1282 * Format the supplied date interval as a retention period. 1283 * 1284 * @param \DateInterval $interval 1285 * @return string 1286 */ 1287 public static function format_retention_period(\DateInterval $interval) : string { 1288 // It is one or another. 1289 if ($interval->y) { 1290 $formattedtime = get_string('numyears', 'moodle', $interval->format('%y')); 1291 } else if ($interval->m) { 1292 $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m')); 1293 } else if ($interval->d) { 1294 $formattedtime = get_string('numdays', 'moodle', $interval->format('%d')); 1295 } else { 1296 $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy'); 1297 } 1298 1299 return $formattedtime; 1300 } 1301 1302 /** 1303 * Whether automatic data request approval is turned on or not for the given request type. 1304 * 1305 * @param int $type The request type. 1306 * @return bool 1307 */ 1308 public static function is_automatic_request_approval_on(int $type): bool { 1309 switch ($type) { 1310 case self::DATAREQUEST_TYPE_EXPORT: 1311 return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval')); 1312 case self::DATAREQUEST_TYPE_DELETE: 1313 return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval')); 1314 } 1315 return false; 1316 } 1317 1318 /** 1319 * Creates an ad-hoc task for the data request. 1320 * 1321 * @param int $requestid The data request ID. 1322 * @param int $userid Optional. The user ID to run the task as, if necessary. 1323 */ 1324 public static function queue_data_request_task(int $requestid, int $userid = null): void { 1325 $task = new process_data_request_task(); 1326 $task->set_custom_data(['requestid' => $requestid]); 1327 if ($userid) { 1328 $task->set_userid($userid); 1329 } 1330 manager::queue_adhoc_task($task, true); 1331 } 1332 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body