2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
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
;
28 use core\invalid_persistent_exception
;
29 use core\message\message
;
30 use core\task\manager
;
35 use required_capability_exception
;
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();
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
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;
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;
100 * Check's whether the current user has the capability to manage data requests.
102 * @param int $userid The user ID.
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
121 public static function check_can_manage_data_registry($contextid = false) {
123 $context = \context_helper
::instance_by_id($contextid);
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'));
141 $context = context_system
::instance();
142 foreach ($dporoles as $roleid) {
143 if (empty($roleid)) {
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).
152 $dpos = get_admins();
158 * Checks whether a given user is a site DPO.
160 * @param int $userid The user ID.
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 = '') {
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
);
188 $datarequest->set('status', self
::DATAREQUEST_STATUS_PENDING
);
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);
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) {
220 // Get the data requests for the user or data requests made by the user.
221 $select = "userid = :userid OR requestedby = :requestedby";
224 'requestedby' => $userid
226 $results = data_request
::get_records_select($select, $params, 'status DESC, timemodified DESC');
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', '');
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.
243 * @throws coding_exception
244 * @throws dml_exception
246 public static function has_ongoing_request($userid, $type) {
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([
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.
271 public static function is_active($status) {
272 // List of statuses which doesn't require any further processing.
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
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);
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
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) {
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);
353 * Rejects a data request based on the request ID.
355 * @param int $requestid The request identifier
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) {
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
387 * @throws coding_exception
388 * @throws dml_exception
389 * @throws moodle_exception
391 public static function notify_dpo($dpo, data_request
$request) {
394 // Create message to send to the Data Protection Officer(s).
396 switch ($request->get('type')) {
397 case self
::DATAREQUEST_TYPE_EXPORT
:
398 $typetext = get_string('requesttypeexport', 'tool_dataprivacy');
400 case self
::DATAREQUEST_TYPE_DELETE
:
401 $typetext = get_string('requesttypedelete', 'tool_dataprivacy');
403 case self
::DATAREQUEST_TYPE_OTHERS
:
404 $typetext = get_string('requesttypeothers', 'tool_dataprivacy');
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.
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'];
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;
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);
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();
487 * Deletes a data purpose.
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);
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();
546 * Deletes a data category.
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)) {
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);
593 $instance = new context_instance(0, $record);
601 * Unsets the context instance record.
603 * @param \tool_dataprivacy\context_instance $instance
606 public static function unset_context_instance(context_instance
$instance) {
607 self
::check_can_manage_data_registry($instance->get('contextid'));
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) {
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)) {
631 $contextlevel->from_record($record);
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()) {
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()) {
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()) {
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()) {
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();
721 'contextid' => $contextid,
722 'status' => expired_context
::STATUS_EXPIRED
,
724 $expiredctx = new expired_context(0, $record);
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
750 public static function set_expired_context_status(expired_context
$expiredctx, $status) {
751 self
::check_can_manage_data_registry();
753 $expiredctx->set('status', $status);