Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }