MDL-72983 tool_dataprivacy: respect capability to export own data.
[moodle.git] / admin / tool / dataprivacy / classes / api.php
blob4cff2c13b39a23de3dbc1b2dcb2f0a2440c7f74a
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Class containing helper methods for processing data requests.
20 * @package tool_dataprivacy
21 * @copyright 2018 Jun Pataleta
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 namespace tool_dataprivacy;
26 use coding_exception;
27 use context_helper;
28 use context_system;
29 use core\invalid_persistent_exception;
30 use core\message\message;
31 use core\task\manager;
32 use core_privacy\local\request\approved_contextlist;
33 use core_privacy\local\request\contextlist_collection;
34 use core_user;
35 use dml_exception;
36 use moodle_exception;
37 use moodle_url;
38 use required_capability_exception;
39 use stdClass;
40 use tool_dataprivacy\external\data_request_exporter;
41 use tool_dataprivacy\local\helper;
42 use tool_dataprivacy\task\process_data_request_task;
43 use tool_dataprivacy\data_request;
45 defined('MOODLE_INTERNAL') || die();
47 /**
48 * Class containing helper methods for processing data requests.
50 * @copyright 2018 Jun Pataleta
51 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53 class api {
55 /** Data export request type. */
56 const DATAREQUEST_TYPE_EXPORT = 1;
58 /** Data deletion request type. */
59 const DATAREQUEST_TYPE_DELETE = 2;
61 /** Other request type. Usually of enquiries to the DPO. */
62 const DATAREQUEST_TYPE_OTHERS = 3;
64 /** Newly submitted and we haven't yet started finding out where they have data. */
65 const DATAREQUEST_STATUS_PENDING = 0;
67 /** Metadata ready and awaiting review and approval by the Data Protection officer. */
68 const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
70 /** Request approved and will be processed soon. */
71 const DATAREQUEST_STATUS_APPROVED = 3;
73 /** The request is now being processed. */
74 const DATAREQUEST_STATUS_PROCESSING = 4;
76 /** Information/other request completed. */
77 const DATAREQUEST_STATUS_COMPLETE = 5;
79 /** Data request cancelled by the user. */
80 const DATAREQUEST_STATUS_CANCELLED = 6;
82 /** Data request rejected by the DPO. */
83 const DATAREQUEST_STATUS_REJECTED = 7;
85 /** Data request download ready. */
86 const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
88 /** Data request expired. */
89 const DATAREQUEST_STATUS_EXPIRED = 9;
91 /** Data delete request completed, account is removed. */
92 const DATAREQUEST_STATUS_DELETED = 10;
94 /** Approve data request. */
95 const DATAREQUEST_ACTION_APPROVE = 1;
97 /** Reject data request. */
98 const DATAREQUEST_ACTION_REJECT = 2;
101 * Determines whether the user can contact the site's Data Protection Officer via Moodle.
103 * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
104 * @throws dml_exception
106 public static function can_contact_dpo() {
107 return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
111 * Checks whether the current user has the capability to manage data requests.
113 * @param int $userid The user ID.
114 * @return bool
116 public static function can_manage_data_requests($userid) {
117 // Privacy officers can manage data requests.
118 return self::is_site_dpo($userid);
122 * Checks if the current user can manage the data registry at the provided id.
124 * @param int $contextid Fallback to system context id.
125 * @throws \required_capability_exception
126 * @return null
128 public static function check_can_manage_data_registry($contextid = false) {
129 if ($contextid) {
130 $context = \context_helper::instance_by_id($contextid);
131 } else {
132 $context = \context_system::instance();
135 require_capability('tool/dataprivacy:managedataregistry', $context);
139 * Fetches the list of configured privacy officer roles.
141 * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
142 * any role that doesn't have the required capability anymore.
144 * @return int[]
145 * @throws dml_exception
147 public static function get_assigned_privacy_officer_roles() {
148 $roleids = [];
150 // Get roles from config.
151 $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
152 if (!empty($configroleids)) {
153 // Fetch roles that have the capability to manage data requests.
154 $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
156 // Extract the configured roles that have the capability from the list of capable roles.
157 $roleids = array_intersect($capableroles, $configroleids);
160 return $roleids;
164 * Fetches the role shortnames of Data Protection Officer roles.
166 * @return array An array of the DPO role shortnames
168 public static function get_dpo_role_names() : array {
169 global $DB;
171 $dporoleids = self::get_assigned_privacy_officer_roles();
172 $dponames = array();
174 if (!empty($dporoleids)) {
175 list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
176 $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
179 return $dponames;
183 * Fetches the list of users with the Privacy Officer role.
185 public static function get_site_dpos() {
186 // Get role(s) that can manage data requests.
187 $dporoles = self::get_assigned_privacy_officer_roles();
189 $dpos = [];
190 $context = context_system::instance();
191 foreach ($dporoles as $roleid) {
192 $userfieldsapi = \core_user\fields::for_name();
193 $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
194 $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
195 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
196 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
197 'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
198 'r.name AS rolename, r.sortorder, '.
199 'r.shortname AS roleshortname, rn.name AS rolecoursealias';
200 // Fetch users that can manage data requests.
201 $dpos += get_role_users($roleid, $context, false, $fields);
204 // If the site has no data protection officer, defer to site admin(s).
205 if (empty($dpos)) {
206 $dpos = get_admins();
208 return $dpos;
212 * Checks whether a given user is a site Privacy Officer.
214 * @param int $userid The user ID.
215 * @return bool
217 public static function is_site_dpo($userid) {
218 $dpos = self::get_site_dpos();
219 return array_key_exists($userid, $dpos) || is_siteadmin();
223 * Lodges a data request and sends the request details to the site Data Protection Officer(s).
225 * @param int $foruser The user whom the request is being made for.
226 * @param int $type The request type.
227 * @param string $comments Request comments.
228 * @param int $creationmethod The creation method of the data request.
229 * @param bool $notify Notify DPOs of this pending request.
230 * @return data_request
231 * @throws invalid_persistent_exception
232 * @throws coding_exception
234 public static function create_data_request($foruser, $type, $comments = '',
235 $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL,
236 $notify = null
238 global $USER;
240 if (null === $notify) {
241 // Only if notifications have not been decided by caller.
242 if ( data_request::DATAREQUEST_CREATION_AUTO == $creationmethod) {
243 // If the request was automatically created, then do not notify unless explicitly set.
244 $notify = false;
245 } else {
246 $notify = true;
250 $datarequest = new data_request();
251 // The user the request is being made for.
252 $datarequest->set('userid', $foruser);
254 // The cron is considered to be a guest user when it creates a data request.
255 // NOTE: This should probably be changed. We should leave the default value for $requestinguser if
256 // the request is not explicitly created by a specific user.
257 $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
258 get_admin()->id : $USER->id;
259 // The user making the request.
260 $datarequest->set('requestedby', $requestinguser);
261 // Set status.
262 $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL;
263 if (self::is_automatic_request_approval_on($type)) {
264 // Set status to approved if automatic data request approval is enabled.
265 $status = self::DATAREQUEST_STATUS_APPROVED;
266 // Set the privacy officer field if the one making the data request is a privacy officer.
267 if (self::is_site_dpo($requestinguser)) {
268 $datarequest->set('dpo', $requestinguser);
270 // Mark this request as system approved.
271 $datarequest->set('systemapproved', true);
272 // No need to notify privacy officer(s) about automatically approved data requests.
273 $notify = false;
275 $datarequest->set('status', $status);
276 // Set request type.
277 $datarequest->set('type', $type);
278 // Set request comments.
279 $datarequest->set('comments', $comments);
280 // Set the creation method.
281 $datarequest->set('creationmethod', $creationmethod);
283 // Store subject access request.
284 $datarequest->create();
286 // Queue the ad-hoc task for automatically approved data requests.
287 if ($status == self::DATAREQUEST_STATUS_APPROVED) {
288 $userid = null;
289 if ($type == self::DATAREQUEST_TYPE_EXPORT) {
290 $userid = $foruser;
292 self::queue_data_request_task($datarequest->get('id'), $userid);
295 if ($notify) {
296 // Get the list of the site Data Protection Officers.
297 $dpos = self::get_site_dpos();
299 // Email the data request to the Data Protection Officer(s)/Admin(s).
300 foreach ($dpos as $dpo) {
301 self::notify_dpo($dpo, $datarequest);
305 return $datarequest;
309 * Fetches the list of the data requests.
311 * If user ID is provided, it fetches the data requests for the user.
312 * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
313 * (e.g. Users with the Data Protection Officer roles)
315 * @param int $userid The User ID.
316 * @param int[] $statuses The status filters.
317 * @param int[] $types The request type filters.
318 * @param int[] $creationmethods The request creation method filters.
319 * @param string $sort The order by clause.
320 * @param int $offset Amount of records to skip.
321 * @param int $limit Amount of records to fetch.
322 * @return data_request[]
323 * @throws coding_exception
324 * @throws dml_exception
326 public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
327 $sort = '', $offset = 0, $limit = 0) {
328 global $DB, $USER;
329 $results = [];
330 $sqlparams = [];
331 $sqlconditions = [];
333 // Set default sort.
334 if (empty($sort)) {
335 $sort = 'status ASC, timemodified ASC';
338 // Set status filters.
339 if (!empty($statuses)) {
340 list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
341 $sqlconditions[] = "status $statusinsql";
344 // Set request type filter.
345 if (!empty($types)) {
346 list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
347 $sqlconditions[] = "type $typeinsql";
348 $sqlparams = array_merge($sqlparams, $typeparams);
351 // Set request creation method filter.
352 if (!empty($creationmethods)) {
353 list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
354 $sqlconditions[] = "creationmethod $typeinsql";
355 $sqlparams = array_merge($sqlparams, $typeparams);
358 if ($userid) {
359 // Get the data requests for the user or data requests made by the user.
360 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
361 $params = [
362 'userid' => $userid,
363 'requestedby' => $userid
366 // Build a list of user IDs that the user is allowed to make data requests for.
367 // Of course, the user should be included in this list.
368 $alloweduserids = [$userid];
369 // Get any users that the user can make data requests for.
370 if ($children = helper::get_children_of_user($userid)) {
371 // Get the list of user IDs of the children and merge to the allowed user IDs.
372 $alloweduserids = array_merge($alloweduserids, array_keys($children));
374 list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
375 $sqlconditions[] .= "userid $insql";
376 $select = implode(' AND ', $sqlconditions);
377 $params = array_merge($params, $inparams, $sqlparams);
379 $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
380 } else {
381 // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
382 if (self::is_site_dpo($USER->id)) {
383 if (!empty($sqlconditions)) {
384 $select = implode(' AND ', $sqlconditions);
385 $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
386 } else {
387 $results = data_request::get_records(null, $sort, '', $offset, $limit);
392 // If any are due to expire, expire them and re-fetch updated data.
393 if (empty($statuses)
394 || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
395 || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
396 $expiredrequests = data_request::get_expired_requests($userid);
398 if (!empty($expiredrequests)) {
399 data_request::expire($expiredrequests);
400 $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
404 return $results;
408 * Fetches the count of data request records based on the given parameters.
410 * @param int $userid The User ID.
411 * @param int[] $statuses The status filters.
412 * @param int[] $types The request type filters.
413 * @param int[] $creationmethods The request creation method filters.
414 * @return int
415 * @throws coding_exception
416 * @throws dml_exception
418 public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
419 global $DB, $USER;
420 $count = 0;
421 $sqlparams = [];
422 $sqlconditions = [];
423 if (!empty($statuses)) {
424 list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
425 $sqlconditions[] = "status $statusinsql";
427 if (!empty($types)) {
428 list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
429 $sqlconditions[] = "type $typeinsql";
430 $sqlparams = array_merge($sqlparams, $typeparams);
432 if (!empty($creationmethods)) {
433 list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
434 $sqlconditions[] = "creationmethod $typeinsql";
435 $sqlparams = array_merge($sqlparams, $typeparams);
437 if ($userid) {
438 // Get the data requests for the user or data requests made by the user.
439 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
440 $params = [
441 'userid' => $userid,
442 'requestedby' => $userid
445 // Build a list of user IDs that the user is allowed to make data requests for.
446 // Of course, the user should be included in this list.
447 $alloweduserids = [$userid];
448 // Get any users that the user can make data requests for.
449 if ($children = helper::get_children_of_user($userid)) {
450 // Get the list of user IDs of the children and merge to the allowed user IDs.
451 $alloweduserids = array_merge($alloweduserids, array_keys($children));
453 list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
454 $sqlconditions[] .= "userid $insql";
455 $select = implode(' AND ', $sqlconditions);
456 $params = array_merge($params, $inparams, $sqlparams);
458 $count = data_request::count_records_select($select, $params);
459 } else {
460 // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
461 if (self::is_site_dpo($USER->id)) {
462 if (!empty($sqlconditions)) {
463 $select = implode(' AND ', $sqlconditions);
464 $count = data_request::count_records_select($select, $sqlparams);
465 } else {
466 $count = data_request::count_records();
471 return $count;
475 * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
477 * @param int $userid The user ID.
478 * @param int $type The request type.
479 * @return bool
480 * @throws coding_exception
481 * @throws dml_exception
483 public static function has_ongoing_request($userid, $type) {
484 global $DB;
486 // Check if the user already has an incomplete data request of the same type.
487 $nonpendingstatuses = [
488 self::DATAREQUEST_STATUS_COMPLETE,
489 self::DATAREQUEST_STATUS_CANCELLED,
490 self::DATAREQUEST_STATUS_REJECTED,
491 self::DATAREQUEST_STATUS_DOWNLOAD_READY,
492 self::DATAREQUEST_STATUS_EXPIRED,
493 self::DATAREQUEST_STATUS_DELETED,
495 list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
496 $select = "type = :type AND userid = :userid AND status {$insql}";
497 $params = array_merge([
498 'type' => $type,
499 'userid' => $userid
500 ], $inparams);
502 return data_request::record_exists_select($select, $params);
506 * Find whether any ongoing requests exist for a set of users.
508 * @param array $userids
509 * @return array
511 public static function find_ongoing_request_types_for_users(array $userids) : array {
512 global $DB;
514 if (empty($userids)) {
515 return [];
518 // Check if the user already has an incomplete data request of the same type.
519 $nonpendingstatuses = [
520 self::DATAREQUEST_STATUS_COMPLETE,
521 self::DATAREQUEST_STATUS_CANCELLED,
522 self::DATAREQUEST_STATUS_REJECTED,
523 self::DATAREQUEST_STATUS_DOWNLOAD_READY,
524 self::DATAREQUEST_STATUS_EXPIRED,
525 self::DATAREQUEST_STATUS_DELETED,
527 list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
528 list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us');
530 $select = "userid {$userinsql} AND status {$statusinsql}";
531 $params = array_merge($statusparams, $userparams);
533 $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type');
535 $returnval = [];
536 foreach ($userids as $userid) {
537 $returnval[$userid] = (object) [];
540 foreach ($requests as $request) {
541 $returnval[$request->userid]->{$request->type} = true;
544 return $returnval;
548 * Determines whether a request is active or not based on its status.
550 * @param int $status The request status.
551 * @return bool
553 public static function is_active($status) {
554 // List of statuses which doesn't require any further processing.
555 $finalstatuses = [
556 self::DATAREQUEST_STATUS_COMPLETE,
557 self::DATAREQUEST_STATUS_CANCELLED,
558 self::DATAREQUEST_STATUS_REJECTED,
559 self::DATAREQUEST_STATUS_DOWNLOAD_READY,
560 self::DATAREQUEST_STATUS_EXPIRED,
561 self::DATAREQUEST_STATUS_DELETED,
564 return !in_array($status, $finalstatuses);
568 * Cancels the data request for a given request ID.
570 * @param int $requestid The request identifier.
571 * @param int $status The request status.
572 * @param int $dpoid The user ID of the Data Protection Officer
573 * @param string $comment The comment about the status update.
574 * @return bool
575 * @throws invalid_persistent_exception
576 * @throws coding_exception
578 public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') {
579 // Update the request.
580 $datarequest = new data_request($requestid);
581 $datarequest->set('status', $status);
582 if ($dpoid) {
583 $datarequest->set('dpo', $dpoid);
585 // Update the comment if necessary.
586 if (!empty(trim($comment))) {
587 $params = [
588 'date' => userdate(time()),
589 'comment' => $comment
591 $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
592 // Check if there's an existing DPO comment.
593 $currentcomment = trim($datarequest->get('dpocomment'));
594 if ($currentcomment) {
595 // Append the new comment to the current comment and give them 1 line space in between.
596 $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
598 $datarequest->set('dpocomment', $commenttosave);
601 return $datarequest->update();
605 * Fetches a request based on the request ID.
607 * @param int $requestid The request identifier
608 * @return data_request
610 public static function get_request($requestid) {
611 return new data_request($requestid);
615 * Approves a data request based on the request ID.
617 * @param int $requestid The request identifier
618 * @return bool
619 * @throws coding_exception
620 * @throws dml_exception
621 * @throws invalid_persistent_exception
622 * @throws required_capability_exception
623 * @throws moodle_exception
625 public static function approve_data_request($requestid) {
626 global $USER;
628 // Check first whether the user can manage data requests.
629 if (!self::can_manage_data_requests($USER->id)) {
630 $context = context_system::instance();
631 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
634 // Check if request is already awaiting for approval.
635 $request = new data_request($requestid);
636 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
637 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
640 // Check if current user has permission to approve delete data request.
641 if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
642 throw new required_capability_exception(context_system::instance(),
643 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
646 // Update the status and the DPO.
647 $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
649 // Fire an ad hoc task to initiate the data request process.
650 $userid = null;
651 if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
652 $userid = $request->get('userid');
654 self::queue_data_request_task($requestid, $userid);
656 return $result;
660 * Rejects a data request based on the request ID.
662 * @param int $requestid The request identifier
663 * @return bool
664 * @throws coding_exception
665 * @throws dml_exception
666 * @throws invalid_persistent_exception
667 * @throws required_capability_exception
668 * @throws moodle_exception
670 public static function deny_data_request($requestid) {
671 global $USER;
673 if (!self::can_manage_data_requests($USER->id)) {
674 $context = context_system::instance();
675 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
678 // Check if request is already awaiting for approval.
679 $request = new data_request($requestid);
680 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
681 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
684 // Check if current user has permission to reject delete data request.
685 if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
686 throw new required_capability_exception(context_system::instance(),
687 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
690 // Update the status and the DPO.
691 return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
695 * Sends a message to the site's Data Protection Officer about a request.
697 * @param stdClass $dpo The DPO user record
698 * @param data_request $request The data request
699 * @return int|false
700 * @throws coding_exception
701 * @throws moodle_exception
703 public static function notify_dpo($dpo, data_request $request) {
704 global $PAGE, $SITE;
706 $output = $PAGE->get_renderer('tool_dataprivacy');
708 $usercontext = \context_user::instance($request->get('requestedby'));
709 $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
710 $requestdata = $requestexporter->export($output);
712 // Create message to send to the Data Protection Officer(s).
713 $typetext = null;
714 $typetext = $requestdata->typename;
715 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
717 $requestedby = $requestdata->requestedbyuser;
718 $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
719 $message = new message();
720 $message->courseid = $SITE->id;
721 $message->component = 'tool_dataprivacy';
722 $message->name = 'contactdataprotectionofficer';
723 $message->userfrom = $requestedby->id;
724 $message->replyto = $requestedby->email;
725 $message->replytoname = $requestedby->fullname;
726 $message->subject = $subject;
727 $message->fullmessageformat = FORMAT_HTML;
728 $message->notification = 1;
729 $message->contexturl = $datarequestsurl;
730 $message->contexturlname = get_string('datarequests', 'tool_dataprivacy');
732 // Prepare the context data for the email message body.
733 $messagetextdata = [
734 'requestedby' => $requestedby->fullname,
735 'requesttype' => $typetext,
736 'requestdate' => userdate($requestdata->timecreated),
737 'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
738 'requestoriginurl' => new moodle_url('/'),
739 'requestcomments' => $requestdata->messagehtml,
740 'datarequestsurl' => $datarequestsurl
742 $requestingfor = $requestdata->foruser;
743 if ($requestedby->id == $requestingfor->id) {
744 $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
745 } else {
746 $messagetextdata['requestfor'] = $requestingfor->fullname;
749 // Email the data request to the Data Protection Officer(s)/Admin(s).
750 $messagetextdata['dponame'] = fullname($dpo);
751 // Render message email body.
752 $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
753 $message->userto = $dpo;
754 $message->fullmessage = html_to_text($messagehtml);
755 $message->fullmessagehtml = $messagehtml;
757 // Send message.
758 return message_send($message);
762 * Checks whether a non-DPO user can make a data request for another user.
764 * @param int $user The user ID of the target user.
765 * @param int $requester The user ID of the user making the request.
766 * @return bool
768 public static function can_create_data_request_for_user($user, $requester = null) {
769 $usercontext = \context_user::instance($user);
771 return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
775 * Require that the current user can make a data request for the specified other user.
777 * @param int $user The user ID of the target user.
778 * @param int $requester The user ID of the user making the request.
779 * @return bool
781 public static function require_can_create_data_request_for_user($user, $requester = null) {
782 $usercontext = \context_user::instance($user);
784 require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
786 return true;
790 * Check if user has permission to create data download request for themselves
792 * @param int|null $userid
793 * @return bool
795 public static function can_create_data_download_request_for_self(int $userid = null): bool {
796 global $USER;
797 $userid = $userid ?: $USER->id;
798 return has_capability('tool/dataprivacy:downloadownrequest', \context_user::instance($userid), $userid);
802 * Check if user has permisson to create data deletion request for themselves.
804 * @param int|null $userid ID of the user.
805 * @return bool
806 * @throws coding_exception
808 public static function can_create_data_deletion_request_for_self(int $userid = null): bool {
809 global $USER;
810 $userid = $userid ?: $USER->id;
811 return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid)
812 && !is_primary_admin($userid);
816 * Check if user has permission to create data deletion request for another user.
818 * @param int|null $userid ID of the user.
819 * @return bool
820 * @throws coding_exception
821 * @throws dml_exception
823 public static function can_create_data_deletion_request_for_other(int $userid = null): bool {
824 global $USER;
825 $userid = $userid ?: $USER->id;
826 return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid);
830 * Check if parent can create data deletion request for their children.
832 * @param int $userid ID of a user being requested.
833 * @param int|null $requesterid ID of a user making request.
834 * @return bool
835 * @throws coding_exception
837 public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool {
838 global $USER;
839 $requesterid = $requesterid ?: $USER->id;
840 return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid),
841 $requesterid) && !is_primary_admin($userid);
845 * Checks whether a user can download a data request.
847 * @param int $userid Target user id (subject of data request)
848 * @param int $requesterid Requester user id (person who requsted it)
849 * @param int|null $downloaderid Person who wants to download user id (default current)
850 * @return bool
851 * @throws coding_exception
853 public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
854 global $USER;
856 if (!$downloaderid) {
857 $downloaderid = $USER->id;
860 $usercontext = \context_user::instance($userid);
861 // If it's your own and you have the right capability, you can download it.
862 if ($userid == $downloaderid && self::can_create_data_download_request_for_self($downloaderid)) {
863 return true;
865 // If you can download anyone's in that context, you can download it.
866 if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
867 return true;
869 // If you can have the 'child access' ability to request in that context, and you are the one
870 // who requested it, then you can download it.
871 if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
872 return true;
874 return false;
878 * Gets an action menu link to download a data request.
880 * @param \context_user $usercontext User context (of user who the data is for)
881 * @param int $requestid Request id
882 * @return \action_menu_link_secondary Action menu link
883 * @throws coding_exception
885 public static function get_download_link(\context_user $usercontext, $requestid) {
886 $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
887 'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
888 $downloadtext = get_string('download', 'tool_dataprivacy');
889 return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
893 * Creates a new data purpose.
895 * @param stdClass $record
896 * @return \tool_dataprivacy\purpose.
898 public static function create_purpose(stdClass $record) {
899 $purpose = new purpose(0, $record);
900 $purpose->create();
902 return $purpose;
906 * Updates an existing data purpose.
908 * @param stdClass $record
909 * @return \tool_dataprivacy\purpose.
911 public static function update_purpose(stdClass $record) {
912 if (!isset($record->sensitivedatareasons)) {
913 $record->sensitivedatareasons = '';
916 $purpose = new purpose($record->id);
917 $purpose->from_record($record);
919 $result = $purpose->update();
921 return $purpose;
925 * Deletes a data purpose.
927 * @param int $id
928 * @return bool
930 public static function delete_purpose($id) {
931 $purpose = new purpose($id);
932 if ($purpose->is_used()) {
933 throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
935 return $purpose->delete();
939 * Get all system data purposes.
941 * @return \tool_dataprivacy\purpose[]
943 public static function get_purposes() {
944 return purpose::get_records([], 'name', 'ASC');
948 * Creates a new data category.
950 * @param stdClass $record
951 * @return \tool_dataprivacy\category.
953 public static function create_category(stdClass $record) {
954 $category = new category(0, $record);
955 $category->create();
957 return $category;
961 * Updates an existing data category.
963 * @param stdClass $record
964 * @return \tool_dataprivacy\category.
966 public static function update_category(stdClass $record) {
967 $category = new category($record->id);
968 $category->from_record($record);
970 $result = $category->update();
972 return $category;
976 * Deletes a data category.
978 * @param int $id
979 * @return bool
981 public static function delete_category($id) {
982 $category = new category($id);
983 if ($category->is_used()) {
984 throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
986 return $category->delete();
990 * Get all system data categories.
992 * @return \tool_dataprivacy\category[]
994 public static function get_categories() {
995 return category::get_records([], 'name', 'ASC');
999 * Sets the context instance purpose and category.
1001 * @param \stdClass $record
1002 * @return \tool_dataprivacy\context_instance
1004 public static function set_context_instance($record) {
1005 if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
1006 // Update.
1007 $instance->from_record($record);
1009 if (empty($record->purposeid) && empty($record->categoryid)) {
1010 // We accept one of them to be null but we delete it if both are null.
1011 self::unset_context_instance($instance);
1012 return;
1015 } else {
1016 // Add.
1017 $instance = new context_instance(0, $record);
1019 $instance->save();
1021 return $instance;
1025 * Unsets the context instance record.
1027 * @param \tool_dataprivacy\context_instance $instance
1028 * @return null
1030 public static function unset_context_instance(context_instance $instance) {
1031 $instance->delete();
1035 * Sets the context level purpose and category.
1037 * @throws \coding_exception
1038 * @param \stdClass $record
1039 * @return contextlevel
1041 public static function set_contextlevel($record) {
1042 global $DB;
1044 if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
1045 throw new \coding_exception('Only context system and context user can set a contextlevel ' .
1046 'purpose and retention');
1049 if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
1050 // Update.
1051 $contextlevel->from_record($record);
1052 } else {
1053 // Add.
1054 $contextlevel = new contextlevel(0, $record);
1056 $contextlevel->save();
1058 // We sync with their defaults as we removed these options from the defaults page.
1059 $classname = \context_helper::get_class_for_level($record->contextlevel);
1060 list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
1061 set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
1062 set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
1064 return $contextlevel;
1068 * Returns the effective category given a context instance.
1070 * @param \context $context
1071 * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
1072 * @return category|false
1074 public static function get_effective_context_category(\context $context, $forcedvalue = false) {
1075 if (!data_registry::defaults_set()) {
1076 return false;
1079 return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
1083 * Returns the effective purpose given a context instance.
1085 * @param \context $context
1086 * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
1087 * @return purpose|false
1089 public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
1090 if (!data_registry::defaults_set()) {
1091 return false;
1094 return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
1098 * Returns the effective category given a context level.
1100 * @param int $contextlevel
1101 * @return category|false
1103 public static function get_effective_contextlevel_category($contextlevel) {
1104 if (!data_registry::defaults_set()) {
1105 return false;
1108 return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
1112 * Returns the effective purpose given a context level.
1114 * @param int $contextlevel
1115 * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
1116 * @return purpose|false
1118 public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
1119 if (!data_registry::defaults_set()) {
1120 return false;
1123 return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
1127 * Creates an expired context record for the provided context id.
1129 * @param int $contextid
1130 * @return \tool_dataprivacy\expired_context
1132 public static function create_expired_context($contextid) {
1133 $record = (object)[
1134 'contextid' => $contextid,
1135 'status' => expired_context::STATUS_EXPIRED,
1137 $expiredctx = new expired_context(0, $record);
1138 $expiredctx->save();
1140 return $expiredctx;
1144 * Deletes an expired context record.
1146 * @param int $id The tool_dataprivacy_ctxexpire id.
1147 * @return bool True on success.
1149 public static function delete_expired_context($id) {
1150 $expiredcontext = new expired_context($id);
1151 return $expiredcontext->delete();
1155 * Updates the status of an expired context.
1157 * @param \tool_dataprivacy\expired_context $expiredctx
1158 * @param int $status
1159 * @return null
1161 public static function set_expired_context_status(expired_context $expiredctx, $status) {
1162 $expiredctx->set('status', $status);
1163 $expiredctx->save();
1167 * Finds all contextlists having at least one approved context, and returns them as in a contextlist_collection.
1169 * @param contextlist_collection $collection The collection of unapproved contextlist objects.
1170 * @param \stdClass $foruser The target user
1171 * @param int $type The purpose of the collection
1172 * @return contextlist_collection The collection of approved_contextlist objects.
1174 public static function get_approved_contextlist_collection_for_collection(contextlist_collection $collection,
1175 \stdClass $foruser, int $type) : contextlist_collection {
1177 // Create the approved contextlist collection object.
1178 $approvedcollection = new contextlist_collection($collection->get_userid());
1179 $isconfigured = data_registry::defaults_set();
1181 foreach ($collection as $contextlist) {
1182 $contextids = [];
1183 foreach ($contextlist as $context) {
1184 if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $type) {
1185 // Data can only be deleted from it if the context is either expired, or unprotected.
1186 // Note: We can only check whether a context is expired or unprotected if the site is configured and
1187 // defaults are set appropriately. If they are not, we treat all contexts as though they are
1188 // unprotected.
1189 $purpose = static::get_effective_context_purpose($context);
1190 if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
1191 continue;
1195 $contextids[] = $context->id;
1198 // The data for the last component contextlist won't have been written yet, so write it now.
1199 if (!empty($contextids)) {
1200 $approvedcollection->add_contextlist(
1201 new approved_contextlist($foruser, $contextlist->get_component(), $contextids)
1206 return $approvedcollection;
1210 * Updates the default category and purpose for a given context level (and optionally, a plugin).
1212 * @param int $contextlevel The context level.
1213 * @param int $categoryid The ID matching the category.
1214 * @param int $purposeid The ID matching the purpose record.
1215 * @param int $activity The name of the activity that we're making a defaults configuration for.
1216 * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
1217 * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
1219 public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
1220 global $DB;
1222 // Get the class name associated with this context level.
1223 $classname = context_helper::get_class_for_level($contextlevel);
1224 list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
1226 // Check the default category to be set.
1227 if ($categoryid == context_instance::INHERIT) {
1228 unset_config($categoryvar, 'tool_dataprivacy');
1230 } else {
1231 // Make sure the given category ID exists first.
1232 $categorypersistent = new category($categoryid);
1233 $categorypersistent->read();
1235 // Then set the new default value.
1236 set_config($categoryvar, $categoryid, 'tool_dataprivacy');
1239 // Check the default purpose to be set.
1240 if ($purposeid == context_instance::INHERIT) {
1241 // If the defaults is set to inherit, just unset the config value.
1242 unset_config($purposevar, 'tool_dataprivacy');
1244 } else {
1245 // Make sure the given purpose ID exists first.
1246 $purposepersistent = new purpose($purposeid);
1247 $purposepersistent->read();
1249 // Then set the new default value.
1250 set_config($purposevar, $purposeid, 'tool_dataprivacy');
1253 // Unset instances that have been assigned with custom purpose and category, if override was specified.
1254 if ($override) {
1255 // We'd like to find context IDs that we want to unset.
1256 $statements = ["SELECT c.id as contextid FROM {context} c"];
1257 // Based on this context level.
1258 $params = ['contextlevel' => $contextlevel];
1260 if ($contextlevel == CONTEXT_MODULE) {
1261 // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
1262 $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
1263 // And that the module is listed on the modules table.
1264 $statements[] = "JOIN {modules} m ON m.id = cm.module";
1266 if ($activity) {
1267 // If we're overriding for an activity module, make sure that the context instance matches that activity.
1268 $statements[] = "AND m.name = :modname";
1269 $params['modname'] = $activity;
1272 // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
1273 $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
1274 // And that the context level of this instance matches the given context level.
1275 $statements[] = "WHERE c.contextlevel = :contextlevel";
1277 // Build our SQL query by gluing the statements.
1278 $sql = implode("\n", $statements);
1280 // Get the context records matching our query.
1281 $contextids = $DB->get_fieldset_sql($sql, $params);
1283 // Delete the matching context instances.
1284 foreach ($contextids as $contextid) {
1285 if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
1286 self::unset_context_instance($instance);
1291 return true;
1295 * Format the supplied date interval as a retention period.
1297 * @param \DateInterval $interval
1298 * @return string
1300 public static function format_retention_period(\DateInterval $interval) : string {
1301 // It is one or another.
1302 if ($interval->y) {
1303 $formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
1304 } else if ($interval->m) {
1305 $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
1306 } else if ($interval->d) {
1307 $formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
1308 } else {
1309 $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
1312 return $formattedtime;
1316 * Whether automatic data request approval is turned on or not for the given request type.
1318 * @param int $type The request type.
1319 * @return bool
1321 public static function is_automatic_request_approval_on(int $type): bool {
1322 switch ($type) {
1323 case self::DATAREQUEST_TYPE_EXPORT:
1324 return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval'));
1325 case self::DATAREQUEST_TYPE_DELETE:
1326 return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval'));
1328 return false;
1332 * Creates an ad-hoc task for the data request.
1334 * @param int $requestid The data request ID.
1335 * @param int $userid Optional. The user ID to run the task as, if necessary.
1337 public static function queue_data_request_task(int $requestid, int $userid = null): void {
1338 $task = new process_data_request_task();
1339 $task->set_custom_data(['requestid' => $requestid]);
1340 if ($userid) {
1341 $task->set_userid($userid);
1343 manager::queue_adhoc_task($task, true);