Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 */
24
namespace tool_dataprivacy;
25
 
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;
44
 
45
defined('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
 */
53
class 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
    /** Newly submitted and we have started to find the location of data. */
68
    const DATAREQUEST_STATUS_PREPROCESSING = 1;
69
 
70
    /** Metadata ready and awaiting review and approval by the Data Protection officer. */
71
    const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
72
 
73
    /** Request approved and will be processed soon. */
74
    const DATAREQUEST_STATUS_APPROVED = 3;
75
 
76
    /** The request is now being processed. */
77
    const DATAREQUEST_STATUS_PROCESSING = 4;
78
 
79
    /** Information/other request completed. */
80
    const DATAREQUEST_STATUS_COMPLETE = 5;
81
 
82
    /** Data request cancelled by the user. */
83
    const DATAREQUEST_STATUS_CANCELLED = 6;
84
 
85
    /** Data request rejected by the DPO. */
86
    const DATAREQUEST_STATUS_REJECTED = 7;
87
 
88
    /** Data request download ready. */
89
    const DATAREQUEST_STATUS_DOWNLOAD_READY = 8;
90
 
91
    /** Data request expired. */
92
    const DATAREQUEST_STATUS_EXPIRED = 9;
93
 
94
    /** Data delete request completed, account is removed. */
95
    const DATAREQUEST_STATUS_DELETED = 10;
96
 
97
    /** Approve data request. */
98
    const DATAREQUEST_ACTION_APPROVE = 1;
99
 
100
    /** Reject data request. */
101
    const DATAREQUEST_ACTION_REJECT = 2;
102
 
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
    }
112
 
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
    }
123
 
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
        }
137
 
138
        require_capability('tool/dataprivacy:managedataregistry', $context);
139
    }
140
 
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 = [];
152
 
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'));
158
 
159
            // Extract the configured roles that have the capability from the list of capable roles.
160
            $roleids = array_intersect($capableroles, $configroleids);
161
        }
162
 
163
        return $roleids;
164
    }
165
 
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;
173
 
174
        $dporoleids = self::get_assigned_privacy_officer_roles();
175
        $dponames = array();
176
 
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
        }
181
 
182
        return $dponames;
183
    }
184
 
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();
191
 
192
        $dpos = [];
193
        $context = context_system::instance();
194
        foreach ($dporoles as $roleid) {
195
            $userfieldsapi = \core_user\fields::for_name();
196
            $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
197
            $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
198
                      'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
199
                      'u.country, u.picture, u.idnumber, u.department, u.institution, '.
200
                      'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
201
                      'r.name AS rolename, r.sortorder, '.
202
                      'r.shortname AS roleshortname, rn.name AS rolecoursealias';
203
            // Fetch users that can manage data requests.
204
            $dpos += get_role_users($roleid, $context, false, $fields);
205
        }
206
 
207
        // If the site has no data protection officer, defer to site admin(s).
208
        if (empty($dpos)) {
209
            $dpos = get_admins();
210
        }
211
        return $dpos;
212
    }
213
 
214
    /**
215
     * Checks whether a given user is a site Privacy Officer.
216
     *
217
     * @param int $userid The user ID.
218
     * @return bool
219
     */
220
    public static function is_site_dpo($userid) {
221
        $dpos = self::get_site_dpos();
222
        return array_key_exists($userid, $dpos) || is_siteadmin();
223
    }
224
 
225
    /**
226
     * Lodges a data request and sends the request details to the site Data Protection Officer(s).
227
     *
228
     * @param int $foruser The user whom the request is being made for.
229
     * @param int $type The request type.
230
     * @param string $comments Request comments.
231
     * @param int $creationmethod The creation method of the data request.
232
     * @param bool $notify Notify DPOs of this pending request.
233
     * @return data_request
234
     * @throws invalid_persistent_exception
235
     * @throws coding_exception
236
     */
237
    public static function create_data_request($foruser, $type, $comments = '',
238
            $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL,
239
            $notify = null
240
        ) {
241
        global $USER;
242
 
243
        if (null === $notify) {
244
            // Only if notifications have not been decided by caller.
245
            if ( data_request::DATAREQUEST_CREATION_AUTO == $creationmethod) {
246
                // If the request was automatically created, then do not notify unless explicitly set.
247
                $notify = false;
248
            } else {
249
                $notify = true;
250
            }
251
        }
252
 
253
        $datarequest = new data_request();
254
        // The user the request is being made for.
255
        $datarequest->set('userid', $foruser);
256
 
257
        // The cron is considered to be a guest user when it creates a data request.
258
        // NOTE: This should probably be changed. We should leave the default value for $requestinguser if
259
        // the request is not explicitly created by a specific user.
260
        $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ?
261
                get_admin()->id : $USER->id;
262
        // The user making the request.
263
        $datarequest->set('requestedby', $requestinguser);
264
        // Set status.
265
 
266
        $allowfiltering = get_config('tool_dataprivacy', 'allowfiltering') && ($type != self::DATAREQUEST_TYPE_DELETE);
267
        if ($allowfiltering) {
268
            $status = self::DATAREQUEST_STATUS_PENDING;
269
        } else {
270
            $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL;
271
            if (self::is_automatic_request_approval_on($type)) {
272
                // Set status to approved if automatic data request approval is enabled.
273
                $status = self::DATAREQUEST_STATUS_APPROVED;
274
                // Set the privacy officer field if the one making the data request is a privacy officer.
275
                if (self::is_site_dpo($requestinguser)) {
276
                    $datarequest->set('dpo', $requestinguser);
277
                }
278
                // Mark this request as system approved.
279
                $datarequest->set('systemapproved', true);
280
                // No need to notify privacy officer(s) about automatically approved data requests.
281
                $notify = false;
282
            }
283
        }
284
        $datarequest->set('status', $status);
285
        // Set request type.
286
        $datarequest->set('type', $type);
287
        // Set request comments.
288
        $datarequest->set('comments', $comments);
289
        // Set the creation method.
290
        $datarequest->set('creationmethod', $creationmethod);
291
 
292
        // Store subject access request.
293
        $datarequest->create();
294
 
295
        // Queue the ad-hoc task for automatically approved data requests.
296
        if ($status == self::DATAREQUEST_STATUS_APPROVED) {
297
            $userid = null;
298
            if ($type == self::DATAREQUEST_TYPE_EXPORT) {
299
                $userid = $foruser;
300
            }
301
            self::queue_data_request_task($datarequest->get('id'), $userid);
302
        }
303
 
304
        if ($notify) {
305
            // Get the list of the site Data Protection Officers.
306
            $dpos = self::get_site_dpos();
307
 
308
            // Email the data request to the Data Protection Officer(s)/Admin(s).
309
            foreach ($dpos as $dpo) {
310
                self::notify_dpo($dpo, $datarequest);
311
            }
312
        }
313
 
314
        if ($status == self::DATAREQUEST_STATUS_PENDING) {
315
            // Fire an ad hoc task to initiate the data request process.
316
            $task = new initiate_data_request_task();
317
            $task->set_custom_data(['requestid' => $datarequest->get('id')]);
318
            manager::queue_adhoc_task($task, true);
319
        }
320
 
321
        return $datarequest;
322
    }
