Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 class for requesting user data.
  19   *
  20   * @package    mod_scorm
  21   * @copyright  2018 Sara Arjona <sara@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace mod_scorm\privacy;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  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\contextlist;
  33  use core_privacy\local\request\helper;
  34  use core_privacy\local\request\transform;
  35  use core_privacy\local\request\userlist;
  36  use core_privacy\local\request\writer;
  37  
  38  /**
  39   * Privacy class for requesting user data.
  40   *
  41   * @copyright  2018 Sara Arjona <sara@moodle.com>
  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       * Return the fields which contain personal data.
  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('scorm_attempt', [
  57                  'userid' => 'privacy:metadata:userid',
  58                  'attempt' => 'privacy:metadata:attempt',
  59              ], 'privacy:metadata:scorm_attempt');
  60  
  61          $collection->add_database_table('scorm_aicc_session', [
  62                  'userid' => 'privacy:metadata:userid',
  63                  'scormmode' => 'privacy:metadata:aicc_session:scormmode',
  64                  'scormstatus' => 'privacy:metadata:aicc_session:scormstatus',
  65                  'attempt' => 'privacy:metadata:attempt',
  66                  'lessonstatus' => 'privacy:metadata:aicc_session:lessonstatus',
  67                  'sessiontime' => 'privacy:metadata:aicc_session:sessiontime',
  68                  'timecreated' => 'privacy:metadata:aicc_session:timecreated',
  69                  'timemodified' => 'privacy:metadata:timemodified',
  70              ], 'privacy:metadata:scorm_aicc_session');
  71  
  72          $collection->add_external_location_link('aicc', [
  73                  'data' => 'privacy:metadata:aicc:data'
  74              ], 'privacy:metadata:aicc:externalpurpose');
  75  
  76          return $collection;
  77      }
  78  
  79      /**
  80       * Get the list of contexts that contain user information for the specified user.
  81       *
  82       * @param int $userid The user to search.
  83       * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
  84       */
  85      public static function get_contexts_for_userid(int $userid) : contextlist {
  86          $sql = "SELECT ctx.id
  87                    FROM {%s} ss
  88                    JOIN {modules} m
  89                      ON m.name = 'scorm'
  90                    JOIN {course_modules} cm
  91                      ON cm.instance = ss.scormid
  92                     AND cm.module = m.id
  93                    JOIN {context} ctx
  94                      ON ctx.instanceid = cm.id
  95                     AND ctx.contextlevel = :modlevel
  96                   WHERE ss.userid = :userid";
  97  
  98          $params = ['modlevel' => CONTEXT_MODULE, 'userid' => $userid];
  99          $contextlist = new contextlist();
 100          $contextlist->add_from_sql(sprintf($sql, 'scorm_attempt'), $params);
 101          $contextlist->add_from_sql(sprintf($sql, 'scorm_aicc_session'), $params);
 102  
 103          return $contextlist;
 104      }
 105  
 106      /**
 107       * Get the list of users who have data within a context.
 108       *
 109       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 110       */
 111      public static function get_users_in_context(userlist $userlist) {
 112          $context = $userlist->get_context();
 113  
 114          if (!is_a($context, \context_module::class)) {
 115              return;
 116          }
 117  
 118          $sql = "SELECT ss.userid
 119                    FROM {%s} ss
 120                    JOIN {modules} m
 121                      ON m.name = 'scorm'
 122                    JOIN {course_modules} cm
 123                      ON cm.instance = ss.scormid
 124                     AND cm.module = m.id
 125                    JOIN {context} ctx
 126                      ON ctx.instanceid = cm.id
 127                     AND ctx.contextlevel = :modlevel
 128                   WHERE ctx.id = :contextid";
 129  
 130          $params = ['modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
 131  
 132          $userlist->add_from_sql('userid', sprintf($sql, 'scorm_attempt'), $params);
 133          $userlist->add_from_sql('userid', sprintf($sql, 'scorm_aicc_session'), $params);
 134      }
 135  
 136      /**
 137       * Export all user data for the specified user, in the specified contexts.
 138       *
 139       * @param approved_contextlist $contextlist The approved contexts to export information for.
 140       */
 141      public static function export_user_data(approved_contextlist $contextlist) {
 142          global $DB;
 143  
 144          // Remove contexts different from COURSE_MODULE.
 145          $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 146              if ($context->contextlevel == CONTEXT_MODULE) {
 147                  $carry[] = $context->id;
 148              }
 149              return $carry;
 150          }, []);
 151  
 152          if (empty($contexts)) {
 153              return;
 154          }
 155  
 156          $user = $contextlist->get_user();
 157          $userid = $user->id;
 158          // Get SCORM data.
 159          foreach ($contexts as $contextid) {
 160              $context = \context::instance_by_id($contextid);
 161              $data = helper::get_context_data($context, $user);
 162              writer::with_context($context)->export_data([], $data);
 163              helper::export_context_files($context, $user);
 164          }
 165  
 166          // Get scoes_track data.
 167          list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
 168          $sql = "SELECT v.id,
 169                         a.attempt,
 170                         e.element,
 171                         v.value,
 172                         v.timemodified,
 173                         ctx.id as contextid
 174                    FROM {scorm_attempt} a
 175                    JOIN {scorm_scoes_value} v ON a.id = v.attemptid
 176                    JOIN {scorm_element} e on e.id = v.elementid
 177                    JOIN {course_modules} cm
 178                      ON cm.instance = a.scormid
 179                    JOIN {context} ctx
 180                      ON ctx.instanceid = cm.id
 181                   WHERE ctx.id $insql
 182                     AND a.userid = :userid";
 183          $params = array_merge($inparams, ['userid' => $userid]);
 184  
 185          $alldata = [];
 186          $scoestracks = $DB->get_recordset_sql($sql, $params);
 187          foreach ($scoestracks as $track) {
 188              $alldata[$track->contextid][$track->attempt][] = (object)[
 189                      'element' => $track->element,
 190                      'value' => $track->value,
 191                      'timemodified' => transform::datetime($track->timemodified),
 192                  ];
 193          }
 194          $scoestracks->close();
 195  
 196          // The scoes_track data is organised in: {Course name}/{SCORM activity name}/{My attempts}/{Attempt X}/data.json
 197          // where X is the attempt number.
 198          array_walk($alldata, function($attemptsdata, $contextid) {
 199              $context = \context::instance_by_id($contextid);
 200              array_walk($attemptsdata, function($data, $attempt) use ($context) {
 201                  $subcontext = [
 202                      get_string('myattempts', 'scorm'),
 203                      get_string('attempt', 'scorm'). " $attempt"
 204                  ];
 205                  writer::with_context($context)->export_data(
 206                      $subcontext,
 207                      (object)['scoestrack' => $data]
 208                  );
 209              });
 210          });
 211  
 212          // Get aicc_session data.
 213          $sql = "SELECT ss.id,
 214                         ss.scormmode,
 215                         ss.scormstatus,
 216                         ss.attempt,
 217                         ss.lessonstatus,
 218                         ss.sessiontime,
 219                         ss.timecreated,
 220                         ss.timemodified,
 221                         ctx.id as contextid
 222                    FROM {scorm_aicc_session} ss
 223                    JOIN {course_modules} cm
 224                      ON cm.instance = ss.scormid
 225                    JOIN {context} ctx
 226                      ON ctx.instanceid = cm.id
 227                   WHERE ctx.id $insql
 228                     AND ss.userid = :userid";
 229          $params = array_merge($inparams, ['userid' => $userid]);
 230  
 231          $alldata = [];
 232          $aiccsessions = $DB->get_recordset_sql($sql, $params);
 233          foreach ($aiccsessions as $aiccsession) {
 234              $alldata[$aiccsession->contextid][] = (object)[
 235                      'scormmode' => $aiccsession->scormmode,
 236                      'scormstatus' => $aiccsession->scormstatus,
 237                      'lessonstatus' => $aiccsession->lessonstatus,
 238                      'attempt' => $aiccsession->attempt,
 239                      'sessiontime' => $aiccsession->sessiontime,
 240                      'timecreated' => transform::datetime($aiccsession->timecreated),
 241                      'timemodified' => transform::datetime($aiccsession->timemodified),
 242                  ];
 243          }
 244          $aiccsessions->close();
 245  
 246          // The aicc_session data is organised in: {Course name}/{SCORM activity name}/{My AICC sessions}/data.json
 247          // In this case, the attempt hasn't been included in the json file because it can be null.
 248          array_walk($alldata, function($data, $contextid) {
 249              $context = \context::instance_by_id($contextid);
 250              $subcontext = [
 251                  get_string('myaiccsessions', 'scorm')
 252              ];
 253              writer::with_context($context)->export_data(
 254                  $subcontext,
 255                  (object)['sessions' => $data]
 256              );
 257          });
 258      }
 259  
 260      /**
 261       * Delete all user data which matches the specified context.
 262       *
 263       * @param context $context A user context.
 264       */
 265      public static function delete_data_for_all_users_in_context(\context $context) {
 266          // This should not happen, but just in case.
 267          if ($context->contextlevel != CONTEXT_MODULE) {
 268              return;
 269          }
 270  
 271          // Prepare SQL to gather all IDs to delete.
 272          $sql = "SELECT ss.id
 273                    FROM {%s} ss
 274                    JOIN {modules} m
 275                      ON m.name = 'scorm'
 276                    JOIN {course_modules} cm
 277                      ON cm.instance = ss.scormid
 278                     AND cm.module = m.id
 279                   WHERE cm.id = :cmid";
 280          $params = ['cmid' => $context->instanceid];
 281  
 282          static::delete_data('scorm_aicc_session', $sql, $params);
 283          $coursemodule = get_coursemodule_from_id('scorm', $context->instanceid);
 284          scorm_delete_tracks($coursemodule->instance);
 285      }
 286  
 287      /**
 288       * Delete all user data for the specified user, in the specified contexts.
 289       *
 290       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 291       */
 292      public static function delete_data_for_user(approved_contextlist $contextlist) {
 293          global $DB;
 294  
 295          // Remove contexts different from COURSE_MODULE.
 296          $contextids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 297              if ($context->contextlevel == CONTEXT_MODULE) {
 298                  $carry[] = $context->id;
 299              }
 300              return $carry;
 301          }, []);
 302  
 303          if (empty($contextids)) {
 304              return;
 305          }
 306          $userid = $contextlist->get_user()->id;
 307          // Prepare SQL to gather all completed IDs.
 308          list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
 309          $sql = "SELECT ss.id
 310                    FROM {%s} ss
 311                    JOIN {modules} m
 312                      ON m.name = 'scorm'
 313                    JOIN {course_modules} cm
 314                      ON cm.instance = ss.scormid
 315                     AND cm.module = m.id
 316                    JOIN {context} ctx
 317                      ON ctx.instanceid = cm.id
 318                   WHERE ss.userid = :userid
 319                     AND ctx.id $insql";
 320          $params = array_merge($inparams, ['userid' => $userid]);
 321  
 322          static::delete_data('scorm_aicc_session', $sql, $params);
 323          foreach ($contextlist->get_contexts() as $context) {
 324              if ($context->contextlevel == CONTEXT_MODULE) {
 325                  $coursemodule = get_coursemodule_from_id('scorm', $context->instanceid);
 326                  scorm_delete_tracks($coursemodule->instance, null, $userid);
 327              }
 328          }
 329      }
 330  
 331      /**
 332       * Delete multiple users within a single context.
 333       *
 334       * @param   approved_userlist       $userlist The approved context and user information to delete information for.
 335       */
 336      public static function delete_data_for_users(approved_userlist $userlist) {
 337          global $DB;
 338          $context = $userlist->get_context();
 339  
 340          if (!is_a($context, \context_module::class)) {
 341              return;
 342          }
 343  
 344          // Prepare SQL to gather all completed IDs.
 345          $userids = $userlist->get_userids();
 346          list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 347  
 348          $sql = "SELECT ss.id
 349                    FROM {%s} ss
 350                    JOIN {modules} m
 351                      ON m.name = 'scorm'
 352                    JOIN {course_modules} cm
 353                      ON cm.instance = ss.scormid
 354                     AND cm.module = m.id
 355                    JOIN {context} ctx
 356                      ON ctx.instanceid = cm.id
 357                   WHERE ctx.id = :contextid
 358                     AND ss.userid $insql";
 359          $params = array_merge($inparams, ['contextid' => $context->id]);
 360  
 361          static::delete_data('scorm_aicc_session', $sql, $params);
 362          $coursemodule = get_coursemodule_from_id('scorm', $context->instanceid);
 363          foreach ($userlist->get_userids() as $userid) {
 364              scorm_delete_tracks($coursemodule->instance, null, $userid);
 365          }
 366      }
 367  
 368      /**
 369       * Delete data from $tablename with the IDs returned by $sql query.
 370       *
 371       * @param  string $tablename  Table name where executing the SQL query.
 372       * @param  string $sql    SQL query for getting the IDs of the scoestrack entries to delete.
 373       * @param  array  $params SQL params for the query.
 374       */
 375      protected static function delete_data(string $tablename, string $sql, array $params) {
 376          global $DB;
 377  
 378          $scoestracksids = $DB->get_fieldset_sql(sprintf($sql, $tablename), $params);
 379          if (!empty($scoestracksids)) {
 380              list($insql, $inparams) = $DB->get_in_or_equal($scoestracksids, SQL_PARAMS_NAMED);
 381              $DB->delete_records_select($tablename, "id $insql", $inparams);
 382          }
 383      }
 384  }