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