See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body