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\approved_userlist;
32use core_privacy\local\request\context;
33use core_privacy\local\request\contextlist;
34use core_privacy\local\request\userlist;
35
36defined('MOODLE_INTERNAL') || die();
37
38/**
39 * Privacy Subsystem for core_analytics implementing metadata and plugin providers.
40 *
41 * @copyright  2018 David Monllaó
42 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 */
44class provider implements
45        \core_privacy\local\metadata\provider,
46        \core_privacy\local\request\core_userlist_provider,
47        \core_privacy\local\request\plugin\provider {
48
49    /**
50     * Returns meta data about this system.
51     *
52     * @param   collection $collection The initialised collection to add items to.
53     * @return  collection     A listing of user data stored through this system.
54     */
55    public static function get_metadata(collection $collection) : collection {
56        $collection->add_database_table(
57            'analytics_indicator_calc',
58            [
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',
67            ],
68            'privacy:metadata:analytics:indicatorcalc'
69        );
70
71        $collection->add_database_table(
72            'analytics_predictions',
73            [
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',
84            ],
85            'privacy:metadata:analytics:predictions'
86        );
87
88        $collection->add_database_table(
89            'analytics_prediction_actions',
90            [
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',
95            ],
96            'privacy:metadata:analytics:predictionactions'
97        );
98
99        return $collection;
100    }
101
102    /**
103     * Get the list of contexts that contain user information for the specified user.
104     *
105     * @param   int $userid The user to search.
106     * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
107     */
108    public static function get_contexts_for_userid(int $userid) : contextlist {
109        global $DB;
110
111        $contextlist = new \core_privacy\local\request\contextlist();
112
113        $models = self::get_models_with_user_data();
114
115        foreach ($models as $modelid => $model) {
116
117            $analyser = $model->get_analyser(['notimesplitting' => true]);
118
119            // Analytics predictions.
120            $joinusersql = $analyser->join_sample_user('ap');
121            $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
122                      {$joinusersql}
123                     WHERE u.id = :userid AND ap.modelid = :modelid";
124            $contextlist->add_from_sql($sql, ['userid' => $userid, 'modelid' => $modelid]);
125
126            // Indicator calculations.
127            $joinusersql = $analyser->join_sample_user('aic');
128            $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
129                      {$joinusersql}
130                     WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin";
131            $contextlist->add_from_sql($sql, ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()]);
132        }
133
134        // We can leave this out of the loop as there is no analyser-dependent stuff.
135        list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, array_keys($models));
136        $sql = "SELECT DISTINCT ap.contextid" . $sql;
137        $contextlist->add_from_sql($sql, $params);
138
139        return $contextlist;
140    }
141
142    /**
143     * Get the list of users who have data within a context.
144     *
145     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
146     */
147    public static function get_users_in_context(userlist $userlist) {
148        global $DB;
149
150        $context = $userlist->get_context();
151        $models = self::get_models_with_user_data();
152
153        foreach ($models as $modelid => $model) {
154
155            $analyser = $model->get_analyser(['notimesplitting' => true]);
156
157            // Analytics predictions.
158            $params = [
159                'contextid' => $context->id,
160                'modelid' => $modelid,
161            ];
162            $joinusersql = $analyser->join_sample_user('ap');
163            $sql = "SELECT u.id AS userid
164                      FROM {analytics_predictions} ap
165                           {$joinusersql}
166                     WHERE ap.contextid = :contextid
167                       AND ap.modelid = :modelid";
168            $userlist->add_from_sql('userid', $sql, $params);
169
170            // Indicator calculations.
171            $params = [
172                'contextid' => $context->id,
173                'analysersamplesorigin' => $analyser->get_samples_origin(),
174            ];
175            $joinusersql = $analyser->join_sample_user('aic');
176            $sql = "SELECT u.id AS userid
177                      FROM {analytics_indicator_calc} aic
178                           {$joinusersql}
179                     WHERE aic.contextid = :contextid
180                       AND aic.sampleorigin = :analysersamplesorigin";
181            $userlist->add_from_sql('userid', $sql, $params);
182        }
183
184        // We can leave this out of the loop as there is no analyser-dependent stuff.
185        list($sql, $params) = self::analytics_prediction_actions_context_sql($context->id, array_keys($models));
186        $sql = "SELECT apa.userid" . $sql;
187        $userlist->add_from_sql('userid', $sql, $params);
188    }
189
190    /**
191     * Export all user data for the specified user, in the specified contexts.
192     *
193     * @param   approved_contextlist $contextlist The approved contexts to export information for.
194     */
195    public static function export_user_data(approved_contextlist $contextlist) {
196        global $DB;
197
198        $userid = intval($contextlist->get_user()->id);
199
200        $models = self::get_models_with_user_data();
201        $modelids = array_keys($models);
202
203        list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
204
205        $rootpath = [get_string('analytics', 'analytics')];
206        $ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
207
208        foreach ($models as $modelid => $model) {
209
210            $analyser = $model->get_analyser(['notimesplitting' => true]);
211
212            // Analytics predictions.
213            $joinusersql = $analyser->join_sample_user('ap');
214            $sql = "SELECT ap.*, $ctxfields FROM {analytics_predictions} ap
215                      JOIN {context} ctx ON ctx.id = ap.contextid
216                      {$joinusersql}
217                     WHERE u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
218            $params = ['userid' => $userid, 'modelid' => $modelid] + $contextparams;
219            $predictions = $DB->get_recordset_sql($sql, $params);
220
221            foreach ($predictions as $prediction) {
222                \context_helper::preload_from_record($prediction);
223                $context = \context::instance_by_id($prediction->contextid);
224                $path = $rootpath;
225                $path[] = get_string('privacy:metadata:analytics:predictions', 'analytics');
226                $path[] = $prediction->id;
227
228                $data = (object)[
229                    'target' => $model->get_target()->get_name()->out(),
230                    'context' => $context->get_context_name(true, true),
231                    'prediction' => $model->get_target()->get_display_value($prediction->prediction),
232                    'timestart' => transform::datetime($prediction->timestart),
233                    'timeend' => transform::datetime($prediction->timeend),
234                    'timecreated' => transform::datetime($prediction->timecreated),
235                ];
236                writer::with_context($context)->export_data($path, $data);
237            }
238            $predictions->close();
239
240            // Indicator calculations.
241            $joinusersql = $analyser->join_sample_user('aic');
242            $sql = "SELECT aic.*, $ctxfields FROM {analytics_indicator_calc} aic
243                      JOIN {context} ctx ON ctx.id = aic.contextid
244                      {$joinusersql}
245                     WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
246            $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $contextparams;
247            $indicatorcalculations = $DB->get_recordset_sql($sql, $params);
248            foreach ($indicatorcalculations as $calculation) {
249                \context_helper::preload_from_record($calculation);
250                $context = \context::instance_by_id($calculation->contextid);
251                $path = $rootpath;
252                $path[] = get_string('privacy:metadata:analytics:indicatorcalc', 'analytics');
253                $path[] = $calculation->id;
254
255                $indicator = \core_analytics\manager::get_indicator($calculation->indicator);
256                $data = (object)[
257                    'indicator' => $indicator::get_name()->out(),
258                    'context' => $context->get_context_name(true, true),
259                    'calculation' => $indicator->get_display_value($calculation->value),
260                    'starttime' => transform::datetime($calculation->starttime),
261                    'endtime' => transform::datetime($calculation->endtime),
262                    'timecreated' => transform::datetime($calculation->timecreated),
263                ];
264                writer::with_context($context)->export_data($path, $data);
265            }
266            $indicatorcalculations->close();
267        }
268
269        // Analytics predictions.
270        // Provided contexts are ignored as we export all user-related stuff.
271        list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
272        $sql = "SELECT apa.*, ap.modelid, ap.contextid, $ctxfields" . $sql;
273        $predictionactions = $DB->get_recordset_sql($sql, $params + $contextparams);
274        foreach ($predictionactions as $predictionaction) {
275
276            \context_helper::preload_from_record($predictionaction);
277            $context = \context::instance_by_id($predictionaction->contextid);
278            $path = $rootpath;
279            $path[] = get_string('privacy:metadata:analytics:predictionactions', 'analytics');
280            $path[] = $predictionaction->id;
281
282            $data = (object)[
283                'target' => $models[$predictionaction->modelid]->get_target()->get_name()->out(),
284                'context' => $context->get_context_name(true, true),
285                'action' => $predictionaction->actionname,
286                'timecreated' => transform::datetime($predictionaction->timecreated),
287            ];
288            writer::with_context($context)->export_data($path, $data);
289        }
290        $predictionactions->close();
291    }
292
293    /**
294     * Delete all data for all users in the specified context.
295     *
296     * @param   context $context The specific context to delete data for.
297     */
298    public static function delete_data_for_all_users_in_context(\context $context) {
299        global $DB;
300
301        $models = self::get_models_with_user_data();
302        $modelids = array_keys($models);
303
304        foreach ($models as $modelid => $model) {
305
306            $idssql = "SELECT ap.id FROM {analytics_predictions} ap
307                        WHERE ap.contextid = :contextid AND ap.modelid = :modelid";
308            $idsparams = ['contextid' => $context->id, 'modelid' => $modelid];
309
310            $DB->delete_records_select('analytics_prediction_actions', "predictionid IN ($idssql)", $idsparams);
311            $DB->delete_records_select('analytics_predictions', "contextid = :contextid AND modelid = :modelid", $idsparams);
312        }
313
314        // We delete them all this table is just a cache and we don't know which model filled it.
315        $DB->delete_records('analytics_indicator_calc', ['contextid' => $context->id]);
316    }
317
318    /**
319     * Delete all user data for the specified user, in the specified contexts.
320     *
321     * @param   approved_contextlist $contextlist The approved contexts and user information to delete information for.
322     */
323    public static function delete_data_for_user(approved_contextlist $contextlist) {
324        global $DB;
325
326        $userid = intval($contextlist->get_user()->id);
327
328        $models = self::get_models_with_user_data();
329        $modelids = array_keys($models);
330
331        list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
332
333        // Analytics prediction actions.
334        list($sql, $apaparams) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
335        $sql = "SELECT apa.id " . $sql;
336
337        $predictionactionids = $DB->get_fieldset_sql($sql, $apaparams + $contextparams);
338        if ($predictionactionids) {
339            list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
340            $DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
341        }
342
343        foreach ($models as $modelid => $model) {
344
345            $analyser = $model->get_analyser(['notimesplitting' => true]);
346
347            // Analytics predictions.
348            $joinusersql = $analyser->join_sample_user('ap');
349            $sql = "SELECT DISTINCT ap.id FROM {analytics_predictions} ap
350                      {$joinusersql}
351                     WHERE u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
352
353            $predictionids = $DB->get_fieldset_sql($sql, ['userid' => $userid, 'modelid' => $modelid] + $contextparams);
354            if ($predictionids) {
355                list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
356                $DB->delete_records_select('analytics_predictions', "id $predictionidssql", $params);
357            }
358
359            // Indicator calculations.
360            $joinusersql = $analyser->join_sample_user('aic');
361            $sql = "SELECT DISTINCT aic.id FROM {analytics_indicator_calc} aic
362                      {$joinusersql}
363                     WHERE u.id = :userid AND aic.sampleorigin = :analysersamplesorigin AND aic.contextid {$contextsql}";
364
365            $params = ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()] + $contextparams;
366            $indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
367            if ($indicatorcalcids) {
368                list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
369                $DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
370            }
371        }
372    }
373
374    /**
375     * Delete multiple users within a single context.
376     *
377     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
378     */
379    public static function delete_data_for_users(approved_userlist $userlist) {
380        global $DB;
381
382        $context = $userlist->get_context();
383        $models = self::get_models_with_user_data();
384        $modelids = array_keys($models);
385        list($usersinsql, $baseparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
386
387        // Analytics prediction actions.
388        list($sql, $apaparams) = self::analytics_prediction_actions_context_sql($context->id, $modelids, $usersinsql);
389        $sql = "SELECT apa.id" . $sql;
390        $predictionactionids = $DB->get_fieldset_sql($sql, $baseparams + $apaparams);
391
392        if ($predictionactionids) {
393            list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
394            $DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
395        }
396
397        $baseparams['contextid'] = $context->id;
398
399        foreach ($models as $modelid => $model) {
400            $analyser = $model->get_analyser(['notimesplitting' => true]);
401
402            // Analytics predictions.
403            $joinusersql = $analyser->join_sample_user('ap');
404            $sql = "SELECT DISTINCT ap.id
405                      FROM {analytics_predictions} ap
406                           {$joinusersql}
407                     WHERE ap.contextid = :contextid
408                       AND ap.modelid = :modelid
409                       AND u.id {$usersinsql}";
410            $params = $baseparams;
411            $params['modelid'] = $modelid;
412            $predictionids = $DB->get_fieldset_sql($sql, $params);
413
414            if ($predictionids) {
415                list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
416                $DB->delete_records_select('analytics_predictions', "id {$predictionidssql}", $params);
417            }
418
419            // Indicator calculations.
420            $joinusersql = $analyser->join_sample_user('aic');
421            $sql = "SELECT DISTINCT aic.id
422                      FROM {analytics_indicator_calc} aic
423                           {$joinusersql}
424                     WHERE aic.contextid = :contextid
425                       AND aic.sampleorigin = :analysersamplesorigin
426                       AND u.id {$usersinsql}";
427            $params = $baseparams;
428            $params['analysersamplesorigin'] = $analyser->get_samples_origin();
429            $indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
430
431            if ($indicatorcalcids) {
432                list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
433                $DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
434            }
435        }
436    }
437
438    /**
439     * Returns a list of models with user data.
440     *
441     * @return \core_analytics\model[]
442     */
443    private static function get_models_with_user_data() {
444        $models = \core_analytics\manager::get_all_models();
445        foreach ($models as $modelid => $model) {
446            $analyser = $model->get_analyser(['notimesplitting' => true]);
447            if (!$analyser->processes_user_data()) {
448                unset($models[$modelid]);
449            }
450        }
451        return $models;
452    }
453
454    /**
455     * Returns the sql query to query analytics_prediction_actions table by user ID.
456     *
457     * @param int $userid The user ID of the analytics prediction.
458     * @param int[] $modelids Model IDs to include in the SQL.
459     * @param string $contextsql Optional "in or equal" SQL to also query by context ID(s).
460     * @return array sql string in [0] and params in [1].
461     */
462    private static function analytics_prediction_actions_user_sql($userid, $modelids, $contextsql = false) {
463        global $DB;
464
465        list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
466        $sql = " FROM {analytics_predictions} ap
467                  JOIN {context} ctx ON ctx.id = ap.contextid
468                  JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
469                  JOIN {analytics_models} am ON ap.modelid = am.id
470                 WHERE apa.userid = :userid AND ap.modelid {$insql}";
471        $params['userid'] = $userid;
472
473        if ($contextsql) {
474            $sql .= " AND ap.contextid $contextsql";
475        }
476
477        return [$sql, $params];
478    }
479
480    /**
481     * Returns the sql query to query analytics_prediction_actions table by context ID.
482     *
483     * @param int $contextid The context ID of the analytics prediction.
484     * @param int[] $modelids Model IDs to include in the SQL.
485     * @param string $usersql Optional "in or equal" SQL to also query by user ID(s).
486     * @return array sql string in [0] and params in [1].
487     */
488    private static function analytics_prediction_actions_context_sql($contextid, $modelids, $usersql = false) {
489        global $DB;
490
491        list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
492        $sql = " FROM {analytics_predictions} ap
493                  JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
494                 WHERE ap.contextid = :contextid
495                   AND ap.modelid {$insql}";
496        $params['contextid'] = $contextid;
497
498        if ($usersql) {
499            $sql .= " AND apa.userid {$usersql}";
500        }
501
502        return [$sql, $params];
503    }
504}
505