Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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  }