MDL-62660 tool_dataprivacy: Add ability to expire data requests
[moodle.git] / admin / tool / dataprivacy / classes / api.php
blob6ee970733d76238e93ebb0159673a95db7445d3a
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_course;
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\initiate_data_request_task;
43 use tool_dataprivacy\task\process_data_request_task;
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 /** Newly submitted and we have started to find the location of data. */
68 const DATAREQUEST_STATUS_PREPROCESSING = 1;
70 /** Metadata ready and awaiting review and approval by the Data Protection officer. */
71 const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
73 /** Request approved and will be processed soon. */
74 const DATAREQUEST_STATUS_APPROVED = 3;
76 /** The request is now being processed. */
77 const DATAREQUEST_STATUS_PROCESSING = 4;
79 /** Information/other request completed. */
80 const DATAREQUEST_STATUS_COMPLETE = 5;
82 /** Data request cancelled by the user. */
83 const DATAREQUEST_STATUS_CANCELLED = 6;
85 /** Data request rejected by the DPO. */
86 const DATAREQUEST_STATUS_REJECTED = 7;
88 /** Data request download ready. */
89 const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
91 /** Data request expired. */
92 const DATAREQUEST_STATUS_EXPIRED = 9;
94 /** Data delete request completed, account is removed. */
95 const DATAREQUEST_STATUS_DELETED = 10;
97 /**
98 * Determines whether the user can contact the site's Data Protection Officer via Moodle.
100 * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
101 * @throws dml_exception
103 public static function can_contact_dpo() {
104 return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
108 * Check's whether the current user has the capability to manage data requests.
110 * @param int $userid The user ID.
111 * @return bool
112 * @throws coding_exception
113 * @throws dml_exception
115 public static function can_manage_data_requests($userid) {
116 $context = context_system::instance();
118 // A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
119 return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
123 * Checks if the current user can manage the data registry at the provided id.
125 * @param int $contextid Fallback to system context id.
126 * @throws \required_capability_exception
127 * @return null
129 public static function check_can_manage_data_registry($contextid = false) {
130 if ($contextid) {
131 $context = \context_helper::instance_by_id($contextid);
132 } else {
133 $context = \context_system::instance();
136 require_capability('tool/dataprivacy:managedataregistry', $context);
140 * Fetches the role shortnames of Data Protection Officer roles.
142 * @return array An array of the DPO role shortnames
144 public static function get_dpo_role_names() : array {
145 global $DB;
147 $dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
148 $dponames = array();
150 if (!empty($dporoleids)) {
151 list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
152 $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
155 return $dponames;
159 * Fetches the list of users with the Data Protection Officer role.
161 * @throws dml_exception
163 public static function get_site_dpos() {
164 // Get role(s) that can manage data requests.
165 $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
167 $dpos = [];
168 $context = context_system::instance();
169 foreach ($dporoles as $roleid) {
170 if (empty($roleid)) {
171 continue;
173 $allnames = get_all_user_name_fields(true, 'u');
174 $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
175 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
176 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
177 'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
178 'r.name AS rolename, r.sortorder, '.
179 'r.shortname AS roleshortname, rn.name AS rolecoursealias';
180 // Fetch users that can manage data requests.
181 $dpos += get_role_users($roleid, $context, false, $fields);
184 // If the site has no data protection officer, defer to site admin(s).
185 if (empty($dpos)) {
186 $dpos = get_admins();
188 return $dpos;
192 * Checks whether a given user is a site DPO.
194 * @param int $userid The user ID.
195 * @return bool
196 * @throws dml_exception
198 public static function is_site_dpo($userid) {
199 $dpos = self::get_site_dpos();
200 return array_key_exists($userid, $dpos);
204 * Lodges a data request and sends the request details to the site Data Protection Officer(s).
206 * @param int $foruser The user whom the request is being made for.
207 * @param int $type The request type.
208 * @param string $comments Request comments.
209 * @return data_request
210 * @throws invalid_persistent_exception
211 * @throws coding_exception
213 public static function create_data_request($foruser, $type, $comments = '') {
214 global $USER;
216 $datarequest = new data_request();
217 // The user the request is being made for.
218 $datarequest->set('userid', $foruser);
220 $requestinguser = $USER->id;
221 // Check when the user is making a request on behalf of another.
222 if ($requestinguser != $foruser) {
223 if (self::is_site_dpo($requestinguser)) {
224 // The user making the request is a DPO. Should be fine.
225 $datarequest->set('dpo', $requestinguser);
226 } else {
227 // If not a DPO, only users with the capability to make data requests for the user should be allowed.
228 // (e.g. users with the Parent role, etc).
229 if (!self::can_create_data_request_for_user($foruser)) {
230 $forusercontext = \context_user::instance($foruser);
231 throw new required_capability_exception($forusercontext,
232 'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
236 // The user making the request.
237 $datarequest->set('requestedby', $requestinguser);
238 // Set status.
239 $datarequest->set('status', self::DATAREQUEST_STATUS_PENDING);
240 // Set request type.
241 $datarequest->set('type', $type);
242 // Set request comments.
243 $datarequest->set('comments', $comments);
245 // Store subject access request.
246 $datarequest->create();
248 // Fire an ad hoc task to initiate the data request process.
249 $task = new initiate_data_request_task();
250 $task->set_custom_data(['requestid' => $datarequest->get('id')]);
251 manager::queue_adhoc_task($task, true);
253 return $datarequest;
257 * Fetches the list of the data requests.
259 * If user ID is provided, it fetches the data requests for the user.
260 * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
261 * (e.g. Users with the Data Protection Officer roles)
263 * @param int $userid The User ID.
264 * @param int[] $statuses The status filters.
265 * @param int[] $types The request type filters.
266 * @param string $sort The order by clause.
267 * @param int $offset Amount of records to skip.
268 * @param int $limit Amount of records to fetch.
269 * @return data_request[]
270 * @throws coding_exception
271 * @throws dml_exception
273 public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
274 global $DB, $USER;
275 $results = [];
276 $sqlparams = [];
277 $sqlconditions = [];
279 // Set default sort.
280 if (empty($sort)) {
281 $sort = 'status ASC, timemodified ASC';
284 // Set status filters.
285 if (!empty($statuses)) {
286 list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
287 $sqlconditions[] = "status $statusinsql";
290 // Set request type filter.
291 if (!empty($types)) {
292 list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
293 $sqlconditions[] = "type $typeinsql";
294 $sqlparams = array_merge($sqlparams, $typeparams);
297 if ($userid) {
298 // Get the data requests for the user or data requests made by the user.
299 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
300 $params = [
301 'userid' => $userid,
302 'requestedby' => $userid
305 // Build a list of user IDs that the user is allowed to make data requests for.
306 // Of course, the user should be included in this list.
307 $alloweduserids = [$userid];
308 // Get any users that the user can make data requests for.
309 if ($children = helper::get_children_of_user($userid)) {
310 // Get the list of user IDs of the children and merge to the allowed user IDs.
311 $alloweduserids = array_merge($alloweduserids, array_keys($children));
313 list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
314 $sqlconditions[] .= "userid $insql";
315 $select = implode(' AND ', $sqlconditions);
316 $params = array_merge($params, $inparams, $sqlparams);
318 $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
319 } else {
320 // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
321 if (self::is_site_dpo($USER->id)) {
322 if (!empty($sqlconditions)) {
323 $select = implode(' AND ', $sqlconditions);
324 $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
325 } else {
326 $results = data_request::get_records(null, $sort, '', $offset, $limit);
331 // If any are due to expire, expire them and re-fetch updated data.
332 if (empty($statuses)
333 || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
334 || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
335 $expiredrequests = data_request::get_expired_requests($userid);
337 if (!empty($expiredrequests)) {
338 data_request::expire($expiredrequests);
339 $results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
343 return $results;
347 * Fetches the count of data request records based on the given parameters.
349 * @param int $userid The User ID.
350 * @param int[] $statuses The status filters.
351 * @param int[] $types The request type filters.
352 * @return int
353 * @throws coding_exception
354 * @throws dml_exception
356 public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
357 global $DB, $USER;
358 $count = 0;
359 $sqlparams = [];
360 $sqlconditions = [];
361 if (!empty($statuses)) {
362 list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
363 $sqlconditions[] = "status $statusinsql";
365 if (!empty($types)) {
366 list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
367 $sqlconditions[] = "type $typeinsql";
368 $sqlparams = array_merge($sqlparams, $typeparams);
370 if ($userid) {
371 // Get the data requests for the user or data requests made by the user.
372 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
373 $params = [
374 'userid' => $userid,
375 'requestedby' => $userid
378 // Build a list of user IDs that the user is allowed to make data requests for.
379 // Of course, the user should be included in this list.
380 $alloweduserids = [$userid];
381 // Get any users that the user can make data requests for.
382 if ($children = helper::get_children_of_user($userid)) {
383 // Get the list of user IDs of the children and merge to the allowed user IDs.
384 $alloweduserids = array_merge($alloweduserids, array_keys($children));
386 list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
387 $sqlconditions[] .= "userid $insql";
388 $select = implode(' AND ', $sqlconditions);
389 $params = array_merge($params, $inparams, $sqlparams);
391 $count = data_request::count_records_select($select, $params);
392 } else {
393 // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
394 if (self::is_site_dpo($USER->id)) {
395 if (!empty($sqlconditions)) {
396 $select = implode(' AND ', $sqlconditions);
397 $count = data_request::count_records_select($select, $sqlparams);
398 } else {
399 $count = data_request::count_records();
404 return $count;
408 * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
410 * @param int $userid The user ID.
411 * @param int $type The request type.
412 * @return bool
413 * @throws coding_exception
414 * @throws dml_exception
416 public static function has_ongoing_request($userid, $type) {
417 global $DB;
419 // Check if the user already has an incomplete data request of the same type.
420 $nonpendingstatuses = [
421 self::DATAREQUEST_STATUS_COMPLETE,
422 self::DATAREQUEST_STATUS_CANCELLED,
423 self::DATAREQUEST_STATUS_REJECTED,
424 self::DATAREQUEST_STATUS_DOWNLOAD_READY,
425 self::DATAREQUEST_STATUS_EXPIRED,
426 self::DATAREQUEST_STATUS_DELETED,
428 list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
429 $select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
430 $params = array_merge([
431 'type' => $type,
432 'userid' => $userid
433 ], $inparams);
435 return data_request::record_exists_select($select, $params);
439 * Determines whether a request is active or not based on its status.
441 * @param int $status The request status.
442 * @return bool
444 public static function is_active($status) {
445 // List of statuses which doesn't require any further processing.
446 $finalstatuses = [
447 self::DATAREQUEST_STATUS_COMPLETE,
448 self::DATAREQUEST_STATUS_CANCELLED,
449 self::DATAREQUEST_STATUS_REJECTED,
450 self::DATAREQUEST_STATUS_DOWNLOAD_READY,
451 self::DATAREQUEST_STATUS_EXPIRED,
452 self::DATAREQUEST_STATUS_DELETED,
455 return !in_array($status, $finalstatuses);
459 * Cancels the data request for a given request ID.
461 * @param int $requestid The request identifier.
462 * @param int $status The request status.
463 * @param int $dpoid The user ID of the Data Protection Officer
464 * @param string $comment The comment about the status update.
465 * @return bool
466 * @throws invalid_persistent_exception
467 * @throws coding_exception
469 public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') {
470 // Update the request.
471 $datarequest = new data_request($requestid);
472 $datarequest->set('status', $status);
473 if ($dpoid) {
474 $datarequest->set('dpo', $dpoid);
476 // Update the comment if necessary.
477 if (!empty(trim($comment))) {
478 $params = [
479 'date' => userdate(time()),
480 'comment' => $comment
482 $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
483 // Check if there's an existing DPO comment.
484 $currentcomment = trim($datarequest->get('dpocomment'));
485 if ($currentcomment) {
486 // Append the new comment to the current comment and give them 1 line space in between.
487 $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
489 $datarequest->set('dpocomment', $commenttosave);
492 return $datarequest->update();
496 * Fetches a request based on the request ID.
498 * @param int $requestid The request identifier
499 * @return data_request
501 public static function get_request($requestid) {
502 return new data_request($requestid);
506 * Approves a data request based on the request ID.
508 * @param int $requestid The request identifier
509 * @return bool
510 * @throws coding_exception
511 * @throws dml_exception
512 * @throws invalid_persistent_exception
513 * @throws required_capability_exception
514 * @throws moodle_exception
516 public static function approve_data_request($requestid) {
517 global $USER;
519 // Check first whether the user can manage data requests.
520 if (!self::can_manage_data_requests($USER->id)) {
521 $context = context_system::instance();
522 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
525 // Check if request is already awaiting for approval.
526 $request = new data_request($requestid);
527 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
528 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
531 // Update the status and the DPO.
532 $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
534 // Approve all the contexts attached to the request.
535 // Currently, approving the request implicitly approves all associated contexts, but this may change in future, allowing
536 // users to selectively approve certain contexts only.
537 self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED);
539 // Fire an ad hoc task to initiate the data request process.
540 $task = new process_data_request_task();
541 $task->set_custom_data(['requestid' => $requestid]);
542 if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
543 $task->set_userid($request->get('userid'));
545 manager::queue_adhoc_task($task, true);
547 return $result;
551 * Rejects a data request based on the request ID.
553 * @param int $requestid The request identifier
554 * @return bool
555 * @throws coding_exception
556 * @throws dml_exception
557 * @throws invalid_persistent_exception
558 * @throws required_capability_exception
559 * @throws moodle_exception
561 public static function deny_data_request($requestid) {
562 global $USER;
564 if (!self::can_manage_data_requests($USER->id)) {
565 $context = context_system::instance();
566 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
569 // Check if request is already awaiting for approval.
570 $request = new data_request($requestid);
571 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
572 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
575 // Update the status and the DPO.
576 return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
580 * Sends a message to the site's Data Protection Officer about a request.
582 * @param stdClass $dpo The DPO user record
583 * @param data_request $request The data request
584 * @return int|false
585 * @throws coding_exception
586 * @throws moodle_exception
588 public static function notify_dpo($dpo, data_request $request) {
589 global $PAGE, $SITE;
591 $output = $PAGE->get_renderer('tool_dataprivacy');
593 $usercontext = \context_user::instance($request->get('requestedby'));
594 $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
595 $requestdata = $requestexporter->export($output);
597 // Create message to send to the Data Protection Officer(s).
598 $typetext = null;
599 $typetext = $requestdata->typename;
600 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
602 $requestedby = $requestdata->requestedbyuser;
603 $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
604 $message = new message();
605 $message->courseid = $SITE->id;
606 $message->component = 'tool_dataprivacy';
607 $message->name = 'contactdataprotectionofficer';
608 $message->userfrom = $requestedby->id;
609 $message->replyto = $requestedby->email;
610 $message->replytoname = $requestedby->fullname;
611 $message->subject = $subject;
612 $message->fullmessageformat = FORMAT_HTML;
613 $message->notification = 1;
614 $message->contexturl = $datarequestsurl;
615 $message->contexturlname = get_string('datarequests', 'tool_dataprivacy');
617 // Prepare the context data for the email message body.
618 $messagetextdata = [
619 'requestedby' => $requestedby->fullname,
620 'requesttype' => $typetext,
621 'requestdate' => userdate($requestdata->timecreated),
622 'requestcomments' => $requestdata->messagehtml,
623 'datarequestsurl' => $datarequestsurl
625 $requestingfor = $requestdata->foruser;
626 if ($requestedby->id == $requestingfor->id) {
627 $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
628 } else {
629 $messagetextdata['requestfor'] = $requestingfor->fullname;
632 // Email the data request to the Data Protection Officer(s)/Admin(s).
633 $messagetextdata['dponame'] = fullname($dpo);
634 // Render message email body.
635 $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
636 $message->userto = $dpo;
637 $message->fullmessage = html_to_text($messagehtml);
638 $message->fullmessagehtml = $messagehtml;
640 // Send message.
641 return message_send($message);
645 * Checks whether a non-DPO user can make a data request for another user.
647 * @param int $user The user ID of the target user.
648 * @param int $requester The user ID of the user making the request.
649 * @return bool
650 * @throws coding_exception
652 public static function can_create_data_request_for_user($user, $requester = null) {
653 $usercontext = \context_user::instance($user);
654 return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
658 * Checks whether a user can download a data request.
660 * @param int $userid Target user id (subject of data request)
661 * @param int $requesterid Requester user id (person who requsted it)
662 * @param int|null $downloaderid Person who wants to download user id (default current)
663 * @return bool
664 * @throws coding_exception
666 public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
667 global $USER;
669 if (!$downloaderid) {
670 $downloaderid = $USER->id;
673 $usercontext = \context_user::instance($userid);
674 // If it's your own and you have the right capability, you can download it.
675 if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
676 return true;
678 // If you can download anyone's in that context, you can download it.
679 if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
680 return true;
682 // If you can have the 'child access' ability to request in that context, and you are the one
683 // who requested it, then you can download it.
684 if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
685 return true;
687 return false;
691 * Gets an action menu link to download a data request.
693 * @param \context_user $usercontext User context (of user who the data is for)
694 * @param int $requestid Request id
695 * @return \action_menu_link_secondary Action menu link
696 * @throws coding_exception
698 public static function get_download_link(\context_user $usercontext, $requestid) {
699 $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
700 'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
701 $downloadtext = get_string('download', 'tool_dataprivacy');
702 return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
706 * Creates a new data purpose.
708 * @param stdClass $record
709 * @return \tool_dataprivacy\purpose.
711 public static function create_purpose(stdClass $record) {
712 self::check_can_manage_data_registry();
714 $purpose = new purpose(0, $record);
715 $purpose->create();
717 return $purpose;
721 * Updates an existing data purpose.
723 * @param stdClass $record
724 * @return \tool_dataprivacy\purpose.
726 public static function update_purpose(stdClass $record) {
727 self::check_can_manage_data_registry();
729 if (!isset($record->sensitivedatareasons)) {
730 $record->sensitivedatareasons = '';
733 $purpose = new purpose($record->id);
734 $purpose->from_record($record);
736 $result = $purpose->update();
738 return $purpose;
742 * Deletes a data purpose.
744 * @param int $id
745 * @return bool
747 public static function delete_purpose($id) {
748 self::check_can_manage_data_registry();
750 $purpose = new purpose($id);
751 if ($purpose->is_used()) {
752 throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
754 return $purpose->delete();
758 * Get all system data purposes.
760 * @return \tool_dataprivacy\purpose[]
762 public static function get_purposes() {
763 self::check_can_manage_data_registry();
765 return purpose::get_records([], 'name', 'ASC');
769 * Creates a new data category.
771 * @param stdClass $record
772 * @return \tool_dataprivacy\category.
774 public static function create_category(stdClass $record) {
775 self::check_can_manage_data_registry();
777 $category = new category(0, $record);
778 $category->create();
780 return $category;
784 * Updates an existing data category.
786 * @param stdClass $record
787 * @return \tool_dataprivacy\category.
789 public static function update_category(stdClass $record) {
790 self::check_can_manage_data_registry();
792 $category = new category($record->id);
793 $category->from_record($record);
795 $result = $category->update();
797 return $category;
801 * Deletes a data category.
803 * @param int $id
804 * @return bool
806 public static function delete_category($id) {
807 self::check_can_manage_data_registry();
809 $category = new category($id);
810 if ($category->is_used()) {
811 throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
813 return $category->delete();
817 * Get all system data categories.
819 * @return \tool_dataprivacy\category[]
821 public static function get_categories() {
822 self::check_can_manage_data_registry();
824 return category::get_records([], 'name', 'ASC');
828 * Sets the context instance purpose and category.
830 * @param \stdClass $record
831 * @return \tool_dataprivacy\context_instance
833 public static function set_context_instance($record) {
834 self::check_can_manage_data_registry($record->contextid);
836 if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
837 // Update.
838 $instance->from_record($record);
840 if (empty($record->purposeid) && empty($record->categoryid)) {
841 // We accept one of them to be null but we delete it if both are null.
842 self::unset_context_instance($instance);
843 return;
846 } else {
847 // Add.
848 $instance = new context_instance(0, $record);
850 $instance->save();
852 return $instance;
856 * Unsets the context instance record.
858 * @param \tool_dataprivacy\context_instance $instance
859 * @return null
861 public static function unset_context_instance(context_instance $instance) {
862 self::check_can_manage_data_registry($instance->get('contextid'));
863 $instance->delete();
867 * Sets the context level purpose and category.
869 * @throws \coding_exception
870 * @param \stdClass $record
871 * @return contextlevel
873 public static function set_contextlevel($record) {
874 global $DB;
876 // Only manager at system level can set this.
877 self::check_can_manage_data_registry();
879 if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
880 throw new \coding_exception('Only context system and context user can set a contextlevel ' .
881 'purpose and retention');
884 if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
885 // Update.
886 $contextlevel->from_record($record);
887 } else {
888 // Add.
889 $contextlevel = new contextlevel(0, $record);
891 $contextlevel->save();
893 // We sync with their defaults as we removed these options from the defaults page.
894 $classname = \context_helper::get_class_for_level($record->contextlevel);
895 list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
896 set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
897 set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
899 return $contextlevel;
903 * Returns the effective category given a context instance.
905 * @param \context $context
906 * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
907 * @return category|false
909 public static function get_effective_context_category(\context $context, $forcedvalue=false) {
910 self::check_can_manage_data_registry($context->id);
911 if (!data_registry::defaults_set()) {
912 return false;
915 return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
919 * Returns the effective purpose given a context instance.
921 * @param \context $context
922 * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
923 * @return purpose|false
925 public static function get_effective_context_purpose(\context $context, $forcedvalue=false) {
926 self::check_can_manage_data_registry($context->id);
927 if (!data_registry::defaults_set()) {
928 return false;
931 return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
935 * Returns the effective category given a context level.
937 * @param int $contextlevel
938 * @param int $forcedvalue Use this categoryid value as if this was this context level category.
939 * @return category|false
941 public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
942 self::check_can_manage_data_registry(\context_system::instance()->id);
943 if (!data_registry::defaults_set()) {
944 return false;
947 return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
951 * Returns the effective purpose given a context level.
953 * @param int $contextlevel
954 * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
955 * @return purpose|false
957 public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
958 self::check_can_manage_data_registry(\context_system::instance()->id);
959 if (!data_registry::defaults_set()) {
960 return false;
963 return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
967 * Creates an expired context record for the provided context id.
969 * @param int $contextid
970 * @return \tool_dataprivacy\expired_context
972 public static function create_expired_context($contextid) {
973 self::check_can_manage_data_registry();
975 $record = (object)[
976 'contextid' => $contextid,
977 'status' => expired_context::STATUS_EXPIRED,
979 $expiredctx = new expired_context(0, $record);
980 $expiredctx->save();
982 return $expiredctx;
986 * Deletes an expired context record.
988 * @param int $id The tool_dataprivacy_ctxexpire id.
989 * @return bool True on success.
991 public static function delete_expired_context($id) {
992 self::check_can_manage_data_registry();
994 $expiredcontext = new expired_context($id);
995 return $expiredcontext->delete();
999 * Updates the status of an expired context.
1001 * @param \tool_dataprivacy\expired_context $expiredctx
1002 * @param int $status
1003 * @return null
1005 public static function set_expired_context_status(expired_context $expiredctx, $status) {
1006 self::check_can_manage_data_registry();
1008 $expiredctx->set('status', $status);
1009 $expiredctx->save();
1013 * Adds the contexts from the contextlist_collection to the request with the status provided.
1015 * @param contextlist_collection $clcollection a collection of contextlists for all components.
1016 * @param int $requestid the id of the request.
1017 * @param int $status the status to set the contexts to.
1019 public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
1020 $request = new data_request($requestid);
1021 foreach ($clcollection as $contextlist) {
1022 // Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
1023 $clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
1024 $clp->create();
1025 $contextlistid = $clp->get('id');
1027 // Store the associated contexts in the contextlist.
1028 foreach ($contextlist->get_contextids() as $contextid) {
1029 if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
1030 $context = \context::instance_by_id($contextid);
1031 if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
1032 continue;
1035 $context = new contextlist_context();
1036 $context->set('contextid', $contextid)
1037 ->set('contextlistid', $contextlistid)
1038 ->set('status', $status)
1039 ->create();
1042 // Create the relation to the request.
1043 $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid);
1044 $requestcontextlist->create();
1049 * Sets the status of all contexts associated with the request.
1051 * @param int $requestid the requestid to which the contexts belong.
1052 * @param int $status the status to set to.
1053 * @throws \dml_exception if the requestid is invalid.
1054 * @throws \moodle_exception if the status is invalid.
1056 public static function update_request_contexts_with_status(int $requestid, int $status) {
1057 // Validate contextlist_context status using the persistent's attribute validation.
1058 $contextlistcontext = new contextlist_context();
1059 $contextlistcontext->set('status', $status);
1060 if (array_key_exists('status', $contextlistcontext->get_errors())) {
1061 throw new moodle_exception("Invalid contextlist_context status: $status");
1064 // Validate requestid using the persistent's record validation.
1065 // A dml_exception is thrown if the record is missing.
1066 $datarequest = new data_request($requestid);
1068 // Bulk update the status of the request contexts.
1069 global $DB;
1071 $select = "SELECT ctx.id as id
1072 FROM {" . request_contextlist::TABLE . "} rcl
1073 JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1074 JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
1075 WHERE rcl.requestid = ?";
1077 // Fetch records IDs to be updated and update by chunks, if applicable (limit of 1000 records per update).
1078 $limit = 1000;
1079 $idstoupdate = $DB->get_fieldset_sql($select, [$requestid]);
1080 $count = count($idstoupdate);
1081 $idchunks = $idstoupdate;
1082 if ($count > $limit) {
1083 $idchunks = array_chunk($idstoupdate, $limit);
1085 $transaction = $DB->start_delegated_transaction();
1086 $initialparams = [$status];
1087 foreach ($idchunks as $chunk) {
1088 list($insql, $inparams) = $DB->get_in_or_equal($chunk);
1089 $update = "UPDATE {" . contextlist_context::TABLE . "}
1090 SET status = ?
1091 WHERE id $insql";
1092 $params = array_merge($initialparams, $inparams);
1093 $DB->execute($update, $params);
1095 $transaction->allow_commit();
1099 * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection.
1101 * @param data_request $request the data request with which the contextlists are associated.
1102 * @return contextlist_collection the collection of approved_contextlist objects.
1104 public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
1105 $foruser = core_user::get_user($request->get('userid'));
1107 // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
1108 global $DB;
1109 $sql = "SELECT cl.component, ctx.contextid
1110 FROM {" . request_contextlist::TABLE . "} rcl
1111 JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1112 JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
1113 WHERE rcl.requestid = ?
1114 AND ctx.status = ?
1115 ORDER BY cl.component, ctx.contextid";
1117 // Create the approved contextlist collection object.
1118 $lastcomponent = null;
1119 $approvedcollection = new contextlist_collection($foruser->id);
1121 $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]);
1122 foreach ($rs as $record) {
1123 // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the
1124 // last (the one we've just finished with) and reset the context array for the next one.
1125 if ($lastcomponent != $record->component) {
1126 if (!empty($contexts)) {
1127 $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
1129 $contexts = [];
1132 $contexts[] = $record->contextid;
1133 $lastcomponent = $record->component;
1135 $rs->close();
1137 // The data for the last component contextlist won't have been written yet, so write it now.
1138 if (!empty($contexts)) {
1139 $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
1142 return $approvedcollection;