323
 
324
    /**
325
     * Fetches the list of the data requests.
326
     *
327
     * If user ID is provided, it fetches the data requests for the user.
328
     * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
329
     * (e.g. Users with the Data Protection Officer roles)
330
     *
331
     * @param int $userid The User ID.
332
     * @param int[] $statuses The status filters.
333
     * @param int[] $types The request type filters.
334
     * @param int[] $creationmethods The request creation method filters.
335
     * @param string $sort The order by clause.
336
     * @param int $offset Amount of records to skip.
337
     * @param int $limit Amount of records to fetch.
338
     * @return data_request[]
339
     * @throws coding_exception
340
     * @throws dml_exception
341
     */
342
    public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [],
343
                                             $sort = '', $offset = 0, $limit = 0) {
344
        global $DB, $USER;
345
        $results = [];
346
        $sqlparams = [];
347
        $sqlconditions = [];
348
 
349
        // Set default sort.
350
        if (empty($sort)) {
351
            $sort = 'status ASC, timemodified ASC';
352
        }
353
 
354
        // Set status filters.
355
        if (!empty($statuses)) {
356
            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
357
            $sqlconditions[] = "status $statusinsql";
358
        }
359
 
360
        // Set request type filter.
361
        if (!empty($types)) {
362
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
363
            $sqlconditions[] = "type $typeinsql";
364
            $sqlparams = array_merge($sqlparams, $typeparams);
365
        }
366
 
367
        // Set request creation method filter.
368
        if (!empty($creationmethods)) {
369
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
370
            $sqlconditions[] = "creationmethod $typeinsql";
371
            $sqlparams = array_merge($sqlparams, $typeparams);
372
        }
373
 
374
        if ($userid) {
375
            // Get the data requests for the user or data requests made by the user.
376
            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
377
            $params = [
378
                'userid' => $userid,
379
                'requestedby' => $userid
380
            ];
381
 
382
            // Build a list of user IDs that the user is allowed to make data requests for.
383
            // Of course, the user should be included in this list.
384
            $alloweduserids = [$userid];
385
            // Get any users that the user can make data requests for.
386
            if ($children = helper::get_children_of_user($userid)) {
387
                // Get the list of user IDs of the children and merge to the allowed user IDs.
388
                $alloweduserids = array_merge($alloweduserids, array_keys($children));
389
            }
390
            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
391
            $sqlconditions[] .= "userid $insql";
392
            $select = implode(' AND ', $sqlconditions);
393
            $params = array_merge($params, $inparams, $sqlparams);
394
 
395
            $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit);
396
        } else {
397
            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
398
            if (self::is_site_dpo($USER->id)) {
399
                if (!empty($sqlconditions)) {
400
                    $select = implode(' AND ', $sqlconditions);
401
                    $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit);
402
                } else {
403
                    $results = data_request::get_records(null, $sort, '', $offset, $limit);
404
                }
405
            }
406
        }
407
 
408
        // If any are due to expire, expire them and re-fetch updated data.
409
        if (empty($statuses)
410
                || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses)
411
                || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) {
412
            $expiredrequests = data_request::get_expired_requests($userid);
413
 
414
            if (!empty($expiredrequests)) {
415
                data_request::expire($expiredrequests);
416
                $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit);
417
            }
418
        }
419
 
420
        return $results;
421
    }
422
 
423
    /**
424
     * Fetches the count of data request records based on the given parameters.
425
     *
426
     * @param int $userid The User ID.
427
     * @param int[] $statuses The status filters.
428
     * @param int[] $types The request type filters.
429
     * @param int[] $creationmethods The request creation method filters.
430
     * @return int
431
     * @throws coding_exception
432
     * @throws dml_exception
433
     */
434
    public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) {
435
        global $DB, $USER;
436
        $count = 0;
437
        $sqlparams = [];
438
        $sqlconditions = [];
439
        if (!empty($statuses)) {
440
            list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED);
441
            $sqlconditions[] = "status $statusinsql";
442
        }
443
        if (!empty($types)) {
444
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED);
445
            $sqlconditions[] = "type $typeinsql";
446
            $sqlparams = array_merge($sqlparams, $typeparams);
447
        }
448
        if (!empty($creationmethods)) {
449
            list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED);
450
            $sqlconditions[] = "creationmethod $typeinsql";
451
            $sqlparams = array_merge($sqlparams, $typeparams);
452
        }
453
        if ($userid) {
454
            // Get the data requests for the user or data requests made by the user.
455
            $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)";
456
            $params = [
457
                'userid' => $userid,
458
                'requestedby' => $userid
459
            ];
460
 
461
            // Build a list of user IDs that the user is allowed to make data requests for.
462
            // Of course, the user should be included in this list.
463
            $alloweduserids = [$userid];
464
            // Get any users that the user can make data requests for.
465
            if ($children = helper::get_children_of_user($userid)) {
466
                // Get the list of user IDs of the children and merge to the allowed user IDs.
467
                $alloweduserids = array_merge($alloweduserids, array_keys($children));
468
            }
469
            list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED);
470
            $sqlconditions[] .= "userid $insql";
471
            $select = implode(' AND ', $sqlconditions);
472
            $params = array_merge($params, $inparams, $sqlparams);
473
 
474
            $count = data_request::count_records_select($select, $params);
475
        } else {
476
            // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
477
            if (self::is_site_dpo($USER->id)) {
478
                if (!empty($sqlconditions)) {
479
                    $select = implode(' AND ', $sqlconditions);
480
                    $count = data_request::count_records_select($select, $sqlparams);
481
                } else {
482
                    $count = data_request::count_records();
483
                }
484
            }
485
        }
486
 
487
        return $count;
488
    }
489
 
490
    /**
491
     * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
492
     *
493
     * @param int $userid The user ID.
494
     * @param int $type The request type.
495
     * @return bool
496
     * @throws coding_exception
497
     * @throws dml_exception
498
     */
499
    public static function has_ongoing_request($userid, $type) {
500
        global $DB;
501
 
502
        // Check if the user already has an incomplete data request of the same type.
503
        $nonpendingstatuses = [
504
            self::DATAREQUEST_STATUS_COMPLETE,
505
            self::DATAREQUEST_STATUS_CANCELLED,
506
            self::DATAREQUEST_STATUS_REJECTED,
507
            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
508
            self::DATAREQUEST_STATUS_EXPIRED,
509
            self::DATAREQUEST_STATUS_DELETED,
510
        ];
511
        list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
512
        $select = "type = :type AND userid = :userid AND status {$insql}";
513
        $params = array_merge([
514
            'type' => $type,
515
            'userid' => $userid
516
        ], $inparams);
517
 
518
        return data_request::record_exists_select($select, $params);
519
    }
