MDL-62218 analytics: Privacy API implementation
[moodle.git] / analytics / classes / privacy / provider.php
blob9900447efa113c9170c4f4fc563d0f0bd01a1c07
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.
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/>.
17 /**
18 * Privacy Subsystem implementation for core_analytics.
20 * @package core_analytics
21 * @copyright 2018 David MonllaĆ³
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 namespace core_analytics\privacy;
27 use core_privacy\local\request\transform;
28 use core_privacy\local\request\writer;
29 use core_privacy\local\metadata\collection;
30 use core_privacy\local\request\approved_contextlist;
31 use core_privacy\local\request\context;
32 use core_privacy\local\request\contextlist;
34 defined('MOODLE_INTERNAL') || die();
36 /**
37 * Privacy Subsystem for core_analytics implementing metadata and plugin providers.
39 * @copyright 2018 David MonllaĆ³
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
44 /**
45 * Returns meta data about this system.
47 * @param collection $collection The initialised collection to add items to.
48 * @return collection A listing of user data stored through this system.
50 public static function get_metadata(collection $collection) : collection {
51 $collection->add_database_table(
52 'analytics_indicator_calc',
54 'starttime' => 'privacy:metadata:analytics:indicatorcalc:starttime',
55 'endtime' => 'privacy:metadata:analytics:indicatorcalc:endtime',
56 'contextid' => 'privacy:metadata:analytics:indicatorcalc:contextid',
57 'sampleorigin' => 'privacy:metadata:analytics:indicatorcalc:sampleorigin',
58 'sampleid' => 'privacy:metadata:analytics:indicatorcalc:sampleid',
59 'indicator' => 'privacy:metadata:analytics:indicatorcalc:indicator',
60 'value' => 'privacy:metadata:analytics:indicatorcalc:value',
61 'timecreated' => 'privacy:metadata:analytics:indicatorcalc:timecreated',
63 'privacy:metadata:analytics:indicatorcalc'
66 $collection->add_database_table(
67 'analytics_predictions',
69 'modelid' => 'privacy:metadata:analytics:predictions:modelid',
70 'contextid' => 'privacy:metadata:analytics:predictions:contextid',
71 'sampleid' => 'privacy:metadata:analytics:predictions:sampleid',
72 'rangeindex' => 'privacy:metadata:analytics:predictions:rangeindex',
73 'prediction' => 'privacy:metadata:analytics:predictions:prediction',
74 'predictionscore' => 'privacy:metadata:analytics:predictions:predictionscore',
75 'calculations' => 'privacy:metadata:analytics:predictions:calculations',
76 'timecreated' => 'privacy:metadata:analytics:predictions:timecreated',
77 'timestart' => 'privacy:metadata:analytics:predictions:timestart',
78 'timeend' => 'privacy:metadata:analytics:predictions:timeend',
80 'privacy:metadata:analytics:predictions'
83 $collection->add_database_table(
84 'analytics_prediction_actions',
86 'predictionid' => 'privacy:metadata:analytics:predictionactions:predictionid',
87 'userid' => 'privacy:metadata:analytics:predictionactions:userid',
88 'actionname' => 'privacy:metadata:analytics:predictionactions:actionname',
89 'timecreated' => 'privacy:metadata:analytics:predictionactions:timecreated',
91 'privacy:metadata:analytics:predictionactions'
94 return $collection;
97 /**
98 * Get the list of contexts that contain user information for the specified user.
100 * @param int $userid The user to search.
101 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
103 public static function get_contexts_for_userid(int $userid) : contextlist {
104 global $DB;
106 $contextlist = new \core_privacy\local\request\contextlist();
108 $models = self::get_models_with_user_data();
110 foreach ($models as $modelid => $model) {
112 $analyser = $model->get_analyser(['notimesplitting' => true]);
114 // Analytics predictions.
115 $joinusersql = $analyser->join_sample_user('ap');
116 $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
117 {$joinusersql}
118 WHERE u.id = :userid AND ap.modelid = :modelid";
119 $contextlist->add_from_sql($sql, ['userid' => $userid, 'modelid' => $modelid]);
121 // Indicator calculations.
122 $joinusersql = $analyser->join_sample_user('aic');
123 $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
124 {$joinusersql}
125 WHERE u.id = :userid";
126 $contextlist->add_from_sql($sql, ['userid' => $userid]);
129 // We can leave this out of the loop as there is no analyser-dependant stuff.
130 list($sql, $params) = self::analytics_prediction_actions_sql($userid, array_keys($models));
131 $sql = "SELECT DISTINCT ap.contextid" . $sql;
132 $contextlist->add_from_sql($sql, $params);
134 return $contextlist;
138 * Export all user data for the specified user, in the specified contexts.
140 * @param approved_contextlist $contextlist The approved contexts to export information for.
142 public static function export_user_data(approved_contextlist $contextlist) {
143 global $DB;
145 $userid = intval($contextlist->get_user()->id);
147 $models = self::get_models_with_user_data();
148 $modelids = array_keys($models);
150 list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
152 $rootpath = [get_string('analytics', 'analytics')];
153 $ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
155 foreach ($models as $modelid => $model) {
157 $analyser = $model->get_analyser(['notimesplitting' => true]);
159 // Analytics predictions.
160 $joinusersql = $analyser->join_sample_user('ap');
161 $sql = "SELECT ap.*, $ctxfields FROM {analytics_predictions} ap
162 JOIN {context} ctx ON ctx.id = ap.contextid
163 {$joinusersql}
164 WHERE u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
165 $params = ['userid' => $userid, 'modelid' => $modelid] + $contextparams;
166 $predictions = $DB->get_recordset_sql($sql, $params);
168 foreach ($predictions as $prediction) {
169 \context_helper::preload_from_record($prediction);
170 $context = \context::instance_by_id($prediction->contextid);
171 $path = $rootpath;
172 $path[] = get_string('privacy:metadata:analytics:predictions', 'analytics');
173 $path[] = $prediction->id;
175 $data = (object)[
176 'target' => $model->get_target()->get_name()->out(),
177 'context' => $context->get_context_name(true, true),
178 'prediction' => $model->get_target()->get_display_value($prediction->prediction),
179 'timestart' => transform::datetime($prediction->timestart),
180 'timeend' => transform::datetime($prediction->timeend),
181 'timecreated' => transform::datetime($prediction->timecreated),
183 writer::with_context($context)->export_data($path, $data);
185 $predictions->close();
187 // Indicator calculations.
188 $joinusersql = $analyser->join_sample_user('aic');
189 $sql = "SELECT aic.*, $ctxfields FROM {analytics_indicator_calc} aic
190 JOIN {context} ctx ON ctx.id = aic.contextid
191 {$joinusersql}
192 WHERE u.id = :userid AND aic.contextid {$contextsql}";
193 $params = ['userid' => $userid] + $contextparams;
194 $indicatorcalculations = $DB->get_recordset_sql($sql, $params);
195 foreach ($indicatorcalculations as $calculation) {
196 \context_helper::preload_from_record($calculation);
197 $context = \context::instance_by_id($calculation->contextid);
198 $path = $rootpath;
199 $path[] = get_string('privacy:metadata:analytics:indicatorcalc', 'analytics');
200 $path[] = $calculation->id;
202 $indicator = \core_analytics\manager::get_indicator($calculation->indicator);
203 $data = (object)[
204 'indicator' => $indicator::get_name()->out(),
205 'context' => $context->get_context_name(true, true),
206 'calculation' => $indicator->get_display_value($calculation->value),
207 'starttime' => transform::datetime($calculation->starttime),
208 'endtime' => transform::datetime($calculation->endtime),
209 'timecreated' => transform::datetime($calculation->timecreated),
211 writer::with_context($context)->export_data($path, $data);
213 $indicatorcalculations->close();
216 // Analytics predictions.
217 // Provided contexts are ignored as we export all user-related stuff.
218 list($sql, $params) = self::analytics_prediction_actions_sql($userid, $modelids, $contextsql);
219 $sql = "SELECT apa.*, ap.modelid, ap.contextid, $ctxfields" . $sql;
220 $predictionactions = $DB->get_recordset_sql($sql, $params + $contextparams);
221 foreach ($predictionactions as $predictionaction) {
223 \context_helper::preload_from_record($predictionaction);
224 $context = \context::instance_by_id($predictionaction->contextid);
225 $path = $rootpath;
226 $path[] = get_string('privacy:metadata:analytics:predictionactions', 'analytics');
227 $path[] = $predictionaction->id;
229 $data = (object)[
230 'target' => $models[$predictionaction->modelid]->get_target()->get_name()->out(),
231 'context' => $context->get_context_name(true, true),
232 'action' => $predictionaction->actionname,
233 'timecreated' => transform::datetime($predictionaction->timecreated),
235 writer::with_context($context)->export_data($path, $data);
237 $predictionactions->close();
241 * Delete all data for all users in the specified context.
243 * @param context $context The specific context to delete data for.
245 public static function delete_data_for_all_users_in_context(\context $context) {
246 global $DB;
248 $models = self::get_models_with_user_data();
249 $modelids = array_keys($models);
251 foreach ($models as $modelid => $model) {
253 $idssql = "SELECT ap.id FROM {analytics_predictions} ap
254 WHERE ap.contextid = :contextid AND ap.modelid = :modelid";
255 $idsparams = ['contextid' => $context->id, 'modelid' => $modelid];
256 $predictionids = $DB->get_fieldset_sql($idssql, $idsparams);
257 if ($predictionids) {
258 list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
260 $DB->delete_records_select('analytics_prediction_actions', "predictionid IN ($idssql)", $idsparams);
261 $DB->delete_records_select('analytics_predictions', "id $predictionidssql", $params);
265 // We delete them all this table is just a cache and we don't know which model filled it.
266 $DB->delete_records('analytics_indicator_calc', ['contextid' => $context->id]);
270 * Delete all user data for the specified user, in the specified contexts.
272 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
274 public static function delete_data_for_user(approved_contextlist $contextlist) {
275 global $DB;
277 $userid = intval($contextlist->get_user()->id);
279 $models = self::get_models_with_user_data();
280 $modelids = array_keys($models);
282 list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
284 // Analytics prediction actions.
285 list($sql, $apaparams) = self::analytics_prediction_actions_sql($userid, $modelids, $contextsql);
286 $sql = "SELECT apa.id " . $sql;
288 $predictionactionids = $DB->get_fieldset_sql($sql, $apaparams + $contextparams);
289 if ($predictionactionids) {
290 list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
291 $DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
294 foreach ($models as $modelid => $model) {
296 $analyser = $model->get_analyser(['notimesplitting' => true]);
298 // Analytics predictions.
299 $joinusersql = $analyser->join_sample_user('ap');
300 $sql = "SELECT DISTINCT ap.id FROM {analytics_predictions} ap
301 {$joinusersql}
302 WHERE u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
304 $predictionids = $DB->get_fieldset_sql($sql, ['userid' => $userid, 'modelid' => $modelid] + $contextparams);
305 if ($predictionids) {
306 list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
307 $DB->delete_records_select('analytics_predictions', "id $predictionidssql", $params);
310 // Indicator calculations.
311 $joinusersql = $analyser->join_sample_user('aic');
312 $sql = "SELECT DISTINCT aic.id FROM {analytics_indicator_calc} aic
313 {$joinusersql}
314 WHERE u.id = :userid AND aic.contextid {$contextsql}";
316 $indicatorcalcids = $DB->get_fieldset_sql($sql, ['userid' => $userid] + $contextparams);
317 if ($indicatorcalcids) {
318 list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
319 $DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
325 * Returns a list of models with user data.
327 * @return \core_analytics\model[]
329 private static function get_models_with_user_data() {
330 $models = \core_analytics\manager::get_all_models();
331 foreach ($models as $modelid => $model) {
332 $analyser = $model->get_analyser(['notimesplitting' => true]);
333 if (!$analyser->processes_user_data()) {
334 unset($models[$modelid]);
337 return $models;
341 * Returns the sql query to query analytics_prediction_actions table.
343 * @param int $userid
344 * @param int[] $modelids
345 * @param string $contextsql
346 * @return array sql string in [0] and params in [1]
348 private static function analytics_prediction_actions_sql($userid, $modelids, $contextsql = false) {
349 global $DB;
351 list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
352 $sql = " FROM {analytics_predictions} ap
353 JOIN {context} ctx ON ctx.id = ap.contextid
354 JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
355 JOIN {analytics_models} am ON ap.modelid = am.id
356 WHERE apa.userid = :userid AND ap.modelid {$insql}";
357 $params['userid'] = $userid;
359 if ($contextsql) {
360 $sql .= " AND ap.contextid $contextsql";
363 return [$sql, $params];