Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
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 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body