520
 
521
    /**
522
     * Find whether any ongoing requests exist for a set of users.
523
     *
524
     * @param   array   $userids
525
     * @return  array
526
     */
527
    public static function find_ongoing_request_types_for_users(array $userids): array {
528
        global $DB;
529
 
530
        if (empty($userids)) {
531
            return [];
532
        }
533
 
534
        // Check if the user already has an incomplete data request of the same type.
535
        $nonpendingstatuses = [
536
            self::DATAREQUEST_STATUS_COMPLETE,
537
            self::DATAREQUEST_STATUS_CANCELLED,
538
            self::DATAREQUEST_STATUS_REJECTED,
539
            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
540
            self::DATAREQUEST_STATUS_EXPIRED,
541
            self::DATAREQUEST_STATUS_DELETED,
542
        ];
543
        list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false);
544
        list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us');
545
 
546
        $select = "userid {$userinsql} AND status {$statusinsql}";
547
        $params = array_merge($statusparams, $userparams);
548
 
549
        $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type');
550
 
551
        $returnval = [];
552
        foreach ($userids as $userid) {
553
            $returnval[$userid] = (object) [];
554
        }
555
 
556
        foreach ($requests as $request) {
557
            $returnval[$request->userid]->{$request->type} = true;
558
        }
559
 
560
        return $returnval;
561
    }
562
 
563
    /**
564
     * Determines whether a request is active or not based on its status.
565
     *
566
     * @param int $status The request status.
567
     * @return bool
568
     */
569
    public static function is_active($status) {
570
        // List of statuses which doesn't require any further processing.
571
        $finalstatuses = [
572
            self::DATAREQUEST_STATUS_COMPLETE,
573
            self::DATAREQUEST_STATUS_CANCELLED,
574
            self::DATAREQUEST_STATUS_REJECTED,
575
            self::DATAREQUEST_STATUS_DOWNLOAD_READY,
576
            self::DATAREQUEST_STATUS_EXPIRED,
577
            self::DATAREQUEST_STATUS_DELETED,
578
        ];
579
 
580
        return !in_array($status, $finalstatuses);
581
    }
582
 
583
    /**
584
     * Cancels the data request for a given request ID.
585
     *
586
     * @param int $requestid The request identifier.
587
     * @param int $status The request status.
588
     * @param int $dpoid The user ID of the Data Protection Officer
589
     * @param string $comment The comment about the status update.
590
     * @return bool
591
     * @throws invalid_persistent_exception
592
     * @throws coding_exception
593
     */
594
    public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') {
595
        // Update the request.
596
        $datarequest = new data_request($requestid);
597
        $datarequest->set('status', $status);
598
        if ($dpoid) {
599
            $datarequest->set('dpo', $dpoid);
600
        }
601
        // Update the comment if necessary.
602
        if (!empty(trim($comment))) {
603
            $params = [
604
                'date' => userdate(time()),
605
                'comment' => $comment
606
            ];
607
            $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params);
608
            // Check if there's an existing DPO comment.
609
            $currentcomment = trim($datarequest->get('dpocomment'));
610
            if ($currentcomment) {
611
                // Append the new comment to the current comment and give them 1 line space in between.
612
                $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave;
613
            }
614
            $datarequest->set('dpocomment', $commenttosave);
615
        }
616
 
617
        return $datarequest->update();
618
    }
619
 
620
    /**
621
     * Fetches a request based on the request ID.
622
     *
623
     * @param int $requestid The request identifier
624
     * @return data_request
625
     */
626
    public static function get_request($requestid) {
627
        return new data_request($requestid);
628
    }
629
 
630
    /**
631
     * Approves a data request based on the request ID.
632
     *
633
     * @param int $requestid The request identifier
634
     * @param array $filtercoursecontexts Apply to export request, only approve contexts belong to these courses.
635
     * @return bool
636
     * @throws coding_exception
637
     * @throws dml_exception
638
     * @throws invalid_persistent_exception
639
     * @throws required_capability_exception
640
     * @throws moodle_exception
641
     */
642
    public static function approve_data_request($requestid, $filtercoursecontexts = []) {
643
        global $USER;
644
 
645
        // Check first whether the user can manage data requests.
646
        if (!self::can_manage_data_requests($USER->id)) {
647
            $context = context_system::instance();
648
            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
649
        }
650
 
651
        // Check if request is already awaiting for approval.
652
        $request = new data_request($requestid);
653
        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
654
            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
655
        }
656
 
657
        // Check if current user has permission to approve delete data request.
658
        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
659
            throw new required_capability_exception(context_system::instance(),
660
                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
661
        }
662
 
663
        // Update the status and the DPO.
664
        $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
665
 
666
        if ($request->get('type') != self::DATAREQUEST_TYPE_DELETE) {
667
            $allowfiltering = get_config('tool_dataprivacy', 'allowfiltering');
668
            if ($allowfiltering) {
669
                if ($filtercoursecontexts) {
670
                    // Only approve the context belong to selected courses.
671
                    self::approve_contexts_belonging_to_request($requestid, $filtercoursecontexts);
672
                } else {
673
                    // Approve all the contexts attached to the request.
674
                    self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED);
675
                }
676
            }
677
        }
678
        // Fire an ad hoc task to initiate the data request process.
679
        $userid = null;
680
        if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
681
            $userid = $request->get('userid');
682
        }
683
        self::queue_data_request_task($requestid, $userid);
684
 
685
        return $result;
686
    }
687
 
688
    /**
689
     * Rejects a data request based on the request ID.
690
     *
691
     * @param int $requestid The request identifier
692
     * @return bool
693
     * @throws coding_exception
694
     * @throws dml_exception
695
     * @throws invalid_persistent_exception
696
     * @throws required_capability_exception
697
     * @throws moodle_exception
698
     */
699
    public static function deny_data_request($requestid) {
700
        global $USER;
701
 
702
        if (!self::can_manage_data_requests($USER->id)) {
703
            $context = context_system::instance();
704
            throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
705
        }
706
 
707
        // Check if request is already awaiting for approval.
708
        $request = new data_request($requestid);
709
        if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
710
            throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
711
        }
712
 
713
        // Check if current user has permission to reject delete data request.
714
        if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
715
            throw new required_capability_exception(context_system::instance(),
716
                'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
717
        }
718
 
719
        // Update the status and the DPO.
720
        return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
721
    }
