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