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 * Class containing the external API functions functions for the Data Privacy tool.
18 *
19 * @package    tool_dataprivacy
20 * @copyright  2018 Jun Pataleta
21 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23namespace tool_dataprivacy;
24defined('MOODLE_INTERNAL') || die();
25
26require_once("$CFG->libdir/externallib.php");
27require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
28
29use coding_exception;
30use context_helper;
31use context_system;
32use context_user;
33use core\invalid_persistent_exception;
34use core\notification;
35use core_user;
36use dml_exception;
37use external_api;
38use external_description;
39use external_function_parameters;
40use external_multiple_structure;
41use external_single_structure;
42use external_value;
43use external_warnings;
44use invalid_parameter_exception;
45use moodle_exception;
46use required_capability_exception;
47use restricted_context_exception;
48use tool_dataprivacy\external\category_exporter;
49use tool_dataprivacy\external\data_request_exporter;
50use tool_dataprivacy\external\purpose_exporter;
51use tool_dataprivacy\output\data_registry_page;
52
53/**
54 * Class external.
55 *
56 * The external API for the Data Privacy tool.
57 *
58 * @copyright  2017 Jun Pataleta
59 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
60 */
61class external extends external_api {
62
63    /**
64     * Parameter description for cancel_data_request().
65     *
66     * @since Moodle 3.5
67     * @return external_function_parameters
68     */
69    public static function cancel_data_request_parameters() {
70        return new external_function_parameters([
71            'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
72        ]);
73    }
74
75    /**
76     * Cancel a data request.
77     *
78     * @since Moodle 3.5
79     * @param int $requestid The request ID.
80     * @return array
81     * @throws invalid_persistent_exception
82     * @throws coding_exception
83     * @throws invalid_parameter_exception
84     * @throws restricted_context_exception
85     */
86    public static function cancel_data_request($requestid) {
87        global $USER;
88
89        $warnings = [];
90        $params = external_api::validate_parameters(self::cancel_data_request_parameters(), [
91            'requestid' => $requestid
92        ]);
93        $requestid = $params['requestid'];
94
95        // Validate context and access to manage the registry.
96        $context = context_user::instance($USER->id);
97        self::validate_context($context);
98
99        // Ensure the request exists.
100        $select = 'id = :id AND (userid = :userid OR requestedby = :requestedby)';
101        $params = ['id' => $requestid, 'userid' => $USER->id, 'requestedby' => $USER->id];
102        $requests = data_request::get_records_select($select, $params);
103        $requestexists = count($requests) === 1;
104
105        $result = false;
106        if ($requestexists) {
107            $request = reset($requests);
108            $datasubject = $request->get('userid');
109
110            if ($datasubject !== $USER->id) {
111                // The user is not the subject. Check that they can cancel this request.
112                if (!api::can_create_data_request_for_user($datasubject)) {
113                    $forusercontext = \context_user::instance($datasubject);
114                    throw new required_capability_exception($forusercontext,
115                            'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
116                }
117            }
118
119            // TODO: Do we want a request to be non-cancellable past a certain point? E.g. When it's already approved/processing.
120            $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED);
121        } else {
122            $warnings[] = [
123                'item' => $requestid,
124                'warningcode' => 'errorrequestnotfound',
125                'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
126            ];
127        }
128
129        return [
130            'result' => $result,
131            'warnings' => $warnings
132        ];
133    }
134
135    /**
136     * Parameter description for cancel_data_request().
137     *
138     * @since Moodle 3.5
139     * @return external_description
140     */
141    public static function cancel_data_request_returns() {
142        return new external_single_structure([
143            'result' => new external_value(PARAM_BOOL, 'The processing result'),
144            'warnings' => new external_warnings()
145        ]);
146    }
147
148    /**
149     * Parameter description for contact_dpo().
150     *
151     * @since Moodle 3.5
152     * @return external_function_parameters
153     */
154    public static function contact_dpo_parameters() {
155        return new external_function_parameters([
156            'message' => new external_value(PARAM_TEXT, 'The user\'s message to the Data Protection Officer(s)', VALUE_REQUIRED)
157        ]);
158    }
159
160    /**
161     * Make a general enquiry to a DPO.
162     *
163     * @since Moodle 3.5
164     * @param string $message The message to be sent to the DPO.
165     * @return array
166     * @throws coding_exception
167     * @throws invalid_parameter_exception
168     * @throws invalid_persistent_exception
169     * @throws restricted_context_exception
170     * @throws dml_exception
171     * @throws moodle_exception
172     */
173    public static function contact_dpo($message) {
174        global $USER;
175
176        $warnings = [];
177        $params = external_api::validate_parameters(self::contact_dpo_parameters(), [
178            'message' => $message
179        ]);
180        $message = $params['message'];
181
182        // Validate context.
183        $userid = $USER->id;
184        $context = context_user::instance($userid);
185        self::validate_context($context);
186
187        // Lodge the request.
188        $datarequest = new data_request();
189        // The user the request is being made for.
190        $datarequest->set('userid', $userid);
191        // The user making the request.
192        $datarequest->set('requestedby', $userid);
193        // Set status.
194        $datarequest->set('status', api::DATAREQUEST_STATUS_PENDING);
195        // Set request type.
196        $datarequest->set('type', api::DATAREQUEST_TYPE_OTHERS);
197        // Set request comments.
198        $datarequest->set('comments', $message);
199
200        // Store subject access request.
201        $datarequest->create();
202
203        // Get the list of the site Data Protection Officers.
204        $dpos = api::get_site_dpos();
205
206        // Email the data request to the Data Protection Officer(s)/Admin(s).
207        $result = true;
208        foreach ($dpos as $dpo) {
209            $sendresult = api::notify_dpo($dpo, $datarequest);
210            if (!$sendresult) {
211                $result = false;
212                $warnings[] = [
213                    'item' => $dpo->id,
214                    'warningcode' => 'errorsendingtodpo',
215                    'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy')
216                ];
217            }
218        }
219
220        return [
221            'result' => $result,
222            'warnings' => $warnings
223        ];
224    }
225
226    /**
227     * Parameter description for contact_dpo().
228     *
229     * @since Moodle 3.5
230     * @return external_description
231     */
232    public static function contact_dpo_returns() {
233        return new external_single_structure([
234            'result' => new external_value(PARAM_BOOL, 'The processing result'),
235            'warnings' => new external_warnings()
236        ]);
237    }
238
239    /**
240     * Parameter description for mark_complete().
241     *
242     * @since Moodle 3.5.2
243     * @return external_function_parameters
244     */
245    public static function mark_complete_parameters() {
246        return new external_function_parameters([
247            'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
248        ]);
249    }
250
251    /**
252     * Mark a user's general enquiry's status as complete.
253     *
254     * @since Moodle 3.5.2
255     * @param int $requestid The request ID of the general enquiry.
256     * @return array
257     * @throws coding_exception
258     * @throws invalid_parameter_exception
259     * @throws invalid_persistent_exception
260     * @throws restricted_context_exception
261     * @throws dml_exception
262     * @throws moodle_exception
263     */
264    public static function mark_complete($requestid) {
265        global $USER;
266
267        $warnings = [];
268        $params = external_api::validate_parameters(self::mark_complete_parameters(), [
269            'requestid' => $requestid,
270        ]);
271        $requestid = $params['requestid'];
272
273        // Validate context and access to manage the registry.
274        $context = context_system::instance();
275        self::validate_context($context);
276        api::check_can_manage_data_registry();
277
278        $message = get_string('markedcomplete', 'tool_dataprivacy');
279        // Update the data request record.
280        if ($result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE, $USER->id, $message)) {
281            // Add notification in the session to be shown when the page is reloaded on the JS side.
282            notification::success(get_string('requestmarkedcomplete', 'tool_dataprivacy'));
283        }
284
285        return [
286            'result' => $result,
287            'warnings' => $warnings
288        ];
289    }
290
291    /**
292     * Parameter description for mark_complete().
293     *
294     * @since Moodle 3.5.2
295     * @return external_description
296     */
297    public static function mark_complete_returns() {
298        return new external_single_structure([
299            'result' => new external_value(PARAM_BOOL, 'The processing result'),
300            'warnings' => new external_warnings()
301        ]);
302    }
303
304    /**
305     * Parameter description for get_data_request().
306     *
307     * @since Moodle 3.5
308     * @return external_function_parameters
309     */
310    public static function get_data_request_parameters() {
311        return new external_function_parameters([
312            'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
313        ]);
314    }
315
316    /**
317     * Fetch the details of a user's data request.
318     *
319     * @since Moodle 3.5
320     * @param int $requestid The request ID.
321     * @return array
322     * @throws coding_exception
323     * @throws dml_exception
324     * @throws invalid_parameter_exception
325     * @throws restricted_context_exception
326     * @throws moodle_exception
327     */
328    public static function get_data_request($requestid) {
329        global $PAGE;
330
331        $warnings = [];
332        $params = external_api::validate_parameters(self::get_data_request_parameters(), [
333            'requestid' => $requestid
334        ]);
335        $requestid = $params['requestid'];
336
337        // Validate context.
338        $context = context_system::instance();
339        self::validate_context($context);
340        $requestpersistent = new data_request($requestid);
341        require_capability('tool/dataprivacy:managedatarequests', $context);
342
343        $exporter = new data_request_exporter($requestpersistent, ['context' => $context]);
344        $renderer = $PAGE->get_renderer('tool_dataprivacy');
345        $result = $exporter->export($renderer);
346
347        return [
348            'result' => $result,
349            'warnings' => $warnings
350        ];
351    }
352
353    /**
354     * Parameter description for get_data_request().
355     *
356     * @since Moodle 3.5
357     * @return external_description
358     */
359    public static function get_data_request_returns() {
360        return new external_single_structure([
361            'result' => data_request_exporter::get_read_structure(),
362            'warnings' => new external_warnings()
363        ]);
364    }
365
366    /**
367     * Parameter description for approve_data_request().
368     *
369     * @since Moodle 3.5
370     * @return external_function_parameters
371     */
372    public static function approve_data_request_parameters() {
373        return new external_function_parameters([
374            'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
375        ]);
376    }
377
378    /**
379     * Approve a data request.
380     *
381     * @since Moodle 3.5
382     * @param int $requestid The request ID.
383     * @return array
384     * @throws coding_exception
385     * @throws dml_exception
386     * @throws invalid_parameter_exception
387     * @throws restricted_context_exception
388     * @throws moodle_exception
389     */
390    public static function approve_data_request($requestid) {
391        $warnings = [];
392        $params = external_api::validate_parameters(self::approve_data_request_parameters(), [
393            'requestid' => $requestid
394        ]);
395        $requestid = $params['requestid'];
396
397        // Validate context.
398        $context = context_system::instance();
399        self::validate_context($context);
400        require_capability('tool/dataprivacy:managedatarequests', $context);
401
402        // Ensure the request exists.
403        $requestexists = data_request::record_exists($requestid);
404
405        $result = false;
406        if ($requestexists) {
407            $result = api::approve_data_request($requestid);
408
409            // Add notification in the session to be shown when the page is reloaded on the JS side.
410            notification::success(get_string('requestapproved', 'tool_dataprivacy'));
411        } else {
412            $warnings[] = [
413                'item' => $requestid,
414                'warningcode' => 'errorrequestnotfound',
415                'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
416            ];
417        }
418
419        return [
420            'result' => $result,
421            'warnings' => $warnings
422        ];
423    }
424
425    /**
426     * Parameter description for approve_data_request().
427     *
428     * @since Moodle 3.5
429     * @return external_description
430     */
431    public static function approve_data_request_returns() {
432        return new external_single_structure([
433            'result' => new external_value(PARAM_BOOL, 'The processing result'),
434            'warnings' => new external_warnings()
435        ]);
436    }
437
438    /**
439     * Parameter description for bulk_approve_data_requests().
440     *
441     * @since Moodle 3.5
442     * @return external_function_parameters
443     */
444    public static function bulk_approve_data_requests_parameters() {
445        return new external_function_parameters([
446            'requestids' => new external_multiple_structure(
447                new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
448            )
449        ]);
450    }
451
452    /**
453     * Bulk approve bulk data request.
454     *
455     * @since Moodle 3.5
456     * @param array $requestids Array consisting the request ID's.
457     * @return array
458     * @throws coding_exception
459     * @throws dml_exception
460     * @throws invalid_parameter_exception
461     * @throws restricted_context_exception
462     * @throws moodle_exception
463     */
464    public static function bulk_approve_data_requests($requestids) {
465        $warnings = [];
466        $result = false;
467        $params = external_api::validate_parameters(self::bulk_approve_data_requests_parameters(), [
468            'requestids' => $requestids
469        ]);
470        $requestids = $params['requestids'];
471
472        // Validate context.
473        $context = context_system::instance();
474        self::validate_context($context);
475        require_capability('tool/dataprivacy:managedatarequests', $context);
476
477        foreach ($requestids as $requestid) {
478            // Ensure the request exists.
479            $requestexists = data_request::record_exists($requestid);
480
481            if ($requestexists) {
482                api::approve_data_request($requestid);
483            } else {
484                $warnings[] = [
485                    'item' => $requestid,
486                    'warningcode' => 'errorrequestnotfound',
487                    'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
488                ];
489            }
490        }
491
492        if (empty($warnings)) {
493            $result = true;
494            // Add notification in the session to be shown when the page is reloaded on the JS side.
495            notification::success(get_string('requestsapproved', 'tool_dataprivacy'));
496        }
497
498        return [
499            'result' => $result,
500            'warnings' => $warnings
501        ];
502    }
503
504    /**
505     * Parameter description for bulk_approve_data_requests().
506     *
507     * @since Moodle 3.5
508     * @return external_description
509     */
510    public static function bulk_approve_data_requests_returns() {
511        return new external_single_structure([
512            'result' => new external_value(PARAM_BOOL, 'The processing result'),
513            'warnings' => new external_warnings()
514        ]);
515    }
516
517    /**
518     * Parameter description for deny_data_request().
519     *
520     * @since Moodle 3.5
521     * @return external_function_parameters
522     */
523    public static function deny_data_request_parameters() {
524        return new external_function_parameters([
525            'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
526        ]);
527    }
528
529    /**
530     * Deny a data request.
531     *
532     * @since Moodle 3.5
533     * @param int $requestid The request ID.
534     * @return array
535     * @throws coding_exception
536     * @throws dml_exception
537     * @throws invalid_parameter_exception
538     * @throws restricted_context_exception
539     * @throws moodle_exception
540     */
541    public static function deny_data_request($requestid) {
542        $warnings = [];
543        $params = external_api::validate_parameters(self::deny_data_request_parameters(), [
544            'requestid' => $requestid
545        ]);
546        $requestid = $params['requestid'];
547
548        // Validate context.
549        $context = context_system::instance();
550        self::validate_context($context);
551        require_capability('tool/dataprivacy:managedatarequests', $context);
552
553        // Ensure the request exists.
554        $requestexists = data_request::record_exists($requestid);
555
556        $result = false;
557        if ($requestexists) {
558            $result = api::deny_data_request($requestid);
559
560            // Add notification in the session to be shown when the page is reloaded on the JS side.
561            notification::success(get_string('requestdenied', 'tool_dataprivacy'));
562        } else {
563            $warnings[] = [
564                'item' => $requestid,
565                'warningcode' => 'errorrequestnotfound',
566                'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
567            ];
568        }
569
570        return [
571            'result' => $result,
572            'warnings' => $warnings
573        ];
574    }
575
576    /**
577     * Parameter description for deny_data_request().
578     *
579     * @since Moodle 3.5
580     * @return external_description
581     */
582    public static function deny_data_request_returns() {
583        return new external_single_structure([
584            'result' => new external_value(PARAM_BOOL, 'The processing result'),
585            'warnings' => new external_warnings()
586        ]);
587    }
588
589    /**
590     * Parameter description for bulk_deny_data_requests().
591     *
592     * @since Moodle 3.5
593     * @return external_function_parameters
594     */
595    public static function bulk_deny_data_requests_parameters() {
596        return new external_function_parameters([
597            'requestids' => new external_multiple_structure(
598                new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED)
599            )
600        ]);
601    }
602
603    /**
604     * Bulk deny data requests.
605     *
606     * @since Moodle 3.5
607     * @param array $requestids Array consisting of request ID's.
608     * @return array
609     * @throws coding_exception
610     * @throws dml_exception
611     * @throws invalid_parameter_exception
612     * @throws restricted_context_exception
613     * @throws moodle_exception
614     */
615    public static function bulk_deny_data_requests($requestids) {
616        $warnings = [];
617        $result = false;
618        $params = external_api::validate_parameters(self::bulk_deny_data_requests_parameters(), [
619            'requestids' => $requestids
620        ]);
621        $requestids = $params['requestids'];
622
623        // Validate context.
624        $context = context_system::instance();
625        self::validate_context($context);
626        require_capability('tool/dataprivacy:managedatarequests', $context);
627
628        foreach ($requestids as $requestid) {
629            // Ensure the request exists.
630            $requestexists = data_request::record_exists($requestid);
631
632            if ($requestexists) {
633                api::deny_data_request($requestid);
634            } else {
635                $warnings[] = [
636                    'item' => $requestid,
637                    'warningcode' => 'errorrequestnotfound',
638                    'message' => get_string('errorrequestnotfound', 'tool_dataprivacy')
639                ];
640            }
641        }
642
643        if (empty($warnings)) {
644            $result = true;
645            // Add notification in the session to be shown when the page is reloaded on the JS side.
646            notification::success(get_string('requestsdenied', 'tool_dataprivacy'));
647        }
648
649        return [
650            'result' => $result,
651            'warnings' => $warnings
652        ];
653    }
654
655    /**
656     * Parameter description for bulk_deny_data_requests().
657     *
658     * @since Moodle 3.5
659     * @return external_description
660     */
661    public static function bulk_deny_data_requests_returns() {
662        return new external_single_structure([
663            'result' => new external_value(PARAM_BOOL, 'The processing result'),
664            'warnings' => new external_warnings()
665        ]);
666    }
667
668    /**
669     * Parameter description for get_data_request().
670     *
671     * @since Moodle 3.5
672     * @return external_function_parameters
673     */
674    public static function get_users_parameters() {
675        return new external_function_parameters([
676            'query' => new external_value(PARAM_TEXT, 'The search query', VALUE_REQUIRED)
677        ]);
678    }
679
680    /**
681     * Fetch the details of a user's data request.
682     *
683     * @since Moodle 3.5
684     * @param string $query The search request.
685     * @return array
686     * @throws required_capability_exception
687     * @throws dml_exception
688     * @throws invalid_parameter_exception
689     * @throws restricted_context_exception
690     */
691    public static function get_users($query) {
692        global $DB;
693        $params = external_api::validate_parameters(self::get_users_parameters(), [
694            'query' => $query
695        ]);
696        $query = $params['query'];
697
698        // Validate context.
699        $context = context_system::instance();
700        self::validate_context($context);
701        require_capability('tool/dataprivacy:managedatarequests', $context);
702
703        $allusernames = get_all_user_name_fields(true);
704        // Exclude admins and guest user.
705        $excludedusers = array_keys(get_admins()) + [guest_user()->id];
706        $sort = 'lastname ASC, firstname ASC';
707        $fields = 'id,' . $allusernames;
708
709        $extrafields = get_extra_user_fields($context);
710        if (!empty($extrafields)) {
711            $fields .= ',' . implode(',', $extrafields);
712        }
713
714        list($sql, $params) = users_search_sql($query, '', false, $extrafields, $excludedusers);
715        $users = $DB->get_records_select('user', $sql, $params, $sort, $fields, 0, 30);
716        $useroptions = [];
717        foreach ($users as $user) {
718            $useroption = (object)[
719                'id' => $user->id,
720                'fullname' => fullname($user)
721            ];
722            $useroption->extrafields = [];
723            foreach ($extrafields as $extrafield) {
724                // Sanitize the extra fields to prevent potential XSS exploit.
725                $useroption->extrafields[] = (object)[
726                    'name' => $extrafield,
727                    'value' => s($user->$extrafield)
728                ];
729            }
730            $useroptions[$user->id] = $useroption;
731        }
732
733        return $useroptions;
734    }
735
736    /**
737     * Parameter description for get_users().
738     *
739     * @since Moodle 3.5
740     * @return external_description
741     * @throws coding_exception
742     */
743    public static function get_users_returns() {
744        return new external_multiple_structure(new external_single_structure(
745            [
746                'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
747                'fullname' => new external_value(core_user::get_property_type('firstname'), 'The fullname of the user'),
748                'extrafields' => new external_multiple_structure(
749                    new external_single_structure([
750                            'name' => new external_value(PARAM_TEXT, 'Name of the extrafield.'),
751                            'value' => new external_value(PARAM_TEXT, 'Value of the extrafield.')
752                        ]
753                    ), 'List of extra fields', VALUE_OPTIONAL
754                )
755            ]
756        ));
757    }
758
759    /**
760     * Parameter description for create_purpose_form().
761     *
762     * @since Moodle 3.5
763     * @return external_function_parameters
764     */
765    public static function create_purpose_form_parameters() {
766        return new external_function_parameters([
767            'jsonformdata' => new external_value(PARAM_RAW, 'The data to create the purpose, encoded as a json array')
768        ]);
769    }
770
771    /**
772     * Creates a data purpose from form data.
773     *
774     * @since Moodle 3.5
775     * @param string $jsonformdata
776     * @return array
777     */
778    public static function create_purpose_form($jsonformdata) {
779        global $PAGE;
780
781        $warnings = [];
782
783        $params = external_api::validate_parameters(self::create_purpose_form_parameters(), [
784            'jsonformdata' => $jsonformdata
785        ]);
786
787        // Validate context and access to manage the registry.
788        self::validate_context(\context_system::instance());
789        api::check_can_manage_data_registry();
790
791        $serialiseddata = json_decode($params['jsonformdata']);
792        $data = array();
793        parse_str($serialiseddata, $data);
794
795        $purpose = new \tool_dataprivacy\purpose(0);
796        $mform = new \tool_dataprivacy\form\purpose(null, ['persistent' => $purpose], 'post', '', null, true, $data);
797
798        $validationerrors = true;
799        if ($validateddata = $mform->get_data()) {
800            $purpose = api::create_purpose($validateddata);
801            $validationerrors = false;
802        } else if ($errors = $mform->is_validated()) {
803            throw new moodle_exception('generalerror');
804        }
805
806        $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]);
807        return [
808            'purpose' => $exporter->export($PAGE->get_renderer('core')),
809            'validationerrors' => $validationerrors,
810            'warnings' => $warnings
811        ];
812    }
813
814    /**
815     * Returns for create_purpose_form().
816     *
817     * @since Moodle 3.5
818     * @return external_single_structure
819     */
820    public static function create_purpose_form_returns() {
821        return new external_single_structure([
822            'purpose' => purpose_exporter::get_read_structure(),
823            'validationerrors' => new external_value(PARAM_BOOL, 'Were there validation errors', VALUE_REQUIRED),
824            'warnings' => new external_warnings()
825        ]);
826    }
827
828    /**
829     * Parameter description for delete_purpose().
830     *
831     * @since Moodle 3.5
832     * @return external_function_parameters
833     */
834    public static function delete_purpose_parameters() {
835        return new external_function_parameters([
836            'id' => new external_value(PARAM_INT, 'The purpose ID', VALUE_REQUIRED)
837        ]);
838    }
839
840    /**
841     * Deletes a data purpose.
842     *
843     * @since Moodle 3.5
844     * @param int $id The ID.
845     * @return array
846     * @throws invalid_persistent_exception
847     * @throws coding_exception
848     * @throws invalid_parameter_exception
849     */
850    public static function delete_purpose($id) {
851        global $USER;
852
853        $params = external_api::validate_parameters(self::delete_purpose_parameters(), [
854            'id' => $id
855        ]);
856
857        // Validate context and access to manage the registry.
858        self::validate_context(\context_system::instance());
859        api::check_can_manage_data_registry();
860
861        $result = api::delete_purpose($params['id']);
862
863        return [
864            'result' => $result,
865            'warnings' => []
866        ];
867    }
868
869    /**
870     * Parameter description for delete_purpose().
871     *
872     * @since Moodle 3.5
873     * @return external_single_structure
874     */
875    public static function delete_purpose_returns() {
876        return new external_single_structure([
877            'result' => new external_value(PARAM_BOOL, 'The processing result'),
878            'warnings' => new external_warnings()
879        ]);
880    }
881
882    /**
883     * Parameter description for create_category_form().
884     *
885     * @since Moodle 3.5
886     * @return external_function_parameters
887     */
888    public static function create_category_form_parameters() {
889        return new external_function_parameters([
890            'jsonformdata' => new external_value(PARAM_RAW, 'The data to create the category, encoded as a json array')
891        ]);
892    }
893
894    /**
895     * Creates a data category from form data.
896     *
897     * @since Moodle 3.5
898     * @param string $jsonformdata
899     * @return array
900     */
901    public static function create_category_form($jsonformdata) {
902        global $PAGE;
903
904        $warnings = [];
905
906        $params = external_api::validate_parameters(self::create_category_form_parameters(), [
907            'jsonformdata' => $jsonformdata
908        ]);
909
910        // Validate context and access to manage the registry.
911        self::validate_context(\context_system::instance());
912        api::check_can_manage_data_registry();
913
914        $serialiseddata = json_decode($params['jsonformdata']);
915        $data = array();
916        parse_str($serialiseddata, $data);
917
918        $category = new \tool_dataprivacy\category(0);
919        $mform = new \tool_dataprivacy\form\category(null, ['persistent' => $category], 'post', '', null, true, $data);
920
921        $validationerrors = true;
922        if ($validateddata = $mform->get_data()) {
923            $category = api::create_category($validateddata);
924            $validationerrors = false;
925        } else if ($errors = $mform->is_validated()) {
926            throw new moodle_exception('generalerror');
927        }
928
929        $exporter = new category_exporter($category, ['context' => \context_system::instance()]);
930        return [
931            'category' => $exporter->export($PAGE->get_renderer('core')),
932            'validationerrors' => $validationerrors,
933            'warnings' => $warnings
934        ];
935    }
936
937    /**
938     * Returns for create_category_form().
939     *
940     * @since Moodle 3.5
941     * @return external_single_structure
942     */
943    public static function create_category_form_returns() {
944        return new external_single_structure([
945            'category' => category_exporter::get_read_structure(),
946            'validationerrors' => new external_value(PARAM_BOOL, 'Were there validation errors', VALUE_REQUIRED),
947            'warnings' => new external_warnings()
948        ]);
949    }
950
951    /**
952     * Parameter description for delete_category().
953     *
954     * @since Moodle 3.5
955     * @return external_function_parameters
956     */
957    public static function delete_category_parameters() {
958        return new external_function_parameters([
959            'id' => new external_value(PARAM_INT, 'The category ID', VALUE_REQUIRED)
960        ]);
961    }
962
963    /**
964     * Deletes a data category.
965     *
966     * @since Moodle 3.5
967     * @param int $id The ID.
968     * @return array
969     * @throws invalid_persistent_exception
970     * @throws coding_exception
971     * @throws invalid_parameter_exception
972     */
973    public static function delete_category($id) {
974        global $USER;
975
976        $params = external_api::validate_parameters(self::delete_category_parameters(), [
977            'id' => $id
978        ]);
979
980        // Validate context and access to manage the registry.
981        self::validate_context(\context_system::instance());
982        api::check_can_manage_data_registry();
983
984        $result = api::delete_category($params['id']);
985
986        return [
987            'result' => $result,
988            'warnings' => []
989        ];
990    }
991
992    /**
993     * Parameter description for delete_category().
994     *
995     * @since Moodle 3.5
996     * @return external_single_structure
997     */
998    public static function delete_category_returns() {
999        return new external_single_structure([
1000            'result' => new external_value(PARAM_BOOL, 'The processing result'),
1001            'warnings' => new external_warnings()
1002        ]);
1003    }
1004
1005    /**
1006     * Parameter description for set_contextlevel_form().
1007     *
1008     * @since Moodle 3.5
1009     * @return external_function_parameters
1010     */
1011    public static function set_contextlevel_form_parameters() {
1012        return new external_function_parameters([
1013            'jsonformdata' => new external_value(PARAM_RAW, 'The context level data, encoded as a json array')
1014        ]);
1015    }
1016
1017    /**
1018     * Creates a data category from form data.
1019     *
1020     * @since Moodle 3.5
1021     * @param string $jsonformdata
1022     * @return array
1023     */
1024    public static function set_contextlevel_form($jsonformdata) {
1025        global $PAGE;
1026
1027        $warnings = [];
1028
1029        $params = external_api::validate_parameters(self::set_contextlevel_form_parameters(), [
1030            'jsonformdata' => $jsonformdata
1031        ]);
1032
1033        // Validate context and access to manage the registry.
1034        self::validate_context(\context_system::instance());
1035        api::check_can_manage_data_registry();
1036
1037        $serialiseddata = json_decode($params['jsonformdata']);
1038        $data = array();
1039        parse_str($serialiseddata, $data);
1040
1041        $contextlevel = $data['contextlevel'];
1042
1043        $customdata = \tool_dataprivacy\form\contextlevel::get_contextlevel_customdata($contextlevel);
1044        $mform = new \tool_dataprivacy\form\contextlevel(null, $customdata, 'post', '', null, true, $data);
1045        if ($validateddata = $mform->get_data()) {
1046            $contextlevel = api::set_contextlevel($validateddata);
1047        } else if ($errors = $mform->is_validated()) {
1048            $warnings[] = json_encode($errors);
1049        }
1050
1051        if ($contextlevel) {
1052            $result = true;
1053        } else {
1054            $result = false;
1055        }
1056        return [
1057            'result' => $result,
1058            'warnings' => $warnings
1059        ];
1060    }
1061
1062    /**
1063     * Returns for set_contextlevel_form().
1064     *
1065     * @since Moodle 3.5
1066     * @return external_single_structure
1067     */
1068    public static function set_contextlevel_form_returns() {
1069        return new external_single_structure([
1070            'result' => new external_value(PARAM_BOOL, 'Whether the data was properly set or not'),
1071            'warnings' => new external_warnings()
1072        ]);
1073    }
1074
1075    /**
1076     * Parameter description for set_context_form().
1077     *
1078     * @since Moodle 3.5
1079     * @return external_function_parameters
1080     */
1081    public static function set_context_form_parameters() {
1082        return new external_function_parameters([
1083            'jsonformdata' => new external_value(PARAM_RAW, 'The context level data, encoded as a json array')
1084        ]);
1085    }
1086
1087    /**
1088     * Creates a data category from form data.
1089     *
1090     * @since Moodle 3.5
1091     * @param string $jsonformdata
1092     * @return array
1093     */
1094    public static function set_context_form($jsonformdata) {
1095        global $PAGE;
1096
1097        $warnings = [];
1098
1099        $params = external_api::validate_parameters(self::set_context_form_parameters(), [
1100            'jsonformdata' => $jsonformdata
1101        ]);
1102
1103        // Validate context and access to manage the registry.
1104        self::validate_context(\context_system::instance());
1105        api::check_can_manage_data_registry();
1106
1107        $serialiseddata = json_decode($params['jsonformdata']);
1108        $data = array();
1109        parse_str($serialiseddata, $data);
1110
1111        $context = context_helper::instance_by_id($data['contextid']);
1112        $customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context);
1113        $mform = new \tool_dataprivacy\form\context_instance(null, $customdata, 'post', '', null, true, $data);
1114        if ($validateddata = $mform->get_data()) {
1115            api::check_can_manage_data_registry($validateddata->contextid);
1116            $context = api::set_context_instance($validateddata);
1117        } else if ($errors = $mform->is_validated()) {
1118            $warnings[] = json_encode($errors);
1119            throw new moodle_exception('generalerror');
1120        }
1121
1122        if ($context) {
1123            $result = true;
1124        } else {
1125            $result = false;
1126        }
1127        return [
1128            'result' => $result,
1129            'warnings' => $warnings
1130        ];
1131    }
1132
1133    /**
1134     * Returns for set_context_form().
1135     *
1136     * @since Moodle 3.5
1137     * @return external_single_structure
1138     */
1139    public static function set_context_form_returns() {
1140        return new external_single_structure([
1141            'result' => new external_value(PARAM_BOOL, 'Whether the data was properly set or not'),
1142            'warnings' => new external_warnings()
1143        ]);
1144    }
1145
1146    /**
1147     * Parameter description for tree_extra_branches().
1148     *
1149     * @since Moodle 3.5
1150     * @return external_function_parameters
1151     */
1152    public static function tree_extra_branches_parameters() {
1153        return new external_function_parameters([
1154            'contextid' => new external_value(PARAM_INT, 'The context id to expand'),
1155            'element' => new external_value(PARAM_ALPHA, 'The element we are interested on')
1156        ]);
1157    }
1158
1159    /**
1160     * Returns tree extra branches.
1161     *
1162     * @since Moodle 3.5
1163     * @param int $contextid
1164     * @param string $element
1165     * @return array
1166     */
1167    public static function tree_extra_branches($contextid, $element) {
1168
1169        $params = external_api::validate_parameters(self::tree_extra_branches_parameters(), [
1170            'contextid' => $contextid,
1171            'element' => $element,
1172        ]);
1173
1174        $context = context_helper::instance_by_id($params['contextid']);
1175
1176        self::validate_context($context);
1177        api::check_can_manage_data_registry($context->id);
1178
1179        switch ($params['element']) {
1180            case 'course':
1181                $branches = data_registry_page::get_courses_branch($context);
1182                break;
1183            case 'module':
1184                $branches = data_registry_page::get_modules_branch($context);
1185                break;
1186            case 'block':
1187                $branches = data_registry_page::get_blocks_branch($context);
1188                break;
1189            default:
1190                throw new \moodle_exception('Unsupported element provided.');
1191        }
1192
1193        return [
1194            'branches' => $branches,
1195            'warnings' => [],
1196        ];
1197    }
1198
1199    /**
1200     * Returns for tree_extra_branches().
1201     *
1202     * @since Moodle 3.5
1203     * @return external_single_structure
1204     */
1205    public static function tree_extra_branches_returns() {
1206        return new external_single_structure([
1207            'branches' => new external_multiple_structure(self::get_tree_node_structure(true)),
1208            'warnings' => new external_warnings()
1209        ]);
1210    }
1211
1212    /**
1213     * Parameters for confirm_contexts_for_deletion().
1214     *
1215     * @since Moodle 3.5
1216     * @return external_function_parameters
1217     */
1218    public static function confirm_contexts_for_deletion_parameters() {
1219        return new external_function_parameters([
1220            'ids' => new external_multiple_structure(
1221                new external_value(PARAM_INT, 'Expired context record ID', VALUE_REQUIRED),
1222                'Array of expired context record IDs', VALUE_DEFAULT, []
1223            ),
1224        ]);
1225    }
1226
1227    /**
1228     * Confirm a given array of expired context record IDs
1229     *
1230     * @since Moodle 3.5
1231     * @param int[] $ids Array of record IDs from the expired contexts table.
1232     * @return array
1233     * @throws coding_exception
1234     * @throws dml_exception
1235     * @throws invalid_parameter_exception
1236     * @throws restricted_context_exception
1237     */
1238    public static function confirm_contexts_for_deletion($ids) {
1239        $warnings = [];
1240        $params = external_api::validate_parameters(self::confirm_contexts_for_deletion_parameters(), [
1241            'ids' => $ids
1242        ]);
1243        $ids = $params['ids'];
1244
1245        // Validate context and access to manage the registry.
1246        self::validate_context(\context_system::instance());
1247        api::check_can_manage_data_registry();
1248
1249        $result = true;
1250        if (!empty($ids)) {
1251            $expiredcontextstoapprove = [];
1252            // Loop through the deletion of expired contexts and their children if necessary.
1253            foreach ($ids as $id) {
1254                $expiredcontext = new expired_context($id);
1255                $targetcontext = context_helper::instance_by_id($expiredcontext->get('contextid'));
1256
1257                if (!$targetcontext instanceof \context_user) {
1258                    // Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion.
1259                    // User context children do not need to be considered.
1260                    $childcontexts = $targetcontext->get_child_contexts();
1261                    foreach ($childcontexts as $child) {
1262                        if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) {
1263                            // Add this child context to the list for approval.
1264                            $expiredcontextstoapprove[] = $expiredchildcontext;
1265                        } else {
1266                            // This context has not yet been flagged for deletion.
1267                            $result = false;
1268                            $message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy',
1269                                $targetcontext->get_context_name(false));
1270                            $warnings[] = [
1271                                'item' => 'tool_dataprivacy_ctxexpired',
1272                                'warningcode' => 'errorcontexthasunexpiredchildren',
1273                                'message' => $message
1274                            ];
1275                            // Exit the process.
1276                            break 2;
1277                        }
1278                    }
1279                }
1280
1281                $expiredcontextstoapprove[] = $expiredcontext;
1282            }
1283
1284            // Proceed with the approval if everything's in order.
1285            if ($result) {
1286                // Mark expired contexts as approved for deletion.
1287                foreach ($expiredcontextstoapprove as $expired) {
1288                    // Only mark expired contexts that are pending approval.
1289                    if ($expired->get('status') == expired_context::STATUS_EXPIRED) {
1290                        api::set_expired_context_status($expired, expired_context::STATUS_APPROVED);
1291                    }
1292                }
1293            }
1294
1295        } else {
1296            // We don't have anything to process.
1297            $result = false;
1298            $warnings[] = [
1299                'item' => 'tool_dataprivacy_ctxexpired',
1300                'warningcode' => 'errornoexpiredcontexts',
1301                'message' => get_string('errornoexpiredcontexts', 'tool_dataprivacy')
1302            ];
1303        }
1304
1305        return [
1306            'result' => $result,
1307            'warnings' => $warnings
1308        ];
1309    }
1310
1311    /**
1312     * Returns for confirm_contexts_for_deletion().
1313     *
1314     * @since Moodle 3.5
1315     * @return external_single_structure
1316     */
1317    public static function confirm_contexts_for_deletion_returns() {
1318        return new external_single_structure([
1319            'result' => new external_value(PARAM_BOOL, 'Whether the record was properly marked for deletion or not'),
1320            'warnings' => new external_warnings()
1321        ]);
1322    }
1323
1324    /**
1325     * Parameters for set_context_defaults().
1326     *
1327     * @return external_function_parameters
1328     */
1329    public static function set_context_defaults_parameters() {
1330        return new external_function_parameters([
1331            'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED),
1332            'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED),
1333            'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED),
1334            'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null),
1335            'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT,
1336                false),
1337        ]);
1338    }
1339
1340    /**
1341     * Updates the default category and purpose for a given context level (and optionally, a plugin).
1342     *
1343     * @param int $contextlevel The context level.
1344     * @param int $category The ID matching the category.
1345     * @param int $purpose The ID matching the purpose record.
1346     * @param int $activity The name of the activity that we're making a defaults configuration for.
1347     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
1348     * @return array
1349     */
1350    public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) {
1351        $warnings = [];
1352
1353        $params = external_api::validate_parameters(self::set_context_defaults_parameters(), [
1354            'contextlevel' => $contextlevel,
1355            'category' => $category,
1356            'purpose' => $purpose,
1357            'activity' => $activity,
1358            'override' => $override,
1359        ]);
1360        $contextlevel = $params['contextlevel'];
1361        $category = $params['category'];
1362        $purpose = $params['purpose'];
1363        $activity = $params['activity'];
1364        $override = $params['override'];
1365
1366        // Validate context.
1367        $context = context_system::instance();
1368        self::validate_context($context);
1369        api::check_can_manage_data_registry();
1370
1371        // Set the context defaults.
1372        $result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
1373
1374        return [
1375            'result' => $result,
1376            'warnings' => $warnings
1377        ];
1378    }
1379
1380    /**
1381     * Returns for set_context_defaults().
1382     *
1383     * @return external_single_structure
1384     */
1385    public static function set_context_defaults_returns() {
1386        return new external_single_structure([
1387            'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'),
1388            'warnings' => new external_warnings()
1389        ]);
1390    }
1391
1392    /**
1393     * Parameters for get_category_options().
1394     *
1395     * @return external_function_parameters
1396     */
1397    public static function get_category_options_parameters() {
1398        return new external_function_parameters([
1399            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
1400            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
1401        ]);
1402    }
1403
1404    /**
1405     * Fetches a list of data category options containing category IDs as keys and the category name for the value.
1406     *
1407     * @param bool $includeinherit Whether to include the "Inherit" option.
1408     * @param bool $includenotset Whether to include the "Not set" option.
1409     * @return array
1410     */
1411    public static function get_category_options($includeinherit, $includenotset) {
1412        $warnings = [];
1413
1414        $params = self::validate_parameters(self::get_category_options_parameters(), [
1415            'includeinherit' => $includeinherit,
1416            'includenotset' => $includenotset
1417        ]);
1418        $includeinherit = $params['includeinherit'];
1419        $includenotset = $params['includenotset'];
1420
1421        $context = context_system::instance();
1422        self::validate_context($context);
1423        api::check_can_manage_data_registry();
1424
1425        $categories = api::get_categories();
1426        $options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
1427        $categoryoptions = [];
1428        foreach ($options as $id => $name) {
1429            $categoryoptions[] = [
1430                'id' => $id,
1431                'name' => $name,
1432            ];
1433        }
1434
1435        return [
1436            'options' => $categoryoptions,
1437            'warnings' => $warnings
1438        ];
1439    }
1440
1441    /**
1442     * Returns for get_category_options().
1443     *
1444     * @return external_single_structure
1445     */
1446    public static function get_category_options_returns() {
1447        $optiondefinition = new external_single_structure(
1448            [
1449                'id' => new external_value(PARAM_INT, 'The category ID'),
1450                'name' => new external_value(PARAM_TEXT, 'The category name'),
1451            ]
1452        );
1453
1454        return new external_single_structure([
1455            'options' => new external_multiple_structure($optiondefinition),
1456            'warnings' => new external_warnings()
1457        ]);
1458    }
1459
1460    /**
1461     * Parameters for get_purpose_options().
1462     *
1463     * @return external_function_parameters
1464     */
1465    public static function get_purpose_options_parameters() {
1466        return new external_function_parameters([
1467            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
1468            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
1469        ]);
1470    }
1471
1472    /**
1473     * Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value.
1474     *
1475     * @param bool $includeinherit Whether to include the "Inherit" option.
1476     * @param bool $includenotset Whether to include the "Not set" option.
1477     * @return array
1478     */
1479    public static function get_purpose_options($includeinherit, $includenotset) {
1480        $warnings = [];
1481
1482        $params = self::validate_parameters(self::get_category_options_parameters(), [
1483            'includeinherit' => $includeinherit,
1484            'includenotset' => $includenotset
1485        ]);
1486        $includeinherit = $params['includeinherit'];
1487        $includenotset = $params['includenotset'];
1488
1489        $context = context_system::instance();
1490        self::validate_context($context);
1491
1492        $purposes = api::get_purposes();
1493        $options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit);
1494        $purposeoptions = [];
1495        foreach ($options as $id => $name) {
1496            $purposeoptions[] = [
1497                'id' => $id,
1498                'name' => $name,
1499            ];
1500        }
1501
1502        return [
1503            'options' => $purposeoptions,
1504            'warnings' => $warnings
1505        ];
1506    }
1507
1508    /**
1509     * Returns for get_purpose_options().
1510     *
1511     * @return external_single_structure
1512     */
1513    public static function get_purpose_options_returns() {
1514        $optiondefinition = new external_single_structure(
1515            [
1516                'id' => new external_value(PARAM_INT, 'The purpose ID'),
1517                'name' => new external_value(PARAM_TEXT, 'The purpose name'),
1518            ]
1519        );
1520
1521        return new external_single_structure([
1522            'options' => new external_multiple_structure($optiondefinition),
1523            'warnings' => new external_warnings()
1524        ]);
1525    }
1526
1527    /**
1528     * Parameters for get_activity_options().
1529     *
1530     * @return external_function_parameters
1531     */
1532    public static function get_activity_options_parameters() {
1533        return new external_function_parameters([
1534            'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults',
1535                VALUE_DEFAULT, false),
1536        ]);
1537    }
1538
1539    /**
1540     * Fetches a list of activity options for setting data registry defaults.
1541     *
1542     * @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities
1543     *                            that don't have defaults yet (e.g. when adding a new activity module defaults).
1544     * @return array
1545     */
1546    public static function get_activity_options($nodefaults) {
1547        $warnings = [];
1548
1549        $params = self::validate_parameters(self::get_activity_options_parameters(), [
1550            'nodefaults' => $nodefaults,
1551        ]);
1552        $nodefaults = $params['nodefaults'];
1553
1554        $context = context_system::instance();
1555        self::validate_context($context);
1556
1557        // Get activity module plugin info.
1558        $pluginmanager = \core_plugin_manager::instance();
1559        $modplugins = $pluginmanager->get_enabled_plugins('mod');
1560        $modoptions = [];
1561
1562        // Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults.
1563        list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE);
1564        foreach ($modplugins as $name) {
1565            // Check if we have default purpose and category for this module if we want don't want to fetch everything.
1566            if ($nodefaults) {
1567                list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name);
1568                // Compare this with the module-level defaults.
1569                if ($purpose !== $levelpurpose || $category !== $levelcategory) {
1570                    // If the defaults for this activity has been already set, there's no need to add this in the list of options.
1571                    continue;
1572                }
1573            }
1574
1575            $displayname = $pluginmanager->plugin_name('mod_' . $name);
1576            $modoptions[] = (object)[
1577                'name' => $name,
1578                'displayname' => $displayname
1579            ];
1580        }
1581
1582        return [
1583            'options' => $modoptions,
1584            'warnings' => $warnings
1585        ];
1586    }
1587
1588    /**
1589     * Returns for get_category_options().
1590     *
1591     * @return external_single_structure
1592     */
1593    public static function get_activity_options_returns() {
1594        $optionsdefinition = new external_single_structure(
1595            [
1596                'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'),
1597                'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'),
1598            ]
1599        );
1600
1601        return new external_single_structure([
1602            'options' => new external_multiple_structure($optionsdefinition),
1603            'warnings' => new external_warnings()
1604        ]);
1605    }
1606
1607    /**
1608     * Gets the structure of a tree node (link + child branches).
1609     *
1610     * @since Moodle 3.5
1611     * @param bool $allowchildbranches
1612     * @return array
1613     */
1614    private static function get_tree_node_structure($allowchildbranches = true) {
1615        $fields = [
1616            'text' => new external_value(PARAM_RAW, 'The node text', VALUE_REQUIRED),
1617            'expandcontextid' => new external_value(PARAM_INT, 'The contextid this node expands', VALUE_REQUIRED),
1618            'expandelement' => new external_value(PARAM_ALPHA, 'What element is this node expanded to', VALUE_REQUIRED),
1619            'contextid' => new external_value(PARAM_INT, 'The node contextid', VALUE_REQUIRED),
1620            'contextlevel' => new external_value(PARAM_INT, 'The node contextlevel', VALUE_REQUIRED),
1621            'expanded' => new external_value(PARAM_INT, 'Is it expanded', VALUE_REQUIRED),
1622        ];
1623
1624        if ($allowchildbranches) {
1625            // Passing false as we will not have more than 1 sub-level.
1626            $fields['branches'] = new external_multiple_structure(
1627                self::get_tree_node_structure(false),
1628                'Children node structure',
1629                VALUE_OPTIONAL
1630            );
1631        } else {
1632            // We will only have 1 sub-level and we don't want an infinite get_tree_node_structure, this is a hacky
1633            // way to prevent this infinite loop when calling get_tree_node_structure recursively.
1634            $fields['branches'] = new external_multiple_structure(
1635                new external_value(
1636                    PARAM_TEXT,
1637                    'Nothing really, it will always be an empty array',
1638                    VALUE_OPTIONAL
1639                )
1640            );
1641        }
1642
1643        return new external_single_structure($fields, 'Node structure', VALUE_OPTIONAL);
1644    }
1645}
1646