Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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