Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.32% covered (success)
90.32%
56 / 62
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
provider
90.32% covered (success)
90.32%
56 / 62
57.14% covered (warning)
57.14%
4 / 7
21.40
0.00% covered (danger)
0.00%
0 / 1
 get_metadata
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 get_contexts_for_userid
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 get_users_in_context
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 export_user_data
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 delete_data_for_all_users_in_context
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 delete_data_for_user
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 delete_data_for_users
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
6.99
1<?php
2// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16
17namespace local_recertify\privacy;
18
19use context;
20use context_course;
21use core_privacy\local\metadata\collection;
22use core_privacy\local\request\approved_contextlist;
23use core_privacy\local\request\approved_userlist;
24use core_privacy\local\request\contextlist;
25use core_privacy\local\request\transform;
26use core_privacy\local\request\userlist;
27use core_privacy\local\request\writer;
28
29/**
30 * Privacy provider: archived certifications and the reminders sent.
31 *
32 * @package    local_recertify
33 * @copyright  2026 DevLMS <contact@devlms.com>
34 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36final class provider implements
37    \core_privacy\local\metadata\provider,
38    \core_privacy\local\request\core_userlist_provider,
39    \core_privacy\local\request\plugin\provider {
40    /** @var string[] Tables with a courseid and a userid. */
41    private const TABLES = ['local_recertify_history', 'local_recertify_notice'];
42
43    /**
44     * Describe the personal data stored.
45     *
46     * @param collection $collection
47     * @return collection
48     */
49    public static function get_metadata(collection $collection): collection {
50        $collection->add_database_table('local_recertify_history', [
51            'userid' => 'privacy:metadata:history:userid',
52            'timecompleted' => 'privacy:metadata:history:timecompleted',
53            'timeexpired' => 'privacy:metadata:history:timeexpired',
54            'finalgrade' => 'privacy:metadata:history:finalgrade',
55            'archive' => 'privacy:metadata:history:archive',
56        ], 'privacy:metadata:history');
57        $collection->add_database_table('local_recertify_notice', [
58            'userid' => 'privacy:metadata:notice:userid',
59            'daysbefore' => 'privacy:metadata:notice:daysbefore',
60            'timesent' => 'privacy:metadata:notice:timesent',
61        ], 'privacy:metadata:notice');
62        $collection->add_subsystem_link('core_message', [], 'privacy:metadata:core_message');
63        return $collection;
64    }
65
66    /**
67     * Courses where the user has history or reminders.
68     *
69     * @param int $userid
70     * @return contextlist
71     */
72    public static function get_contexts_for_userid(int $userid): contextlist {
73        $list = new contextlist();
74        foreach (self::TABLES as $table) {
75            $list->add_from_sql("SELECT ctx.id
76                                   FROM {context} ctx
77                                   JOIN {{$table}} t ON t.courseid = ctx.instanceid AND ctx.contextlevel = :level
78                                  WHERE t.userid = :userid", ['level' => CONTEXT_COURSE, 'userid' => $userid]);
79        }
80        return $list;
81    }
82
83    /**
84     * Users with data in a course.
85     *
86     * @param userlist $userlist
87     */
88    public static function get_users_in_context(userlist $userlist) {
89        $context = $userlist->get_context();
90        if (!$context instanceof context_course) {
91            return;
92        }
93        foreach (self::TABLES as $table) {
94            $userlist->add_from_sql(
95                'userid',
96                "SELECT userid FROM {{$table}} WHERE courseid = :courseid",
97                ['courseid' => $context->instanceid]
98            );
99        }
100    }
101
102    /**
103     * Export certifications and reminders, one file per course.
104     *
105     * @param approved_contextlist $contextlist
106     */
107    public static function export_user_data(approved_contextlist $contextlist) {
108        global $DB;
109        $userid = $contextlist->get_user()->id;
110        foreach ($contextlist->get_contexts() as $context) {
111            if (!$context instanceof context_course) {
112                continue;
113            }
114            $params = ['courseid' => $context->instanceid, 'userid' => $userid];
115            $certifications = array_map(fn($h) => (object) [
116                'timecompleted' => transform::datetime($h->timecompleted),
117                'timeexpired' => transform::datetime($h->timeexpired),
118                'finalgrade' => $h->finalgrade === null ? null : (float) $h->finalgrade,
119                'archive' => json_decode($h->archive),
120            ], array_values($DB->get_records('local_recertify_history', $params, 'timecompleted')));
121            $reminders = array_map(fn($n) => (object) [
122                'daysbefore' => (int) $n->daysbefore,
123                'timesent' => transform::datetime($n->timesent),
124            ], array_values($DB->get_records('local_recertify_notice', $params, 'timesent')));
125            writer::with_context($context)->export_data(
126                [get_string('pluginname', 'local_recertify')],
127                (object) ['certifications' => $certifications, 'reminders' => $reminders]
128            );
129        }
130    }
131
132    /**
133     * Delete everything in a course.
134     *
135     * @param context $context
136     */
137    public static function delete_data_for_all_users_in_context(context $context) {
138        global $DB;
139        if ($context instanceof context_course) {
140            foreach (self::TABLES as $table) {
141                $DB->delete_records($table, ['courseid' => $context->instanceid]);
142            }
143        }
144    }
145
146    /**
147     * Delete one user's data in the approved courses.
148     *
149     * @param approved_contextlist $contextlist
150     */
151    public static function delete_data_for_user(approved_contextlist $contextlist) {
152        global $DB;
153        foreach ($contextlist->get_contexts() as $context) {
154            if ($context instanceof context_course) {
155                foreach (self::TABLES as $table) {
156                    $DB->delete_records($table, ['courseid' => $context->instanceid, 'userid' => $contextlist->get_user()->id]);
157                }
158            }
159        }
160    }
161
162    /**
163     * Delete several users' data in one course.
164     *
165     * @param approved_userlist $userlist
166     */
167    public static function delete_data_for_users(approved_userlist $userlist) {
168        global $DB;
169        $context = $userlist->get_context();
170        if (!$context instanceof context_course || !$userlist->get_userids()) {
171            return;
172        }
173        [$insql, $params] = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
174        $params['courseid'] = $context->instanceid;
175        foreach (self::TABLES as $table) {
176            $DB->delete_records_select($table, "courseid = :courseid AND userid $insql", $params);
177        }
178    }
179}