722
 
723
    /**
724
     * Sends a message to the site's Data Protection Officer about a request.
725
     *
726
     * @param stdClass $dpo The DPO user record
727
     * @param data_request $request The data request
728
     * @return int|false
729
     * @throws coding_exception
730
     * @throws moodle_exception
731
     */
732
    public static function notify_dpo($dpo, data_request $request) {
733
        global $PAGE, $SITE;
734
 
735
        $output = $PAGE->get_renderer('tool_dataprivacy');
736
 
737
        $usercontext = \context_user::instance($request->get('requestedby'));
738
        $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
739
        $requestdata = $requestexporter->export($output);
740
 
741
        // Create message to send to the Data Protection Officer(s).
742
        $typetext = null;
743
        $typetext = $requestdata->typename;
744
        $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
745
 
746
        $requestedby = $requestdata->requestedbyuser;
747
        $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
748
        $message = new message();
749
        $message->courseid          = $SITE->id;
750
        $message->component         = 'tool_dataprivacy';
751
        $message->name              = 'contactdataprotectionofficer';
752
        $message->userfrom          = $requestedby->id;
753
        $message->replyto           = $requestedby->email;
754
        $message->replytoname       = $requestedby->fullname;
755
        $message->subject           = $subject;
756
        $message->fullmessageformat = FORMAT_HTML;
757
        $message->notification      = 1;
758
        $message->contexturl        = $datarequestsurl;
759
        $message->contexturlname    = get_string('datarequests', 'tool_dataprivacy');
760
 
761
        // Prepare the context data for the email message body.
762
        $messagetextdata = [
763
            'requestedby' => $requestedby->fullname,
764
            'requesttype' => $typetext,
765
            'requestdate' => userdate($requestdata->timecreated),
766
            'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
767
            'requestoriginurl' => new moodle_url('/'),
768
            'requestcomments' => $requestdata->messagehtml,
769
            'datarequestsurl' => $datarequestsurl
770
        ];
771
        $requestingfor = $requestdata->foruser;
772
        if ($requestedby->id == $requestingfor->id) {
773
            $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
774
        } else {
775
            $messagetextdata['requestfor'] = $requestingfor->fullname;
776
        }
777
 
778
        // Email the data request to the Data Protection Officer(s)/Admin(s).
779
        $messagetextdata['dponame'] = fullname($dpo);
780
        // Render message email body.
781
        $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
782
        $message->userto = $dpo;
783
        $message->fullmessage = html_to_text($messagehtml);
784
        $message->fullmessagehtml = $messagehtml;
785
 
786
        // Send message.
787
        return message_send($message);
788
    }
789
 
790
    /**
791
     * Checks whether a non-DPO user can make a data request for another user.
792
     *
793
     * @param   int     $user The user ID of the target user.
794
     * @param   int     $requester The user ID of the user making the request.
795
     * @return  bool
796
     */
797
    public static function can_create_data_request_for_user($user, $requester = null) {
798
        $usercontext = \context_user::instance($user);
799
 
800
        return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
801
    }
802
 
803
    /**
804
     * Require that the current user can make a data request for the specified other user.
805
     *
806
     * @param   int     $user The user ID of the target user.
807
     * @param   int     $requester The user ID of the user making the request.
808
     * @return  bool
809
     */
810
    public static function require_can_create_data_request_for_user($user, $requester = null) {
811
        $usercontext = \context_user::instance($user);
812
 
813
        require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
814
 
815
        return true;
816
    }
817
 
818
    /**
819
     * Check if user has permission to create data download request for themselves
820
     *
821
     * @param int|null $userid
822
     * @return bool
823
     */
824
    public static function can_create_data_download_request_for_self(int $userid = null): bool {
825
        global $USER;
826
        $userid = $userid ?: $USER->id;
827
        return has_capability('tool/dataprivacy:downloadownrequest', \context_user::instance($userid), $userid);
828
    }
829
 
830
    /**
831
     * Check if user has permisson to create data deletion request for themselves.
832
     *
833
     * @param int|null $userid ID of the user.
834
     * @return bool
835
     * @throws coding_exception
836
     */
837
    public static function can_create_data_deletion_request_for_self(int $userid = null): bool {
838
        global $USER;
839
        $userid = $userid ?: $USER->id;
840
        return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid)
841
            && !is_primary_admin($userid);
842
    }
843
 
844
    /**
845
     * Check if user has permission to create data deletion request for another user.
846
     *
847
     * @param int|null $userid ID of the user.
848
     * @return bool
849
     * @throws coding_exception
850
     * @throws dml_exception
851
     */
852
    public static function can_create_data_deletion_request_for_other(int $userid = null): bool {
853
        global $USER;
854
        $userid = $userid ?: $USER->id;
855
        return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid);
856
    }
857
 
858
    /**
859
     * Check if parent can create data deletion request for their children.
860
     *
861
     * @param int $userid ID of a user being requested.
862
     * @param int|null $requesterid ID of a user making request.
863
     * @return bool
864
     * @throws coding_exception
865
     */
866
    public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool {
867
        global $USER;
868
        $requesterid = $requesterid ?: $USER->id;
869
        return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid),
870
            $requesterid) && !is_primary_admin($userid);
871
    }
872
 
873
    /**
874
     * Checks whether a user can download a data request.
875
     *
876
     * @param int $userid Target user id (subject of data request)
877
     * @param int $requesterid Requester user id (person who requsted it)
878
     * @param int|null $downloaderid Person who wants to download user id (default current)
879
     * @return bool
880
     * @throws coding_exception
881
     */
882
    public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
883
        global $USER;
884
 
885
        if (!$downloaderid) {
886
            $downloaderid = $USER->id;
887
        }
888
 
889
        $usercontext = \context_user::instance($userid);
890
        // If it's your own and you have the right capability, you can download it.
891
        if ($userid == $downloaderid && self::can_create_data_download_request_for_self($downloaderid)) {
892
            return true;
893
        }
894
        // If you can download anyone's in that context, you can download it.
895
        if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
896
            return true;
897
        }
898
        // If you can have the 'child access' ability to request in that context, and you are the one
899
        // who requested it, then you can download it.
900
        if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
901
            return true;
902
        }
903
        return false;
904
    }
905
 
906
    /**
907
     * Gets an action menu link to download a data request.
908
     *
909
     * @param \context_user $usercontext User context (of user who the data is for)
910
     * @param int $requestid Request id
911
     * @return \action_menu_link_secondary Action menu link
912
     * @throws coding_exception
913
     */
914
    public static function get_download_link(\context_user $usercontext, $requestid) {
915
        $downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
916
                'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
917
        $downloadtext = get_string('download', 'tool_dataprivacy');
918
        return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
919
    }
920
 
