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
;
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
;
38 use required_capability_exception
;
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();
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
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;
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.
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
129 public static function check_can_manage_data_registry($contextid = false) {
131 $context = \context_helper
::instance_by_id($contextid);
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 {
147 $dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
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);
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'));
168 $context = context_system
::instance();
169 foreach ($dporoles as $roleid) {
170 if (empty($roleid)) {
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).
186 $dpos = get_admins();
192 * Checks whether a given user is a site DPO.
194 * @param int $userid The user ID.
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 = '') {
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);
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);
239 $datarequest->set('status', self
::DATAREQUEST_STATUS_PENDING
);
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);
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) {
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);
298 // Get the data requests for the user or data requests made by the user.
299 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
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);
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);
326 $results = data_request
::get_records(null, $sort, '', $offset, $limit);
331 // If any are due to expire, expire them and re-fetch updated data.
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);
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.
353 * @throws coding_exception
354 * @throws dml_exception
356 public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
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);
371 // Get the data requests for the user or data requests made by the user.
372 $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
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);
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);
399 $count = data_request
::count_records();
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.
413 * @throws coding_exception
414 * @throws dml_exception
416 public static function has_ongoing_request($userid, $type) {
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([
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.
444 public static function is_active($status) {
445 // List of statuses which doesn't require any further processing.
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.
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);
474 $datarequest->set('dpo', $dpoid);
476 // Update the comment if necessary.
477 if (!empty(trim($comment))) {
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
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) {
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);
551 * Rejects a data request based on the request ID.
553 * @param int $requestid The request identifier
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) {
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
585 * @throws coding_exception
586 * @throws moodle_exception
588 public static function notify_dpo($dpo, data_request
$request) {
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).
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.
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'];
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;
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.
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)
664 * @throws coding_exception
666 public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
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)) {
678 // If you can download anyone's in that context, you can download it.
679 if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
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)) {
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);
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();
742 * Deletes a data purpose.
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);
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();
801 * Deletes a data category.
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)) {
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);
848 $instance = new context_instance(0, $record);
856 * Unsets the context instance record.
858 * @param \tool_dataprivacy\context_instance $instance
861 public static function unset_context_instance(context_instance
$instance) {
862 self
::check_can_manage_data_registry($instance->get('contextid'));
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) {
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)) {
886 $contextlevel->from_record($record);
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()) {
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()) {
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()) {
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()) {
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();
976 'contextid' => $contextid,
977 'status' => expired_context
::STATUS_EXPIRED
,
979 $expiredctx = new expired_context(0, $record);
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
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);
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'))) {
1035 $context = new contextlist_context();
1036 $context->set('contextid', $contextid)
1037 ->set('contextlistid', $contextlistid)
1038 ->set('status', $status)
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.
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).
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
. "}
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.
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 = ?
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));
1132 $contexts[] = $record->contextid
;
1133 $lastcomponent = $record->component
;
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;