MDL-78408 core: fix restoration of anchor to wantsurl during login
[moodle.git] / analytics / classes / privacy / provider.php
blob68e306db83c88d59cc79653eec603f94e7c1151a
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\approved_userlist;
32 use core_privacy\local\request\context;
33 use core_privacy\local\request\contextlist;
34 use core_privacy\local\request\userlist;
36 defined('MOODLE_INTERNAL') || die();
38 /**
39 * Privacy Subsystem for core_analytics implementing metadata and plugin providers.
41 * @copyright 2018 David MonllaĆ³
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 class provider implements
45 \core_privacy\local\metadata\provider,
46 \core_privacy\local\request\core_userlist_provider,
47 \core_privacy\local\request\plugin\provider {
49 /**
50 * Returns meta data about this system.
52 * @param collection $collection The initialised collection to add items to.
53 * @return collection A listing of user data stored through this system.
55 public static function get_metadata(collection $collection) : collection {
56 $collection->add_database_table(
57 'analytics_indicator_calc',
59 'starttime' => 'privacy:metadata:analytics:indicatorcalc:starttime',
60 'endtime' => 'privacy:metadata:analytics:indicatorcalc:endtime',
61 'contextid' => 'privacy:metadata:analytics:indicatorcalc:contextid',
62 'sampleorigin' => 'privacy:metadata:analytics:indicatorcalc:sampleorigin',
63 'sampleid' => 'privacy:metadata:analytics:indicatorcalc:sampleid',
64 'indicator' => 'privacy:metadata:analytics:indicatorcalc:indicator',
65 'value' => 'privacy:metadata:analytics:indicatorcalc:value',
66 'timecreated' => 'privacy:metadata:analytics:indicatorcalc:timecreated',
68 'privacy:metadata:analytics:indicatorcalc'
71 $collection->add_database_table(
72 'analytics_predictions',
74 'modelid' => 'privacy:metadata:analytics:predictions:modelid',
75 'contextid' => 'privacy:metadata:analytics:predictions:contextid',
76 'sampleid' => 'privacy:metadata:analytics:predictions:sampleid',
77 'rangeindex' => 'privacy:metadata:analytics:predictions:rangeindex',
78 'prediction' => 'privacy:metadata:analytics:predictions:prediction',
79 'predictionscore' => 'privacy:metadata:analytics:predictions:predictionscore',
80 'calculations' => 'privacy:metadata:analytics:predictions:calculations',
81 'timecreated' => 'privacy:metadata:analytics:predictions:timecreated',
82 'timestart' => 'privacy:metadata:analytics:predictions:timestart',
83 'timeend' => 'privacy:metadata:analytics:predictions:timeend',
85 'privacy:metadata:analytics:predictions'
88 $collection->add_database_table(
89 'analytics_prediction_actions',
91 'predictionid' => 'privacy:metadata:analytics:predictionactions:predictionid',
92 'userid' => 'privacy:metadata:analytics:predictionactions:userid',
93 'actionname' => 'privacy:metadata:analytics:predictionactions:actionname',
94 'timecreated' => 'privacy:metadata:analytics:predictionactions:timecreated',
96 'privacy:metadata:analytics:predictionactions'
99 // Regarding this block, we are unable to export or purge this data, as
100 // it would damage the analytics data across the whole site.
101 $collection->add_database_table(
102 'analytics_models',
104 'usermodified' => 'privacy:metadata:analytics:analyticsmodels:usermodified',
106 'privacy:metadata:analytics:analyticsmodels'
109 // Regarding this block, we are unable to export or purge this data, as
110 // it would damage the analytics log data across the whole site.
111 $collection->add_database_table(
112 'analytics_models_log',
114 'usermodified' => 'privacy:metadata:analytics:analyticsmodelslog:usermodified',
116 'privacy:metadata:analytics:analyticsmodelslog'
119 return $collection;
123 * Get the list of contexts that contain user information for the specified user.
125 * @param int $userid The user to search.
126 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
128 public static function get_contexts_for_userid(int $userid) : contextlist {
129 global $DB;
131 $contextlist = new \core_privacy\local\request\contextlist();
133 $models = self::get_models_with_user_data();
135 foreach ($models as $modelid => $model) {
137 $analyser = $model->get_analyser(['notimesplitting' => true]);
139 // Analytics predictions.
140 $joinusersql = $analyser->join_sample_user('ap');
141 $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
142 {$joinusersql}
143 WHERE u.id = :userid AND ap.modelid = :modelid";
144 $contextlist->add_from_sql($sql, ['userid' => $userid, 'modelid' => $modelid]);
146 // Indicator calculations.
147 $joinusersql = $analyser->join_sample_user('aic');
148 $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
149 {$joinusersql}
150 WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin";
151 $contextlist->add_from_sql($sql, ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()]);
154 // We can leave this out of the loop as there is no analyser-dependent stuff.
155 list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, array_keys($models));
156 $sql = "SELECT DISTINCT ap.contextid" . $sql;
157 $contextlist->add_from_sql($sql, $params);
159 return $contextlist;
163 * Get the list of users who have data within a context.
165 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
167 public static function get_users_in_context(userlist $userlist) {
168 global $DB;
170 $context = $userlist->get_context();
171 $models = self::get_models_with_user_data();
173 foreach ($models as $modelid => $model) {
175 $analyser = $model->get_analyser(['notimesplitting' => true]);
177 // Analytics predictions.
178 $params = [
179 'contextid' => $context->id,
180 'modelid' => $modelid,
182 $joinusersql = $analyser->join_sample_user('ap');
183 $sql = "SELECT u.id AS userid
184 FROM {analytics_predictions} ap
185 {$joinusersql}
186 WHERE ap.contextid = :contextid
187 AND ap.modelid = :modelid";
188 $userlist->add_from_sql('userid', $sql, $params);
190 // Indicator calculations.
191 $params = [
192 'contextid' => $context->id,
193 'analysersamplesorigin' => $analyser->get_samples_origin(),
195 $joinusersql = $analyser->join_sample_user('aic');
196 $sql = "SELECT u.id AS userid
197 FROM {analytics_indicator_calc} aic
198 {$joinusersql}
199 WHERE aic.contextid = :contextid
200 AND aic.sampleorigin = :analysersamplesorigin";
201 $userlist->add_from_sql('userid', $sql, $params);
204 // We can leave this out of the loop as there is no analyser-dependent stuff.
205 list($sql, $params) = self::analytics_prediction_actions_context_sql($context->id, array_keys($models));
206 $sql = "SELECT apa.userid" . $sql;
207 $userlist->add_from_sql('userid', $sql, $params);
211 * Export all user data for the specified user, in the specified contexts.
213 * @param approved_contextlist $contextlist The approved contexts to export information for.
215 public static function export_user_data(approved_contextlist $contextlist) {
216 global $DB;
218 $userid = intval($contextlist->get_user()->id);
220 $models = self::get_models_with_user_data();
221 $modelids = array_keys($models);
223 list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
225 $rootpath = [get_string('analytics', 'analytics')];
226 $ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
228 foreach ($models as $modelid => $model) {
230 $analyser = $model->get_analyser(['notimesplitting' => true]);
232 // Analytics predictions.
233 $joinusersql = $analyser->join_sample_user('ap');
234 $sql = "SELECT ap.*, $ctxfields FROM {analytics_predictions} ap
235 JOIN {context} ctx ON ctx.id = ap.contextid
236 {$joinusersql}
237 WHERE u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
238 $params = ['userid' => $userid, 'modelid' => $modelid] + $contextparams;
239 $predictions = $DB->get_recordset_sql($sql, $params);
241 foreach ($predictions as $prediction) {
242 \context_helper::preload_from_record($prediction);
243 $context = \context::instance_by_id($prediction->contextid);
244 $path = $rootpath;
245 $path[] = get_string('privacy:metadata:analytics:predictions', 'analytics');
246 $path[] = $prediction->id;
248 $data = (object)[
249 'target' => $model->get_target()->get_name()->out(),
250 'context' => $context->get_context_name(true, true),
251 'prediction' => $model->get_target()->get_display_value($prediction->prediction),
252 'timestart' => transform::datetime($prediction->timestart),
253 'timeend' => transform::datetime($prediction->timeend),
254 'timecreated' => transform::datetime($prediction->timecreated),
256 writer::with_context($context)->export_data($path, $data);
258 $predictions->close();
260 // Indicator calculations.
261 $joinusersql = $analyser->join_sample_user('aic');
262 $sql = "SELECT aic.*, $ctxfields FROM {analytics_indicator_calc} aic
263 JOIN {context} ctx ON ctx.id = aic.contextid
264 {$joinusersql}
265 WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
266 $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $contextparams;
267 $indicatorcalculations = $DB->get_recordset_sql($sql, $params);
268 foreach ($indicatorcalculations as $calculation) {
269 \context_helper::preload_from_record($calculation);
270 $context = \context::instance_by_id($calculation->contextid);
271 $path = $rootpath;
272 $path[] = get_string('privacy:metadata:analytics:indicatorcalc', 'analytics');
273 $path[] = $calculation->id;
275 $indicator = \core_analytics\manager::get_indicator($calculation->indicator);
276 $data = (object)[
277 'indicator' => $indicator::get_name()->out(),
278 'context' => $context->get_context_name(true, true),
279 'calculation' => $indicator->get_display_value($calculation->value),
280 'starttime' => transform::datetime($calculation->starttime),
281 'endtime' => transform::datetime($calculation->endtime),
282 'timecreated' => transform::datetime($calculation->timecreated),
284 writer::with_context($context)->export_data($path, $data);
286 $indicatorcalculations->close();
289 // Analytics predictions.
290 // Provided contexts are ignored as we export all user-related stuff.
291 list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
292 $sql = "SELECT apa.*, ap.modelid, ap.contextid, $ctxfields" . $sql;
293 $predictionactions = $DB->get_recordset_sql($sql, $params + $contextparams);
294 foreach ($predictionactions as $predictionaction) {
296 \context_helper::preload_from_record($predictionaction);
297 $context = \context::instance_by_id($predictionaction->contextid);
298 $path = $rootpath;
299 $path[] = get_string('privacy:metadata:analytics:predictionactions', 'analytics');
300 $path[] = $predictionaction->id;
302 $data = (object)[
303 'target' => $models[$predictionaction->modelid]->get_target()->get_name()->out(),
304 'context' => $context->get_context_name(true, true),
305 'action' => $predictionaction->actionname,
306 'timecreated' => transform::datetime($predictionaction->timecreated),
308 writer::with_context($context)->export_data($path, $data);
310 $predictionactions->close();
314 * Delete all data for all users in the specified context.
316 * @param context $context The specific context to delete data for.
318 public static function delete_data_for_all_users_in_context(\context $context) {
319 global $DB;
321 $models = self::get_models_with_user_data();
322 $modelids = array_keys($models);
324 foreach ($models as $modelid => $model) {
326 $idssql = "SELECT ap.id FROM {analytics_predictions} ap
327 WHERE ap.contextid = :contextid AND ap.modelid = :modelid";
328 $idsparams = ['contextid' => $context->id, 'modelid' => $modelid];
330 $DB->delete_records_select('analytics_prediction_actions', "predictionid IN ($idssql)", $idsparams);
331 $DB->delete_records_select('analytics_predictions', "contextid = :contextid AND modelid = :modelid", $idsparams);
334 // We delete them all this table is just a cache and we don't know which model filled it.
335 $DB->delete_records('analytics_indicator_calc', ['contextid' => $context->id]);
339 * Delete all user data for the specified user, in the specified contexts.
341 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
343 public static function delete_data_for_user(approved_contextlist $contextlist) {
344 global $DB;
346 $userid = intval($contextlist->get_user()->id);
348 $models = self::get_models_with_user_data();
349 $modelids = array_keys($models);
351 list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
353 // Analytics prediction actions.
354 list($sql, $apaparams) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
355 $sql = "SELECT apa.id " . $sql;
357 $predictionactionids = $DB->get_fieldset_sql($sql, $apaparams + $contextparams);
358 if ($predictionactionids) {
359 list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
360 $DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
363 foreach ($models as $modelid => $model) {
365 $analyser = $model->get_analyser(['notimesplitting' => true]);
367 // Analytics predictions.
368 $joinusersql = $analyser->join_sample_user('ap');
369 $sql = "SELECT DISTINCT ap.id FROM {analytics_predictions} ap
370 {$joinusersql}
371 WHERE u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
373 $predictionids = $DB->get_fieldset_sql($sql, ['userid' => $userid, 'modelid' => $modelid] + $contextparams);
374 if ($predictionids) {
375 list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
376 $DB->delete_records_select('analytics_predictions', "id $predictionidssql", $params);
379 // Indicator calculations.
380 $joinusersql = $analyser->join_sample_user('aic');
381 $sql = "SELECT DISTINCT aic.id FROM {analytics_indicator_calc} aic
382 {$joinusersql}
383 WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
385 $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $contextparams;
386 $indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
387 if ($indicatorcalcids) {
388 list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
389 $DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
395 * Delete multiple users within a single context.
397 * @param approved_userlist $userlist The approved context and user information to delete information for.
399 public static function delete_data_for_users(approved_userlist $userlist) {
400 global $DB;
402 $context = $userlist->get_context();
403 $models = self::get_models_with_user_data();
404 $modelids = array_keys($models);
405 list($usersinsql, $baseparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
407 // Analytics prediction actions.
408 list($sql, $apaparams) = self::analytics_prediction_actions_context_sql($context->id, $modelids, $usersinsql);
409 $sql = "SELECT apa.id" . $sql;
410 $predictionactionids = $DB->get_fieldset_sql($sql, $baseparams + $apaparams);
412 if ($predictionactionids) {
413 list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
414 $DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
417 $baseparams['contextid'] = $context->id;
419 foreach ($models as $modelid => $model) {
420 $analyser = $model->get_analyser(['notimesplitting' => true]);
422 // Analytics predictions.
423 $joinusersql = $analyser->join_sample_user('ap');
424 $sql = "SELECT DISTINCT ap.id
425 FROM {analytics_predictions} ap
426 {$joinusersql}
427 WHERE ap.contextid = :contextid
428 AND ap.modelid = :modelid
429 AND u.id {$usersinsql}";
430 $params = $baseparams;
431 $params['modelid'] = $modelid;
432 $predictionids = $DB->get_fieldset_sql($sql, $params);
434 if ($predictionids) {
435 list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
436 $DB->delete_records_select('analytics_predictions', "id {$predictionidssql}", $params);
439 // Indicator calculations.
440 $joinusersql = $analyser->join_sample_user('aic');
441 $sql = "SELECT DISTINCT aic.id
442 FROM {analytics_indicator_calc} aic
443 {$joinusersql}
444 WHERE aic.contextid = :contextid
445 AND aic.sampleorigin = :analysersamplesorigin
446 AND u.id {$usersinsql}";
447 $params = $baseparams;
448 $params['analysersamplesorigin'] = $analyser->get_samples_origin();
449 $indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
451 if ($indicatorcalcids) {
452 list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
453 $DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
459 * Returns a list of models with user data.
461 * @return \core_analytics\model[]
463 private static function get_models_with_user_data() {
464 $models = \core_analytics\manager::get_all_models();
465 foreach ($models as $modelid => $model) {
466 $analyser = $model->get_analyser(['notimesplitting' => true]);
467 if (!$analyser->processes_user_data()) {
468 unset($models[$modelid]);
471 return $models;
475 * Returns the sql query to query analytics_prediction_actions table by user ID.
477 * @param int $userid The user ID of the analytics prediction.
478 * @param int[] $modelids Model IDs to include in the SQL.
479 * @param string $contextsql Optional "in or equal" SQL to also query by context ID(s).
480 * @return array sql string in [0] and params in [1].
482 private static function analytics_prediction_actions_user_sql($userid, $modelids, $contextsql = false) {
483 global $DB;
485 list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
486 $sql = " FROM {analytics_predictions} ap
487 JOIN {context} ctx ON ctx.id = ap.contextid
488 JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
489 JOIN {analytics_models} am ON ap.modelid = am.id
490 WHERE apa.userid = :userid AND ap.modelid {$insql}";
491 $params['userid'] = $userid;
493 if ($contextsql) {
494 $sql .= " AND ap.contextid $contextsql";
497 return [$sql, $params];
501 * Returns the sql query to query analytics_prediction_actions table by context ID.
503 * @param int $contextid The context ID of the analytics prediction.
504 * @param int[] $modelids Model IDs to include in the SQL.
505 * @param string $usersql Optional "in or equal" SQL to also query by user ID(s).
506 * @return array sql string in [0] and params in [1].
508 private static function analytics_prediction_actions_context_sql($contextid, $modelids, $usersql = false) {
509 global $DB;
511 list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
512 $sql = " FROM {analytics_predictions} ap
513 JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
514 WHERE ap.contextid = :contextid
515 AND ap.modelid {$insql}";
516 $params['contextid'] = $contextid;
518 if ($usersql) {
519 $sql .= " AND apa.userid {$usersql}";
522 return [$sql, $params];