921
    /**
922
     * Creates a new data purpose.
923
     *
924
     * @param stdClass $record
925
     * @return \tool_dataprivacy\purpose.
926
     */
927
    public static function create_purpose(stdClass $record) {
928
        $purpose = new purpose(0, $record);
929
        $purpose->create();
930
 
931
        return $purpose;
932
    }
933
 
934
    /**
935
     * Updates an existing data purpose.
936
     *
937
     * @param stdClass $record
938
     * @return \tool_dataprivacy\purpose.
939
     */
940
    public static function update_purpose(stdClass $record) {
941
        if (!isset($record->sensitivedatareasons)) {
942
            $record->sensitivedatareasons = '';
943
        }
944
 
945
        $purpose = new purpose($record->id);
946
        $purpose->from_record($record);
947
 
948
        $result = $purpose->update();
949
 
950
        return $purpose;
951
    }
952
 
953
    /**
954
     * Deletes a data purpose.
955
     *
956
     * @param int $id
957
     * @return bool
958
     */
959
    public static function delete_purpose($id) {
960
        $purpose = new purpose($id);
961
        if ($purpose->is_used()) {
962
            throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
963
        }
964
        return $purpose->delete();
965
    }
966
 
967
    /**
968
     * Get all system data purposes.
969
     *
970
     * @return \tool_dataprivacy\purpose[]
971
     */
972
    public static function get_purposes() {
973
        return purpose::get_records([], 'name', 'ASC');
974
    }
975
 
976
    /**
977
     * Creates a new data category.
978
     *
979
     * @param stdClass $record
980
     * @return \tool_dataprivacy\category.
981
     */
982
    public static function create_category(stdClass $record) {
983
        $category = new category(0, $record);
984
        $category->create();
985
 
986
        return $category;
987
    }
988
 
989
    /**
990
     * Updates an existing data category.
991
     *
992
     * @param stdClass $record
993
     * @return \tool_dataprivacy\category.
994
     */
995
    public static function update_category(stdClass $record) {
996
        $category = new category($record->id);
997
        $category->from_record($record);
998
 
999
        $result = $category->update();
1000
 
1001
        return $category;
1002
    }
1003
 
1004
    /**
1005
     * Deletes a data category.
1006
     *
1007
     * @param int $id
1008
     * @return bool
1009
     */
1010
    public static function delete_category($id) {
1011
        $category = new category($id);
1012
        if ($category->is_used()) {
1013
            throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
1014
        }
1015
        return $category->delete();
1016
    }
1017
 
1018
    /**
1019
     * Get all system data categories.
1020
     *
1021
     * @return \tool_dataprivacy\category[]
1022
     */
1023
    public static function get_categories() {
1024
        return category::get_records([], 'name', 'ASC');
1025
    }
1026
 
1027
    /**
1028
     * Sets the context instance purpose and category.
1029
     *
1030
     * @param \stdClass $record
1031
     * @return \tool_dataprivacy\context_instance
1032
     */
1033
    public static function set_context_instance($record) {
1034
        if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
1035
            // Update.
1036
            $instance->from_record($record);
1037
 
1038
            if (empty($record->purposeid) && empty($record->categoryid)) {
1039
                // We accept one of them to be null but we delete it if both are null.
1040
                self::unset_context_instance($instance);
1041
                return;
1042
            }
1043
 
1044
        } else {
1045
            // Add.
1046
            $instance = new context_instance(0, $record);
1047
        }
1048
        $instance->save();
1049
 
1050
        return $instance;
1051
    }
1052
 
1053
    /**
1054
     * Unsets the context instance record.
1055
     *
1056
     * @param \tool_dataprivacy\context_instance $instance
1057
     * @return null
1058
     */
1059
    public static function unset_context_instance(context_instance $instance) {
1060
        $instance->delete();
1061
    }
1062
 
1063
    /**
1064
     * Sets the context level purpose and category.
1065
     *
1066
     * @throws \coding_exception
1067
     * @param \stdClass $record
1068
     * @return contextlevel
1069
     */
1070
    public static function set_contextlevel($record) {
1071
        global $DB;
1072
 
1073
        if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
1074
            throw new \coding_exception('Only context system and context user can set a contextlevel ' .
1075
                'purpose and retention');
1076
        }
1077
 
1078
        if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
1079
            // Update.
1080
            $contextlevel->from_record($record);
1081
        } else {
1082
            // Add.
1083
            $contextlevel = new contextlevel(0, $record);
1084
        }
1085
        $contextlevel->save();
1086
 
1087
        // We sync with their defaults as we removed these options from the defaults page.
1088
        $classname = \context_helper::get_class_for_level($record->contextlevel);
1089
        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
1090
        set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
1091
        set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
1092
 
1093
        return $contextlevel;
1094
    }
1095
 
1096
    /**
1097
     * Returns the effective category given a context instance.
1098
     *
1099
     * @param \context $context
1100
     * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
1101
     * @return category|false
1102
     */
1103
    public static function get_effective_context_category(\context $context, $forcedvalue = false) {
1104
        if (!data_registry::defaults_set()) {
1105
            return false;
1106
        }
1107
 
1108
        return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
1109
    }
1110
 
1111
    /**
1112
     * Returns the effective purpose given a context instance.
1113
     *
1114
     * @param \context $context
1115
     * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
1116
     * @return purpose|false
1117
     */
1118
    public static function get_effective_context_purpose(\context $context, $forcedvalue = false) {
1119
        if (!data_registry::defaults_set()) {
1120
            return false;
1121
        }
1122
 
1123
        return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
1124
    }
1125
 
1126
    /**
1127
     * Returns the effective category given a context level.
1128
     *
1129
     * @param int $contextlevel
1130
     * @return category|false
1131
     */
1132
    public static function get_effective_contextlevel_category($contextlevel) {
1133
        if (!data_registry::defaults_set()) {
1134
            return false;
1135
        }
1136
 
1137
        return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
1138
    }
1139
 
1140
    /**
1141
     * Returns the effective purpose given a context level.
1142
     *
1143
     * @param int $contextlevel
1144
     * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
1145
     * @return purpose|false
1146
     */
1147
    public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
1148
        if (!data_registry::defaults_set()) {
1149
            return false;
1150
        }
1151
 
1152
        return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
1153
    }
1154
 
1155
    /**
1156
     * Creates an expired context record for the provided context id.
1157
     *
1158
     * @param int $contextid
1159
     * @return \tool_dataprivacy\expired_context
1160
     */
1161
    public static function create_expired_context($contextid) {
1162
        $record = (object)[
1163
            'contextid' => $contextid,
1164
            'status' => expired_context::STATUS_EXPIRED,
1165
        ];
1166
        $expiredctx = new expired_context(0, $record);
1167
        $expiredctx->save();
1168
 
1169
        return $expiredctx;
1170
    }
