Ir a la última revisión | Autoría | Comparar con el anterior | 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.contextidFROM {" . request_contextlist::TABLE . "} rclJOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.idJOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistidWHERE 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 idFROM {" . request_contextlist::TABLE . "} rclJOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.idJOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistidWHERE 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.contextlevelFROM {" . request_contextlist::TABLE . "} rclJOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.idJOIN {" . contextlist_context::TABLE . "} clc ON cl.id = clc.contextlistidJOIN {context} ctx ON clc.contextid = ctx.idWHERE 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 ctxlockedFROM {" . \tool_dataprivacy\request_contextlist::TABLE . "} rclJOIN {" . \tool_dataprivacy\dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.idJOIN {" . \tool_dataprivacy\contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistidJOIN {context} c ON c.id = ctx.contextidWHERE 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;}}