MDL-61899 tool_dataprivacy: Refined patch fixing cibot complains
[moodle.git] / admin / tool / dataprivacy / classes / api.php
CommitLineData
5efc1f9e
DM
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.
13//
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/>.
16
17/**
18 * Class containing helper methods for processing data requests.
19 *
20 * @package tool_dataprivacy
21 * @copyright 2018 Jun Pataleta
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24namespace tool_dataprivacy;
25
26use coding_exception;
27use context_system;
28use core\invalid_persistent_exception;
29use core\message\message;
30use core\task\manager;
31use core_user;
32use dml_exception;
33use moodle_exception;
34use moodle_url;
35use required_capability_exception;
36use stdClass;
37use tool_dataprivacy\task\initiate_data_request_task;
38use tool_dataprivacy\task\process_data_request_task;
39use tool_dataprivacy\purpose;
40use tool_dataprivacy\category;
41use tool_dataprivacy\contextlevel;
42use tool_dataprivacy\context_instance;
43use tool_dataprivacy\data_registry;
44use tool_dataprivacy\expired_context;
45
46defined('MOODLE_INTERNAL') || die();
47
48/**
49 * Class containing helper methods for processing data requests.
50 *
51 * @copyright 2018 Jun Pataleta
52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
53 */
54class api {
55
56 /** Data export request type. */
57 const DATAREQUEST_TYPE_EXPORT = 1;
58
59 /** Data deletion request type. */
60 const DATAREQUEST_TYPE_DELETE = 2;
61
62 /** Other request type. Usually of enquiries to the DPO. */
63 const DATAREQUEST_TYPE_OTHERS = 3;
64
65 /** Newly submitted and we haven't yet started finding out where they have data. */
66 const DATAREQUEST_STATUS_PENDING = 0;
67
68 /** Newly submitted and we have started to find the location of data. */
69 const DATAREQUEST_STATUS_PREPROCESSING = 1;
70
71 /** Metadata ready and awaiting review and approval by the Data Protection officer. */
72 const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
73
74 /** Request approved and will be processed soon. */
75 const DATAREQUEST_STATUS_APPROVED = 3;
76
77 /** The request is now being processed. */
78 const DATAREQUEST_STATUS_PROCESSING = 4;
79
80 /** Data request completed. */
81 const DATAREQUEST_STATUS_COMPLETE = 5;
82
83 /** Data request cancelled by the user. */
84 const DATAREQUEST_STATUS_CANCELLED = 6;
85
86 /** Data request rejected by the DPO. */
87 const DATAREQUEST_STATUS_REJECTED = 7;
88
89 /**
90 * Determines whether the user can contact the site's Data Protection Officer via Moodle.
91 *
92 * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
93 * @throws dml_exception
94 */
95 public static function can_contact_dpo() {
96 return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
97 }
98
99 /**
100 * Check's whether the current user has the capability to manage data requests.
101 *
102 * @param int $userid The user ID.
103 * @return bool
104 * @throws coding_exception
105 * @throws dml_exception
106 */
107 public static function can_manage_data_requests($userid) {
108 $context = context_system::instance();
109
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);
112 }
113
114 /**
115 * Checks if the current user can manage the data registry at the provided id.
116 *
117 * @param int $contextid Fallback to system context id.
118 * @throws \required_capability_exception
119 * @return null
120 */
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();
126 }
127
128 require_capability('tool/dataprivacy:managedataregistry', $context);
129 }
130
131 /**
132 * Fetches the list of users with the Data Protection Officer role.
133 *
134 * @throws dml_exception
135 */
136 public static function get_site_dpos() {
137 // Get role(s) that can manage data requests.
138 $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
139
140 $dpos = [];
141 $context = context_system::instance();
142 foreach ($dporoles as $roleid) {
143 if (empty($roleid)) {
144 continue;
145 }
146 // Fetch users that can manage data requests.
147 $dpos += get_role_users($roleid, $context, false, 'u.*');
148 }
149
150 // If the site has no data protection officer, defer to site admin(s).
151 if (empty($dpos)) {
152 $dpos = get_admins();
153 }
154 return $dpos;
155 }
156
157 /**
158 * Checks whether a given user is a site DPO.
159 *
160 * @param int $userid The user ID.
161 * @return bool
162 * @throws dml_exception
163 */
164 public static function is_site_dpo($userid) {
165 $dpos = self::get_site_dpos();
166 return array_key_exists($userid, $dpos);
167 }
168
169 /**
170 * Lodges a data request and sends the request details to the site Data Protection Officer(s).
171 *
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
178 */
179 public static function create_data_request($foruser, $type, $comments = '') {
180 global $USER;
181
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);
193
194 // Store subject access request.
195 $datarequest->create();
196
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);
201
202 return $datarequest;
203 }
204
205 /**
206 * Fetches the list of the data requests.
207 *
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)
211 *
212 * @param int $userid The User ID.
213 * @return data_request[]
214 * @throws dml_exception
215 */
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
225 ];
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', '');
231 }
232 }
233
234 return $results;
235 }
236
237 /**
238 * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
239 *
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
245 */
246 public static function has_ongoing_request($userid, $type) {
247 global $DB;
248
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,
254 ];
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);
261
262 return data_request::record_exists_select($select, $params);
263 }
264
265 /**
266 * Determines whether a request is active or not based on its status.
267 *
268 * @param int $status The request status.
269 * @return bool
270 */
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,
277 ];
278
279 return !in_array($status, $finalstatuses);
280 }
281
282 /**
283 * Cancels the data request for a given request ID.
284 *
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
291 */
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);
298 }
299 return $datarequest->update();
300 }
301
302 /**
303 * Fetches a request based on the request ID.
304 *
305 * @param int $requestid The request identifier
306 * @return data_request
307 */
308 public static function get_request($requestid) {
309 return new data_request($requestid);
310 }
311
312 /**
313 * Approves a data request based on the request ID.
314 *
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
322 */
323 public static function approve_data_request($requestid) {
324 global $USER;
325
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', '');
330 }
331
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');
336 }
337
338 // Update the status and the DPO.
339 $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
340
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'));
346 }
347 manager::queue_adhoc_task($task, true);
348
349 return $result;
350 }
351
352 /**
353 * Rejects a data request based on the request ID.
354 *
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
362 */
363 public static function deny_data_request($requestid) {
364 global $USER;
365
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', '');
369 }
370
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');
375 }
376
377 // Update the status and the DPO.
378 return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
379 }
380
381 /**
382 * Sends a message to the site's Data Protection Officer about a request.
383 *
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
390 */
391 public static function notify_dpo($dpo, data_request $request) {
392 global $PAGE, $SITE;
393
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');
408 }
409 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
410
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');
425
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
433 ];
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);
439 }
440
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;
449
450 // Send message.
451 return message_send($message);
452 }
453
454 /**
455 * Creates a new data purpose.
456 *
457 * @param stdClass $record
458 * @return \tool_dataprivacy\purpose.
459 */
460 public static function create_purpose(stdClass $record) {
461 self::check_can_manage_data_registry();
462
463 $purpose = new purpose(0, $record);
464 $purpose->create();
465
466 return $purpose;
467 }
468
469 /**
470 * Updates an existing data purpose.
471 *
472 * @param stdClass $record
473 * @return \tool_dataprivacy\purpose.
474 */
475 public static function update_purpose(stdClass $record) {
476 self::check_can_manage_data_registry();
477
478 $purpose = new purpose($record->id);
479 $purpose->from_record($record);
480
481 $result = $purpose->update();
482
483 return $purpose;
484 }
485
486 /**
487 * Deletes a data purpose.
488 *
489 * @param int $id
490 * @return bool
491 */
492 public static function delete_purpose($id) {
493 self::check_can_manage_data_registry();
494
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.');
498 }
499 return $purpose->delete();
500 }
501
502 /**
503 * Get all system data purposes.
504 *
505 * @return \tool_dataprivacy\purpose[]
506 */
507 public static function get_purposes() {
508 self::check_can_manage_data_registry();
509
510 return purpose::get_records([], 'name', 'ASC');
511 }
512
513 /**
514 * Creates a new data category.
515 *
516 * @param stdClass $record
517 * @return \tool_dataprivacy\category.
518 */
519 public static function create_category(stdClass $record) {
520 self::check_can_manage_data_registry();
521
522 $category = new category(0, $record);
523 $category->create();
524
525 return $category;
526 }
527
528 /**
529 * Updates an existing data category.
530 *
531 * @param stdClass $record
532 * @return \tool_dataprivacy\category.
533 */
534 public static function update_category(stdClass $record) {
535 self::check_can_manage_data_registry();
536
537 $category = new category($record->id);
538 $category->from_record($record);
539
540 $result = $category->update();
541
542 return $category;
543 }
544
545 /**
546 * Deletes a data category.
547 *
548 * @param int $id
549 * @return bool
550 */
551 public static function delete_category($id) {
552 self::check_can_manage_data_registry();
553
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.');
557 }
558 return $category->delete();
559 }
560
561 /**
562 * Get all system data categories.
563 *
564 * @return \tool_dataprivacy\category[]
565 */
566 public static function get_categories() {
567 self::check_can_manage_data_registry();
568
569 return category::get_records([], 'name', 'ASC');
570 }
571
572 /**
573 * Sets the context instance purpose and category.
574 *
575 * @param \stdClass $record
576 * @return \tool_dataprivacy\context_instance
577 */
578 public static function set_context_instance($record) {
579 self::check_can_manage_data_registry($record->contextid);
580
581 if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
582 // Update.
583 $instance->from_record($record);
584
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;
589 }
590
591 } else {
592 // Add.
593 $instance = new context_instance(0, $record);
594 }
595 $instance->save();
596
597 return $instance;
598 }
599
600 /**
601 * Unsets the context instance record.
602 *
603 * @param \tool_dataprivacy\context_instance $instance
604 * @return null
605 */
606 public static function unset_context_instance(context_instance $instance) {
607 self::check_can_manage_data_registry($instance->get('contextid'));
608 $instance->delete();
609 }
610
611 /**
612 * Sets the context level purpose and category.
613 *
614 * @throws \coding_exception
615 * @param \stdClass $record
616 * @return contextlevel
617 */
618 public static function set_contextlevel($record) {
619 global $DB;
620
621 // Only manager at system level can set this.
622 self::check_can_manage_data_registry();
623
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');
627 }
628
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);
635 }
636 $contextlevel->save();
637
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');
643
644 return $contextlevel;
645 }
646
647 /**
648 * Returns the effective category given a context instance.
649 *
650 * @param \context $context
651 * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
652 * @return category|false
653 */
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;
658 }
659
660 return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
661 }
662
663 /**
664 * Returns the effective purpose given a context instance.
665 *
666 * @param \context $context
667 * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
668 * @return purpose|false
669 */
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;
674 }
675
676 return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
677 }
678
679 /**
680 * Returns the effective category given a context level.
681 *
682 * @param int $contextlevel
683 * @param int $forcedvalue Use this categoryid value as if this was this context level category.
684 * @return category|false
685 */
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;
690 }
691
692 return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
693 }
694
695 /**
696 * Returns the effective purpose given a context level.
697 *
698 * @param int $contextlevel
699 * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
700 * @return purpose|false
701 */
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;
706 }
707
708 return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
709 }
710
711 /**
712 * Creates an expired context record for the provided context id.
713 *
714 * @param int $contextid
715 * @return \tool_dataprivacy\expired_context
716 */
717 public static function create_expired_context($contextid) {
718 self::check_can_manage_data_registry();
719
720 $record = (object)[
721 'contextid' => $contextid,
722 'status' => expired_context::STATUS_EXPIRED,
723 ];
724 $expiredctx = new expired_context(0, $record);
725 $expiredctx->save();
726
727 return $expiredctx;
728 }
729
730 /**
731 * Deletes an expired context record.
732 *
733 * @param int $id The tool_dataprivacy_ctxexpire id.
734 * @return bool True on success.
735 */
736 public static function delete_expired_context($id) {
737 self::check_can_manage_data_registry();
738
739 $expiredcontext = new expired_context($id);
740 return $expiredcontext->delete();
741 }
742
743 /**
744 * Updates the status of an expired context.
745 *
746 * @param \tool_dataprivacy\expired_context $expiredctx
747 * @param int $status
748 * @return null
749 */
750 public static function set_expired_context_status(expired_context $expiredctx, $status) {
751 self::check_can_manage_data_registry();
752
753 $expiredctx->set('status', $status);
754 $expiredctx->save();
755 }
756}