MDL-62218 analytics: Add missing sample origin checking
[moodle.git] / analytics / classes / privacy / provider.php
CommitLineData
97b0a6cb
DM
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Privacy Subsystem implementation for core_analytics.
19 *
20 * @package core_analytics
21 * @copyright 2018 David MonllaĆ³
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace core_analytics\privacy;
26
27use core_privacy\local\request\transform;
28use core_privacy\local\request\writer;
29use core_privacy\local\metadata\collection;
30use core_privacy\local\request\approved_contextlist;
31use core_privacy\local\request\context;
32use core_privacy\local\request\contextlist;
33
34defined('MOODLE_INTERNAL') || die();
35
36/**
37 * Privacy Subsystem for core_analytics implementing metadata and plugin providers.
38 *
39 * @copyright 2018 David MonllaĆ³
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 */
42class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
43
44 /**
45 * Returns meta data about this system.
46 *
47 * @param collection $collection The initialised collection to add items to.
48 * @return collection A listing of user data stored through this system.
49 */
50 public static function get_metadata(collection $collection) : collection {
51 $collection->add_database_table(
52 'analytics_indicator_calc',
53 [
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',
62 ],
63 'privacy:metadata:analytics:indicatorcalc'
64 );
65
66 $collection->add_database_table(
67 'analytics_predictions',
68 [
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',
79 ],
80 'privacy:metadata:analytics:predictions'
81 );
82
83 $collection->add_database_table(
84 'analytics_prediction_actions',
85 [
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',
90 ],
91 'privacy:metadata:analytics:predictionactions'
92 );
93
94 return $collection;
95 }
96
97 /**
98 * Get the list of contexts that contain user information for the specified user.
99 *
100 * @param int $userid The user to search.
101 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
102 */
103 public static function get_contexts_for_userid(int $userid) : contextlist {
104 global $DB;
105
106 $contextlist = new \core_privacy\local\request\contextlist();
107
108 $models = self::get_models_with_user_data();
109
110 foreach ($models as $modelid => $model) {
111
112 $analyser = $model->get_analyser(['notimesplitting' => true]);
113
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]);
120
121 // Indicator calculations.
122 $joinusersql = $analyser->join_sample_user('aic');
123 $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
124 {$joinusersql}
70e8515e
DM
125 WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin";
126 $contextlist->add_from_sql($sql, ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()]);
97b0a6cb
DM
127 }
128
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);
133
134 return $contextlist;
135 }
136
137 /**
138 * Export all user data for the specified user, in the specified contexts.
139 *
140 * @param approved_contextlist $contextlist The approved contexts to export information for.
141 */
142 public static function export_user_data(approved_contextlist $contextlist) {
143 global $DB;
144
145 $userid = intval($contextlist->get_user()->id);
146
147 $models = self::get_models_with_user_data();
148 $modelids = array_keys($models);
149
150 list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
151
152 $rootpath = [get_string('analytics', 'analytics')];
153 $ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
154
155 foreach ($models as $modelid => $model) {
156
157 $analyser = $model->get_analyser(['notimesplitting' => true]);
158
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);
167
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;
174
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),
182 ];
183 writer::with_context($context)->export_data($path, $data);
184 }
185 $predictions->close();
186
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}
70e8515e
DM
192 WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
193 $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $contextparams;
97b0a6cb
DM
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;
201
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),
210 ];
211 writer::with_context($context)->export_data($path, $data);
212 }
213 $indicatorcalculations->close();
214 }
215
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) {
222
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;
228
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),
234 ];
235 writer::with_context($context)->export_data($path, $data);
236 }
237 $predictionactions->close();
238 }
239
240 /**
241 * Delete all data for all users in the specified context.
242 *
243 * @param context $context The specific context to delete data for.
244 */
245 public static function delete_data_for_all_users_in_context(\context $context) {
246 global $DB;
247
248 $models = self::get_models_with_user_data();
249 $modelids = array_keys($models);
250
251 foreach ($models as $modelid => $model) {
252
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);
259
260 $DB->delete_records_select('analytics_prediction_actions', "predictionid IN ($idssql)", $idsparams);
261 $DB->delete_records_select('analytics_predictions', "id $predictionidssql", $params);
262 }
263 }
264
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]);
267 }
268
269 /**
270 * Delete all user data for the specified user, in the specified contexts.
271 *
272 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
273 */
274 public static function delete_data_for_user(approved_contextlist $contextlist) {
275 global $DB;
276
277 $userid = intval($contextlist->get_user()->id);
278
279 $models = self::get_models_with_user_data();
280 $modelids = array_keys($models);
281
282 list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
283
284 // Analytics prediction actions.
285 list($sql, $apaparams) = self::analytics_prediction_actions_sql($userid, $modelids, $contextsql);
286 $sql = "SELECT apa.id " . $sql;
287
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);
292 }
293
294 foreach ($models as $modelid => $model) {
295
296 $analyser = $model->get_analyser(['notimesplitting' => true]);
297
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}";
303
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);
308 }
309
310 // Indicator calculations.
311 $joinusersql = $analyser->join_sample_user('aic');
312 $sql = "SELECT DISTINCT aic.id FROM {analytics_indicator_calc} aic
313 {$joinusersql}
70e8515e 314 WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
97b0a6cb 315
70e8515e
DM
316 $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $contextparams;
317 $indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
97b0a6cb
DM
318 if ($indicatorcalcids) {
319 list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
320 $DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
321 }
322 }
323 }
324
325 /**
326 * Returns a list of models with user data.
327 *
328 * @return \core_analytics\model[]
329 */
330 private static function get_models_with_user_data() {
331 $models = \core_analytics\manager::get_all_models();
332 foreach ($models as $modelid => $model) {
333 $analyser = $model->get_analyser(['notimesplitting' => true]);
334 if (!$analyser->processes_user_data()) {
335 unset($models[$modelid]);
336 }
337 }
338 return $models;
339 }
340
341 /**
342 * Returns the sql query to query analytics_prediction_actions table.
343 *
344 * @param int $userid
345 * @param int[] $modelids
346 * @param string $contextsql
347 * @return array sql string in [0] and params in [1]
348 */
349 private static function analytics_prediction_actions_sql($userid, $modelids, $contextsql = false) {
350 global $DB;
351
352 list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
353 $sql = " FROM {analytics_predictions} ap
354 JOIN {context} ctx ON ctx.id = ap.contextid
355 JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
356 JOIN {analytics_models} am ON ap.modelid = am.id
357 WHERE apa.userid = :userid AND ap.modelid {$insql}";
358 $params['userid'] = $userid;
359
360 if ($contextsql) {
361 $sql .= " AND ap.contextid $contextsql";
362 }
363
364 return [$sql, $params];
365 }
366}