1171
 
1172
    /**
1173
     * Deletes an expired context record.
1174
     *
1175
     * @param int $id The tool_dataprivacy_ctxexpire id.
1176
     * @return bool True on success.
1177
     */
1178
    public static function delete_expired_context($id) {
1179
        $expiredcontext = new expired_context($id);
1180
        return $expiredcontext->delete();
1181
    }
1182
 
1183
    /**
1184
     * Updates the status of an expired context.
1185
     *
1186
     * @param \tool_dataprivacy\expired_context $expiredctx
1187
     * @param int $status
1188
     * @return null
1189
     */
1190
    public static function set_expired_context_status(expired_context $expiredctx, $status) {
1191
        $expiredctx->set('status', $status);
1192
        $expiredctx->save();
1193
    }
1194
 
1195
    /**
1196
     * Finds all contextlists having at least one approved context, and returns them as in a contextlist_collection.
1197
     *
1198
     * @param   contextlist_collection  $collection The collection of unapproved contextlist objects.
1199
     * @param   \stdClass               $foruser The target user
1200
     * @param   int                     $type The purpose of the collection
1201
     * @return  contextlist_collection  The collection of approved_contextlist objects.
1202
     */
1203
    public static function get_approved_contextlist_collection_for_collection(contextlist_collection $collection,
1204
            \stdClass $foruser, int $type): contextlist_collection {
1205
 
1206
        // Create the approved contextlist collection object.
1207
        $approvedcollection = new contextlist_collection($collection->get_userid());
1208
        $isconfigured = data_registry::defaults_set();
1209
 
1210
        foreach ($collection as $contextlist) {
1211
            $contextids = [];
1212
            foreach ($contextlist as $context) {
1213
                if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $type) {
1214
                    // Data can only be deleted from it if the context is either expired, or unprotected.
1215
                    // Note: We can only check whether a context is expired or unprotected if the site is configured and
1216
                    // defaults are set appropriately. If they are not, we treat all contexts as though they are
1217
                    // unprotected.
1218
                    $purpose = static::get_effective_context_purpose($context);
1219
                    if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
1220
                        continue;
1221
                    }
1222
                }
1223
 
1224
                $contextids[] = $context->id;
1225
            }
1226
 
1227
            // The data for the last component contextlist won't have been written yet, so write it now.
1228
            if (!empty($contextids)) {
1229
                $approvedcollection->add_contextlist(
1230
                        new approved_contextlist($foruser, $contextlist->get_component(), $contextids)
1231
                    );
1232
            }
1233
        }
1234
 
1235
        return $approvedcollection;
1236
    }
1237
 
1238
    /**
1239
     * Updates the default category and purpose for a given context level (and optionally, a plugin).
1240
     *
1241
     * @param int $contextlevel The context level.
1242
     * @param int $categoryid The ID matching the category.
1243
     * @param int $purposeid The ID matching the purpose record.
1244
     * @param int $activity The name of the activity that we're making a defaults configuration for.
1245
     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
1246
     * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
1247
     */
1248
    public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
1249
        global $DB;
1250
 
1251
        // Get the class name associated with this context level.
1252
        $classname = context_helper::get_class_for_level($contextlevel);
1253
        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
1254
 
1255
        // Check the default category to be set.
1256
        if ($categoryid == context_instance::INHERIT) {
1257
            unset_config($categoryvar, 'tool_dataprivacy');
1258
 
1259
        } else {
1260
            // Make sure the given category ID exists first.
1261
            $categorypersistent = new category($categoryid);
1262
            $categorypersistent->read();
1263
 
1264
            // Then set the new default value.
1265
            set_config($categoryvar, $categoryid, 'tool_dataprivacy');
1266
        }
1267
 
1268
        // Check the default purpose to be set.
1269
        if ($purposeid == context_instance::INHERIT) {
1270
            // If the defaults is set to inherit, just unset the config value.
1271
            unset_config($purposevar, 'tool_dataprivacy');
1272
 
1273
        } else {
1274
            // Make sure the given purpose ID exists first.
1275
            $purposepersistent = new purpose($purposeid);
1276
            $purposepersistent->read();
1277
 
1278
            // Then set the new default value.
1279
            set_config($purposevar, $purposeid, 'tool_dataprivacy');
1280
        }
1281
 
1282
        // Unset instances that have been assigned with custom purpose and category, if override was specified.
1283
        if ($override) {
1284
            // We'd like to find context IDs that we want to unset.
1285
            $statements = ["SELECT c.id as contextid FROM {context} c"];
1286
            // Based on this context level.
1287
            $params = ['contextlevel' => $contextlevel];
1288
 
1289
            if ($contextlevel == CONTEXT_MODULE) {
1290
                // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
1291
                $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
1292
                // And that the module is listed on the modules table.
1293
                $statements[] = "JOIN {modules} m ON m.id = cm.module";
1294
 
1295
                if ($activity) {
1296
                    // If we're overriding for an activity module, make sure that the context instance matches that activity.
1297
                    $statements[] = "AND m.name = :modname";
1298
                    $params['modname'] = $activity;
1299
                }
1300
            }
1301
            // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
1302
            $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
1303
            // And that the context level of this instance matches the given context level.
1304
            $statements[] = "WHERE c.contextlevel = :contextlevel";
1305
 
1306
            // Build our SQL query by gluing the statements.
1307
            $sql = implode("\n", $statements);
1308
 
1309
            // Get the context records matching our query.
1310
            $contextids = $DB->get_fieldset_sql($sql, $params);
1311
 
1312
            // Delete the matching context instances.
1313
            foreach ($contextids as $contextid) {
1314
                if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
1315
                    self::unset_context_instance($instance);
1316
                }
1317
            }
1318
        }
1319
 
1320
        return true;
1321
    }
1322
 
1323
    /**
1324
     * Format the supplied date interval as a retention period.
1325
     *
1326
     * @param   \DateInterval   $interval
1327
     * @return  string
1328
     */
1329
    public static function format_retention_period(\DateInterval $interval): string {
1330
        // It is one or another.
1331
        if ($interval->y) {
1332
            $formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
1333
        } else if ($interval->m) {
1334
            $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
1335
        } else if ($interval->d) {
1336
            $formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
1337
        } else {
1338
            $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
1339
        }
1340
 
1341
        return $formattedtime;
1342
    }
1343
 
1344
    /**
1345
     * Whether automatic data request approval is turned on or not for the given request type.
1346
     *
1347
     * @param int $type The request type.
1348
     * @return bool
1349
     */
1350
    public static function is_automatic_request_approval_on(int $type): bool {
1351
        switch ($type) {
1352
            case self::DATAREQUEST_TYPE_EXPORT:
1353
                return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval'));
1354
            case self::DATAREQUEST_TYPE_DELETE:
1355
                return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval'));
1356
        }
1357
        return false;
1358
    }
