MDL-62218 analytics: Add missing sample origin checking
[moodle.git] / analytics / classes / privacy / provider.php
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/>.
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  */
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.
38  *
39  * @copyright  2018 David Monlla├│
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
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         );
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         );
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         );
94         return $collection;
95     }
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;
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 AND aic.sampleorigin = :analysersamplesorigin";
126             $contextlist->add_from_sql($sql, ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()]);
127         }
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;
135     }
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;
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),
182                 ];
183                 writer::with_context($context)->export_data($path, $data);
184             }
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.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
193             $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $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),
210                 ];
211                 writer::with_context($context)->export_data($path, $data);
212             }
213             $indicatorcalculations->close();
214         }
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),
234             ];
235             writer::with_context($context)->export_data($path, $data);
236         }
237         $predictionactions->close();
238     }
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;
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);
262             }
263         }
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     }
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;
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);
292         }
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);
308             }
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.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
316             $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $contextparams;
317             $indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
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     }
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     }
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;
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;
360         if ($contextsql) {
361             $sql .= " AND ap.contextid $contextsql";
362         }
364         return [$sql, $params];
365     }