Merge branch 'MDL-63903-master' of https://github.com/snake/moodle
[moodle.git] / admin / tool / dataprivacy / classes / api.php
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/>.
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  */
24 namespace tool_dataprivacy;
26 use coding_exception;
27 use context_helper;
28 use context_system;
29 use core\invalid_persistent_exception;
30 use core\message\message;
31 use core\task\manager;
32 use core_privacy\local\request\approved_contextlist;
33 use core_privacy\local\request\contextlist_collection;
34 use core_user;
35 use dml_exception;
36 use moodle_exception;
37 use moodle_url;
38 use required_capability_exception;
39 use stdClass;
40 use tool_dataprivacy\external\data_request_exporter;
41 use tool_dataprivacy\local\helper;
42 use tool_dataprivacy\task\initiate_data_request_task;
43 use tool_dataprivacy\task\process_data_request_task;
45 defined('MOODLE_INTERNAL') || die();
47 /**
48  * Class containing helper methods for processing data requests.
49  *
50  * @copyright  2018 Jun Pataleta
51  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
52  */
53 class api {
55     /** Data export request type. */
56     const DATAREQUEST_TYPE_EXPORT = 1;
58     /** Data deletion request type. */
59     const DATAREQUEST_TYPE_DELETE = 2;
61     /** Other request type. Usually of enquiries to the DPO. */
62     const DATAREQUEST_TYPE_OTHERS = 3;
64     /** Newly submitted and we haven't yet started finding out where they have data. */
65     const DATAREQUEST_STATUS_PENDING = 0;
67     /** Newly submitted and we have started to find the location of data. */
68     const DATAREQUEST_STATUS_PREPROCESSING = 1;
70     /** Metadata ready and awaiting review and approval by the Data Protection officer. */
71     const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
73     /** Request approved and will be processed soon. */
74     const DATAREQUEST_STATUS_APPROVED = 3;
76     /** The request is now being processed. */
77     const DATAREQUEST_STATUS_PROCESSING = 4;
79     /** Information/other request completed. */
80     const DATAREQUEST_STATUS_COMPLETE = 5;
82     /** Data request cancelled by the user. */
83     const DATAREQUEST_STATUS_CANCELLED = 6;
85     /** Data request rejected by the DPO. */
86     const DATAREQUEST_STATUS_REJECTED = 7;
88     /** Data request download ready. */
89     const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
91     /** Data request expired. */
92     const DATAREQUEST_STATUS_EXPIRED = 9;
94     /** Data delete request completed, account is removed. */
95     const DATAREQUEST_STATUS_DELETED = 10;
97     /** Approve data request. */
98     const DATAREQUEST_ACTION_APPROVE = 1;
100     /** Reject data request. */
101     const DATAREQUEST_ACTION_REJECT = 2;
103     /**
104      * Determines whether the user can contact the site's Data Protection Officer via Moodle.
105      *
106      * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
107      * @throws dml_exception
108      */
109     public static function can_contact_dpo() {
110         return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
111     }
113     /**
114      * Checks whether the current user has the capability to manage data requests.
115      *
116      * @param int $userid The user ID.
117      * @return bool
118      */
119     public static function can_manage_data_requests($userid) {
120         // Privacy officers can manage data requests.
121         return self::is_site_dpo($userid);
122     }
124     /**
125      * Checks if the current user can manage the data registry at the provided id.
126      *
127      * @param int $contextid Fallback to system context id.
128      * @throws \required_capability_exception
129      * @return null
130      */
131     public static function check_can_manage_data_registry($contextid = false) {
132         if ($contextid) {
133             $context = \context_helper::instance_by_id($contextid);
134         } else {
135             $context = \context_system::instance();
136         }
138         require_capability('tool/dataprivacy:managedataregistry', $context);
139     }
141     /**
142      * Fetches the list of configured privacy officer roles.
143      *
144      * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
145      * any role that doesn't have the required capability anymore.
146      *
147      * @return int[]
148      * @throws dml_exception
149      */
150     public static function get_assigned_privacy_officer_roles() {
151         $roleids = [];
153         // Get roles from config.
154         $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
155         if (!empty($configroleids)) {
156             // Fetch roles that have the capability to manage data requests.
157             $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
159             // Extract the configured roles that have the capability from the list of capable roles.
160             $roleids = array_intersect($capableroles, $configroleids);
161         }
163         return $roleids;
164     }
166     /**
167      * Fetches the role shortnames of Data Protection Officer roles.
168      *
169      * @return array An array of the DPO role shortnames
170      */
171     public static function get_dpo_role_names() : array {
172         global $DB;
174         $dporoleids = self::get_assigned_privacy_officer_roles();
175         $dponames = array();
177         if (!empty($dporoleids)) {
178             list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
179             $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
180         }
182         return $dponames;
183     }
185     /**
186      * Fetches the list of users with the Privacy Officer role.
187      */
188     public static function get_site_dpos() {
189         // Get role(s) that can manage data requests.
190         $dporoles = self::get_assigned_privacy_officer_roles();
192         $dpos = [];
193         $context = context_system::instance();
194         foreach ($dporoles as $roleid) {
195             $allnames = get_all_user_name_fields(true, 'u');
196             $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
197                       'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
198                       'u.country, u.picture, u.idnumber, u.department, u.institution, '.
199                       'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
200                       'r.name AS rolename, r.sortorder, '.
201                       'r.shortname AS roleshortname, rn.name AS rolecoursealias';
202             // Fetch users that can manage data requests.
203             $dpos += get_role_users($roleid, $context, false, $fields);
204         }
206         // If the site has no data protection officer, defer to site admin(s).
207         if (empty($dpos)) {
208             $dpos = get_admins();
209         }
210         return $dpos;
211     }
213     /**
214      * Checks whether a given user is a site Privacy Officer.
215      *
216      * @param int $userid The user ID.
217      * @return bool
218      */
219     public static function is_site_dpo($userid) {
220         $dpos = self::get_site_dpos();
221         return array_key_exists($userid, $dpos) || is_siteadmin();
222     }
224     /**
225      * Lodges a data request and sends the request details to the site Data Protection Officer(s).
226      *
227      * @param int $foruser The user whom the request is being made for.
228      * @param int $type The request type.
229      * @param string $comments Request comments.
230      * @param int $creationmethod The creation method of the data request.
231      * @return data_request
232      * @throws invalid_persistent_exception
233      * @throws coding_exception
234      */
235     public static function create_data_request($foruser, $type, $comments = '',
236                                                $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL) {
237         global $USER, $ADMIN;
239         $datarequest = new data_request();
240         // The user the request is being made for.
241         $datarequest->set('userid', $foruser);
243         // The cron is considered to be a guest user when it creates a data request.
244         // NOTE: This should probably be changed. We should leave the default value for $requestinguser if
245         // the request is not explicitly created by a specific user.
246         $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
247                 $ADMIN->id : $USER->id;
248         // The user making the request.
249         $datarequest->set('requestedby', $requestinguser);
250         // Set status.
251         $datarequest->set('status', self::DATAREQUEST_STATUS_PENDING);
252         // Set request type.
253         $datarequest->set('type', $type);
254         // Set request comments.
255         $datarequest->set('comments', $comments);
257         // Store subject access request.
258         $datarequest->create();
260         // Fire an ad hoc task to initiate the data request process.
261         $task = new initiate_data_request_task();
262         $task->set_custom_data(['requestid' => $datarequest->get('id')]);
263         manager::queue_adhoc_task($task, true);
265         return $datarequest;
266     }
268     /**
269      * Fetches the list of the data requests.
270      *
271      * If user ID is provided, it fetches the data requests for the user.
272      * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
273      * (e.g. Users with the Data Protection Officer roles)
274      *
275      * @param int $userid The User ID.
276      * @param int[] $statuses The status filters.
277      * @param int[] $types The request type filters.
278      * @param string $sort The order by clause.
279      * @param int $offset Amount of records to skip.
280      * @param int $limit Amount of records to fetch.
281      * @return data_request[]
282      * @throws coding_exception
283      * @throws dml_exception
284      */
285     public static function get_data_requests($userid = 0, $statuses = [], $types = [], $sort = '', $offset = 0, $limit = 0) {
286         global $DB, $USER;
287         $results = [];
288         $sqlparams = [];
289         $sqlconditions = [];
291         // Set default sort.
292         if (empty($sort)) {
293             $sort = 'status ASC, timemodified ASC';
294         }
296         // Set status filters.
297         if (!empty($statuses)) {
298             list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
299             $sqlconditions[] = "status $statusinsql";
300         }
302         // Set request type filter.
303         if (!empty($types)) {
304             list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
305             $sqlconditions[] = "type $typeinsql";
306             $sqlparams = array_merge($sqlparams, $typeparams);
307         }
309         if ($userid) {
310             // Get the data requests for the user or data requests made by the user.
311             $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
312             $params = [
313                 'userid' => $userid,
314                 'requestedby' => $userid
315             ];
317             // Build a list of user IDs that the user is allowed to make data requests for.
318             // Of course, the user should be included in this list.
319             $alloweduserids = [$userid];
320             // Get any users that the user can make data requests for.
321             if ($children = helper::get_children_of_user($userid)) {
322                 // Get the list of user IDs of the children and merge to the allowed user IDs.
323                 $alloweduserids = array_merge($alloweduserids, array_keys($children));
324             }
325             list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
326             $sqlconditions[] .= "userid $insql";
327             $select = implode(' AND ', $sqlconditions);
328             $params = array_merge($params, $inparams, $sqlparams);
330             $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
331         } else {
332             // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
333             if (self::is_site_dpo($USER->id)) {
334                 if (!empty($sqlconditions)) {
335                     $select = implode(' AND ', $sqlconditions);
336                     $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
337                 } else {
338                     $results = data_request::get_records(null, $sort, '', $offset, $limit);
339                 }
340             }
341         }
343         // If any are due to expire, expire them and re-fetch updated data.
344         if (empty($statuses)
345                 || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
346                 || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
347             $expiredrequests = data_request::get_expired_requests($userid);
349             if (!empty($expiredrequests)) {
350                 data_request::expire($expiredrequests);
351                 $results = self::get_data_requests($userid, $statuses, $types, $sort, $offset, $limit);
352             }
353         }
355         return $results;
356     }
358     /**
359      * Fetches the count of data request records based on the given parameters.
360      *
361      * @param int $userid The User ID.
362      * @param int[] $statuses The status filters.
363      * @param int[] $types The request type filters.
364      * @return int
365      * @throws coding_exception
366      * @throws dml_exception
367      */
368     public static function get_data_requests_count($userid = 0, $statuses = [], $types = []) {
369         global $DB, $USER;
370         $count = 0;
371         $sqlparams = [];
372         $sqlconditions = [];
373         if (!empty($statuses)) {
374             list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
375             $sqlconditions[] = "status $statusinsql";
376         }
377         if (!empty($types)) {
378             list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
379             $sqlconditions[] = "type $typeinsql";
380             $sqlparams = array_merge($sqlparams, $typeparams);
381         }
382         if ($userid) {
383             // Get the data requests for the user or data requests made by the user.
384             $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
385             $params = [
386                 'userid' => $userid,
387                 'requestedby' => $userid
388             ];
390             // Build a list of user IDs that the user is allowed to make data requests for.
391             // Of course, the user should be included in this list.
392             $alloweduserids = [$userid];
393             // Get any users that the user can make data requests for.
394             if ($children = helper::get_children_of_user($userid)) {
395                 // Get the list of user IDs of the children and merge to the allowed user IDs.
396                 $alloweduserids = array_merge($alloweduserids, array_keys($children));
397             }
398             list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
399             $sqlconditions[] .= "userid $insql";
400             $select = implode(' AND ', $sqlconditions);
401             $params = array_merge($params, $inparams, $sqlparams);
403             $count = data_request::count_records_select($select, $params);
404         } else {
405             // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
406             if (self::is_site_dpo($USER->id)) {
407                 if (!empty($sqlconditions)) {
408                     $select = implode(' AND ', $sqlconditions);
409                     $count = data_request::count_records_select($select, $sqlparams);
410                 } else {
411                     $count = data_request::count_records();
412                 }
413             }
414         }
416         return $count;
417     }
419     /**
420      * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
421      *
422      * @param int $userid The user ID.
423      * @param int $type The request type.
424      * @return bool
425      * @throws coding_exception
426      * @throws dml_exception
427      */
428     public static function has_ongoing_request($userid, $type) {
429         global $DB;
431         // Check if the user already has an incomplete data request of the same type.
432         $nonpendingstatuses = [
433             self::DATAREQUEST_STATUS_COMPLETE,
434             self::DATAREQUEST_STATUS_CANCELLED,
435             self::DATAREQUEST_STATUS_REJECTED,
436             self::DATAREQUEST_STATUS_DOWNLOAD_READY,
437             self::DATAREQUEST_STATUS_EXPIRED,
438             self::DATAREQUEST_STATUS_DELETED,
439         ];
440         list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
441         $select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
442         $params = array_merge([
443             'type' => $type,
444             'userid' => $userid
445         ], $inparams);
447         return data_request::record_exists_select($select, $params);
448     }
450     /**
451      * Determines whether a request is active or not based on its status.
452      *
453      * @param int $status The request status.
454      * @return bool
455      */
456     public static function is_active($status) {
457         // List of statuses which doesn't require any further processing.
458         $finalstatuses = [
459             self::DATAREQUEST_STATUS_COMPLETE,
460             self::DATAREQUEST_STATUS_CANCELLED,
461             self::DATAREQUEST_STATUS_REJECTED,
462             self::DATAREQUEST_STATUS_DOWNLOAD_READY,
463             self::DATAREQUEST_STATUS_EXPIRED,
464             self::DATAREQUEST_STATUS_DELETED,
465         ];
467         return !in_array($status, $finalstatuses);
468     }
470     /**
471      * Cancels the data request for a given request ID.
472      *
473      * @param int $requestid The request identifier.
474      * @param int $status The request status.
475      * @param int $dpoid The user ID of the Data Protection Officer
476      * @param string $comment The comment about the status update.
477      * @return bool
478      * @throws invalid_persistent_exception
479      * @throws coding_exception
480      */
481     public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') {
482         // Update the request.
483         $datarequest = new data_request($requestid);
484         $datarequest->set('status', $status);
485         if ($dpoid) {
486             $datarequest->set('dpo', $dpoid);
487         }
488         // Update the comment if necessary.
489         if (!empty(trim($comment))) {
490             $params = [
491                 'date' => userdate(time()),
492                 'comment' => $comment
493             ];
494             $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
495             // Check if there's an existing DPO comment.
496             $currentcomment = trim($datarequest->get('dpocomment'));
497             if ($currentcomment) {
498                 // Append the new comment to the current comment and give them 1 line space in between.
499                 $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
500             }
501             $datarequest->set('dpocomment', $commenttosave);
502         }
504         return $datarequest->update();
505     }
507     /**
508      * Fetches a request based on the request ID.
509      *
510      * @param int $requestid The request identifier
511      * @return data_request
512      */
513     public static function get_request($requestid) {
514         return new data_request($requestid);
515     }
517     /**
518      * Approves a data request based on the request ID.
519      *
520      * @param int $requestid The request identifier
521      * @return bool
522      * @throws coding_exception
523      * @throws dml_exception
524      * @throws invalid_persistent_exception
525      * @throws required_capability_exception
526      * @throws moodle_exception
527      */
528     public static function approve_data_request($requestid) {
529         global $USER;
531         // Check first whether the user can manage data requests.
532         if (!self::can_manage_data_requests($USER->id)) {
533             $context = context_system::instance();
534             throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
535         }
537         // Check if request is already awaiting for approval.
538         $request = new data_request($requestid);
539         if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
540             throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
541         }
543         // Update the status and the DPO.
544         $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
546         // Approve all the contexts attached to the request.
547         // Currently, approving the request implicitly approves all associated contexts, but this may change in future, allowing
548         // users to selectively approve certain contexts only.
549         self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED);
551         // Fire an ad hoc task to initiate the data request process.
552         $task = new process_data_request_task();
553         $task->set_custom_data(['requestid' => $requestid]);
554         if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
555             $task->set_userid($request->get('userid'));
556         }
557         manager::queue_adhoc_task($task, true);
559         return $result;
560     }
562     /**
563      * Rejects a data request based on the request ID.
564      *
565      * @param int $requestid The request identifier
566      * @return bool
567      * @throws coding_exception
568      * @throws dml_exception
569      * @throws invalid_persistent_exception
570      * @throws required_capability_exception
571      * @throws moodle_exception
572      */
573     public static function deny_data_request($requestid) {
574         global $USER;
576         if (!self::can_manage_data_requests($USER->id)) {
577             $context = context_system::instance();
578             throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
579         }
581         // Check if request is already awaiting for approval.
582         $request = new data_request($requestid);
583         if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
584             throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
585         }
587         // Update the status and the DPO.
588         return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
589     }
591     /**
592      * Sends a message to the site's Data Protection Officer about a request.
593      *
594      * @param stdClass $dpo The DPO user record
595      * @param data_request $request The data request
596      * @return int|false
597      * @throws coding_exception
598      * @throws moodle_exception
599      */
600     public static function notify_dpo($dpo, data_request $request) {
601         global $PAGE, $SITE;
603         $output = $PAGE->get_renderer('tool_dataprivacy');
605         $usercontext = \context_user::instance($request->get('requestedby'));
606         $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
607         $requestdata = $requestexporter->export($output);
609         // Create message to send to the Data Protection Officer(s).
610         $typetext = null;
611         $typetext = $requestdata->typename;
612         $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
614         $requestedby = $requestdata->requestedbyuser;
615         $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
616         $message = new message();
617         $message->courseid          = $SITE->id;
618         $message->component         = 'tool_dataprivacy';
619         $message->name              = 'contactdataprotectionofficer';
620         $message->userfrom          = $requestedby->id;
621         $message->replyto           = $requestedby->email;
622         $message->replytoname       = $requestedby->fullname;
623         $message->subject           = $subject;
624         $message->fullmessageformat = FORMAT_HTML;
625         $message->notification      = 1;
626         $message->contexturl        = $datarequestsurl;
627         $message->contexturlname    = get_string('datarequests', 'tool_dataprivacy');
629         // Prepare the context data for the email message body.
630         $messagetextdata = [
631             'requestedby' => $requestedby->fullname,
632             'requesttype' => $typetext,
633             'requestdate' => userdate($requestdata->timecreated),
634             'requestorigin' => $SITE->fullname,
635             'requestoriginurl' => new moodle_url('/'),
636             'requestcomments' => $requestdata->messagehtml,
637             'datarequestsurl' => $datarequestsurl
638         ];
639         $requestingfor = $requestdata->foruser;
640         if ($requestedby->id == $requestingfor->id) {
641             $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
642         } else {
643             $messagetextdata['requestfor'] = $requestingfor->fullname;
644         }
646         // Email the data request to the Data Protection Officer(s)/Admin(s).
647         $messagetextdata['dponame'] = fullname($dpo);
648         // Render message email body.
649         $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
650         $message->userto = $dpo;
651         $message->fullmessage = html_to_text($messagehtml);
652         $message->fullmessagehtml = $messagehtml;
654         // Send message.
655         return message_send($message);
656     }
658     /**
659      * Checks whether a non-DPO user can make a data request for another user.
660      *
661      * @param   int     $user The user ID of the target user.
662      * @param   int     $requester The user ID of the user making the request.
663      * @return  bool
664      */
665     public static function can_create_data_request_for_user($user, $requester = null) {
666         $usercontext = \context_user::instance($user);
668         return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
669     }
671     /**
672      * Require that the current user can make a data request for the specified other user.
673      *
674      * @param   int     $user The user ID of the target user.
675      * @param   int     $requester The user ID of the user making the request.
676      * @return  bool
677      */
678     public static function require_can_create_data_request_for_user($user, $requester = null) {
679         $usercontext = \context_user::instance($user);
681         require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
683         return true;
684     }
686     /**
687      * Checks whether a user can download a data request.
688      *
689      * @param int $userid Target user id (subject of data request)
690      * @param int $requesterid Requester user id (person who requsted it)
691      * @param int|null $downloaderid Person who wants to download user id (default current)
692      * @return bool
693      * @throws coding_exception
694      */
695     public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
696         global $USER;
698         if (!$downloaderid) {
699             $downloaderid = $USER->id;
700         }
702         $usercontext = \context_user::instance($userid);
703         // If it's your own and you have the right capability, you can download it.
704         if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
705             return true;
706         }
707         // If you can download anyone's in that context, you can download it.
708         if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
709             return true;
710         }
711         // If you can have the 'child access' ability to request in that context, and you are the one
712         // who requested it, then you can download it.
713         if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
714             return true;
715         }
716         return false;
717     }
719     /**
720      * Gets an action menu link to download a data request.
721      *
722      * @param \context_user $usercontext User context (of user who the data is for)
723      * @param int $requestid Request id
724      * @return \action_menu_link_secondary Action menu link
725      * @throws coding_exception
726      */
727     public static function get_download_link(\context_user $usercontext, $requestid) {
728         $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
729                 'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
730         $downloadtext = get_string('download', 'tool_dataprivacy');
731         return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
732     }
734     /**
735      * Creates a new data purpose.
736      *
737      * @param stdClass $record
738      * @return \tool_dataprivacy\purpose.
739      */
740     public static function create_purpose(stdClass $record) {
741         $purpose = new purpose(0, $record);
742         $purpose->create();
744         return $purpose;
745     }
747     /**
748      * Updates an existing data purpose.
749      *
750      * @param stdClass $record
751      * @return \tool_dataprivacy\purpose.
752      */
753     public static function update_purpose(stdClass $record) {
754         if (!isset($record->sensitivedatareasons)) {
755             $record->sensitivedatareasons = '';
756         }
758         $purpose = new purpose($record->id);
759         $purpose->from_record($record);
761         $result = $purpose->update();
763         return $purpose;
764     }
766     /**
767      * Deletes a data purpose.
768      *
769      * @param int $id
770      * @return bool
771      */
772     public static function delete_purpose($id) {
773         $purpose = new purpose($id);
774         if ($purpose->is_used()) {
775             throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
776         }
777         return $purpose->delete();
778     }
780     /**
781      * Get all system data purposes.
782      *
783      * @return \tool_dataprivacy\purpose[]
784      */
785     public static function get_purposes() {
786         return purpose::get_records([], 'name', 'ASC');
787     }
789     /**
790      * Creates a new data category.
791      *
792      * @param stdClass $record
793      * @return \tool_dataprivacy\category.
794      */
795     public static function create_category(stdClass $record) {
796         $category = new category(0, $record);
797         $category->create();
799         return $category;
800     }
802     /**
803      * Updates an existing data category.
804      *
805      * @param stdClass $record
806      * @return \tool_dataprivacy\category.
807      */
808     public static function update_category(stdClass $record) {
809         $category = new category($record->id);
810         $category->from_record($record);
812         $result = $category->update();
814         return $category;
815     }
817     /**
818      * Deletes a data category.
819      *
820      * @param int $id
821      * @return bool
822      */
823     public static function delete_category($id) {
824         $category = new category($id);
825         if ($category->is_used()) {
826             throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
827         }
828         return $category->delete();
829     }
831     /**
832      * Get all system data categories.
833      *
834      * @return \tool_dataprivacy\category[]
835      */
836     public static function get_categories() {
837         return category::get_records([], 'name', 'ASC');
838     }
840     /**
841      * Sets the context instance purpose and category.
842      *
843      * @param \stdClass $record
844      * @return \tool_dataprivacy\context_instance
845      */
846     public static function set_context_instance($record) {
847         if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
848             // Update.
849             $instance->from_record($record);
851             if (empty($record->purposeid) && empty($record->categoryid)) {
852                 // We accept one of them to be null but we delete it if both are null.
853                 self::unset_context_instance($instance);
854                 return;
855             }
857         } else {
858             // Add.
859             $instance = new context_instance(0, $record);
860         }
861         $instance->save();
863         return $instance;
864     }
866     /**
867      * Unsets the context instance record.
868      *
869      * @param \tool_dataprivacy\context_instance $instance
870      * @return null
871      */
872     public static function unset_context_instance(context_instance $instance) {
873         $instance->delete();
874     }
876     /**
877      * Sets the context level purpose and category.
878      *
879      * @throws \coding_exception
880      * @param \stdClass $record
881      * @return contextlevel
882      */
883     public static function set_contextlevel($record) {
884         global $DB;
886         if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
887             throw new \coding_exception('Only context system and context user can set a contextlevel ' .
888                 'purpose and retention');
889         }
891         if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
892             // Update.
893             $contextlevel->from_record($record);
894         } else {
895             // Add.
896             $contextlevel = new contextlevel(0, $record);
897         }
898         $contextlevel->save();
900         // We sync with their defaults as we removed these options from the defaults page.
901         $classname = \context_helper::get_class_for_level($record->contextlevel);
902         list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
903         set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
904         set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
906         return $contextlevel;
907     }
909     /**
910      * Returns the effective category given a context instance.
911      *
912      * @param \context $context
913      * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
914      * @return category|false
915      */
916     public static function get_effective_context_category(\context $context, $forcedvalue = false) {
917         if (!data_registry::defaults_set()) {
918             return false;
919         }
921         return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
922     }
924     /**
925      * Returns the effective purpose given a context instance.
926      *
927      * @param \context $context
928      * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
929      * @return purpose|false
930      */
931     public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
932         if (!data_registry::defaults_set()) {
933             return false;
934         }
936         return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
937     }
939     /**
940      * Returns the effective category given a context level.
941      *
942      * @param int $contextlevel
943      * @return category|false
944      */
945     public static function get_effective_contextlevel_category($contextlevel) {
946         if (!data_registry::defaults_set()) {
947             return false;
948         }
950         return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
951     }
953     /**
954      * Returns the effective purpose given a context level.
955      *
956      * @param int $contextlevel
957      * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
958      * @return purpose|false
959      */
960     public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
961         if (!data_registry::defaults_set()) {
962             return false;
963         }
965         return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
966     }
968     /**
969      * Updates the status of an expired context.
970      *
971      * @param \tool_dataprivacy\expired_context $expiredctx
972      * @param int $status
973      * @return null
974      */
975     public static function set_expired_context_status(expired_context $expiredctx, $status) {
976         $expiredctx->set('status', $status);
977         $expiredctx->save();
978     }
980     /**
981      * Adds the contexts from the contextlist_collection to the request with the status provided.
982      *
983      * @param contextlist_collection $clcollection a collection of contextlists for all components.
984      * @param int $requestid the id of the request.
985      * @param int $status the status to set the contexts to.
986      */
987     public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
988         $request = new data_request($requestid);
989         $user = \core_user::get_user($request->get('userid'));
990         foreach ($clcollection as $contextlist) {
991             // Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
992             $clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
993             $clp->create();
994             $contextlistid = $clp->get('id');
996             // Store the associated contexts in the contextlist.
997             foreach ($contextlist->get_contextids() as $contextid) {
998                 if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
999                     $context = \context::instance_by_id($contextid);
1000                     $purpose = static::get_effective_context_purpose($context);
1002                     // Data can only be deleted from it if the context is either expired, or unprotected.
1003                     if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $user)) {
1004                         continue;
1005                     }
1006                 }
1008                 $context = new contextlist_context();
1009                 $context->set('contextid', $contextid)
1010                     ->set('contextlistid', $contextlistid)
1011                     ->set('status', $status)
1012                     ->create();
1013             }
1015             // Create the relation to the request.
1016             $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid);
1017             $requestcontextlist->create();
1018         }
1019     }
1021     /**
1022      * Sets the status of all contexts associated with the request.
1023      *
1024      * @param int $requestid the requestid to which the contexts belong.
1025      * @param int $status the status to set to.
1026      * @throws \dml_exception if the requestid is invalid.
1027      * @throws \moodle_exception if the status is invalid.
1028      */
1029     public static function update_request_contexts_with_status(int $requestid, int $status) {
1030         // Validate contextlist_context status using the persistent's attribute validation.
1031         $contextlistcontext = new contextlist_context();
1032         $contextlistcontext->set('status', $status);
1033         if (array_key_exists('status', $contextlistcontext->get_errors())) {
1034             throw new moodle_exception("Invalid contextlist_context status: $status");
1035         }
1037         // Validate requestid using the persistent's record validation.
1038         // A dml_exception is thrown if the record is missing.
1039         $datarequest = new data_request($requestid);
1041         // Bulk update the status of the request contexts.
1042         global $DB;
1044         $select = "SELECT ctx.id as id
1045                      FROM {" . request_contextlist::TABLE . "} rcl
1046                      JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1047                      JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
1048                     WHERE rcl.requestid = ?";
1050         // Fetch records IDs to be updated and update by chunks, if applicable (limit of 1000 records per update).
1051         $limit = 1000;
1052         $idstoupdate = $DB->get_fieldset_sql($select, [$requestid]);
1053         $count = count($idstoupdate);
1054         $idchunks = $idstoupdate;
1055         if ($count > $limit) {
1056             $idchunks = array_chunk($idstoupdate, $limit);
1057         }
1058         $transaction = $DB->start_delegated_transaction();
1059         $initialparams = [$status];
1060         foreach ($idchunks as $chunk) {
1061             list($insql, $inparams) = $DB->get_in_or_equal($chunk);
1062             $update = "UPDATE {" . contextlist_context::TABLE . "}
1063                           SET status = ?
1064                         WHERE id $insql";
1065             $params = array_merge($initialparams, $inparams);
1066             $DB->execute($update, $params);
1067         }
1068         $transaction->allow_commit();
1069     }
1071     /**
1072      * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection.
1073      *
1074      * @param data_request $request the data request with which the contextlists are associated.
1075      * @return contextlist_collection the collection of approved_contextlist objects.
1076      */
1077     public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
1078         $foruser = core_user::get_user($request->get('userid'));
1080         // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
1081         global $DB;
1082         $sql = "SELECT cl.component, ctx.contextid
1083                   FROM {" . request_contextlist::TABLE . "} rcl
1084                   JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1085                   JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
1086                  WHERE rcl.requestid = ?
1087                    AND ctx.status = ?
1088               ORDER BY cl.component, ctx.contextid";
1090         // Create the approved contextlist collection object.
1091         $lastcomponent = null;
1092         $approvedcollection = new contextlist_collection($foruser->id);
1094         $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]);
1095         foreach ($rs as $record) {
1096             // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the
1097             // last (the one we've just finished with) and reset the context array for the next one.
1098             if ($lastcomponent != $record->component) {
1099                 if (!empty($contexts)) {
1100                     $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
1101                 }
1102                 $contexts = [];
1103             }
1105             if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
1106                 $context = \context::instance_by_id($record->contextid);
1107                 $purpose = static::get_effective_context_purpose($context);
1108                 // Data can only be deleted from it if the context is either expired, or unprotected.
1109                 if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
1110                     continue;
1111                 }
1112             }
1114             $contexts[] = $record->contextid;
1115             $lastcomponent = $record->component;
1116         }
1117         $rs->close();
1119         // The data for the last component contextlist won't have been written yet, so write it now.
1120         if (!empty($contexts)) {
1121             $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
1122         }
1124         return $approvedcollection;
1125     }
1127     /**
1128      * Updates the default category and purpose for a given context level (and optionally, a plugin).
1129      *
1130      * @param int $contextlevel The context level.
1131      * @param int $categoryid The ID matching the category.
1132      * @param int $purposeid The ID matching the purpose record.
1133      * @param int $activity The name of the activity that we're making a defaults configuration for.
1134      * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
1135      * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
1136      */
1137     public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
1138         global $DB;
1140         // Get the class name associated with this context level.
1141         $classname = context_helper::get_class_for_level($contextlevel);
1142         list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
1144         // Check the default category to be set.
1145         if ($categoryid == context_instance::INHERIT) {
1146             unset_config($categoryvar, 'tool_dataprivacy');
1148         } else {
1149             // Make sure the given category ID exists first.
1150             $categorypersistent = new category($categoryid);
1151             $categorypersistent->read();
1153             // Then set the new default value.
1154             set_config($categoryvar, $categoryid, 'tool_dataprivacy');
1155         }
1157         // Check the default purpose to be set.
1158         if ($purposeid == context_instance::INHERIT) {
1159             // If the defaults is set to inherit, just unset the config value.
1160             unset_config($purposevar, 'tool_dataprivacy');
1162         } else {
1163             // Make sure the given purpose ID exists first.
1164             $purposepersistent = new purpose($purposeid);
1165             $purposepersistent->read();
1167             // Then set the new default value.
1168             set_config($purposevar, $purposeid, 'tool_dataprivacy');
1169         }
1171         // Unset instances that have been assigned with custom purpose and category, if override was specified.
1172         if ($override) {
1173             // We'd like to find context IDs that we want to unset.
1174             $statements = ["SELECT c.id as contextid FROM {context} c"];
1175             // Based on this context level.
1176             $params = ['contextlevel' => $contextlevel];
1178             if ($contextlevel == CONTEXT_MODULE) {
1179                 // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
1180                 $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
1181                 // And that the module is listed on the modules table.
1182                 $statements[] = "JOIN {modules} m ON m.id = cm.module";
1184                 if ($activity) {
1185                     // If we're overriding for an activity module, make sure that the context instance matches that activity.
1186                     $statements[] = "AND m.name = :modname";
1187                     $params['modname'] = $activity;
1188                 }
1189             }
1190             // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
1191             $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
1192             // And that the context level of this instance matches the given context level.
1193             $statements[] = "WHERE c.contextlevel = :contextlevel";
1195             // Build our SQL query by gluing the statements.
1196             $sql = implode("\n", $statements);
1198             // Get the context records matching our query.
1199             $contextids = $DB->get_fieldset_sql($sql, $params);
1201             // Delete the matching context instances.
1202             foreach ($contextids as $contextid) {
1203                 if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
1204                     self::unset_context_instance($instance);
1205                 }
1206             }
1207         }
1209         return true;
1210     }
1212     /**
1213      * Format the supplied date interval as a retention period.
1214      *
1215      * @param   \DateInterval   $interval
1216      * @return  string
1217      */
1218     public static function format_retention_period(\DateInterval $interval) : string {
1219         // It is one or another.
1220         if ($interval->y) {
1221             $formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
1222         } else if ($interval->m) {
1223             $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
1224         } else if ($interval->d) {
1225             $formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
1226         } else {
1227             $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
1228         }
1230         return $formattedtime;
1231     }