1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Class containing helper methods for processing data requests.
19 *
20 * @package    tool_dataprivacy
21 * @copyright  2018 Jun Pataleta
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24namespace tool_dataprivacy;
25
26use coding_exception;
27use context_helper;
28use context_system;
29use core\invalid_persistent_exception;
30use core\message\message;
31use core\task\manager;
32use core_privacy\local\request\approved_contextlist;
33use core_privacy\local\request\contextlist_collection;
34use core_user;
35use dml_exception;
36use moodle_exception;
37use moodle_url;
38use required_capability_exception;
39use stdClass;
40use tool_dataprivacy\external\data_request_exporter;
41use tool_dataprivacy\local\helper;
42use tool_dataprivacy\task\process_data_request_task;
43use tool_dataprivacy\data_request;
44
45defined('MOODLE_INTERNAL') || die();
46
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 */
53class api {
54
55    /** Data export request type. */
56    const DATAREQUEST_TYPE_EXPORT = 1;
57
58    /** Data deletion request type. */
59    const DATAREQUEST_TYPE_DELETE = 2;
60
61    /** Other request type. Usually of enquiries to the DPO. */
62    const DATAREQUEST_TYPE_OTHERS = 3;
63
64    /** Newly submitted and we haven't yet started finding out where they have data. */
65    const DATAREQUEST_STATUS_PENDING = 0;
66
67    /** Metadata ready and awaiting review and approval by the Data Protection officer. */
68    const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
69
70    /** Request approved and will be processed soon. */
71    const DATAREQUEST_STATUS_APPROVED = 3;
72
73    /** The request is now being processed. */
74    const DATAREQUEST_STATUS_PROCESSING = 4;
75
76    /** Information/other request completed. */
77    const DATAREQUEST_STATUS_COMPLETE = 5;
78
79    /** Data request cancelled by the user. */
80    const DATAREQUEST_STATUS_CANCELLED = 6;
81
82    /** Data request rejected by the DPO. */
83    const DATAREQUEST_STATUS_REJECTED = 7;
84
85    /** Data request download ready. */
86    const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
87
88    /** Data request expired. */
89    const DATAREQUEST_STATUS_EXPIRED = 9;
90
91    /** Data delete request completed, account is removed. */
92    const DATAREQUEST_STATUS_DELETED = 10;
93
94    /** Approve data request. */
95    const DATAREQUEST_ACTION_APPROVE = 1;
96
97    /** Reject data request. */
98    const DATAREQUEST_ACTION_REJECT = 2;
99
100    /**
101     * Determines whether the user can contact the site's Data Protection Officer via Moodle.
102     *
103     * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
104     * @throws dml_exception
105     */
106    public static function can_contact_dpo() {
107        return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
108    }
109
110    /**
111     * Checks whether the current user has the capability to manage data requests.
112     *
113     * @param int $userid The user ID.
114     * @return bool
115     */
116    public static function can_manage_data_requests($userid) {
117        // Privacy officers can manage data requests.
118        return self::is_site_dpo($userid);
119    }
120
121    /**
122     * Checks if the current user can manage the data registry at the provided id.
123     *
124     * @param int $contextid Fallback to system context id.
125     * @throws \required_capability_exception
126     * @return null
127     */
128    public static function check_can_manage_data_registry($contextid = false) {
129        if ($contextid) {
130            $context = \context_helper::instance_by_id($contextid);
131        } else {
132            $context = \context_system::instance();
133        }
134
135        require_capability('tool/dataprivacy:managedataregistry', $context);
136    }
137
138    /**
139     * Fetches the list of configured privacy officer roles.
140     *
141     * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
142     * any role that doesn't have the required capability anymore.
143     *
144     * @return int[]
145     * @throws dml_exception
146     */
147    public static function get_assigned_privacy_officer_roles() {
148        $roleids = [];
149
150        // Get roles from config.
151        $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
152        if (!empty($configroleids)) {
153            // Fetch roles that have the capability to manage data requests.
154            $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
155
156            // Extract the configured roles that have the capability from the list of capable roles.
157            $roleids = array_intersect($capableroles, $configroleids);
158        }
159
160        return $roleids;
161    }
162
163    /**
164     * Fetches the role shortnames of Data Protection Officer roles.
165     *
166     * @return array An array of the DPO role shortnames
167     */
168    public static function get_dpo_role_names() : array {
169        global $DB;
170
171        $dporoleids = self::get_assigned_privacy_officer_roles();
172        $dponames = array();
173
174        if (!empty($dporoleids)) {
175            list($insql, $inparams) = $DB->get_in_or_equal($dporoleids);
176            $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams);
177        }
178
179        return $dponames;
180    }
181
182    /**
183     * Fetches the list of users with the Privacy Officer role.
184     */
185    public static function get_site_dpos() {
186        // Get role(s) that can manage data requests.
187        $dporoles = self::get_assigned_privacy_officer_roles();
188
189        $dpos = [];
190        $context = context_system::instance();
191        foreach ($dporoles as $roleid) {
192            $allnames = get_all_user_name_fields(true, 'u');
193            $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
194                      'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
195                      'u.country, u.picture, u.idnumber, u.department, u.institution, '.
196                      'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
197                      'r.name AS rolename, r.sortorder, '.
198                      'r.shortname AS roleshortname, rn.name AS rolecoursealias';
199            // Fetch users that can manage data requests.
200            $dpos += get_role_users($roleid, $context, false, $fields);
201        }
202
203        // If the site has no data protection officer, defer to site admin(s).
204        if (empty($dpos)) {
205            $dpos = get_admins();
206        }
207        return $dpos;
208    }
209
210    /**
211     * Checks whether a given user is a site Privacy Officer.
212     *
213     * @param int $userid The user ID.
214     * @return bool
215     */
216    public static function is_site_dpo($userid) {
217        $dpos = self::get_site_dpos();
218        return array_key_exists($userid, $dpos) || is_siteadmin();
219    }
220
221    /**
222     * Lodges a data request and sends the request details to the site Data Protection Officer(s).
223     *
224     * @param int $foruser The user whom the request is being made for.
225     * @param int $type The request type.
226     * @param string $comments Request comments.
227     * @param int $creationmethod The creation method of the data request.
228     * @param bool $notify Notify DPOs of this pending request.
229     * @return data_request
230     * @throws invalid_persistent_exception
231     * @throws coding_exception
232     */
233    public static function create_data_request($foruser, $type, $comments = '',
234            $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL,
235            $notify = null
236        ) {
237        global $USER;
238
239        if (null === $notify) {
240            // Only if notifications have not been decided by caller.
241            if ( data_request::DATAREQUEST_CREATION_AUTO == $creationmethod) {
242                // If the request was automatically created, then do not notify unless explicitly set.
243                $notify = false;
244            } else {
245                $notify = true;
246            }
247        }
248
249        $datarequest = new data_request();
250        // The user the request is being made for.
251        $datarequest->set('userid', $foruser);
252
253        // The cron is considered to be a guest user when it creates a data request.
254        // NOTE: This should probably be changed. We should leave the default value for $requestinguser if
255        // the request is not explicitly created by a specific user.
256        $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
257                get_admin()->id : $USER->id;
258        // The user making the request.
259        $datarequest->set('requestedby', $requestinguser);
260        // Set status.
261        $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL;
262        if (self::is_automatic_request_approval_on($type)) {
263            // Set status to approved if automatic data request approval is enabled.
264            $status = self::DATAREQUEST_STATUS_APPROVED;
265            // Set the privacy officer field if the one making the data request is a privacy officer.
266            if (self::is_site_dpo($requestinguser)) {
267                $datarequest->set('dpo', $requestinguser);
268            }
269            // Mark this request as system approved.
270            $datarequest->set('systemapproved', true);
271            // No need to notify privacy officer(s) about automatically approved data requests.
272            $notify = false;
273        }
274        $datarequest->set('status', $status);
275        // Set request type.
276        $datarequest->set('type', $type);
277        // Set request comments.
278        $datarequest->set('comments', $comments);
279        // Set the creation method.
280        $datarequest->set('creationmethod', $creationmethod);
281
282        // Store subject access request.
283        $datarequest->create();
284
285        // Queue the ad-hoc task for automatically approved data requests.
286        if ($status == self::DATAREQUEST_STATUS_APPROVED) {
287            $userid = null;
288            if ($type == self::DATAREQUEST_TYPE_EXPORT) {
289                $userid = $foruser;
290            }
291            self::queue_data_request_task($datarequest->get('id'), $userid);
292        }
293
294        if ($notify) {
295            // Get the list of the site Data Protection Officers.
296            $dpos = self::get_site_dpos();
297
298            // Email the data request to the Data Protection Officer(s)/Admin(s).
299            foreach ($dpos as $dpo) {
300                self::notify_dpo($dpo, $datarequest);
301            }
302        }
303
304        return $datarequest;
305    }
306
307    /**
308     * Fetches the list of the data requests.
309     *
310     * If user ID is provided, it fetches the data requests for the user.
311     * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
312     * (e.g. Users with the Data Protection Officer roles)
313     *
314     * @param int $userid The User ID.
315     * @param int[] $statuses The status filters.
316     * @param int[] $types The request type filters.
317     * @param int[] $creationmethods The request creation method filters.
318     * @param string $sort The order by clause.
319     * @param int $offset Amount of records to skip.
320     * @param int $limit Amount of records to fetch.
321     * @return data_request[]
322     * @throws coding_exception
323     * @throws dml_exception
324     */
325    public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
326                                             $sort = '', $offset = 0, $limit = 0) {
327        global $DB, $USER;
328        $results = [];
329        $sqlparams = [];
330        $sqlconditions = [];
331
332        // Set default sort.
333        if (empty($sort)) {
334            $sort = 'status ASC, timemodified ASC';
335        }
336
337        // Set status filters.
338        if (!empty($statuses)) {
339            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
340            $sqlconditions[] = "status $statusinsql";
341        }
342
343        // Set request type filter.
344        if (!empty($types)) {
345            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
346            $sqlconditions[] = "type $typeinsql";
347            $sqlparams = array_merge($sqlparams, $typeparams);
348        }
349
350        // Set request creation method filter.
351        if (!empty($creationmethods)) {
352            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
353            $sqlconditions[] = "creationmethod $typeinsql";
354            $sqlparams = array_merge($sqlparams, $typeparams);
355        }
356
357        if ($userid) {
358            // Get the data requests for the user or data requests made by the user.
359            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
360            $params = [
361                'userid' => $userid,
362                'requestedby' => $userid
363            ];
364
365            // Build a list of user IDs that the user is allowed to make data requests for.
366            // Of course, the user should be included in this list.
367            $alloweduserids = [$userid];
368            // Get any users that the user can make data requests for.
369            if ($children = helper::get_children_of_user($userid)) {
370                // Get the list of user IDs of the children and merge to the allowed user IDs.
371                $alloweduserids = array_merge($alloweduserids, array_keys($children));
372            }
373            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
374            $sqlconditions[] .= "userid $insql";
375            $select = implode(' AND ', $sqlconditions);
376            $params = array_merge($params, $inparams, $sqlparams);
377
378            $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
379        } else {
380            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
381            if (self::is_site_dpo($USER->id)) {
382                if (!empty($sqlconditions)) {
383                    $select = implode(' AND ', $sqlconditions);
384                    $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
385                } else {
386                    $results = data_request::get_records(null, $sort, '', $offset, $limit);
387                }
388            }
389        }
390
391        // If any are due to expire, expire them and re-fetch updated data.
392        if (empty($statuses)
393                || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
394                || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
395            $expiredrequests = data_request::get_expired_requests($userid);
396
397            if (!empty($expiredrequests)) {
398                data_request::expire($expiredrequests);
399                $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
400            }
401        }
402
403        return $results;
404    }
405
406    /**
407     * Fetches the count of data request records based on the given parameters.
408     *
409     * @param int $userid The User ID.
410     * @param int[] $statuses The status filters.
411     * @param int[] $types The request type filters.
412     * @param int[] $creationmethods The request creation method filters.
413     * @return int
414     * @throws coding_exception
415     * @throws dml_exception
416     */
417    public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
418        global $DB, $USER;
419        $count = 0;
420        $sqlparams = [];
421        $sqlconditions = [];
422        if (!empty($statuses)) {
423            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
424            $sqlconditions[] = "status $statusinsql";
425        }
426        if (!empty($types)) {
427            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
428            $sqlconditions[] = "type $typeinsql";
429            $sqlparams = array_merge($sqlparams, $typeparams);
430        }
431        if (!empty($creationmethods)) {
432            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
433            $sqlconditions[] = "creationmethod $typeinsql";
434            $sqlparams = array_merge($sqlparams, $typeparams);
435        }
436        if ($userid) {
437            // Get the data requests for the user or data requests made by the user.
438            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
439            $params = [
440                'userid' => $userid,
441                'requestedby' => $userid
442            ];
443
444            // Build a list of user IDs that the user is allowed to make data requests for.
445            // Of course, the user should be included in this list.
446            $alloweduserids = [$userid];
447            // Get any users that the user can make data requests for.
448            if ($children = helper::get_children_of_user($userid)) {
449                // Get the list of user IDs of the children and merge to the allowed user IDs.
450                $alloweduserids = array_merge($alloweduserids, array_keys($children));
451            }
452            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
453            $sqlconditions[] .= "userid $insql";
454            $select = implode(' AND ', $sqlconditions);
455            $params = array_merge($params, $inparams, $sqlparams);
456
457            $count = data_request::count_records_select($select, $params);
458        } else {
459            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
460            if (self::is_site_dpo($USER->id)) {
461                if (!empty($sqlconditions)) {
462                    $select = implode(' AND ', $sqlconditions);
463                    $count = data_request::count_records_select($select, $sqlparams);
464                } else {
465                    $count = data_request::count_records();
466                }
467            }
468        }
469
470        return $count;
471    }
472
473    /**
474     * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
475     *
476     * @param int $userid The user ID.
477     * @param int $type The request type.
478     * @return bool
479     * @throws coding_exception
480     * @throws dml_exception
481     */
482    public static function has_ongoing_request($userid, $type) {
483        global $DB;
484
485        // Check if the user already has an incomplete data request of the same type.
486        $nonpendingstatuses = [
487            self::DATAREQUEST_STATUS_COMPLETE,
488            self::DATAREQUEST_STATUS_CANCELLED,
489            self::DATAREQUEST_STATUS_REJECTED,
490            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
491            self::DATAREQUEST_STATUS_EXPIRED,
492            self::DATAREQUEST_STATUS_DELETED,
493        ];
494        list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
495        $select = "type = :type AND userid = :userid AND status {$insql}";
496        $params = array_merge([
497            'type' => $type,
498            'userid' => $userid
499        ], $inparams);
500
501        return data_request::record_exists_select($select, $params);
502    }
503
504    /**
505     * Find whether any ongoing requests exist for a set of users.
506     *
507     * @param   array   $userids
508     * @return  array
509     */
510    public static function find_ongoing_request_types_for_users(array $userids) : array {
511        global $DB;
512
513        if (empty($userids)) {
514            return [];
515        }
516
517        // Check if the user already has an incomplete data request of the same type.
518        $nonpendingstatuses = [
519            self::DATAREQUEST_STATUS_COMPLETE,
520            self::DATAREQUEST_STATUS_CANCELLED,
521            self::DATAREQUEST_STATUS_REJECTED,
522            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
523            self::DATAREQUEST_STATUS_EXPIRED,
524            self::DATAREQUEST_STATUS_DELETED,
525        ];
526        list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
527        list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us');
528
529        $select = "userid {$userinsql} AND status {$statusinsql}";
530        $params = array_merge($statusparams, $userparams);
531
532        $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type');
533
534        $returnval = [];
535        foreach ($userids as $userid) {
536            $returnval[$userid] = (object) [];
537        }
538
539        foreach ($requests as $request) {
540            $returnval[$request->userid]->{$request->type} = true;
541        }
542
543        return $returnval;
544    }
545
546    /**
547     * Determines whether a request is active or not based on its status.
548     *
549     * @param int $status The request status.
550     * @return bool
551     */
552    public static function is_active($status) {
553        // List of statuses which doesn't require any further processing.
554        $finalstatuses = [
555            self::DATAREQUEST_STATUS_COMPLETE,
556            self::DATAREQUEST_STATUS_CANCELLED,
557            self::DATAREQUEST_STATUS_REJECTED,
558            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
559            self::DATAREQUEST_STATUS_EXPIRED,
560            self::DATAREQUEST_STATUS_DELETED,
561        ];
562
563        return !in_array($status, $finalstatuses);
564    }
565
566    /**
567     * Cancels the data request for a given request ID.
568     *
569     * @param int $requestid The request identifier.
570     * @param int $status The request status.
571     * @param int $dpoid The user ID of the Data Protection Officer
572     * @param string $comment The comment about the status update.
573     * @return bool
574     * @throws invalid_persistent_exception
575     * @throws coding_exception
576     */
577    public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') {
578        // Update the request.
579        $datarequest = new data_request($requestid);
580        $datarequest->set('status', $status);
581        if ($dpoid) {
582            $datarequest->set('dpo', $dpoid);
583        }
584        // Update the comment if necessary.
585        if (!empty(trim($comment))) {
586            $params = [
587                'date' => userdate(time()),
588                'comment' => $comment
589            ];
590            $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
591            // Check if there's an existing DPO comment.
592            $currentcomment = trim($datarequest->get('dpocomment'));
593            if ($currentcomment) {
594                // Append the new comment to the current comment and give them 1 line space in between.
595                $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
596            }
597            $datarequest->set('dpocomment', $commenttosave);
598        }
599
600        return $datarequest->update();
601    }
602
603    /**
604     * Fetches a request based on the request ID.
605     *
606     * @param int $requestid The request identifier
607     * @return data_request
608     */
609    public static function get_request($requestid) {
610        return new data_request($requestid);
611    }
612
613    /**
614     * Approves a data request based on the request ID.
615     *
616     * @param int $requestid The request identifier
617     * @return bool
618     * @throws coding_exception
619     * @throws dml_exception
620     * @throws invalid_persistent_exception
621     * @throws required_capability_exception
622     * @throws moodle_exception
623     */
624    public static function approve_data_request($requestid) {
625        global $USER;
626
627        // Check first whether the user can manage data requests.
628        if (!self::can_manage_data_requests($USER->id)) {
629            $context = context_system::instance();
630            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
631        }
632
633        // Check if request is already awaiting for approval.
634        $request = new data_request($requestid);
635        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
636            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
637        }
638
639        // Check if current user has permission to approve delete data request.
640        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
641            throw new required_capability_exception(context_system::instance(),
642                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
643        }
644
645        // Update the status and the DPO.
646        $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
647
648        // Fire an ad hoc task to initiate the data request process.
649        $userid = null;
650        if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
651            $userid = $request->get('userid');
652        }
653        self::queue_data_request_task($requestid, $userid);
654
655        return $result;
656    }
657
658    /**
659     * Rejects a data request based on the request ID.
660     *
661     * @param int $requestid The request identifier
662     * @return bool
663     * @throws coding_exception
664     * @throws dml_exception
665     * @throws invalid_persistent_exception
666     * @throws required_capability_exception
667     * @throws moodle_exception
668     */
669    public static function deny_data_request($requestid) {
670        global $USER;
671
672        if (!self::can_manage_data_requests($USER->id)) {
673            $context = context_system::instance();
674            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
675        }
676
677        // Check if request is already awaiting for approval.
678        $request = new data_request($requestid);
679        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
680            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
681        }
682
683        // Check if current user has permission to reject delete data request.
684        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
685            throw new required_capability_exception(context_system::instance(),
686                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
687        }
688
689        // Update the status and the DPO.
690        return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
691    }
692
693    /**
694     * Sends a message to the site's Data Protection Officer about a request.
695     *
696     * @param stdClass $dpo The DPO user record
697     * @param data_request $request The data request
698     * @return int|false
699     * @throws coding_exception
700     * @throws moodle_exception
701     */
702    public static function notify_dpo($dpo, data_request $request) {
703        global $PAGE, $SITE;
704
705        $output = $PAGE->get_renderer('tool_dataprivacy');
706
707        $usercontext = \context_user::instance($request->get('requestedby'));
708        $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
709        $requestdata = $requestexporter->export($output);
710
711        // Create message to send to the Data Protection Officer(s).
712        $typetext = null;
713        $typetext = $requestdata->typename;
714        $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
715
716        $requestedby = $requestdata->requestedbyuser;
717        $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
718        $message = new message();
719        $message->courseid          = $SITE->id;
720        $message->component         = 'tool_dataprivacy';
721        $message->name              = 'contactdataprotectionofficer';
722        $message->userfrom          = $requestedby->id;
723        $message->replyto           = $requestedby->email;
724        $message->replytoname       = $requestedby->fullname;
725        $message->subject           = $subject;
726        $message->fullmessageformat = FORMAT_HTML;
727        $message->notification      = 1;
728        $message->contexturl        = $datarequestsurl;
729        $message->contexturlname    = get_string('datarequests', 'tool_dataprivacy');
730
731        // Prepare the context data for the email message body.
732        $messagetextdata = [
733            'requestedby' => $requestedby->fullname,
734            'requesttype' => $typetext,
735            'requestdate' => userdate($requestdata->timecreated),
736            'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
737            'requestoriginurl' => new moodle_url('/'),
738            'requestcomments' => $requestdata->messagehtml,
739            'datarequestsurl' => $datarequestsurl
740        ];
741        $requestingfor = $requestdata->foruser;
742        if ($requestedby->id == $requestingfor->id) {
743            $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
744        } else {
745            $messagetextdata['requestfor'] = $requestingfor->fullname;
746        }
747
748        // Email the data request to the Data Protection Officer(s)/Admin(s).
749        $messagetextdata['dponame'] = fullname($dpo);
750        // Render message email body.
751        $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
752        $message->userto = $dpo;
753        $message->fullmessage = html_to_text($messagehtml);
754        $message->fullmessagehtml = $messagehtml;
755
756        // Send message.
757        return message_send($message);
758    }
759
760    /**
761     * Checks whether a non-DPO user can make a data request for another user.
762     *
763     * @param   int     $user The user ID of the target user.
764     * @param   int     $requester The user ID of the user making the request.
765     * @return  bool
766     */
767    public static function can_create_data_request_for_user($user, $requester = null) {
768        $usercontext = \context_user::instance($user);
769
770        return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
771    }
772
773    /**
774     * Require that the current user can make a data request for the specified other user.
775     *
776     * @param   int     $user The user ID of the target user.
777     * @param   int     $requester The user ID of the user making the request.
778     * @return  bool
779     */
780    public static function require_can_create_data_request_for_user($user, $requester = null) {
781        $usercontext = \context_user::instance($user);
782
783        require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
784
785        return true;
786    }
787
788    /**
789     * Check if user has permisson to create data deletion request for themselves.
790     *
791     * @param int|null $userid ID of the user.
792     * @return bool
793     * @throws coding_exception
794     */
795    public static function can_create_data_deletion_request_for_self(int $userid = null): bool {
796        global $USER;
797        $userid = $userid ?: $USER->id;
798        return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid)
799            && !is_primary_admin($userid);
800    }
801
802    /**
803     * Check if user has permission to create data deletion request for another user.
804     *
805     * @param int|null $userid ID of the user.
806     * @return bool
807     * @throws coding_exception
808     * @throws dml_exception
809     */
810    public static function can_create_data_deletion_request_for_other(int $userid = null): bool {
811        global $USER;
812        $userid = $userid ?: $USER->id;
813        return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid);
814    }
815
816    /**
817     * Check if parent can create data deletion request for their children.
818     *
819     * @param int $userid ID of a user being requested.
820     * @param int|null $requesterid ID of a user making request.
821     * @return bool
822     * @throws coding_exception
823     */
824    public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool {
825        global $USER;
826        $requesterid = $requesterid ?: $USER->id;
827        return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid),
828            $requesterid) && !is_primary_admin($userid);
829    }
830
831    /**
832     * Checks whether a user can download a data request.
833     *
834     * @param int $userid Target user id (subject of data request)
835     * @param int $requesterid Requester user id (person who requsted it)
836     * @param int|null $downloaderid Person who wants to download user id (default current)
837     * @return bool
838     * @throws coding_exception
839     */
840    public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
841        global $USER;
842
843        if (!$downloaderid) {
844            $downloaderid = $USER->id;
845        }
846
847        $usercontext = \context_user::instance($userid);
848        // If it's your own and you have the right capability, you can download it.
849        if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
850            return true;
851        }
852        // If you can download anyone's in that context, you can download it.
853        if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
854            return true;
855        }
856        // If you can have the 'child access' ability to request in that context, and you are the one
857        // who requested it, then you can download it.
858        if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
859            return true;
860        }
861        return false;
862    }
863
864    /**
865     * Gets an action menu link to download a data request.
866     *
867     * @param \context_user $usercontext User context (of user who the data is for)
868     * @param int $requestid Request id
869     * @return \action_menu_link_secondary Action menu link
870     * @throws coding_exception
871     */
872    public static function get_download_link(\context_user $usercontext, $requestid) {
873        $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
874                'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
875        $downloadtext = get_string('download', 'tool_dataprivacy');
876        return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
877    }
878
879    /**
880     * Creates a new data purpose.
881     *
882     * @param stdClass $record
883     * @return \tool_dataprivacy\purpose.
884     */
885    public static function create_purpose(stdClass $record) {
886        $purpose = new purpose(0, $record);
887        $purpose->create();
888
889        return $purpose;
890    }
891
892    /**
893     * Updates an existing data purpose.
894     *
895     * @param stdClass $record
896     * @return \tool_dataprivacy\purpose.
897     */
898    public static function update_purpose(stdClass $record) {
899        if (!isset($record->sensitivedatareasons)) {
900            $record->sensitivedatareasons = '';
901        }
902
903        $purpose = new purpose($record->id);
904        $purpose->from_record($record);
905
906        $result = $purpose->update();
907
908        return $purpose;
909    }
910
911    /**
912     * Deletes a data purpose.
913     *
914     * @param int $id
915     * @return bool
916     */
917    public static function delete_purpose($id) {
918        $purpose = new purpose($id);
919        if ($purpose->is_used()) {
920            throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
921        }
922        return $purpose->delete();
923    }
924
925    /**
926     * Get all system data purposes.
927     *
928     * @return \tool_dataprivacy\purpose[]
929     */
930    public static function get_purposes() {
931        return purpose::get_records([], 'name', 'ASC');
932    }
933
934    /**
935     * Creates a new data category.
936     *
937     * @param stdClass $record
938     * @return \tool_dataprivacy\category.
939     */
940    public static function create_category(stdClass $record) {
941        $category = new category(0, $record);
942        $category->create();
943
944        return $category;
945    }
946
947    /**
948     * Updates an existing data category.
949     *
950     * @param stdClass $record
951     * @return \tool_dataprivacy\category.
952     */
953    public static function update_category(stdClass $record) {
954        $category = new category($record->id);
955        $category->from_record($record);
956
957        $result = $category->update();
958
959        return $category;
960    }
961
962    /**
963     * Deletes a data category.
964     *
965     * @param int $id
966     * @return bool
967     */
968    public static function delete_category($id) {
969        $category = new category($id);
970        if ($category->is_used()) {
971            throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
972        }
973        return $category->delete();
974    }
975
976    /**
977     * Get all system data categories.
978     *
979     * @return \tool_dataprivacy\category[]
980     */
981    public static function get_categories() {
982        return category::get_records([], 'name', 'ASC');
983    }
984
985    /**
986     * Sets the context instance purpose and category.
987     *
988     * @param \stdClass $record
989     * @return \tool_dataprivacy\context_instance
990     */
991    public static function set_context_instance($record) {
992        if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
993            // Update.
994            $instance->from_record($record);
995
996            if (empty($record->purposeid) && empty($record->categoryid)) {
997                // We accept one of them to be null but we delete it if both are null.
998                self::unset_context_instance($instance);
999                return;
1000            }
1001
1002        } else {
1003            // Add.
1004            $instance = new context_instance(0, $record);
1005        }
1006        $instance->save();
1007
1008        return $instance;
1009    }
1010
1011    /**
1012     * Unsets the context instance record.
1013     *
1014     * @param \tool_dataprivacy\context_instance $instance
1015     * @return null
1016     */
1017    public static function unset_context_instance(context_instance $instance) {
1018        $instance->delete();
1019    }
1020
1021    /**
1022     * Sets the context level purpose and category.
1023     *
1024     * @throws \coding_exception
1025     * @param \stdClass $record
1026     * @return contextlevel
1027     */
1028    public static function set_contextlevel($record) {
1029        global $DB;
1030
1031        if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
1032            throw new \coding_exception('Only context system and context user can set a contextlevel ' .
1033                'purpose and retention');
1034        }
1035
1036        if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
1037            // Update.
1038            $contextlevel->from_record($record);
1039        } else {
1040            // Add.
1041            $contextlevel = new contextlevel(0, $record);
1042        }
1043        $contextlevel->save();
1044
1045        // We sync with their defaults as we removed these options from the defaults page.
1046        $classname = \context_helper::get_class_for_level($record->contextlevel);
1047        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
1048        set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
1049        set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
1050
1051        return $contextlevel;
1052    }
1053
1054    /**
1055     * Returns the effective category given a context instance.
1056     *
1057     * @param \context $context
1058     * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
1059     * @return category|false
1060     */
1061    public static function get_effective_context_category(\context $context, $forcedvalue = false) {
1062        if (!data_registry::defaults_set()) {
1063            return false;
1064        }
1065
1066        return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
1067    }
1068
1069    /**
1070     * Returns the effective purpose given a context instance.
1071     *
1072     * @param \context $context
1073     * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
1074     * @return purpose|false
1075     */
1076    public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
1077        if (!data_registry::defaults_set()) {
1078            return false;
1079        }
1080
1081        return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
1082    }
1083
1084    /**
1085     * Returns the effective category given a context level.
1086     *
1087     * @param int $contextlevel
1088     * @return category|false
1089     */
1090    public static function get_effective_contextlevel_category($contextlevel) {
1091        if (!data_registry::defaults_set()) {
1092            return false;
1093        }
1094
1095        return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
1096    }
1097
1098    /**
1099     * Returns the effective purpose given a context level.
1100     *
1101     * @param int $contextlevel
1102     * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
1103     * @return purpose|false
1104     */
1105    public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
1106        if (!data_registry::defaults_set()) {
1107            return false;
1108        }
1109
1110        return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
1111    }
1112
1113    /**
1114     * Creates an expired context record for the provided context id.
1115     *
1116     * @param int $contextid
1117     * @return \tool_dataprivacy\expired_context
1118     */
1119    public static function create_expired_context($contextid) {
1120        $record = (object)[
1121            'contextid' => $contextid,
1122            'status' => expired_context::STATUS_EXPIRED,
1123        ];
1124        $expiredctx = new expired_context(0, $record);
1125        $expiredctx->save();
1126
1127        return $expiredctx;
1128    }
1129
1130    /**
1131     * Deletes an expired context record.
1132     *
1133     * @param int $id The tool_dataprivacy_ctxexpire id.
1134     * @return bool True on success.
1135     */
1136    public static function delete_expired_context($id) {
1137        $expiredcontext = new expired_context($id);
1138        return $expiredcontext->delete();
1139    }
1140
1141    /**
1142     * Updates the status of an expired context.
1143     *
1144     * @param \tool_dataprivacy\expired_context $expiredctx
1145     * @param int $status
1146     * @return null
1147     */
1148    public static function set_expired_context_status(expired_context $expiredctx, $status) {
1149        $expiredctx->set('status', $status);
1150        $expiredctx->save();
1151    }
1152
1153    /**
1154     * Finds all contextlists having at least one approved context, and returns them as in a contextlist_collection.
1155     *
1156     * @param   contextlist_collection  $collection The collection of unapproved contextlist objects.
1157     * @param   \stdClass               $foruser The target user
1158     * @param   int                     $type The purpose of the collection
1159     * @return  contextlist_collection  The collection of approved_contextlist objects.
1160     */
1161    public static function get_approved_contextlist_collection_for_collection(contextlist_collection $collection,
1162            \stdClass $foruser, int $type) : contextlist_collection {
1163
1164        // Create the approved contextlist collection object.
1165        $approvedcollection = new contextlist_collection($collection->get_userid());
1166        $isconfigured = data_registry::defaults_set();
1167
1168        foreach ($collection as $contextlist) {
1169            $contextids = [];
1170            foreach ($contextlist as $context) {
1171                if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $type) {
1172                    // Data can only be deleted from it if the context is either expired, or unprotected.
1173                    // Note: We can only check whether a context is expired or unprotected if the site is configured and
1174                    // defaults are set appropriately. If they are not, we treat all contexts as though they are
1175                    // unprotected.
1176                    $purpose = static::get_effective_context_purpose($context);
1177                    if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
1178                        continue;
1179                    }
1180                }
1181
1182                $contextids[] = $context->id;
1183            }
1184
1185            // The data for the last component contextlist won't have been written yet, so write it now.
1186            if (!empty($contextids)) {
1187                $approvedcollection->add_contextlist(
1188                        new approved_contextlist($foruser, $contextlist->get_component(), $contextids)
1189                    );
1190            }
1191        }
1192
1193        return $approvedcollection;
1194    }
1195
1196    /**
1197     * Updates the default category and purpose for a given context level (and optionally, a plugin).
1198     *
1199     * @param int $contextlevel The context level.
1200     * @param int $categoryid The ID matching the category.
1201     * @param int $purposeid The ID matching the purpose record.
1202     * @param int $activity The name of the activity that we're making a defaults configuration for.
1203     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
1204     * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
1205     */
1206    public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
1207        global $DB;
1208
1209        // Get the class name associated with this context level.
1210        $classname = context_helper::get_class_for_level($contextlevel);
1211        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
1212
1213        // Check the default category to be set.
1214        if ($categoryid == context_instance::INHERIT) {
1215            unset_config($categoryvar, 'tool_dataprivacy');
1216
1217        } else {
1218            // Make sure the given category ID exists first.
1219            $categorypersistent = new category($categoryid);
1220            $categorypersistent->read();
1221
1222            // Then set the new default value.
1223            set_config($categoryvar, $categoryid, 'tool_dataprivacy');
1224        }
1225
1226        // Check the default purpose to be set.
1227        if ($purposeid == context_instance::INHERIT) {
1228            // If the defaults is set to inherit, just unset the config value.
1229            unset_config($purposevar, 'tool_dataprivacy');
1230
1231        } else {
1232            // Make sure the given purpose ID exists first.
1233            $purposepersistent = new purpose($purposeid);
1234            $purposepersistent->read();
1235
1236            // Then set the new default value.
1237            set_config($purposevar, $purposeid, 'tool_dataprivacy');
1238        }
1239
1240        // Unset instances that have been assigned with custom purpose and category, if override was specified.
1241        if ($override) {
1242            // We'd like to find context IDs that we want to unset.
1243            $statements = ["SELECT c.id as contextid FROM {context} c"];
1244            // Based on this context level.
1245            $params = ['contextlevel' => $contextlevel];
1246
1247            if ($contextlevel == CONTEXT_MODULE) {
1248                // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
1249                $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
1250                // And that the module is listed on the modules table.
1251                $statements[] = "JOIN {modules} m ON m.id = cm.module";
1252
1253                if ($activity) {
1254                    // If we're overriding for an activity module, make sure that the context instance matches that activity.
1255                    $statements[] = "AND m.name = :modname";
1256                    $params['modname'] = $activity;
1257                }
1258            }
1259            // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
1260            $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
1261            // And that the context level of this instance matches the given context level.
1262            $statements[] = "WHERE c.contextlevel = :contextlevel";
1263
1264            // Build our SQL query by gluing the statements.
1265            $sql = implode("\n", $statements);
1266
1267            // Get the context records matching our query.
1268            $contextids = $DB->get_fieldset_sql($sql, $params);
1269
1270            // Delete the matching context instances.
1271            foreach ($contextids as $contextid) {
1272                if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
1273                    self::unset_context_instance($instance);
1274                }
1275            }
1276        }
1277
1278        return true;
1279    }
1280
1281    /**
1282     * Format the supplied date interval as a retention period.
1283     *
1284     * @param   \DateInterval   $interval
1285     * @return  string
1286     */
1287    public static function format_retention_period(\DateInterval $interval) : string {
1288        // It is one or another.
1289        if ($interval->y) {
1290            $formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
1291        } else if ($interval->m) {
1292            $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
1293        } else if ($interval->d) {
1294            $formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
1295        } else {
1296            $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
1297        }
1298
1299        return $formattedtime;
1300    }
1301
1302    /**
1303     * Whether automatic data request approval is turned on or not for the given request type.
1304     *
1305     * @param int $type The request type.
1306     * @return bool
1307     */
1308    public static function is_automatic_request_approval_on(int $type): bool {
1309        switch ($type) {
1310            case self::DATAREQUEST_TYPE_EXPORT:
1311                return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval'));
1312            case self::DATAREQUEST_TYPE_DELETE:
1313                return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval'));
1314        }
1315        return false;
1316    }
1317
1318    /**
1319     * Creates an ad-hoc task for the data request.
1320     *
1321     * @param int $requestid The data request ID.
1322     * @param int $userid Optional. The user ID to run the task as, if necessary.
1323     */
1324    public static function queue_data_request_task(int $requestid, int $userid = null): void {
1325        $task = new process_data_request_task();
1326        $task->set_custom_data(['requestid' => $requestid]);
1327        if ($userid) {
1328            $task->set_userid($userid);
1329        }
1330        manager::queue_adhoc_task($task, true);
1331    }
1332}
1333