Merge branch 'MDL-62277-master' of git://github.com/bmbrands/moodle
[moodle.git] / admin / tool / dataprivacy / classes / api.php
CommitLineData
5efc1f9e
DM
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_system;
28use core\invalid_persistent_exception;
29use core\message\message;
30use core\task\manager;
cb775057
JP
31use core_privacy\local\request\approved_contextlist;
32use core_privacy\local\request\contextlist_collection;
5efc1f9e
DM
33use core_user;
34use dml_exception;
35use moodle_exception;
36use moodle_url;
37use required_capability_exception;
38use stdClass;
ba5b59c0 39use tool_dataprivacy\external\data_request_exporter;
5efc1f9e
DM
40use tool_dataprivacy\task\initiate_data_request_task;
41use tool_dataprivacy\task\process_data_request_task;
5efc1f9e
DM
42
43defined('MOODLE_INTERNAL') || die();
44
45/**
46 * Class containing helper methods for processing data requests.
47 *
48 * @copyright 2018 Jun Pataleta
49 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
50 */
51class api {
52
53 /** Data export request type. */
54 const DATAREQUEST_TYPE_EXPORT = 1;
55
56 /** Data deletion request type. */
57 const DATAREQUEST_TYPE_DELETE = 2;
58
59 /** Other request type. Usually of enquiries to the DPO. */
60 const DATAREQUEST_TYPE_OTHERS = 3;
61
62 /** Newly submitted and we haven't yet started finding out where they have data. */
63 const DATAREQUEST_STATUS_PENDING = 0;
64
65 /** Newly submitted and we have started to find the location of data. */
66 const DATAREQUEST_STATUS_PREPROCESSING = 1;
67
68 /** Metadata ready and awaiting review and approval by the Data Protection officer. */
69 const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2;
70
71 /** Request approved and will be processed soon. */
72 const DATAREQUEST_STATUS_APPROVED = 3;
73
74 /** The request is now being processed. */
75 const DATAREQUEST_STATUS_PROCESSING = 4;
76
77 /** Data request completed. */
78 const DATAREQUEST_STATUS_COMPLETE = 5;
79
80 /** Data request cancelled by the user. */
81 const DATAREQUEST_STATUS_CANCELLED = 6;
82
83 /** Data request rejected by the DPO. */
84 const DATAREQUEST_STATUS_REJECTED = 7;
85
86 /**
87 * Determines whether the user can contact the site's Data Protection Officer via Moodle.
88 *
89 * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled.
90 * @throws dml_exception
91 */
92 public static function can_contact_dpo() {
93 return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1;
94 }
95
96 /**
97 * Check's whether the current user has the capability to manage data requests.
98 *
99 * @param int $userid The user ID.
100 * @return bool
101 * @throws coding_exception
102 * @throws dml_exception
103 */
104 public static function can_manage_data_requests($userid) {
105 $context = context_system::instance();
106
107 // A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
108 return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
109 }
110
111 /**
112 * Checks if the current user can manage the data registry at the provided id.
113 *
114 * @param int $contextid Fallback to system context id.
115 * @throws \required_capability_exception
116 * @return null
117 */
118 public static function check_can_manage_data_registry($contextid = false) {
119 if ($contextid) {
120 $context = \context_helper::instance_by_id($contextid);
121 } else {
122 $context = \context_system::instance();
123 }
124
125 require_capability('tool/dataprivacy:managedataregistry', $context);
126 }
127
128 /**
129 * Fetches the list of users with the Data Protection Officer role.
130 *
131 * @throws dml_exception
132 */
133 public static function get_site_dpos() {
134 // Get role(s) that can manage data requests.
135 $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
136
137 $dpos = [];
138 $context = context_system::instance();
139 foreach ($dporoles as $roleid) {
140 if (empty($roleid)) {
141 continue;
142 }
05564a4c
DM
143 $allnames = get_all_user_name_fields(true, 'u');
144 $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
145 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
146 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
147 'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' .
148 'r.name AS rolename, r.sortorder, '.
149 'r.shortname AS roleshortname, rn.name AS rolecoursealias';
5efc1f9e 150 // Fetch users that can manage data requests.
05564a4c 151 $dpos += get_role_users($roleid, $context, false, $fields);
5efc1f9e
DM
152 }
153
154 // If the site has no data protection officer, defer to site admin(s).
155 if (empty($dpos)) {
156 $dpos = get_admins();
157 }
158 return $dpos;
159 }
160
161 /**
162 * Checks whether a given user is a site DPO.
163 *
164 * @param int $userid The user ID.
165 * @return bool
166 * @throws dml_exception
167 */
168 public static function is_site_dpo($userid) {
169 $dpos = self::get_site_dpos();
170 return array_key_exists($userid, $dpos);
171 }
172
173 /**
174 * Lodges a data request and sends the request details to the site Data Protection Officer(s).
175 *
176 * @param int $foruser The user whom the request is being made for.
177 * @param int $type The request type.
178 * @param string $comments Request comments.
179 * @return data_request
180 * @throws invalid_persistent_exception
181 * @throws coding_exception
182 */
183 public static function create_data_request($foruser, $type, $comments = '') {
184 global $USER;
185
186 $datarequest = new data_request();
187 // The user the request is being made for.
188 $datarequest->set('userid', $foruser);
189 // The user making the request.
190 $datarequest->set('requestedby', $USER->id);
191 // Set status.
192 $datarequest->set('status', self::DATAREQUEST_STATUS_PENDING);
193 // Set request type.
194 $datarequest->set('type', $type);
195 // Set request comments.
196 $datarequest->set('comments', $comments);
197
198 // Store subject access request.
199 $datarequest->create();
200
201 // Fire an ad hoc task to initiate the data request process.
202 $task = new initiate_data_request_task();
203 $task->set_custom_data(['requestid' => $datarequest->get('id')]);
204 manager::queue_adhoc_task($task, true);
205
206 return $datarequest;
207 }
208
209 /**
210 * Fetches the list of the data requests.
211 *
212 * If user ID is provided, it fetches the data requests for the user.
213 * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests.
214 * (e.g. Users with the Data Protection Officer roles)
215 *
216 * @param int $userid The User ID.
217 * @return data_request[]
218 * @throws dml_exception
219 */
220 public static function get_data_requests($userid = 0) {
221 global $USER;
222 $results = [];
38ffa48c 223 $sort = 'status ASC, timemodified ASC';
5efc1f9e
DM
224 if ($userid) {
225 // Get the data requests for the user or data requests made by the user.
226 $select = "userid = :userid OR requestedby = :requestedby";
227 $params = [
228 'userid' => $userid,
229 'requestedby' => $userid
230 ];
38ffa48c 231 $results = data_request::get_records_select($select, $params, $sort);
5efc1f9e
DM
232 } else {
233 // If the current user is one of the site's Data Protection Officers, then fetch all data requests.
234 if (self::is_site_dpo($USER->id)) {
38ffa48c 235 $results = data_request::get_records(null, $sort, '');
5efc1f9e
DM
236 }
237 }
238
239 return $results;
240 }
241
242 /**
243 * Checks whether there is already an existing pending/in-progress data request for a user for a given request type.
244 *
245 * @param int $userid The user ID.
246 * @param int $type The request type.
247 * @return bool
248 * @throws coding_exception
249 * @throws dml_exception
250 */
251 public static function has_ongoing_request($userid, $type) {
252 global $DB;
253
254 // Check if the user already has an incomplete data request of the same type.
255 $nonpendingstatuses = [
256 self::DATAREQUEST_STATUS_COMPLETE,
257 self::DATAREQUEST_STATUS_CANCELLED,
258 self::DATAREQUEST_STATUS_REJECTED,
259 ];
260 list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED);
261 $select = 'type = :type AND userid = :userid AND status NOT ' . $insql;
262 $params = array_merge([
263 'type' => $type,
264 'userid' => $userid
265 ], $inparams);
266
267 return data_request::record_exists_select($select, $params);
268 }
269
270 /**
271 * Determines whether a request is active or not based on its status.
272 *
273 * @param int $status The request status.
274 * @return bool
275 */
276 public static function is_active($status) {
277 // List of statuses which doesn't require any further processing.
278 $finalstatuses = [
279 self::DATAREQUEST_STATUS_COMPLETE,
280 self::DATAREQUEST_STATUS_CANCELLED,
281 self::DATAREQUEST_STATUS_REJECTED,
282 ];
283
284 return !in_array($status, $finalstatuses);
285 }
286
287 /**
288 * Cancels the data request for a given request ID.
289 *
290 * @param int $requestid The request identifier.
291 * @param int $status The request status.
292 * @param int $dpoid The user ID of the Data Protection Officer
293 * @return bool
294 * @throws invalid_persistent_exception
295 * @throws coding_exception
296 */
297 public static function update_request_status($requestid, $status, $dpoid = 0) {
298 // Update the request.
299 $datarequest = new data_request($requestid);
300 $datarequest->set('status', $status);
301 if ($dpoid) {
302 $datarequest->set('dpo', $dpoid);
303 }
304 return $datarequest->update();
305 }
306
307 /**
308 * Fetches a request based on the request ID.
309 *
310 * @param int $requestid The request identifier
311 * @return data_request
312 */
313 public static function get_request($requestid) {
314 return new data_request($requestid);
315 }
316
317 /**
318 * Approves a data request based on the request ID.
319 *
320 * @param int $requestid The request identifier
321 * @return bool
322 * @throws coding_exception
323 * @throws dml_exception
324 * @throws invalid_persistent_exception
325 * @throws required_capability_exception
326 * @throws moodle_exception
327 */
328 public static function approve_data_request($requestid) {
329 global $USER;
330
331 // Check first whether the user can manage data requests.
332 if (!self::can_manage_data_requests($USER->id)) {
333 $context = context_system::instance();
334 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
335 }
336
337 // Check if request is already awaiting for approval.
338 $request = new data_request($requestid);
339 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
340 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
341 }
342
343 // Update the status and the DPO.
344 $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);
345
cb775057
JP
346 // Approve all the contexts attached to the request.
347 // Currently, approving the request implicitly approves all associated contexts, but this may change in future, allowing
348 // users to selectively approve certain contexts only.
349 self::update_request_contexts_with_status($requestid, contextlist_context::STATUS_APPROVED);
350
5efc1f9e
DM
351 // Fire an ad hoc task to initiate the data request process.
352 $task = new process_data_request_task();
353 $task->set_custom_data(['requestid' => $requestid]);
354 if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) {
355 $task->set_userid($request->get('userid'));
356 }
357 manager::queue_adhoc_task($task, true);
358
359 return $result;
360 }
361
362 /**
363 * Rejects a data request based on the request ID.
364 *
365 * @param int $requestid The request identifier
366 * @return bool
367 * @throws coding_exception
368 * @throws dml_exception
369 * @throws invalid_persistent_exception
370 * @throws required_capability_exception
371 * @throws moodle_exception
372 */
373 public static function deny_data_request($requestid) {
374 global $USER;
375
376 if (!self::can_manage_data_requests($USER->id)) {
377 $context = context_system::instance();
378 throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', '');
379 }
380
381 // Check if request is already awaiting for approval.
382 $request = new data_request($requestid);
383 if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
384 throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
385 }
386
387 // Update the status and the DPO.
388 return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
389 }
390
391 /**
392 * Sends a message to the site's Data Protection Officer about a request.
393 *
394 * @param stdClass $dpo The DPO user record
395 * @param data_request $request The data request
396 * @return int|false
397 * @throws coding_exception
398 * @throws dml_exception
399 * @throws moodle_exception
400 */
401 public static function notify_dpo($dpo, data_request $request) {
402 global $PAGE, $SITE;
403
ba5b59c0
JP
404 $output = $PAGE->get_renderer('tool_dataprivacy');
405
406 $usercontext = \context_user::instance($request->get('requestedby'));
407 $requestexporter = new data_request_exporter($request, ['context' => $usercontext]);
408 $requestdata = $requestexporter->export($output);
409
5efc1f9e
DM
410 // Create message to send to the Data Protection Officer(s).
411 $typetext = null;
ba5b59c0 412 $typetext = $requestdata->typename;
5efc1f9e
DM
413 $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext);
414
ba5b59c0 415 $requestedby = $requestdata->requestedbyuser;
5efc1f9e
DM
416 $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
417 $message = new message();
418 $message->courseid = $SITE->id;
419 $message->component = 'tool_dataprivacy';
420 $message->name = 'contactdataprotectionofficer';
421 $message->userfrom = $requestedby;
422 $message->replyto = $requestedby->email;
ba5b59c0 423 $message->replytoname = $requestedby->fullname;
5efc1f9e
DM
424 $message->subject = $subject;
425 $message->fullmessageformat = FORMAT_HTML;
426 $message->notification = 1;
427 $message->contexturl = $datarequestsurl;
428 $message->contexturlname = get_string('datarequests', 'tool_dataprivacy');
429
430 // Prepare the context data for the email message body.
431 $messagetextdata = [
ba5b59c0 432 'requestedby' => $requestedby->fullname,
5efc1f9e 433 'requesttype' => $typetext,
ba5b59c0
JP
434 'requestdate' => userdate($requestdata->timecreated),
435 'requestcomments' => $requestdata->messagehtml,
5efc1f9e
DM
436 'datarequestsurl' => $datarequestsurl
437 ];
ba5b59c0 438 $requestingfor = $requestdata->foruser;
5efc1f9e
DM
439 if ($requestedby->id == $requestingfor->id) {
440 $messagetextdata['requestfor'] = $messagetextdata['requestedby'];
441 } else {
ba5b59c0 442 $messagetextdata['requestfor'] = $requestingfor->fullname;
5efc1f9e
DM
443 }
444
5efc1f9e
DM
445 // Email the data request to the Data Protection Officer(s)/Admin(s).
446 $messagetextdata['dponame'] = fullname($dpo);
447 // Render message email body.
448 $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata);
449 $message->userto = $dpo;
450 $message->fullmessage = html_to_text($messagehtml);
451 $message->fullmessagehtml = $messagehtml;
452
453 // Send message.
454 return message_send($message);
455 }
456
457 /**
458 * Creates a new data purpose.
459 *
460 * @param stdClass $record
461 * @return \tool_dataprivacy\purpose.
462 */
463 public static function create_purpose(stdClass $record) {
464 self::check_can_manage_data_registry();
465
466 $purpose = new purpose(0, $record);
467 $purpose->create();
468
469 return $purpose;
470 }
471
472 /**
473 * Updates an existing data purpose.
474 *
475 * @param stdClass $record
476 * @return \tool_dataprivacy\purpose.
477 */
478 public static function update_purpose(stdClass $record) {
479 self::check_can_manage_data_registry();
480
481 $purpose = new purpose($record->id);
482 $purpose->from_record($record);
483
484 $result = $purpose->update();
485
486 return $purpose;
487 }
488
489 /**
490 * Deletes a data purpose.
491 *
492 * @param int $id
493 * @return bool
494 */
495 public static function delete_purpose($id) {
496 self::check_can_manage_data_registry();
497
498 $purpose = new purpose($id);
499 if ($purpose->is_used()) {
500 throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.');
501 }
502 return $purpose->delete();
503 }
504
505 /**
506 * Get all system data purposes.
507 *
508 * @return \tool_dataprivacy\purpose[]
509 */
510 public static function get_purposes() {
511 self::check_can_manage_data_registry();
512
513 return purpose::get_records([], 'name', 'ASC');
514 }
515
516 /**
517 * Creates a new data category.
518 *
519 * @param stdClass $record
520 * @return \tool_dataprivacy\category.
521 */
522 public static function create_category(stdClass $record) {
523 self::check_can_manage_data_registry();
524
525 $category = new category(0, $record);
526 $category->create();
527
528 return $category;
529 }
530
531 /**
532 * Updates an existing data category.
533 *
534 * @param stdClass $record
535 * @return \tool_dataprivacy\category.
536 */
537 public static function update_category(stdClass $record) {
538 self::check_can_manage_data_registry();
539
540 $category = new category($record->id);
541 $category->from_record($record);
542
543 $result = $category->update();
544
545 return $category;
546 }
547
548 /**
549 * Deletes a data category.
550 *
551 * @param int $id
552 * @return bool
553 */
554 public static function delete_category($id) {
555 self::check_can_manage_data_registry();
556
557 $category = new category($id);
558 if ($category->is_used()) {
559 throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.');
560 }
561 return $category->delete();
562 }
563
564 /**
565 * Get all system data categories.
566 *
567 * @return \tool_dataprivacy\category[]
568 */
569 public static function get_categories() {
570 self::check_can_manage_data_registry();
571
572 return category::get_records([], 'name', 'ASC');
573 }
574
575 /**
576 * Sets the context instance purpose and category.
577 *
578 * @param \stdClass $record
579 * @return \tool_dataprivacy\context_instance
580 */
581 public static function set_context_instance($record) {
582 self::check_can_manage_data_registry($record->contextid);
583
584 if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) {
585 // Update.
586 $instance->from_record($record);
587
588 if (empty($record->purposeid) && empty($record->categoryid)) {
589 // We accept one of them to be null but we delete it if both are null.
590 self::unset_context_instance($instance);
591 return;
592 }
593
594 } else {
595 // Add.
596 $instance = new context_instance(0, $record);
597 }
598 $instance->save();
599
600 return $instance;
601 }
602
603 /**
604 * Unsets the context instance record.
605 *
606 * @param \tool_dataprivacy\context_instance $instance
607 * @return null
608 */
609 public static function unset_context_instance(context_instance $instance) {
610 self::check_can_manage_data_registry($instance->get('contextid'));
611 $instance->delete();
612 }
613
614 /**
615 * Sets the context level purpose and category.
616 *
617 * @throws \coding_exception
618 * @param \stdClass $record
619 * @return contextlevel
620 */
621 public static function set_contextlevel($record) {
622 global $DB;
623
624 // Only manager at system level can set this.
625 self::check_can_manage_data_registry();
626
627 if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) {
628 throw new \coding_exception('Only context system and context user can set a contextlevel ' .
629 'purpose and retention');
630 }
631
632 if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) {
633 // Update.
634 $contextlevel->from_record($record);
635 } else {
636 // Add.
637 $contextlevel = new contextlevel(0, $record);
638 }
639 $contextlevel->save();
640
641 // We sync with their defaults as we removed these options from the defaults page.
642 $classname = \context_helper::get_class_for_level($record->contextlevel);
643 list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname);
644 set_config($purposevar, $record->purposeid, 'tool_dataprivacy');
645 set_config($categoryvar, $record->categoryid, 'tool_dataprivacy');
646
647 return $contextlevel;
648 }
649
650 /**
651 * Returns the effective category given a context instance.
652 *
653 * @param \context $context
654 * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
655 * @return category|false
656 */
657 public static function get_effective_context_category(\context $context, $forcedvalue=false) {
658 self::check_can_manage_data_registry($context->id);
659 if (!data_registry::defaults_set()) {
660 return false;
661 }
662
663 return data_registry::get_effective_context_value($context, 'category', $forcedvalue);
664 }
665
666 /**
667 * Returns the effective purpose given a context instance.
668 *
669 * @param \context $context
670 * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose.
671 * @return purpose|false
672 */
673 public static function get_effective_context_purpose(\context $context, $forcedvalue=false) {
674 self::check_can_manage_data_registry($context->id);
675 if (!data_registry::defaults_set()) {
676 return false;
677 }
678
679 return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue);
680 }
681
682 /**
683 * Returns the effective category given a context level.
684 *
685 * @param int $contextlevel
686 * @param int $forcedvalue Use this categoryid value as if this was this context level category.
687 * @return category|false
688 */
689 public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
690 self::check_can_manage_data_registry(\context_system::instance()->id);
691 if (!data_registry::defaults_set()) {
692 return false;
693 }
694
695 return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
696 }
697
698 /**
699 * Returns the effective purpose given a context level.
700 *
701 * @param int $contextlevel
702 * @param int $forcedvalue Use this purposeid value as if this was this context level purpose.
703 * @return purpose|false
704 */
705 public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) {
706 self::check_can_manage_data_registry(\context_system::instance()->id);
707 if (!data_registry::defaults_set()) {
708 return false;
709 }
710
711 return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue);
712 }
713
714 /**
715 * Creates an expired context record for the provided context id.
716 *
717 * @param int $contextid
718 * @return \tool_dataprivacy\expired_context
719 */
720 public static function create_expired_context($contextid) {
721 self::check_can_manage_data_registry();
722
723 $record = (object)[
724 'contextid' => $contextid,
725 'status' => expired_context::STATUS_EXPIRED,
726 ];
727 $expiredctx = new expired_context(0, $record);
728 $expiredctx->save();
729
730 return $expiredctx;
731 }
732
733 /**
734 * Deletes an expired context record.
735 *
736 * @param int $id The tool_dataprivacy_ctxexpire id.
737 * @return bool True on success.
738 */
739 public static function delete_expired_context($id) {
740 self::check_can_manage_data_registry();
741
742 $expiredcontext = new expired_context($id);
743 return $expiredcontext->delete();
744 }
745
746 /**
747 * Updates the status of an expired context.
748 *
749 * @param \tool_dataprivacy\expired_context $expiredctx
750 * @param int $status
751 * @return null
752 */
753 public static function set_expired_context_status(expired_context $expiredctx, $status) {
754 self::check_can_manage_data_registry();
755
756 $expiredctx->set('status', $status);
757 $expiredctx->save();
758 }
cb775057
JP
759
760 /**
761 * Adds the contexts from the contextlist_collection to the request with the status provided.
762 *
763 * @param contextlist_collection $clcollection a collection of contextlists for all components.
764 * @param int $requestid the id of the request.
765 * @param int $status the status to set the contexts to.
766 */
767 public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
00293f90 768 $request = new data_request($requestid);
cb775057
JP
769 foreach ($clcollection as $contextlist) {
770 // Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
771 $clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
772 $clp->create();
773 $contextlistid = $clp->get('id');
774
775 // Store the associated contexts in the contextlist.
776 foreach ($contextlist->get_contextids() as $contextid) {
00293f90
AN
777 if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
778 $context = \context::instance_by_id($contextid);
779 if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
780 continue;
781 }
782 }
cb775057
JP
783 $context = new contextlist_context();
784 $context->set('contextid', $contextid)
785 ->set('contextlistid', $contextlistid)
786 ->set('status', $status)
787 ->create();
788 }
789
790 // Create the relation to the request.
791 $requestcontextlist = request_contextlist::create_relation($requestid, $contextlistid);
792 $requestcontextlist->create();
793 }
794 }
795
796 /**
797 * Sets the status of all contexts associated with the request.
798 *
799 * @param int $requestid the requestid to which the contexts belong.
800 * @param int $status the status to set to.
801 * @throws \dml_exception if the requestid is invalid.
802 * @throws \moodle_exception if the status is invalid.
803 */
804 public static function update_request_contexts_with_status(int $requestid, int $status) {
805 // Validate contextlist_context status using the persistent's attribute validation.
806 $contextlistcontext = new contextlist_context();
807 $contextlistcontext->set('status', $status);
808 if (array_key_exists('status', $contextlistcontext->get_errors())) {
809 throw new moodle_exception("Invalid contextlist_context status: $status");
810 }
811
812 // Validate requestid using the persistent's record validation.
813 // A dml_exception is thrown if the record is missing.
814 $datarequest = new data_request($requestid);
815
816 // Bulk update the status of the request contexts.
817 global $DB;
818
819 $select = "SELECT ctx.id as id
820 FROM {" . request_contextlist::TABLE . "} rcl
821 JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
822 JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
823 WHERE rcl.requestid = ?";
824
6f094d49
JP
825 // Fetch records IDs to be updated and update by chunks, if applicable (limit of 1000 records per update).
826 $limit = 1000;
827 $idstoupdate = $DB->get_fieldset_sql($select, [$requestid]);
828 $count = count($idstoupdate);
829 $idchunks = $idstoupdate;
830 if ($count > $limit) {
831 $idchunks = array_chunk($idstoupdate, $limit);
832 }
833 $transaction = $DB->start_delegated_transaction();
834 $initialparams = [$status];
835 foreach ($idchunks as $chunk) {
836 list($insql, $inparams) = $DB->get_in_or_equal($chunk);
837 $update = "UPDATE {" . contextlist_context::TABLE . "}
838 SET status = ?
839 WHERE id $insql";
840 $params = array_merge($initialparams, $inparams);
841 $DB->execute($update, $params);
842 }
843 $transaction->allow_commit();
cb775057
JP
844 }
845
846 /**
847 * Finds all request contextlists having at least on approved context, and returns them as in a contextlist_collection.
848 *
849 * @param data_request $request the data request with which the contextlists are associated.
850 * @return contextlist_collection the collection of approved_contextlist objects.
851 */
852 public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
853 $foruser = core_user::get_user($request->get('userid'));
854
855 // Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
856 global $DB;
857 $sql = "SELECT cl.component, ctx.contextid
858 FROM {" . request_contextlist::TABLE . "} rcl
859 JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
860 JOIN {" . contextlist_context::TABLE . "} ctx ON cl.id = ctx.contextlistid
861 WHERE rcl.requestid = ?
862 AND ctx.status = ?
863 ORDER BY cl.component, ctx.contextid";
864
865 // Create the approved contextlist collection object.
866 $lastcomponent = null;
867 $approvedcollection = new contextlist_collection($foruser->id);
868
869 $rs = $DB->get_recordset_sql($sql, [$request->get('id'), contextlist_context::STATUS_APPROVED]);
870 foreach ($rs as $record) {
871 // If we encounter a new component, and we've built up contexts for the last, then add the approved_contextlist for the
872 // last (the one we've just finished with) and reset the context array for the next one.
873 if ($lastcomponent != $record->component) {
874 if (!empty($contexts)) {
875 $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
876 }
877 $contexts = [];
878 }
00293f90 879
cb775057
JP
880 $contexts[] = $record->contextid;
881 $lastcomponent = $record->component;
882 }
883 $rs->close();
884
885 // The data for the last component contextlist won't have been written yet, so write it now.
886 if (!empty($contexts)) {
887 $approvedcollection->add_contextlist(new approved_contextlist($foruser, $lastcomponent, $contexts));
888 }
889
890 return $approvedcollection;
891 }
5efc1f9e 892}