MDL-61899 tool_dataprivacy: Subject access requests tool
[moodle.git] / admin / tool / dataprivacy / classes / api.php
blob9fca09602433608fab763ffc252ccca3289960d9
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_system;
28 use core\invalid_persistent_exception;
29 use core\message\message;
30 use core\task\manager;
31 use core_user;
32 use dml_exception;
33 use moodle_exception;
34 use moodle_url;
35 use required_capability_exception;
36 use stdClass;
37 use tool_dataprivacy\task\initiate_data_request_task;
38 use tool_dataprivacy\task\process_data_request_task;
39 use tool_dataprivacy\purpose;
40 use tool_dataprivacy\category;
41 use tool_dataprivacy\contextlevel;
42 use tool_dataprivacy\context_instance;
43 use tool_dataprivacy\data_registry;
44 use tool_dataprivacy\expired_context;
46 defined('MOODLE_INTERNAL') || die();
48 /**
49 * Class containing helper methods for processing data requests.
51 * @copyright 2018 Jun Pataleta
52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
54 class api {
56 /** Data export request type. */
57 const DATAREQUEST_TYPE_EXPORT = 1;
59 /** Data deletion request type. */
60 const DATAREQUEST_TYPE_DELETE = 2;
62 /** Other request type. Usually of enquiries to the DPO. */
63 const DATAREQUEST_TYPE_OTHERS = 3;
65 /** Newly submitted and we haven't yet started finding out where they have data. */
66 const DATAREQUEST_STATUS_PENDING = 0;
68 /** Newly submitted and we have started to find the location of data. */
69 const DATAREQUEST_STATUS_PREPROCESSING = 1;
71 /** Metadata ready and awaiting review and approval by the Data Protection officer. */
72 const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
74 /** Request approved and will be processed soon. */
75 const DATAREQUEST_STATUS_APPROVED = 3;
77 /** The request is now being processed. */
78 const DATAREQUEST_STATUS_PROCESSING = 4;
80 /** Data request completed. */
81 const DATAREQUEST_STATUS_COMPLETE = 5;
83 /** Data request cancelled by the user. */
84 const DATAREQUEST_STATUS_CANCELLED = 6;
86 /** Data request rejected by the DPO. */
87 const DATAREQUEST_STATUS_REJECTED = 7;
89 /**
90 * Determines whether the user can contact the site's Data Protection Officer via Moodle.
92 * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
93 * @throws dml_exception
95 public static function can_contact_dpo() {
96 return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
99 /**
100 * Check's whether the current user has the capability to manage data requests.
102 * @param int $userid The user ID.
103 * @return bool
104 * @throws coding_exception
105 * @throws dml_exception
107 public static function can_manage_data_requests($userid) {
108 $context = context_system::instance();
110 // A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
111 return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
115 * Checks if the current user can manage the data registry at the provided id.
117 * @param int $contextid Fallback to system context id.
118 * @throws \required_capability_exception
119 * @return null
121 public static function check_can_manage_data_registry($contextid = false) {
122 if ($contextid) {
123 $context = \context_helper::instance_by_id($contextid);
124 } else {
125 $context = \context_system::instance();
128 require_capability('tool/dataprivacy:managedataregistry', $context);
132 * Fetches the list of users with the Data Protection Officer role.
134 * @throws dml_exception
136 public static function get_site_dpos() {
137 // Get role(s) that can manage data requests.
138 $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
140 $dpos = [];
141 $context = context_system::instance();
142 foreach ($dporoles as $roleid) {
143 if (empty($roleid)) {
144 continue;
146 // Fetch users that can manage data requests.
147 $dpos += get_role_users($roleid, $context, false, 'u.*');
150 // If the site has no data protection officer, defer to site admin(s).
151 if (empty($dpos)) {
152 $dpos = get_admins();
154 return $dpos;
158 * Checks whether a given user is a site DPO.
160 * @param int $userid The user ID.
161 * @return bool
162 * @throws dml_exception
164 public static function is_site_dpo($userid) {
165 $dpos = self::get_site_dpos();
166 return array_key_exists($userid, $dpos);
170 * Lodges a data request and sends the request details to the site Data Protection Officer(s).
172 * @param int $foruser The user whom the request is being made for.
173 * @param int $type The request type.
174 * @param string $comments Request comments.
175 * @return data_request
176 * @throws invalid_persistent_exception
177 * @throws coding_exception
179 public static function create_data_request($foruser, $type, $comments = '') {
180 global $USER;
182 $datarequest = new data_request();
183 // The user the request is being made for.
184 $datarequest->set('userid', $foruser);
185 // The user making the request.
186 $datarequest->set('requestedby', $USER->id);
187 // Set status.
188 $datarequest->set('status', self::DATAREQUEST_STATUS_PENDING);
189 // Set request type.
190 $datarequest->set('type', $type);
191 // Set request comments.
192 $datarequest->set('comments', $comments);
194 // Store subject access request.
195 $datarequest->create();
197 // Fire an ad hoc task to initiate the data request process.
198 $task = new initiate_data_request_task();
199 $task->set_custom_data(['requestid' => $datarequest->get('id')]);
200 manager::queue_adhoc_task($task, true);
202 return $datarequest;
206 * Fetches the list of the data requests.
208 * If user ID is provided, it fetches the data requests for the user.
209 * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
210 * (e.g. Users with the Data Protection Officer roles)
212 * @param int $userid The User ID.
213 * @return data_request[]
214 * @throws dml_exception
216 public static function get_data_requests($userid = 0) {
217 global $USER;
218 $results = [];
219 if ($userid) {
220 // Get the data requests for the user or data requests made by the user.
221 $select = "userid = :userid OR requestedby = :requestedby";
222 $params = [
223 'userid' => $userid,
224 'requestedby' => $userid
226 $results = data_request::get_records_select($select, $params, 'status DESC, timemodified DESC');
227 } else {
228 // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
229 if (self::is_site_dpo($USER->id)) {
230 $results = data_request::get_records(null, 'status DESC, timemodified DESC', '');
234 return $results;
238 * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
240 * @param int $userid The user ID.
241 * @param int $type The request type.
242 * @return bool
243 * @throws coding_exception
244 * @throws dml_exception
246 public static function has_ongoing_request($userid, $type) {
247 global $DB;
249 // Check if the user already has an incomplete data request of the same type.
250 $nonpendingstatuses = [
251 self::DATAREQUEST_STATUS_COMPLETE,
252 self::DATAREQUEST_STATUS_CANCELLED,
253 self::DATAREQUEST_STATUS_REJECTED,
255 list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
256 $select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
257 $params = array_merge([
258 'type' => $type,
259 'userid' => $userid
260 ], $inparams);
262 return data_request::record_exists_select($select, $params);
266 * Determines whether a request is active or not based on its status.
268 * @param int $status The request status.
269 * @return bool
271 public static function is_active($status) {
272 // List of statuses which doesn't require any further processing.
273 $finalstatuses = [
274 self::DATAREQUEST_STATUS_COMPLETE,
275 self::DATAREQUEST_STATUS_CANCELLED,
276 self::DATAREQUEST_STATUS_REJECTED,
279 return !in_array($status, $finalstatuses);
283 * Cancels the data request for a given request ID.
285 * @param int $requestid The request identifier.
286 * @param int $status The request status.
287 * @param int $dpoid The user ID of the Data Protection Officer
288 * @return bool
289 * @throws invalid_persistent_exception
290 * @throws coding_exception
292 public static function update_request_status($requestid, $status, $dpoid = 0) {
293 // Update the request.
294 $datarequest = new data_request($requestid);
295 $datarequest->set('status', $status);
296 if ($dpoid) {
297 $datarequest->set('dpo', $dpoid);
299 return $datarequest->update();
303 * Fetches a request based on the request ID.
305 * @param int $requestid The request identifier
306 * @return data_request
308 public static function get_request($requestid) {
309 return new data_request($requestid);
313 * Approves a data request based on the request ID.
315 * @param int $requestid The request identifier
316 * @return bool
317 * @throws coding_exception
318 * @throws dml_exception
319 * @throws invalid_persistent_exception
320 * @throws required_capability_exception
321 * @throws moodle_exception
323 public static function approve_data_request($requestid) {
324 global $USER;
326 // Check first whether the user can manage data requests.
327 if (!self::can_manage_data_requests($USER->id)) {
328 $context = context_system::instance();
329 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
332 // Check if request is already awaiting for approval.
333 $request = new data_request($requestid);
334 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
335 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
338 // Update the status and the DPO.
339 $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
341 // Fire an ad hoc task to initiate the data request process.
342 $task = new process_data_request_task();
343 $task->set_custom_data(['requestid' => $requestid]);
344 if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
345 $task->set_userid($request->get('userid'));
347 manager::queue_adhoc_task($task, true);
349 return $result;
353 * Rejects a data request based on the request ID.
355 * @param int $requestid The request identifier
356 * @return bool
357 * @throws coding_exception
358 * @throws dml_exception
359 * @throws invalid_persistent_exception
360 * @throws required_capability_exception
361 * @throws moodle_exception
363 public static function deny_data_request($requestid) {
364 global $USER;
366 if (!self::can_manage_data_requests($USER->id)) {
367 $context = context_system::instance();
368 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
371 // Check if request is already awaiting for approval.
372 $request = new data_request($requestid);
373 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
374 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
377 // Update the status and the DPO.
378 return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
382 * Sends a message to the site's Data Protection Officer about a request.
384 * @param stdClass $dpo The DPO user record
385 * @param data_request $request The data request
386 * @return int|false
387 * @throws coding_exception
388 * @throws dml_exception
389 * @throws moodle_exception
391 public static function notify_dpo($dpo, data_request $request) {
392 global $PAGE, $SITE;
394 // Create message to send to the Data Protection Officer(s).
395 $typetext = null;
396 switch ($request->get('type')) {
397 case self::DATAREQUEST_TYPE_EXPORT:
398 $typetext = get_string('requesttypeexport', 'tool_dataprivacy');
399 break;
400 case self::DATAREQUEST_TYPE_DELETE:
401 $typetext = get_string('requesttypedelete', 'tool_dataprivacy');
402 break;
403 case self::DATAREQUEST_TYPE_OTHERS:
404 $typetext = get_string('requesttypeothers', 'tool_dataprivacy');
405 break;
406 default:
407 throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
409 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
411 $requestedby = core_user::get_user($request->get('requestedby'));
412 $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
413 $message = new message();
414 $message->courseid = $SITE->id;
415 $message->component = 'tool_dataprivacy';
416 $message->name = 'contactdataprotectionofficer';
417 $message->userfrom = $requestedby;
418 $message->replyto = $requestedby->email;
419 $message->replytoname = fullname($requestedby->email);
420 $message->subject = $subject;
421 $message->fullmessageformat = FORMAT_HTML;
422 $message->notification = 1;
423 $message->contexturl = $datarequestsurl;
424 $message->contexturlname = get_string('datarequests', 'tool_dataprivacy');
426 // Prepare the context data for the email message body.
427 $messagetextdata = [
428 'requestedby' => fullname($requestedby),
429 'requesttype' => $typetext,
430 'requestdate' => userdate($request->get('timecreated')),
431 'requestcomments' => text_to_html($request->get('comments')),
432 'datarequestsurl' => $datarequestsurl
434 $requestingfor = core_user::get_user($request->get('userid'));
435 if ($requestedby->id == $requestingfor->id) {
436 $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
437 } else {
438 $messagetextdata['requestfor'] = fullname($requestingfor);
441 $output = $PAGE->get_renderer('tool_dataprivacy');
442 // Email the data request to the Data Protection Officer(s)/Admin(s).
443 $messagetextdata['dponame'] = fullname($dpo);
444 // Render message email body.
445 $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
446 $message->userto = $dpo;
447 $message->fullmessage = html_to_text($messagehtml);
448 $message->fullmessagehtml = $messagehtml;
450 // Send message.
451 return message_send($message);
455 * Creates a new data purpose.
457 * @param stdClass $record
458 * @return \tool_dataprivacy\purpose.
460 public static function create_purpose(stdClass $record) {
461 self::check_can_manage_data_registry();
463 $purpose = new purpose(0, $record);
464 $purpose->create();
466 return $purpose;
470 * Updates an existing data purpose.
472 * @param stdClass $record
473 * @return \tool_dataprivacy\purpose.
475 public static function update_purpose(stdClass $record) {
476 self::check_can_manage_data_registry();
478 $purpose = new purpose($record->id);
479 $purpose->from_record($record);
481 $result = $purpose->update();
483 return $purpose;
487 * Deletes a data purpose.
489 * @param int $id
490 * @return bool
492 public static function delete_purpose($id) {
493 self::check_can_manage_data_registry();
495 $purpose = new purpose($id);
496 if ($purpose->is_used()) {
497 throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
499 return $purpose->delete();
503 * Get all system data purposes.
505 * @return \tool_dataprivacy\purpose[]
507 public static function get_purposes() {
508 self::check_can_manage_data_registry();
510 return purpose::get_records([], 'name', 'ASC');
514 * Creates a new data category.
516 * @param stdClass $record
517 * @return \tool_dataprivacy\category.
519 public static function create_category(stdClass $record) {
520 self::check_can_manage_data_registry();
522 $category = new category(0, $record);
523 $category->create();
525 return $category;
529 * Updates an existing data category.
531 * @param stdClass $record
532 * @return \tool_dataprivacy\category.
534 public static function update_category(stdClass $record) {
535 self::check_can_manage_data_registry();
537 $category = new category($record->id);
538 $category->from_record($record);
540 $result = $category->update();
542 return $category;
546 * Deletes a data category.
548 * @param int $id
549 * @return bool
551 public static function delete_category($id) {
552 self::check_can_manage_data_registry();
554 $category = new category($id);
555 if ($category->is_used()) {
556 throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
558 return $category->delete();
562 * Get all system data categories.
564 * @return \tool_dataprivacy\category[]
566 public static function get_categories() {
567 self::check_can_manage_data_registry();
569 return category::get_records([], 'name', 'ASC');
573 * Sets the context instance purpose and category.
575 * @param \stdClass $record
576 * @return \tool_dataprivacy\context_instance
578 public static function set_context_instance($record) {
579 self::check_can_manage_data_registry($record->contextid);
581 if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
582 // Update.
583 $instance->from_record($record);
585 if (empty($record->purposeid) && empty($record->categoryid)) {
586 // We accept one of them to be null but we delete it if both are null.
587 self::unset_context_instance($instance);
588 return;
591 } else {
592 // Add.
593 $instance = new context_instance(0, $record);
595 $instance->save();
597 return $instance;
601 * Unsets the context instance record.
603 * @param \tool_dataprivacy\context_instance $instance
604 * @return null
606 public static function unset_context_instance(context_instance $instance) {
607 self::check_can_manage_data_registry($instance->get('contextid'));
608 $instance->delete();
612 * Sets the context level purpose and category.
614 * @throws \coding_exception
615 * @param \stdClass $record
616 * @return contextlevel
618 public static function set_contextlevel($record) {
619 global $DB;
621 // Only manager at system level can set this.
622 self::check_can_manage_data_registry();
624 if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
625 throw new \coding_exception('Only context system and context user can set a contextlevel ' .
626 'purpose and retention');
629 if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
630 // Update.
631 $contextlevel->from_record($record);
632 } else {
633 // Add.
634 $contextlevel = new contextlevel(0, $record);
636 $contextlevel->save();
638 // We sync with their defaults as we removed these options from the defaults page.
639 $classname = \context_helper::get_class_for_level($record->contextlevel);
640 list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
641 set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
642 set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
644 return $contextlevel;
648 * Returns the effective category given a context instance.
650 * @param \context $context
651 * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
652 * @return category|false
654 public static function get_effective_context_category(\context $context, $forcedvalue=false) {
655 self::check_can_manage_data_registry($context->id);
656 if (!data_registry::defaults_set()) {
657 return false;
660 return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
664 * Returns the effective purpose given a context instance.
666 * @param \context $context
667 * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
668 * @return purpose|false
670 public static function get_effective_context_purpose(\context $context, $forcedvalue=false) {
671 self::check_can_manage_data_registry($context->id);
672 if (!data_registry::defaults_set()) {
673 return false;
676 return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
680 * Returns the effective category given a context level.
682 * @param int $contextlevel
683 * @param int $forcedvalue Use this categoryid value as if this was this context level category.
684 * @return category|false
686 public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
687 self::check_can_manage_data_registry(\context_system::instance()->id);
688 if (!data_registry::defaults_set()) {
689 return false;
692 return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
696 * Returns the effective purpose given a context level.
698 * @param int $contextlevel
699 * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
700 * @return purpose|false
702 public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
703 self::check_can_manage_data_registry(\context_system::instance()->id);
704 if (!data_registry::defaults_set()) {
705 return false;
708 return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
712 * Creates an expired context record for the provided context id.
714 * @param int $contextid
715 * @return \tool_dataprivacy\expired_context
717 public static function create_expired_context($contextid) {
718 self::check_can_manage_data_registry();
720 $record = (object)[
721 'contextid' => $contextid,
722 'status' => expired_context::STATUS_EXPIRED,
724 $expiredctx = new expired_context(0, $record);
725 $expiredctx->save();
727 return $expiredctx;
731 * Deletes an expired context record.
733 * @param int $id The tool_dataprivacy_ctxexpire id.
734 * @return bool True on success.
736 public static function delete_expired_context($id) {
737 self::check_can_manage_data_registry();
739 $expiredcontext = new expired_context($id);
740 return $expiredcontext->delete();
744 * Updates the status of an expired context.
746 * @param \tool_dataprivacy\expired_context $expiredctx
747 * @param int $status
748 * @return null
750 public static function set_expired_context_status(expired_context $expiredctx, $status) {
751 self::check_can_manage_data_registry();
753 $expiredctx->set('status', $status);
754 $expiredctx->save();