Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Class containing helper methods for processing data requests.
 *
 * @package    tool_dataprivacy
 * @copyright  2018 Jun Pataleta
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
namespace tool_dataprivacy;

use coding_exception;
use context_helper;
use context_system;
use core\invalid_persistent_exception;
use core\message\message;
use core\task\manager;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\contextlist_collection;
use core_user;
use dml_exception;
use moodle_exception;
use moodle_url;
use required_capability_exception;
use stdClass;
use tool_dataprivacy\external\data_request_exporter;
use tool_dataprivacy\local\helper;
use tool_dataprivacy\task\initiate_data_request_task;
use tool_dataprivacy\task\process_data_request_task;

defined('MOODLE_INTERNAL') || die();

/**
 * Class containing helper methods for processing data requests.
 *
 * @copyright  2018 Jun Pataleta
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class api {

    /** Data export request type. */
    const DATAREQUEST_TYPE_EXPORT = 1;

    /** Data deletion request type. */
    const DATAREQUEST_TYPE_DELETE = 2;

    /** Other request type. Usually of enquiries to the DPO. */
    const DATAREQUEST_TYPE_OTHERS = 3;

    /** Newly submitted and we haven't yet started finding out where they have data. */
    const DATAREQUEST_STATUS_PENDING = 0;

    /** Newly submitted and we have started to find the location of data. */
    const DATAREQUEST_STATUS_PREPROCESSING = 1;

    /** Metadata ready and awaiting review and approval by the Data Protection officer. */
    const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;

    /** Request approved and will be processed soon. */
    const DATAREQUEST_STATUS_APPROVED = 3;

    /** The request is now being processed. */
    const DATAREQUEST_STATUS_PROCESSING = 4;

    /** Information/other request completed. */
    const DATAREQUEST_STATUS_COMPLETE = 5;

    /** Data request cancelled by the user. */
    const DATAREQUEST_STATUS_CANCELLED = 6;

    /** Data request rejected by the DPO. */
    const DATAREQUEST_STATUS_REJECTED = 7;

    /** Data request download ready. */
    const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;

    /** Data request expired. */
    const DATAREQUEST_STATUS_EXPIRED = 9;

    /** Data delete request completed, account is removed. */
    const DATAREQUEST_STATUS_DELETED = 10;

    /** Approve data request. */
    const DATAREQUEST_ACTION_APPROVE = 1;

    /** Reject data request. */
    const DATAREQUEST_ACTION_REJECT = 2;

    /**
     * Determines whether the user can contact the site's Data Protection Officer via Moodle.
     *
     * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
     * @throws dml_exception
     */
    public static function can_contact_dpo() {
        return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
    }

    /**
     * Checks whether the current user has the capability to manage data requests.
     *
     * @param int $userid The user ID.
     * @return bool
     */
    public static function can_manage_data_requests($userid) {
        // Privacy officers can manage data requests.
        return self::is_site_dpo($userid);
    }

    /**
     * Checks if the current user can manage the data registry at the provided id.
     *
     * @param int $contextid Fallback to system context id.
     * @throws \required_capability_exception
     * @return null
     */
    public static function check_can_manage_data_registry($contextid = false) {
        if ($contextid) {
            $context = \context_helper::instance_by_id($contextid);
        } else {
            $context = \context_system::instance();
        }

        require_capability('tool/dataprivacy:managedataregistry', $context);
    }

    /**
     * Fetches the list of configured privacy officer roles.
     *
     * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
     * any role that doesn't have the required capability anymore.
     *
     * @return int[]
     * @throws dml_exception
     */
    public static function get_assigned_privacy_officer_roles() {
        $roleids = [];

        // Get roles from config.
        $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
        if (!empty($configroleids)) {
            // Fetch roles that have the capability to manage data requests.
            $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));

            // Extract the configured roles that have the capability from the list of capable roles.
            $roleids = array_intersect($capableroles, $configroleids);
        }

        return $roleids;
    }

    /**
     * Fetches the role shortnames of Data Protection Officer roles.
     *
     * @return array An array of the DPO role shortnames
     */
    public static function get_dpo_role_names(): array {
        global $DB;

        $dporoleids = self::get_assigned_privacy_officer_roles();
        $dponames = array();

        if (!empty($dporoleids)) {
            list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
            $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
        }

        return $dponames;
    }

    /**
     * Fetches the list of users with the Privacy Officer role.
     */
    public static function get_site_dpos() {
        // Get role(s) that can manage data requests.
        $dporoles = self::get_assigned_privacy_officer_roles();

        $dpos = [];
        $context = context_system::instance();
        foreach ($dporoles as $roleid) {
            $userfieldsapi = \core_user\fields::for_name();
            $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
            $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
                      'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
                      'u.country, u.picture, u.idnumber, u.department, u.institution, '.
                      'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
                      'r.name AS rolename, r.sortorder, '.
                      'r.shortname AS roleshortname, rn.name AS rolecoursealias';
            // Fetch users that can manage data requests.
            $dpos += get_role_users($roleid, $context, false, $fields);
        }

        // If the site has no data protection officer, defer to site admin(s).
        if (empty($dpos)) {
            $dpos = get_admins();
        }
        return $dpos;
    }

    /**
     * Checks whether a given user is a site Privacy Officer.
     *
     * @param int $userid The user ID.
     * @return bool
     */
    public static function is_site_dpo($userid) {
        $dpos = self::get_site_dpos();
        return array_key_exists($userid, $dpos) || is_siteadmin();
    }

    /**
     * Lodges a data request and sends the request details to the site Data Protection Officer(s).
     *
     * @param int $foruser The user whom the request is being made for.
     * @param int $type The request type.
     * @param string $comments Request comments.
     * @param int $creationmethod The creation method of the data request.
     * @param bool $notify Notify DPOs of this pending request.
     * @return data_request
     * @throws invalid_persistent_exception
     * @throws coding_exception
     */
    public static function create_data_request($foruser, $type, $comments = '',
            $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL,
            $notify = null
        ) {
        global $USER;

        if (null === $notify) {
            // Only if notifications have not been decided by caller.
            if ( data_request::DATAREQUEST_CREATION_AUTO == $creationmethod) {
                // If the request was automatically created, then do not notify unless explicitly set.
                $notify = false;
            } else {
                $notify = true;
            }
        }

        $datarequest = new data_request();
        // The user the request is being made for.
        $datarequest->set('userid', $foruser);

        // The cron is considered to be a guest user when it creates a data request.
        // NOTE: This should probably be changed. We should leave the default value for $requestinguser if
        // the request is not explicitly created by a specific user.
        $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
                get_admin()->id : $USER->id;
        // The user making the request.
        $datarequest->set('requestedby', $requestinguser);
        // Set status.

        $allowfiltering = get_config('tool_dataprivacy', 'allowfiltering') && ($type != self::DATAREQUEST_TYPE_DELETE);
        if ($allowfiltering) {
            $status = self::DATAREQUEST_STATUS_PENDING;
        } else {
            $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL;
            if (self::is_automatic_request_approval_on($type)) {
                // Set status to approved if automatic data request approval is enabled.
                $status = self::DATAREQUEST_STATUS_APPROVED;
                // Set the privacy officer field if the one making the data request is a privacy officer.
                if (self::is_site_dpo($requestinguser)) {
                    $datarequest->set('dpo', $requestinguser);
                }
                // Mark this request as system approved.
                $datarequest->set('systemapproved', true);
                // No need to notify privacy officer(s) about automatically approved data requests.
                $notify = false;
            }
        }
        $datarequest->set('status', $status);
        // Set request type.
        $datarequest->set('type', $type);
        // Set request comments.
        $datarequest->set('comments', $comments);
        // Set the creation method.
        $datarequest->set('creationmethod', $creationmethod);

        // Store subject access request.
        $datarequest->create();

        // Queue the ad-hoc task for automatically approved data requests.
        if ($status == self::DATAREQUEST_STATUS_APPROVED) {
            $userid = null;
            if ($type == self::DATAREQUEST_TYPE_EXPORT) {
                $userid = $foruser;
            }
            self::queue_data_request_task($datarequest->get('id'), $userid);
        }

        if ($notify) {
            // Get the list of the site Data Protection Officers.
            $dpos = self::get_site_dpos();

            // Email the data request to the Data Protection Officer(s)/Admin(s).
            foreach ($dpos as $dpo) {
                self::notify_dpo($dpo, $datarequest);
            }
        }

        if ($status == self::DATAREQUEST_STATUS_PENDING) {
            // Fire an ad hoc task to initiate the data request process.
            $task = new initiate_data_request_task();
            $task->set_custom_data(['requestid' => $datarequest->get('id')]);
            manager::queue_adhoc_task($task, true);
        }

        return $datarequest;
    }

    /**
     * Fetches the list of the data requests.
     *
     * If user ID is provided, it fetches the data requests for the user.
     * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
     * (e.g. Users with the Data Protection Officer roles)
     *
     * @param int $userid The User ID.
     * @param int[] $statuses The status filters.
     * @param int[] $types The request type filters.
     * @param int[] $creationmethods The request creation method filters.
     * @param string $sort The order by clause.
     * @param int $offset Amount of records to skip.
     * @param int $limit Amount of records to fetch.
     * @return data_request[]
     * @throws coding_exception
     * @throws dml_exception
     */
    public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
                                             $sort = '', $offset = 0, $limit = 0) {
        global $DB, $USER;
        $results = [];
        $sqlparams = [];
        $sqlconditions = [];

        // Set default sort.
        if (empty($sort)) {
            $sort = 'status ASC, timemodified ASC';
        }

        // Set status filters.
        if (!empty($statuses)) {
            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
            $sqlconditions[] = "status $statusinsql";
        }

        // Set request type filter.
        if (!empty($types)) {
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
            $sqlconditions[] = "type $typeinsql";
            $sqlparams = array_merge($sqlparams, $typeparams);
        }

        // Set request creation method filter.
        if (!empty($creationmethods)) {
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
            $sqlconditions[] = "creationmethod $typeinsql";
            $sqlparams = array_merge($sqlparams, $typeparams);
        }

        if ($userid) {
            // Get the data requests for the user or data requests made by the user.
            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
            $params = [
                'userid' => $userid,
                'requestedby' => $userid
            ];

            // Build a list of user IDs that the user is allowed to make data requests for.
            // Of course, the user should be included in this list.
            $alloweduserids = [$userid];
            // Get any users that the user can make data requests for.
            if ($children = helper::get_children_of_user($userid)) {
                // Get the list of user IDs of the children and merge to the allowed user IDs.
                $alloweduserids = array_merge($alloweduserids, array_keys($children));
            }
            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
            $sqlconditions[] .= "userid $insql";
            $select = implode(' AND ', $sqlconditions);
            $params = array_merge($params, $inparams, $sqlparams);

            $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
        } else {
            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
            if (self::is_site_dpo($USER->id)) {
                if (!empty($sqlconditions)) {
                    $select = implode(' AND ', $sqlconditions);
                    $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
                } else {
                    $results = data_request::get_records(null, $sort, '', $offset, $limit);
                }
            }
        }

        // If any are due to expire, expire them and re-fetch updated data.
        if (empty($statuses)
                || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
                || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
            $expiredrequests = data_request::get_expired_requests($userid);

            if (!empty($expiredrequests)) {
                data_request::expire($expiredrequests);
                $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
            }
        }

        return $results;
    }

    /**
     * Fetches the count of data request records based on the given parameters.
     *
     * @param int $userid The User ID.
     * @param int[] $statuses The status filters.
     * @param int[] $types The request type filters.
     * @param int[] $creationmethods The request creation method filters.
     * @return int
     * @throws coding_exception
     * @throws dml_exception
     */
    public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
        global $DB, $USER;
        $count = 0;
        $sqlparams = [];
        $sqlconditions = [];
        if (!empty($statuses)) {
            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
            $sqlconditions[] = "status $statusinsql";
        }
        if (!empty($types)) {
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
            $sqlconditions[] = "type $typeinsql";
            $sqlparams = array_merge($sqlparams, $typeparams);
        }
        if (!empty($creationmethods)) {
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
            $sqlconditions[] = "creationmethod $typeinsql";
            $sqlparams = array_merge($sqlparams, $typeparams);
        }
        if ($userid) {
            // Get the data requests for the user or data requests made by the user.
            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
            $params = [
                'userid' => $userid,
                'requestedby' => $userid
            ];

            // Build a list of user IDs that the user is allowed to make data requests for.
            // Of course, the user should be included in this list.
            $alloweduserids = [$userid];
            // Get any users that the user can make data requests for.
            if ($children = helper::get_children_of_user($userid)) {
                // Get the list of user IDs of the children and merge to the allowed user IDs.
                $alloweduserids = array_merge($alloweduserids, array_keys($children));
            }
            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
            $sqlconditions[] .= "userid $insql";
            $select = implode(' AND ', $sqlconditions);
            $params = array_merge($params, $inparams, $sqlparams);

            $count = data_request::count_records_select($select, $params);
        } else {
            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
            if (self::is_site_dpo($USER->id)) {
                if (!empty($sqlconditions)) {
                    $select = implode(' AND ', $sqlconditions);
                    $count = data_request::count_records_select($select, $sqlparams);
                } else {
                    $count = data_request::count_records();
                }
            }
        }

        return $count;
    }

    /**
     * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
     *
     * @param int $userid The user ID.
     * @param int $type The request type.
     * @return bool
     * @throws coding_exception
     * @throws dml_exception
     */
    public static function has_ongoing_request($userid, $type) {
        global $DB;

        // Check if the user already has an incomplete data request of the same type.
        $nonpendingstatuses = [
            self::DATAREQUEST_STATUS_COMPLETE,
            self::DATAREQUEST_STATUS_CANCELLED,
            self::DATAREQUEST_STATUS_REJECTED,
            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
            self::DATAREQUEST_STATUS_EXPIRED,
            self::DATAREQUEST_STATUS_DELETED,
        ];
        list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
        $select = "type = :type AND userid = :userid AND status {$insql}";
        $params = array_merge([
            'type' => $type,
            'userid' => $userid
        ], $inparams);

        return data_request::record_exists_select($select, $params);
    }

    /**
     * Find whether any ongoing requests exist for a set of users.
     *
     * @param   array   $userids
     * @return  array
     */
    public static function find_ongoing_request_types_for_users(array $userids): array {
        global $DB;

        if (empty($userids)) {
            return [];
        }

        // Check if the user already has an incomplete data request of the same type.
        $nonpendingstatuses = [
            self::DATAREQUEST_STATUS_COMPLETE,
            self::DATAREQUEST_STATUS_CANCELLED,
            self::DATAREQUEST_STATUS_REJECTED,
            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
            self::DATAREQUEST_STATUS_EXPIRED,
            self::DATAREQUEST_STATUS_DELETED,
        ];
        list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
        list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us');

        $select = "userid {$userinsql} AND status {$statusinsql}";
        $params = array_merge($statusparams, $userparams);

        $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type');

        $returnval = [];
        foreach ($userids as $userid) {
            $returnval[$userid] = (object) [];
        }

        foreach ($requests as $request) {
            $returnval[$request->userid]->{$request->type} = true;
        }

        return $returnval;
    }

    /**
     * Determines whether a request is active or not based on its status.
     *
     * @param int $status The request status.
     * @return bool
     */
    public static function is_active($status) {
        // List of statuses which doesn't require any further processing.
        $finalstatuses = [
            self::DATAREQUEST_STATUS_COMPLETE,
            self::DATAREQUEST_STATUS_CANCELLED,
            self::DATAREQUEST_STATUS_REJECTED,
            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
            self::DATAREQUEST_STATUS_EXPIRED,
            self::DATAREQUEST_STATUS_DELETED,
        ];

        return !in_array($status, $finalstatuses);
    }

    /**
     * Cancels the data request for a given request ID.
     *
     * @param int $requestid The request identifier.
     * @param int $status The request status.
     * @param int $dpoid The user ID of the Data Protection Officer
     * @param string $comment The comment about the status update.
     * @return bool
     * @throws invalid_persistent_exception
     * @throws coding_exception
     */
    public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') {
        // Update the request.
        $datarequest = new data_request($requestid);
        $datarequest->set('status', $status);
        if ($dpoid) {
            $datarequest->set('dpo', $dpoid);
        }
        // Update the comment if necessary.
        if (!empty(trim($comment))) {
            $params = [
                'date' => userdate(time()),
                'comment' => $comment
            ];
            $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
            // Check if there's an existing DPO comment.
            $currentcomment = trim($datarequest->get('dpocomment'));
            if ($currentcomment) {
                // Append the new comment to the current comment and give them 1 line space in between.
                $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
            }
            $datarequest->set('dpocomment', $commenttosave);
        }

        return $datarequest->update();
    }

    /**
     * Fetches a request based on the request ID.
     *
     * @param int $requestid The request identifier
     * @return data_request
     */
    public static function get_request($requestid) {
        return new data_request($requestid);
    }

    /**
     * Approves a data request based on the request ID.
     *
     * @param int $requestid The request identifier
     * @param array $filtercoursecontexts Apply to export request, only approve contexts belong to these courses.
     * @return bool
     * @throws coding_exception
     * @throws dml_exception
     * @throws invalid_persistent_exception
     * @throws required_capability_exception
     * @throws moodle_exception
     */
    public static function approve_data_request($requestid, $filtercoursecontexts = []) {
        global $USER;

        // Check first whether the user can manage data requests.
        if (!self::can_manage_data_requests($USER->id)) {
            $context = context_system::instance();
            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
        }

        // Check if request is already awaiting for approval.
        $request = new data_request($requestid);
        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
        }

        // Check if current user has permission to approve delete data request.
        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
            throw new required_capability_exception(context_system::instance(),
                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
        }

        // Update the status and the DPO.
        $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);

        if ($request->get('type') != self::DATAREQUEST_TYPE_DELETE) {
            $allowfiltering = get_config('tool_dataprivacy', 'allowfiltering');
            if ($allowfiltering) {
                if ($filtercoursecontexts) {
                    // Only approve the context belong to selected courses.
                    self::approve_contexts_belonging_to_request($requestid, $filtercoursecontexts);
                } else {
                    // Approve all the contexts attached to the request.
                    self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED);
                }
            }
        }
        // Fire an ad hoc task to initiate the data request process.
        $userid = null;
        if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
            $userid = $request->get('userid');
        }
        self::queue_data_request_task($requestid, $userid);

        return $result;
    }

    /**
     * Rejects a data request based on the request ID.
     *
     * @param int $requestid The request identifier
     * @return bool
     * @throws coding_exception
     * @throws dml_exception
     * @throws invalid_persistent_exception
     * @throws required_capability_exception
     * @throws moodle_exception
     */
    public static function deny_data_request($requestid) {
        global $USER;

        if (!self::can_manage_data_requests($USER->id)) {
            $context = context_system::instance();
            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
        }

        // Check if request is already awaiting for approval.
        $request = new data_request($requestid);
        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
        }

        // Check if current user has permission to reject delete data request.
        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
            throw new required_capability_exception(context_system::instance(),
                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
        }

        // Update the status and the DPO.
        return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
    }

    /**
     * Sends a message to the site's Data Protection Officer about a request.
     *
     * @param stdClass $dpo The DPO user record
     * @param data_request $request The data request
     * @return int|false
     * @throws coding_exception
     * @throws moodle_exception
     */
    public static function notify_dpo($dpo, data_request $request) {
        global $PAGE, $SITE;

        $output = $PAGE->get_renderer('tool_dataprivacy');

        $usercontext = \context_user::instance($request->get('requestedby'));
        $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
        $requestdata = $requestexporter->export($output);

        // Create message to send to the Data Protection Officer(s).
        $typetext = null;
        $typetext = $requestdata->typename;
        $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);

        $requestedby = $requestdata->requestedbyuser;
        $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
        $message = new message();
        $message->courseid          = $SITE->id;
        $message->component         = 'tool_dataprivacy';
        $message->name              = 'contactdataprotectionofficer';
        $message->userfrom          = $requestedby->id;
        $message->replyto           = $requestedby->email;
        $message->replytoname       = $requestedby->fullname;
        $message->subject           = $subject;
        $message->fullmessageformat = FORMAT_HTML;
        $message->notification      = 1;
        $message->contexturl        = $datarequestsurl;
        $message->contexturlname    = get_string('datarequests', 'tool_dataprivacy');

        // Prepare the context data for the email message body.
        $messagetextdata = [
            'requestedby' => $requestedby->fullname,
            'requesttype' => $typetext,
            'requestdate' => userdate($requestdata->timecreated),
            'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
            'requestoriginurl' => new moodle_url('/'),
            'requestcomments' => $requestdata->messagehtml,
            'datarequestsurl' => $datarequestsurl
        ];
        $requestingfor = $requestdata->foruser;
        if ($requestedby->id == $requestingfor->id) {
            $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
        } else {
            $messagetextdata['requestfor'] = $requestingfor->fullname;
        }

        // Email the data request to the Data Protection Officer(s)/Admin(s).
        $messagetextdata['dponame'] = fullname($dpo);
        // Render message email body.
        $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
        $message->userto = $dpo;
        $message->fullmessage = html_to_text($messagehtml);
        $message->fullmessagehtml = $messagehtml;

        // Send message.
        return message_send($message);
    }

    /**
     * Checks whether a non-DPO user can make a data request for another user.
     *
     * @param   int     $user The user ID of the target user.
     * @param   int     $requester The user ID of the user making the request.
     * @return  bool
     */
    public static function can_create_data_request_for_user($user, $requester = null) {
        $usercontext = \context_user::instance($user);

        return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
    }

    /**
     * Require that the current user can make a data request for the specified other user.
     *
     * @param   int     $user The user ID of the target user.
     * @param   int     $requester The user ID of the user making the request.
     * @return  bool
     */
    public static function require_can_create_data_request_for_user($user, $requester = null) {
        $usercontext = \context_user::instance($user);

        require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);

        return true;
    }

    /**
     * Check if user has permission to create data download request for themselves
     *
     * @param int|null $userid
     * @return bool
     */
    public static function can_create_data_download_request_for_self(int $userid = null): bool {
        global $USER;
        $userid = $userid ?: $USER->id;
        return has_capability('tool/dataprivacy:downloadownrequest', \context_user::instance($userid), $userid);
    }

    /**
     * Check if user has permisson to create data deletion request for themselves.
     *
     * @param int|null $userid ID of the user.
     * @return bool
     * @throws coding_exception
     */
    public static function can_create_data_deletion_request_for_self(int $userid = null): bool {
        global $USER;
        $userid = $userid ?: $USER->id;
        return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid)
            && !is_primary_admin($userid);
    }

    /**
     * Check if user has permission to create data deletion request for another user.
     *
     * @param int|null $userid ID of the user.
     * @return bool
     * @throws coding_exception
     * @throws dml_exception
     */
    public static function can_create_data_deletion_request_for_other(int $userid = null): bool {
        global $USER;
        $userid = $userid ?: $USER->id;
        return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid);
    }

    /**
     * Check if parent can create data deletion request for their children.
     *
     * @param int $userid ID of a user being requested.
     * @param int|null $requesterid ID of a user making request.
     * @return bool
     * @throws coding_exception
     */
    public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool {
        global $USER;
        $requesterid = $requesterid ?: $USER->id;
        return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid),
            $requesterid) && !is_primary_admin($userid);
    }

    /**
     * Checks whether a user can download a data request.
     *
     * @param int $userid Target user id (subject of data request)
     * @param int $requesterid Requester user id (person who requsted it)
     * @param int|null $downloaderid Person who wants to download user id (default current)
     * @return bool
     * @throws coding_exception
     */
    public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
        global $USER;

        if (!$downloaderid) {
            $downloaderid = $USER->id;
        }

        $usercontext = \context_user::instance($userid);
        // If it's your own and you have the right capability, you can download it.
        if ($userid == $downloaderid && self::can_create_data_download_request_for_self($downloaderid)) {
            return true;
        }
        // If you can download anyone's in that context, you can download it.
        if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
            return true;
        }
        // If you can have the 'child access' ability to request in that context, and you are the one
        // who requested it, then you can download it.
        if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
            return true;
        }
        return false;
    }

    /**
     * Gets an action menu link to download a data request.
     *
     * @param \context_user $usercontext User context (of user who the data is for)
     * @param int $requestid Request id
     * @return \action_menu_link_secondary Action menu link
     * @throws coding_exception
     */
    public static function get_download_link(\context_user $usercontext, $requestid) {
        $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
                'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
        $downloadtext = get_string('download', 'tool_dataprivacy');
        return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
    }

    /**
     * Creates a new data purpose.
     *
     * @param stdClass $record
     * @return \tool_dataprivacy\purpose.
     */
    public static function create_purpose(stdClass $record) {
        $purpose = new purpose(0, $record);
        $purpose->create();

        return $purpose;
    }

    /**
     * Updates an existing data purpose.
     *
     * @param stdClass $record
     * @return \tool_dataprivacy\purpose.
     */
    public static function update_purpose(stdClass $record) {
        if (!isset($record->sensitivedatareasons)) {
            $record->sensitivedatareasons = '';
        }

        $purpose = new purpose($record->id);
        $purpose->from_record($record);

        $result = $purpose->update();

        return $purpose;
    }

    /**
     * Deletes a data purpose.
     *
     * @param int $id
     * @return bool
     */
    public static function delete_purpose($id) {
        $purpose = new purpose($id);
        if ($purpose->is_used()) {
            throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
        }
        return $purpose->delete();
    }

    /**
     * Get all system data purposes.
     *
     * @return \tool_dataprivacy\purpose[]
     */
    public static function get_purposes() {
        return purpose::get_records([], 'name', 'ASC');
    }

    /**
     * Creates a new data category.
     *
     * @param stdClass $record
     * @return \tool_dataprivacy\category.
     */
    public static function create_category(stdClass $record) {
        $category = new category(0, $record);
        $category->create();

        return $category;
    }

    /**
     * Updates an existing data category.
     *
     * @param stdClass $record
     * @return \tool_dataprivacy\category.
     */
    public static function update_category(stdClass $record) {
        $category = new category($record->id);
        $category->from_record($record);

        $result = $category->update();

        return $category;
    }

    /**
     * Deletes a data category.
     *
     * @param int $id
     * @return bool
     */
    public static function delete_category($id) {
        $category = new category($id);
        if ($category->is_used()) {
            throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
        }
        return $category->delete();
    }

    /**
     * Get all system data categories.
     *
     * @return \tool_dataprivacy\category[]
     */
    public static function get_categories() {
        return category::get_records([], 'name', 'ASC');
    }

    /**
     * Sets the context instance purpose and category.
     *
     * @param \stdClass $record
     * @return \tool_dataprivacy\context_instance
     */
    public static function set_context_instance($record) {
        if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
            // Update.
            $instance->from_record($record);

            if (empty($record->purposeid) && empty($record->categoryid)) {
                // We accept one of them to be null but we delete it if both are null.
                self::unset_context_instance($instance);
                return;
            }

        } else {
            // Add.
            $instance = new context_instance(0, $record);
        }
        $instance->save();

        return $instance;
    }

    /**
     * Unsets the context instance record.
     *
     * @param \tool_dataprivacy\context_instance $instance
     * @return null
     */
    public static function unset_context_instance(context_instance $instance) {
        $instance->delete();
    }

    /**
     * Sets the context level purpose and category.
     *
     * @throws \coding_exception
     * @param \stdClass $record
     * @return contextlevel
     */
    public static function set_contextlevel($record) {
        global $DB;

        if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
            throw new \coding_exception('Only context system and context user can set a contextlevel ' .
                'purpose and retention');
        }

        if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
            // Update.
            $contextlevel->from_record($record);
        } else {
            // Add.
            $contextlevel = new contextlevel(0, $record);
        }
        $contextlevel->save();

        // We sync with their defaults as we removed these options from the defaults page.
        $classname = \context_helper::get_class_for_level($record->contextlevel);
        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
        set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
        set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');

        return $contextlevel;
    }

    /**
     * Returns the effective category given a context instance.
     *
     * @param \context $context
     * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
     * @return category|false
     */
    public static function get_effective_context_category(\context $context, $forcedvalue = false) {
        if (!data_registry::defaults_set()) {
            return false;
        }

        return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
    }

    /**
     * Returns the effective purpose given a context instance.
     *
     * @param \context $context
     * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
     * @return purpose|false
     */
    public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
        if (!data_registry::defaults_set()) {
            return false;
        }

        return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
    }

    /**
     * Returns the effective category given a context level.
     *
     * @param int $contextlevel
     * @return category|false
     */
    public static function get_effective_contextlevel_category($contextlevel) {
        if (!data_registry::defaults_set()) {
            return false;
        }

        return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
    }

    /**
     * Returns the effective purpose given a context level.
     *
     * @param int $contextlevel
     * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
     * @return purpose|false
     */
    public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
        if (!data_registry::defaults_set()) {
            return false;
        }

        return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
    }

    /**
     * Creates an expired context record for the provided context id.
     *
     * @param int $contextid
     * @return \tool_dataprivacy\expired_context
     */
    public static function create_expired_context($contextid) {
        $record = (object)[
            'contextid' => $contextid,
            'status' => expired_context::STATUS_EXPIRED,
        ];
        $expiredctx = new expired_context(0, $record);
        $expiredctx->save();

        return $expiredctx;
    }

    /**
     * Deletes an expired context record.
     *
     * @param int $id The tool_dataprivacy_ctxexpire id.
     * @return bool True on success.
     */
    public static function delete_expired_context($id) {
        $expiredcontext = new expired_context($id);
        return $expiredcontext->delete();
    }

    /**
     * Updates the status of an expired context.
     *
     * @param \tool_dataprivacy\expired_context $expiredctx
     * @param int $status
     * @return null
     */
    public static function set_expired_context_status(expired_context $expiredctx, $status) {
        $expiredctx->set('status', $status);
        $expiredctx->save();
    }

    /**
     * Finds all contextlists having at least one approved context, and returns them as in a contextlist_collection.
     *
     * @param   contextlist_collection  $collection The collection of unapproved contextlist objects.
     * @param   \stdClass               $foruser The target user
     * @param   int                     $type The purpose of the collection
     * @return  contextlist_collection  The collection of approved_contextlist objects.
     */
    public static function get_approved_contextlist_collection_for_collection(contextlist_collection $collection,
            \stdClass $foruser, int $type): contextlist_collection {

        // Create the approved contextlist collection object.
        $approvedcollection = new contextlist_collection($collection->get_userid());
        $isconfigured = data_registry::defaults_set();

        foreach ($collection as $contextlist) {
            $contextids = [];
            foreach ($contextlist as $context) {
                if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $type) {
                    // Data can only be deleted from it if the context is either expired, or unprotected.
                    // Note: We can only check whether a context is expired or unprotected if the site is configured and
                    // defaults are set appropriately. If they are not, we treat all contexts as though they are
                    // unprotected.
                    $purpose = static::get_effective_context_purpose($context);
                    if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
                        continue;
                    }
                }

                $contextids[] = $context->id;
            }

            // The data for the last component contextlist won't have been written yet, so write it now.
            if (!empty($contextids)) {
                $approvedcollection->add_contextlist(
                        new approved_contextlist($foruser, $contextlist->get_component(), $contextids)
                    );
            }
        }

        return $approvedcollection;
    }

    /**
     * Updates the default category and purpose for a given context level (and optionally, a plugin).
     *
     * @param int $contextlevel The context level.
     * @param int $categoryid The ID matching the category.
     * @param int $purposeid The ID matching the purpose record.
     * @param int $activity The name of the activity that we're making a defaults configuration for.
     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
     * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
     */
    public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
        global $DB;

        // Get the class name associated with this context level.
        $classname = context_helper::get_class_for_level($contextlevel);
        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);

        // Check the default category to be set.
        if ($categoryid == context_instance::INHERIT) {
            unset_config($categoryvar, 'tool_dataprivacy');

        } else {
            // Make sure the given category ID exists first.
            $categorypersistent = new category($categoryid);
            $categorypersistent->read();

            // Then set the new default value.
            set_config($categoryvar, $categoryid, 'tool_dataprivacy');
        }

        // Check the default purpose to be set.
        if ($purposeid == context_instance::INHERIT) {
            // If the defaults is set to inherit, just unset the config value.
            unset_config($purposevar, 'tool_dataprivacy');

        } else {
            // Make sure the given purpose ID exists first.
            $purposepersistent = new purpose($purposeid);
            $purposepersistent->read();

            // Then set the new default value.
            set_config($purposevar, $purposeid, 'tool_dataprivacy');
        }

        // Unset instances that have been assigned with custom purpose and category, if override was specified.
        if ($override) {
            // We'd like to find context IDs that we want to unset.
            $statements = ["SELECT c.id as contextid FROM {context} c"];
            // Based on this context level.
            $params = ['contextlevel' => $contextlevel];

            if ($contextlevel == CONTEXT_MODULE) {
                // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
                $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
                // And that the module is listed on the modules table.
                $statements[] = "JOIN {modules} m ON m.id = cm.module";

                if ($activity) {
                    // If we're overriding for an activity module, make sure that the context instance matches that activity.
                    $statements[] = "AND m.name = :modname";
                    $params['modname'] = $activity;
                }
            }
            // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
            $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
            // And that the context level of this instance matches the given context level.
            $statements[] = "WHERE c.contextlevel = :contextlevel";

            // Build our SQL query by gluing the statements.
            $sql = implode("\n", $statements);

            // Get the context records matching our query.
            $contextids = $DB->get_fieldset_sql($sql, $params);

            // Delete the matching context instances.
            foreach ($contextids as $contextid) {
                if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
                    self::unset_context_instance($instance);
                }
            }
        }

        return true;
    }

    /**
     * Format the supplied date interval as a retention period.
     *
     * @param   \DateInterval   $interval
     * @return  string
     */
    public static function format_retention_period(\DateInterval $interval): string {
        // It is one or another.
        if ($interval->y) {
            $formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
        } else if ($interval->m) {
            $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
        } else if ($interval->d) {
            $formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
        } else {
            $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
        }

        return $formattedtime;
    }

    /**
     * Whether automatic data request approval is turned on or not for the given request type.
     *
     * @param int $type The request type.
     * @return bool
     */
    public static function is_automatic_request_approval_on(int $type): bool {
        switch ($type) {
            case self::DATAREQUEST_TYPE_EXPORT:
                return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval'));
            case self::DATAREQUEST_TYPE_DELETE:
                return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval'));
        }
        return false;
    }

    /**
     * Creates an ad-hoc task for the data request.
     *
     * @param int $requestid The data request ID.
     * @param int $userid Optional. The user ID to run the task as, if necessary.
     */
    public static function queue_data_request_task(int $requestid, int $userid = null): void {
        $task = new process_data_request_task();
        $task->set_custom_data(['requestid' => $requestid]);
        if ($userid) {
            $task->set_userid($userid);
        }
        manager::queue_adhoc_task($task, true);
    }

    /**
     * Adds the contexts from the contextlist_collection to the request with the status provided.
     *
     * @since Moodle 4.3
     * @param contextlist_collection $clcollection a collection of contextlists for all components.
     * @param int $requestid the id of the request.
     * @param int $status the status to set the contexts to.
     */
    public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
        global $DB;

        // Wrap the SQL queries in a transaction.
        $transaction = $DB->start_delegated_transaction();

        foreach ($clcollection as $contextlist) {
            // Convert the \core_privacy\local\request\contextlist into a dataprivacy_contextlist persistent and store it.
            $clp = \tool_dataprivacy\dataprivacy_contextlist::from_contextlist($contextlist);
            $clp->create();
            $contextlistid = $clp->get('id');

            // Store the associated contexts in the contextlist.
            foreach ($contextlist->get_contextids() as $contextid) {
                mtrace('Pushing data for ' . \context::instance_by_id($contextid)->get_context_name());
                $context = new contextlist_context();
                $context->set('contextid', $contextid)
                    ->set('contextlistid', $contextlistid)
                    ->set('status', $status)
                    ->create();
            }

            // Create the relation to the request.
            $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid);
            $requestcontextlist->create();
        }

        $transaction->allow_commit();
    }

    /**
     * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection.
     *
     * @since Moodle 4.3
     * @param data_request $request the data request with which the contextlists are associated.
     * @return contextlist_collection the collection of approved_contextlist objects.
     * @throws coding_exception
     * @throws dml_exception
     * @throws moodle_exception
     */
    public static function get_approved_contextlist_collection_for_request(data_request $request): contextlist_collection {
        global $DB;
        $foruser = core_user::get_user($request->get('userid'));

        // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
        $sql = "SELECT cl.component, ctx.contextid
                  FROM {" . request_contextlist::TABLE . "} rcl
                  JOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
                  JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
                 WHERE rcl.requestid = ? AND ctx.status = ?
              ORDER BY cl.component, ctx.contextid";

        // Create the approved contextlist collection object.
        $lastcomponent = null;
        $approvedcollection = new contextlist_collection($foruser->id);

        $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]);
        $contexts = [];
        foreach ($rs as $record) {
            // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the
            // last (the one we've just finished with) and reset the context array for the next one.
            if ($lastcomponent != $record->component) {
                if ($contexts) {
                    $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
                }
                $contexts = [];
            }
            $contexts[] = $record->contextid;
            $lastcomponent = $record->component;
        }
        $rs->close();

        // The data for the last component contextlist won't have been written yet, so write it now.
        if ($contexts) {
            $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
        }

        return $approvedcollection;
    }

    /**
     * Sets the status of all contexts associated with the request.
     *
     * @since Moodle 4.3
     * @param int $requestid the requestid to which the contexts belong.
     * @param int $status the status to set to.
     * @throws \dml_exception if the requestid is invalid.
     * @throws \coding_exception if the status is invalid.
     */
    public static function update_request_contexts_with_status(int $requestid, int $status) {
        // Validate contextlist_context status using the persistent's attribute validation.
        $contextlistcontext = new contextlist_context();
        $contextlistcontext->set('status', $status);
        if (array_key_exists('status', $contextlistcontext->get_errors())) {
            throw new coding_exception("Invalid contextlist_context status: $status");
        }

        global $DB;
        $select = "SELECT ctx.id as id
                     FROM {" . request_contextlist::TABLE . "} rcl
                     JOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
                     JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
                    WHERE rcl.requestid = ?";

        // Fetch records IDs to be updated and update by chunks, if applicable (limit of 1000 records per update).
        $limit = 1000;
        $idstoupdate = $DB->get_fieldset_sql($select, [$requestid]);
        $count = count($idstoupdate);
        $idchunks = $idstoupdate;
        if ($count > $limit) {
            $idchunks = array_chunk($idstoupdate, $limit);
        } else {
            $idchunks  = [$idchunks];
        }
        $transaction = $DB->start_delegated_transaction();
        $initialparams = [$status];
        foreach ($idchunks as $chunk) {
            list($insql, $inparams) = $DB->get_in_or_equal($chunk);
            $update = "UPDATE {" . contextlist_context::TABLE . "}
                          SET status = ?
                        WHERE id $insql";
            $params = array_merge($initialparams, $inparams);
            $DB->execute($update, $params);
        }
        $transaction->allow_commit();
    }

    /**
     * Only approve the contexts which are children of the provided course contexts.
     *
     * @since Moodle 4.3
     * @param int $requestid Request identifier
     * @param array $coursecontextids List of course context identifier.
     * @throws \dml_transaction_exception
     * @throws coding_exception
     * @throws dml_exception
     */
    public static function approve_contexts_belonging_to_request(int $requestid, array $coursecontextids = []) {
        global $DB;
        $select = "SELECT clc.id as id, ctx.id as contextid, ctx.path, ctx.contextlevel
                     FROM {" . request_contextlist::TABLE . "} rcl
                     JOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
                     JOIN {" . contextlist_context::TABLE . "} clc ON cl.id = clc.contextlistid
                     JOIN {context} ctx ON clc.contextid = ctx.id
                    WHERE rcl.requestid = ?";
        $items = $DB->get_records_sql($select, [$requestid]);
        $acceptcourses = [];
        $listidstoapprove = [];
        $listidstoreject = [];
        foreach ($items as $item) {
            if (in_array($item->contextid, $coursecontextids) && ($item->contextlevel == CONTEXT_COURSE)
                && !in_array($item->contextid, $acceptcourses)) {
                $acceptcourses[$item->contextid] = $item;
            }
        }

        foreach ($items as $item) {
            if ($item->contextlevel >= CONTEXT_COURSE) {
                $approve = false;
                foreach ($acceptcourses as $acceptcourse) {
                    if (strpos($item->path, $acceptcourse->path) === 0) {
                        $approve = true;
                        break;
                    }
                }
                if ($approve) {
                    $listidstoapprove[] = $item->id;
                } else {
                    $listidstoreject[] = $item->id;
                }
            } else {
                $listidstoapprove[] = $item->id;
            }
        }

        $limit = 1000;
        $count = count($listidstoapprove);
        if ($count > $limit) {
            $listidstoapprove = array_chunk($listidstoapprove, $limit);
        } else {
            $listidstoapprove = [$listidstoapprove];
        }
        $count = count($listidstoreject);
        if ($count > $limit) {
            $listidstoreject = array_chunk($listidstoreject, $limit);
        } else {
            $listidstoreject = [$listidstoreject];
        }
        $transaction = $DB->start_delegated_transaction();

        $initialparams = [contextlist_context::STATUS_APPROVED];
        foreach ($listidstoapprove as $chunk) {
            if (!empty($chunk)) {
                list($insql, $inparams) = $DB->get_in_or_equal($chunk);
                $update = "UPDATE {" . contextlist_context::TABLE . "}
                              SET status = ?
                            WHERE id $insql";
                $params = array_merge($initialparams, $inparams);
                $DB->execute($update, $params);
            }
        }

        $initialparams = [contextlist_context::STATUS_REJECTED];
        foreach ($listidstoreject as $chunk) {
            if (!empty($chunk)) {
                list($insql, $inparams) = $DB->get_in_or_equal($chunk);
                $update = "UPDATE {" . contextlist_context::TABLE . "}
                              SET status = ?
                            WHERE id $insql";

                $params = array_merge($initialparams, $inparams);
                $DB->execute($update, $params);
            }
        }

        $transaction->allow_commit();
    }

    /**
     * Get list of course context for user to filter.
     *
     * @since Moodle 4.3
     * @param int $requestid Request identifier.
     * @return array
     * @throws dml_exception
     * @throws coding_exception
     */
    public static function get_course_contexts_for_view_filter(int $requestid): array {
        global $DB;

        $contexts = [];

        $query = "SELECT DISTINCT c.id as ctxid, c.contextlevel as ctxlevel, c.instanceid as ctxinstance, c.path as ctxpath,
                        c.depth as ctxdepth, c.locked as ctxlocked
                    FROM {" . \tool_dataprivacy\request_contextlist::TABLE . "} rcl
                    JOIN {" . \tool_dataprivacy\dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
                    JOIN {" . \tool_dataprivacy\contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
                    JOIN {context} c ON c.id = ctx.contextid
                   WHERE rcl.requestid = ? AND c.contextlevel = ?
                ORDER BY c.path ASC";

        $result = $DB->get_records_sql($query, [$requestid, CONTEXT_COURSE]);
        foreach ($result as $item) {
            $ctxid = $item->ctxid;
            context_helper::preload_from_record($item);
            $contexts[$ctxid] = \context::instance_by_id($ctxid);
        }

        return $contexts;
    }

    /**
     * Validates a data request creation.
     *
     * @param stdClass $data the data request information, including userid and type
     * @return array array of errors, empty if everything went ok
     */
    public static function validate_create_data_request(stdClass $data): array {
        global $USER;

        $errors = [];
        $validrequesttypes = [
            self::DATAREQUEST_TYPE_EXPORT,
            self::DATAREQUEST_TYPE_DELETE,
        ];
        if (!in_array($data->type, $validrequesttypes)) {
            $errors['errorinvalidrequesttype'] = get_string('errorinvalidrequesttype', 'tool_dataprivacy');
        }

        $userid = $data->userid;

        if (self::has_ongoing_request($userid, $data->type)) {
            $errors['errorrequestalreadyexists'] = get_string('errorrequestalreadyexists', 'tool_dataprivacy');
        }

        // Check if current user can create data requests.
        if ($data->type == self::DATAREQUEST_TYPE_DELETE) {
            if ($userid == $USER->id) {
                if (!self::can_create_data_deletion_request_for_self()) {
                    $errors['errorcannotrequestdeleteforself'] = get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy');
                }
            } else if (!self::can_create_data_deletion_request_for_other()
                && !self::can_create_data_deletion_request_for_children($userid)) {
                $errors['errorcannotrequestdeleteforother'] = get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy');
            }
        } else if ($data->type == self::DATAREQUEST_TYPE_EXPORT) {
            if ($userid == $USER->id && !self::can_create_data_download_request_for_self()) {
                $errors['errorcannotrequestexportforself'] = get_string('errorcannotrequestexportforself', 'tool_dataprivacy');
            }
        }

        return $errors;
    }
}