1359
 
1360
    /**
1361
     * Creates an ad-hoc task for the data request.
1362
     *
1363
     * @param int $requestid The data request ID.
1364
     * @param int $userid Optional. The user ID to run the task as, if necessary.
1365
     */
1366
    public static function queue_data_request_task(int $requestid, int $userid = null): void {
1367
        $task = new process_data_request_task();
1368
        $task->set_custom_data(['requestid' => $requestid]);
1369
        if ($userid) {
1370
            $task->set_userid($userid);
1371
        }
1372
        manager::queue_adhoc_task($task, true);
1373
    }
1374
 
1375
    /**
1376
     * Adds the contexts from the contextlist_collection to the request with the status provided.
1377
     *
1378
     * @since Moodle 4.3
1379
     * @param contextlist_collection $clcollection a collection of contextlists for all components.
1380
     * @param int $requestid the id of the request.
1381
     * @param int $status the status to set the contexts to.
1382
     */
1383
    public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
1384
        global $DB;
1385
 
1386
        // Wrap the SQL queries in a transaction.
1387
        $transaction = $DB->start_delegated_transaction();
1388
 
1389
        foreach ($clcollection as $contextlist) {
1390
            // Convert the \core_privacy\local\request\contextlist into a dataprivacy_contextlist persistent and store it.
1391
            $clp = \tool_dataprivacy\dataprivacy_contextlist::from_contextlist($contextlist);
1392
            $clp->create();
1393
            $contextlistid = $clp->get('id');
1394
 
1395
            // Store the associated contexts in the contextlist.
1396
            foreach ($contextlist->get_contextids() as $contextid) {
1397
                mtrace('Pushing data for ' . \context::instance_by_id($contextid)->get_context_name());
1398
                $context = new contextlist_context();
1399
                $context->set('contextid', $contextid)
1400
                    ->set('contextlistid', $contextlistid)
1401
                    ->set('status', $status)
1402
                    ->create();
1403
            }
1404
 
1405
            // Create the relation to the request.
1406
            $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid);
1407
            $requestcontextlist->create();
1408
        }
1409
 
1410
        $transaction->allow_commit();
1411
    }
1412
 
1413
    /**
1414
     * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection.
1415
     *
1416
     * @since Moodle 4.3
1417
     * @param data_request $request the data request with which the contextlists are associated.
1418
     * @return contextlist_collection the collection of approved_contextlist objects.
1419
     * @throws coding_exception
1420
     * @throws dml_exception
1421
     * @throws moodle_exception
1422
     */
1423
    public static function get_approved_contextlist_collection_for_request(data_request $request): contextlist_collection {
1424
        global $DB;
1425
        $foruser = core_user::get_user($request->get('userid'));
1426
 
1427
        // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
1428
        $sql = "SELECT cl.component, ctx.contextid
1429
                  FROM {" . request_contextlist::TABLE . "} rcl
1430
                  JOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1431
                  JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
1432
                 WHERE rcl.requestid = ? AND ctx.status = ?
1433
              ORDER BY cl.component, ctx.contextid";
1434
 
1435
        // Create the approved contextlist collection object.
1436
        $lastcomponent = null;
1437
        $approvedcollection = new contextlist_collection($foruser->id);
1438
 
1439
        $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]);
1440
        $contexts = [];
1441
        foreach ($rs as $record) {
1442
            // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the
1443
            // last (the one we've just finished with) and reset the context array for the next one.
1444
            if ($lastcomponent != $record->component) {
1445
                if ($contexts) {
1446
                    $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
1447
                }
1448
                $contexts = [];
1449
            }
1450
            $contexts[] = $record->contextid;
1451
            $lastcomponent = $record->component;
1452
        }
1453
        $rs->close();
1454
 
1455
        // The data for the last component contextlist won't have been written yet, so write it now.
1456
        if ($contexts) {
1457
            $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
1458
        }
1459
 
1460
        return $approvedcollection;
1461
    }
1462
 
1463
    /**
1464
     * Sets the status of all contexts associated with the request.
1465
     *
1466
     * @since Moodle 4.3
1467
     * @param int $requestid the requestid to which the contexts belong.
1468
     * @param int $status the status to set to.
1469
     * @throws \dml_exception if the requestid is invalid.
1470
     * @throws \coding_exception if the status is invalid.
1471
     */
1472
    public static function update_request_contexts_with_status(int $requestid, int $status) {
1473
        // Validate contextlist_context status using the persistent's attribute validation.
1474
        $contextlistcontext = new contextlist_context();
1475
        $contextlistcontext->set('status', $status);
1476
        if (array_key_exists('status', $contextlistcontext->get_errors())) {
1477
            throw new coding_exception("Invalid contextlist_context status: $status");
1478
        }
1479
 
1480
        global $DB;
1481
        $select = "SELECT ctx.id as id
1482
                     FROM {" . request_contextlist::TABLE . "} rcl
1483
                     JOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1484
                     JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
1485
                    WHERE rcl.requestid = ?";
1486
 
1487
        // Fetch records IDs to be updated and update by chunks, if applicable (limit of 1000 records per update).
1488
        $limit = 1000;
1489
        $idstoupdate = $DB->get_fieldset_sql($select, [$requestid]);
1490
        $count = count($idstoupdate);
1491
        $idchunks = $idstoupdate;
1492
        if ($count > $limit) {
1493
            $idchunks = array_chunk($idstoupdate, $limit);
1494
        } else {
1495
            $idchunks  = [$idchunks];
1496
        }
1497
        $transaction = $DB->start_delegated_transaction();
1498
        $initialparams = [$status];
1499
        foreach ($idchunks as $chunk) {
1500
            list($insql, $inparams) = $DB->get_in_or_equal($chunk);
1501
            $update = "UPDATE {" . contextlist_context::TABLE . "}
1502
                          SET status = ?
1503
                        WHERE id $insql";
1504
            $params = array_merge($initialparams, $inparams);
1505
            $DB->execute($update, $params);
1506
        }
1507
        $transaction->allow_commit();
1508
    }
1509
 
1510
    /**
1511
     * Only approve the contexts which are children of the provided course contexts.
1512
     *
1513
     * @since Moodle 4.3
1514
     * @param int $requestid Request identifier
1515
     * @param array $coursecontextids List of course context identifier.
1516
     * @throws \dml_transaction_exception
1517
     * @throws coding_exception
1518
     * @throws dml_exception
1519
     */
