Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
25
namespace core_analytics\privacy;
26
 
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;
35
 
36
defined('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
 */
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 {
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
        // 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',
103
            [
104
                'usermodified' => 'privacy:metadata:analytics:analyticsmodels:usermodified',
105
            ],
106
            'privacy:metadata:analytics:analyticsmodels'
107
        );
108
 
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',
113
            [
114
                'usermodified' => 'privacy:metadata:analytics:analyticsmodelslog:usermodified',
115
            ],
116
            'privacy:metadata:analytics:analyticsmodelslog'
117
        );
118
 
119
        return $collection;
120
    }
121
 
122
    /**
123
     * Get the list of contexts that contain user information for the specified user.
124
     *
125
     * @param   int $userid The user to search.
126
     * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
127
     */
128
    public static function get_contexts_for_userid(int $userid): contextlist {
129
        global $DB;
130
 
131
        $contextlist = new \core_privacy\local\request\contextlist();
132
 
133
        $models = self::get_models_with_user_data();
134
 
135
        foreach ($models as $modelid => $model) {
136
 
137
            $analyser = $model->get_analyser(['notimesplitting' => true]);
138
 
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]);
145
 
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()]);
152
        }
153
 
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);
158
 
159
        return $contextlist;
160
    }
161
 
162
    /**
163
     * Get the list of users who have data within a context.
164
     *
165
     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
166
     */
167
    public static function get_users_in_context(userlist $userlist) {
168
        global $DB;
169
 
170
        $context = $userlist->get_context();
171
        $models = self::get_models_with_user_data();
172
 
173
        foreach ($models as $modelid => $model) {
174
 
175
            $analyser = $model->get_analyser(['notimesplitting' => true]);
176
 
177
            // Analytics predictions.
178
            $params = [
179
                'contextid' => $context->id,
180
                'modelid' => $modelid,
181
            ];
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);
189
 
190
            // Indicator calculations.
191
            $params = [
192
                'contextid' => $context->id,
193
                'analysersamplesorigin' => $analyser->get_samples_origin(),
194
            ];
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);
202
        }
203
 
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);
208
    }
209
 
210
    /**
211
     * Export all user data for the specified user, in the specified contexts.
212
     *
213
     * @param   approved_contextlist $contextlist The approved contexts to export information for.
214
     */
215
    public static function export_user_data(approved_contextlist $contextlist) {
216
        global $DB;
217
 
218
        $userid = intval($contextlist->get_user()->id);
219
 
220
        $models = self::get_models_with_user_data();
221
        $modelids = array_keys($models);
222
 
223
        list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
224
 
225
        $rootpath = [get_string('analytics', 'analytics')];
226
        $ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
227
 
228
        foreach ($models as $modelid => $model) {
229
 
230
            $analyser = $model->get_analyser(['notimesplitting' => true]);
231
 
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);
240
 
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;
247
 
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),
255
                ];
256
                writer::with_context($context)->export_data($path, $data);
257
            }
258
            $predictions->close();
259
 
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;
274
 
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),
283
                ];
284
                writer::with_context($context)->export_data($path, $data);
285
            }
286
            $indicatorcalculations->close();
287
        }
288
 
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) {
295
 
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;
301
 
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),
307
            ];
308
            writer::with_context($context)->export_data($path, $data);
309
        }
310
        $predictionactions->close();
311
    }
312
 
313
    /**
314
     * Delete all data for all users in the specified context.
315
     *
316
     * @param   context $context The specific context to delete data for.
317
     */
318
    public static function delete_data_for_all_users_in_context(\context $context) {
319
        global $DB;
320
 
321
        $models = self::get_models_with_user_data();
322
        $modelids = array_keys($models);
323
 
324
        foreach ($models as $modelid => $model) {
325
 
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];
329
 
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);
332
        }
333
 
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]);
336
    }
337
 
338
    /**
339
     * Delete all user data for the specified user, in the specified contexts.
340
     *
341
     * @param   approved_contextlist $contextlist The approved contexts and user information to delete information for.
342
     */
343
    public static function delete_data_for_user(approved_contextlist $contextlist) {
344
        global $DB;
345
 
346
        $userid = intval($contextlist->get_user()->id);
347
 
348
        $models = self::get_models_with_user_data();
349
        $modelids = array_keys($models);
350
 
351
        list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
352
 
353
        // Analytics prediction actions.
354
        list($sql, $apaparams) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
355
        $sql = "SELECT apa.id " . $sql;
356
 
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);
361
        }
362
 
363
        foreach ($models as $modelid => $model) {
364
 
365
            $analyser = $model->get_analyser(['notimesplitting' => true]);
366
 
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}";
372
 
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);
377
            }
378
 
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}";
384
 
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);
390
            }
391
        }
392
    }
393
 
394
    /**
395
     * Delete multiple users within a single context.
396
     *
397
     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
398
     */
399
    public static function delete_data_for_users(approved_userlist $userlist) {
400
        global $DB;
401
 
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);
406
 
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);
411
 
412
        if ($predictionactionids) {
413
            list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
414
            $DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
415
        }
416
 
417
        $baseparams['contextid'] = $context->id;
418
 
419
        foreach ($models as $modelid => $model) {
420
            $analyser = $model->get_analyser(['notimesplitting' => true]);
421
 
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);
433
 
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);
437
            }
438
 
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);
450
 
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);
454
            }
455
        }
456
    }
457
 
458
    /**
459
     * Returns a list of models with user data.
460
     *
461
     * @return \core_analytics\model[]
462
     */
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]);
469
            }
470
        }
471
        return $models;
472
    }
473
 
474
    /**
475
     * Returns the sql query to query analytics_prediction_actions table by user ID.
476
     *
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].
481
     */
482
    private static function analytics_prediction_actions_user_sql($userid, $modelids, $contextsql = false) {
483
        global $DB;
484
 
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;
492
 
493
        if ($contextsql) {
494
            $sql .= " AND ap.contextid $contextsql";
495
        }
496
 
497
        return [$sql, $params];
498
    }
499
 
500
    /**
501
     * Returns the sql query to query analytics_prediction_actions table by context ID.
502
     *
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].
507
     */
508
    private static function analytics_prediction_actions_context_sql($contextid, $modelids, $usersql = false) {
509
        global $DB;
510
 
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;
517
 
518
        if ($usersql) {
519
            $sql .= " AND apa.userid {$usersql}";
520
        }
521
 
522
        return [$sql, $params];
523
    }
524
}