Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.70% |
85 / 87 |
|
80.00% |
8 / 10 |
CRAP | |
0.00% |
0 / 1 |
| provider | |
97.70% |
85 / 87 |
|
80.00% |
8 / 10 |
29 | |
0.00% |
0 / 1 |
| get_metadata | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
1 | |||
| get_contexts_for_userid | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| get_users_in_context | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| export_user_data | |
96.67% |
29 / 30 |
|
0.00% |
0 / 1 |
12 | |||
| delete_data_for_all_users_in_context | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| delete_data_for_user | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| delete_data_for_users | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| delete_user | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
| delete_case | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| course_contexts | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 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 | |
| 17 | namespace local_integritycase\privacy; |
| 18 | |
| 19 | use context; |
| 20 | use context_course; |
| 21 | use core_privacy\local\metadata\collection; |
| 22 | use core_privacy\local\request\approved_contextlist; |
| 23 | use core_privacy\local\request\approved_userlist; |
| 24 | use core_privacy\local\request\contextlist; |
| 25 | use core_privacy\local\request\transform; |
| 26 | use core_privacy\local\request\userlist; |
| 27 | use core_privacy\local\request\writer; |
| 28 | use local_integritycase\local\case_manager; |
| 29 | |
| 30 | /** |
| 31 | * Privacy provider. |
| 32 | * |
| 33 | * A student's request covers whole cases about them, staff notes included. A staff member's request |
| 34 | * covers their own actions; deleting it keeps the case for the record and removes their name. |
| 35 | * |
| 36 | * @package local_integritycase |
| 37 | * @copyright 2026 DevLMS <contact@devlms.com> |
| 38 | * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
| 39 | */ |
| 40 | final class provider implements |
| 41 | \core_privacy\local\metadata\provider, |
| 42 | \core_privacy\local\request\core_userlist_provider, |
| 43 | \core_privacy\local\request\plugin\provider { |
| 44 | /** |
| 45 | * Describe the personal data stored. |
| 46 | * |
| 47 | * @param collection $collection |
| 48 | * @return collection |
| 49 | */ |
| 50 | public static function get_metadata(collection $collection): collection { |
| 51 | $collection->add_database_table('local_integritycase', [ |
| 52 | 'studentid' => 'privacy:metadata:case:studentid', |
| 53 | 'handlerid' => 'privacy:metadata:case:handlerid', |
| 54 | 'createdby' => 'privacy:metadata:case:createdby', |
| 55 | 'summary' => 'privacy:metadata:case:summary', |
| 56 | 'stage' => 'privacy:metadata:case:stage', |
| 57 | 'outcome' => 'privacy:metadata:case:outcome', |
| 58 | 'timecreated' => 'privacy:metadata:case:timecreated', |
| 59 | ], 'privacy:metadata:case'); |
| 60 | $collection->add_database_table('local_integritycase_event', [ |
| 61 | 'userid' => 'privacy:metadata:event:userid', |
| 62 | 'note' => 'privacy:metadata:event:note', |
| 63 | 'timecreated' => 'privacy:metadata:event:timecreated', |
| 64 | ], 'privacy:metadata:event'); |
| 65 | $collection->add_subsystem_link('core_files', [], 'privacy:metadata:core_files'); |
| 66 | $collection->add_subsystem_link('core_message', [], 'privacy:metadata:core_message'); |
| 67 | return $collection; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Course contexts where the user is in a case: as student, handler, opener or actor. |
| 72 | * |
| 73 | * @param int $userid |
| 74 | * @return contextlist |
| 75 | */ |
| 76 | public static function get_contexts_for_userid(int $userid): contextlist { |
| 77 | $sql = "SELECT ctx.id |
| 78 | FROM {context} ctx |
| 79 | JOIN {local_integritycase} c ON c.courseid = ctx.instanceid AND ctx.contextlevel = :level |
| 80 | LEFT JOIN {local_integritycase_event} e ON e.caseid = c.id |
| 81 | WHERE c.studentid = :u1 OR c.handlerid = :u2 OR c.createdby = :u3 OR e.userid = :u4"; |
| 82 | $list = new contextlist(); |
| 83 | $list->add_from_sql($sql, ['level' => CONTEXT_COURSE, 'u1' => $userid, 'u2' => $userid, 'u3' => $userid, |
| 84 | 'u4' => $userid]); |
| 85 | return $list; |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * Users with data in a course context. |
| 90 | * |
| 91 | * @param userlist $userlist |
| 92 | */ |
| 93 | public static function get_users_in_context(userlist $userlist) { |
| 94 | $context = $userlist->get_context(); |
| 95 | if (!$context instanceof context_course) { |
| 96 | return; |
| 97 | } |
| 98 | $params = ['courseid' => $context->instanceid]; |
| 99 | foreach (['studentid', 'handlerid', 'createdby'] as $field) { |
| 100 | $userlist->add_from_sql($field, "SELECT $field FROM {local_integritycase} WHERE courseid = :courseid", $params); |
| 101 | } |
| 102 | $userlist->add_from_sql('userid', "SELECT e.userid |
| 103 | FROM {local_integritycase_event} e |
| 104 | JOIN {local_integritycase} c ON c.id = e.caseid |
| 105 | WHERE c.courseid = :courseid", $params); |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * Export the user's data, one folder per case. |
| 110 | * |
| 111 | * @param approved_contextlist $contextlist |
| 112 | */ |
| 113 | public static function export_user_data(approved_contextlist $contextlist) { |
| 114 | global $DB; |
| 115 | $userid = (int) $contextlist->get_user()->id; |
| 116 | foreach (self::course_contexts($contextlist->get_contexts()) as $context) { |
| 117 | foreach ($DB->get_records('local_integritycase', ['courseid' => $context->instanceid]) as $case) { |
| 118 | $isstudent = (int) $case->studentid === $userid; |
| 119 | $history = []; |
| 120 | foreach (case_manager::events($case, false) as $event) { |
| 121 | if ($isstudent || (int) $event->userid === $userid) { |
| 122 | $history[] = (object) [ |
| 123 | 'action' => get_string('action:' . $event->action, 'local_integritycase'), |
| 124 | 'stage' => $event->tostage ? case_manager::stage_name($event->tostage) : null, |
| 125 | 'note' => $event->note, |
| 126 | 'byyou' => transform::yesno((int) $event->userid === $userid), |
| 127 | 'time' => transform::datetime($event->timecreated), |
| 128 | ]; |
| 129 | } |
| 130 | } |
| 131 | $involved = $isstudent || in_array($userid, [(int) $case->handlerid, (int) $case->createdby], true); |
| 132 | if (!$involved && !$history) { |
| 133 | continue; |
| 134 | } |
| 135 | $data = (object) [ |
| 136 | 'stage' => case_manager::stage_name($case->stage), |
| 137 | 'outcome' => $case->outcome ? get_string('outcome:' . $case->outcome, 'local_integritycase') : null, |
| 138 | 'youarethehandler' => transform::yesno((int) $case->handlerid === $userid), |
| 139 | 'youopenedit' => transform::yesno((int) $case->createdby === $userid), |
| 140 | 'timecreated' => transform::datetime($case->timecreated), |
| 141 | 'history' => $history, |
| 142 | ]; |
| 143 | $path = [get_string('pluginname', 'local_integritycase'), $case->id]; |
| 144 | if ($isstudent) { |
| 145 | $data->summary = $case->summary; |
| 146 | writer::with_context($context)->export_area_files($path, 'local_integritycase', 'evidence', $case->id); |
| 147 | } |
| 148 | writer::with_context($context)->export_data($path, $data); |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Delete every case in a course context. |
| 155 | * |
| 156 | * @param context $context |
| 157 | */ |
| 158 | public static function delete_data_for_all_users_in_context(context $context) { |
| 159 | global $DB; |
| 160 | if (!$context instanceof context_course) { |
| 161 | return; |
| 162 | } |
| 163 | foreach ($DB->get_fieldset_select('local_integritycase', 'id', 'courseid = ?', [$context->instanceid]) as $caseid) { |
| 164 | self::delete_case($context, (int) $caseid); |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Delete one user's data in the approved contexts. |
| 170 | * |
| 171 | * @param approved_contextlist $contextlist |
| 172 | */ |
| 173 | public static function delete_data_for_user(approved_contextlist $contextlist) { |
| 174 | foreach (self::course_contexts($contextlist->get_contexts()) as $context) { |
| 175 | self::delete_user($context, (int) $contextlist->get_user()->id); |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * Delete several users' data in one context. |
| 181 | * |
| 182 | * @param approved_userlist $userlist |
| 183 | */ |
| 184 | public static function delete_data_for_users(approved_userlist $userlist) { |
| 185 | $context = $userlist->get_context(); |
| 186 | if (!$context instanceof context_course) { |
| 187 | return; |
| 188 | } |
| 189 | foreach ($userlist->get_userids() as $userid) { |
| 190 | self::delete_user($context, (int) $userid); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * Remove a student's cases, and a staff member's name from the rest. |
| 196 | * |
| 197 | * @param context_course $context |
| 198 | * @param int $userid |
| 199 | */ |
| 200 | private static function delete_user(context_course $context, int $userid): void { |
| 201 | global $DB; |
| 202 | $courseid = $context->instanceid; |
| 203 | $own = $DB->get_fieldset_select('local_integritycase', 'id', 'courseid = ? AND studentid = ?', [$courseid, $userid]); |
| 204 | foreach ($own as $caseid) { |
| 205 | self::delete_case($context, (int) $caseid); |
| 206 | } |
| 207 | $DB->set_field('local_integritycase', 'handlerid', 0, ['courseid' => $courseid, 'handlerid' => $userid]); |
| 208 | $DB->set_field('local_integritycase', 'createdby', 0, ['courseid' => $courseid, 'createdby' => $userid]); |
| 209 | $DB->execute( |
| 210 | "UPDATE {local_integritycase_event} |
| 211 | SET userid = 0 |
| 212 | WHERE userid = :userid |
| 213 | AND caseid IN (SELECT id FROM {local_integritycase} WHERE courseid = :courseid)", |
| 214 | ['userid' => $userid, 'courseid' => $courseid] |
| 215 | ); |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Delete a case, its trail and its evidence. |
| 220 | * |
| 221 | * @param context_course $context |
| 222 | * @param int $caseid |
| 223 | */ |
| 224 | private static function delete_case(context_course $context, int $caseid): void { |
| 225 | global $DB; |
| 226 | get_file_storage()->delete_area_files($context->id, 'local_integritycase', 'evidence', $caseid); |
| 227 | $DB->delete_records('local_integritycase_event', ['caseid' => $caseid]); |
| 228 | $DB->delete_records('local_integritycase', ['id' => $caseid]); |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * Keep only course contexts. |
| 233 | * |
| 234 | * @param context[] $contexts |
| 235 | * @return context_course[] |
| 236 | */ |
| 237 | private static function course_contexts(array $contexts): array { |
| 238 | return array_filter($contexts, fn($c) => $c instanceof context_course); |
| 239 | } |
| 240 | } |