1520
    public static function approve_contexts_belonging_to_request(int $requestid, array $coursecontextids = []) {
1521
        global $DB;
1522
        $select = "SELECT clc.id as id, ctx.id as contextid, ctx.path, ctx.contextlevel
1523
                     FROM {" . request_contextlist::TABLE . "} rcl
1524
                     JOIN {" . dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1525
                     JOIN {" . contextlist_context::TABLE . "} clc ON cl.id = clc.contextlistid
1526
                     JOIN {context} ctx ON clc.contextid = ctx.id
1527
                    WHERE rcl.requestid = ?";
1528
        $items = $DB->get_records_sql($select, [$requestid]);
1529
        $acceptcourses = [];
1530
        $listidstoapprove = [];
1531
        $listidstoreject = [];
1532
        foreach ($items as $item) {
1533
            if (in_array($item->contextid, $coursecontextids) && ($item->contextlevel == CONTEXT_COURSE)
1534
                && !in_array($item->contextid, $acceptcourses)) {
1535
                $acceptcourses[$item->contextid] = $item;
1536
            }
1537
        }
1538
 
1539
        foreach ($items as $item) {
1540
            if ($item->contextlevel >= CONTEXT_COURSE) {
1541
                $approve = false;
1542
                foreach ($acceptcourses as $acceptcourse) {
1543
                    if (strpos($item->path, $acceptcourse->path) === 0) {
1544
                        $approve = true;
1545
                        break;
1546
                    }
1547
                }
1548
                if ($approve) {
1549
                    $listidstoapprove[] = $item->id;
1550
                } else {
1551
                    $listidstoreject[] = $item->id;
1552
                }
1553
            } else {
1554
                $listidstoapprove[] = $item->id;
1555
            }
1556
        }
1557
 
1558
        $limit = 1000;
1559
        $count = count($listidstoapprove);
1560
        if ($count > $limit) {
1561
            $listidstoapprove = array_chunk($listidstoapprove, $limit);
1562
        } else {
1563
            $listidstoapprove = [$listidstoapprove];
1564
        }
1565
        $count = count($listidstoreject);
1566
        if ($count > $limit) {
1567
            $listidstoreject = array_chunk($listidstoreject, $limit);
1568
        } else {
1569
            $listidstoreject = [$listidstoreject];
1570
        }
1571
        $transaction = $DB->start_delegated_transaction();
1572
 
1573
        $initialparams = [contextlist_context::STATUS_APPROVED];
1574
        foreach ($listidstoapprove as $chunk) {
1575
            if (!empty($chunk)) {
1576
                list($insql, $inparams) = $DB->get_in_or_equal($chunk);
1577
                $update = "UPDATE {" . contextlist_context::TABLE . "}
1578
                              SET status = ?
1579
                            WHERE id $insql";
1580
                $params = array_merge($initialparams, $inparams);
1581
                $DB->execute($update, $params);
1582
            }
1583
        }
1584
 
1585
        $initialparams = [contextlist_context::STATUS_REJECTED];
1586
        foreach ($listidstoreject as $chunk) {
1587
            if (!empty($chunk)) {
1588
                list($insql, $inparams) = $DB->get_in_or_equal($chunk);
1589
                $update = "UPDATE {" . contextlist_context::TABLE . "}
1590
                              SET status = ?
1591
                            WHERE id $insql";
1592
 
1593
                $params = array_merge($initialparams, $inparams);
1594
                $DB->execute($update, $params);
1595
            }
1596
        }
1597
 
1598
        $transaction->allow_commit();
1599
    }
1600
 
1601
    /**
1602
     * Get list of course context for user to filter.
1603
     *
1604
     * @since Moodle 4.3
1605
     * @param int $requestid Request identifier.
1606
     * @return array
1607
     * @throws dml_exception
1608
     * @throws coding_exception
1609
     */
1610
    public static function get_course_contexts_for_view_filter(int $requestid): array {
1611
        global $DB;
1612
 
1613
        $contexts = [];
1614
 
1615
        $query = "SELECT DISTINCT c.id as ctxid, c.contextlevel as ctxlevel, c.instanceid as ctxinstance, c.path as ctxpath,
1616
                        c.depth as ctxdepth, c.locked as ctxlocked
1617
                    FROM {" . \tool_dataprivacy\request_contextlist::TABLE . "} rcl
1618
                    JOIN {" . \tool_dataprivacy\dataprivacy_contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
1619
                    JOIN {" . \tool_dataprivacy\contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
1620
                    JOIN {context} c ON c.id = ctx.contextid
1621
                   WHERE rcl.requestid = ? AND c.contextlevel = ?
1622
                ORDER BY c.path ASC";
1623
 
1624
        $result = $DB->get_records_sql($query, [$requestid, CONTEXT_COURSE]);
1625
        foreach ($result as $item) {
1626
            $ctxid = $item->ctxid;
1627
            context_helper::preload_from_record($item);
1628
            $contexts[$ctxid] = \context::instance_by_id($ctxid);
1629
        }
1630
 
1631
        return $contexts;
1632
    }
1633
 
1634
    /**
1635
     * Validates a data request creation.
1636
     *
1637
     * @param stdClass $data the data request information, including userid and type
1638
     * @return array array of errors, empty if everything went ok
1639
     */
1640
    public static function validate_create_data_request(stdClass $data): array {
1641
        global $USER;
1642
 
1643
        $errors = [];
1644
        $validrequesttypes = [
1645
            self::DATAREQUEST_TYPE_EXPORT,
1646
            self::DATAREQUEST_TYPE_DELETE,
1647
        ];
1648
        if (!in_array($data->type, $validrequesttypes)) {
1649
            $errors['errorinvalidrequesttype'] = get_string('errorinvalidrequesttype', 'tool_dataprivacy');
1650
        }
1651
 
1652
        $userid = $data->userid;
1653
 
1654
        if (self::has_ongoing_request($userid, $data->type)) {
1655
            $errors['errorrequestalreadyexists'] = get_string('errorrequestalreadyexists', 'tool_dataprivacy');
1656
        }
1657
 
1658
        // Check if current user can create data requests.
1659
        if ($data->type == self::DATAREQUEST_TYPE_DELETE) {
1660
            if ($userid == $USER->id) {
1661
                if (!self::can_create_data_deletion_request_for_self()) {
1662
                    $errors['errorcannotrequestdeleteforself'] = get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy');
1663
                }
1664
            } else if (!self::can_create_data_deletion_request_for_other()
1665
                && !self::can_create_data_deletion_request_for_children($userid)) {
1666
                $errors['errorcannotrequestdeleteforother'] = get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy');
1667
            }
1668
        } else if ($data->type == self::DATAREQUEST_TYPE_EXPORT) {
1669
            if ($userid == $USER->id && !self::can_create_data_download_request_for_self()) {
1670
                $errors['errorcannotrequestexportforself'] = get_string('errorcannotrequestexportforself', 'tool_dataprivacy');
1671
            }
1672
        }
1673
 
1674
        return $errors;
1675
    }
1676
}