Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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