Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401] [Versions 401 and 403]

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