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.
/mod/scorm/ -> lib.php (source)

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   * @package   mod_scorm
  19   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  20   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21   */
  22  defined('MOODLE_INTERNAL') || die();
  23  
  24  /** SCORM_TYPE_LOCAL = local */
  25  define('SCORM_TYPE_LOCAL', 'local');
  26  /** SCORM_TYPE_LOCALSYNC = localsync */
  27  define('SCORM_TYPE_LOCALSYNC', 'localsync');
  28  /** SCORM_TYPE_EXTERNAL = external */
  29  define('SCORM_TYPE_EXTERNAL', 'external');
  30  /** SCORM_TYPE_AICCURL = external AICC url */
  31  define('SCORM_TYPE_AICCURL', 'aiccurl');
  32  
  33  define('SCORM_TOC_SIDE', 0);
  34  define('SCORM_TOC_HIDDEN', 1);
  35  define('SCORM_TOC_POPUP', 2);
  36  define('SCORM_TOC_DISABLED', 3);
  37  
  38  // Used to show/hide navigation buttons and set their position.
  39  define('SCORM_NAV_DISABLED', 0);
  40  define('SCORM_NAV_UNDER_CONTENT', 1);
  41  define('SCORM_NAV_FLOATING', 2);
  42  
  43  // Used to check what SCORM version is being used.
  44  define('SCORM_12', 1);
  45  define('SCORM_13', 2);
  46  define('SCORM_AICC', 3);
  47  
  48  // List of possible attemptstatusdisplay options.
  49  define('SCORM_DISPLAY_ATTEMPTSTATUS_NO', 0);
  50  define('SCORM_DISPLAY_ATTEMPTSTATUS_ALL', 1);
  51  define('SCORM_DISPLAY_ATTEMPTSTATUS_MY', 2);
  52  define('SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY', 3);
  53  
  54  define('SCORM_EVENT_TYPE_OPEN', 'open');
  55  define('SCORM_EVENT_TYPE_CLOSE', 'close');
  56  
  57  require_once (__DIR__ . '/deprecatedlib.php');
  58  
  59  /**
  60   * Return an array of status options
  61   *
  62   * Optionally with translated strings
  63   *
  64   * @param   bool    $with_strings   (optional)
  65   * @return  array
  66   */
  67  function scorm_status_options($withstrings = false) {
  68      // Id's are important as they are bits.
  69      $options = array(
  70          2 => 'passed',
  71          4 => 'completed'
  72      );
  73  
  74      if ($withstrings) {
  75          foreach ($options as $key => $value) {
  76              $options[$key] = get_string('completionstatus_'.$value, 'scorm');
  77          }
  78      }
  79  
  80      return $options;
  81  }
  82  
  83  
  84  /**
  85   * Given an object containing all the necessary data,
  86   * (defined by the form in mod_form.php) this function
  87   * will create a new instance and return the id number
  88   * of the new instance.
  89   *
  90   * @global stdClass
  91   * @global object
  92   * @uses CONTEXT_MODULE
  93   * @uses SCORM_TYPE_LOCAL
  94   * @uses SCORM_TYPE_LOCALSYNC
  95   * @uses SCORM_TYPE_EXTERNAL
  96   * @param object $scorm Form data
  97   * @param object $mform
  98   * @return int new instance id
  99   */
 100  function scorm_add_instance($scorm, $mform=null) {
 101      global $CFG, $DB;
 102  
 103      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 104  
 105      if (empty($scorm->timeopen)) {
 106          $scorm->timeopen = 0;
 107      }
 108      if (empty($scorm->timeclose)) {
 109          $scorm->timeclose = 0;
 110      }
 111      if (empty($scorm->completionstatusallscos)) {
 112          $scorm->completionstatusallscos = 0;
 113      }
 114      $cmid       = $scorm->coursemodule;
 115      $cmidnumber = $scorm->cmidnumber;
 116      $courseid   = $scorm->course;
 117  
 118      $context = context_module::instance($cmid);
 119  
 120      $scorm = scorm_option2text($scorm);
 121      $scorm->width  = (int)str_replace('%', '', $scorm->width);
 122      $scorm->height = (int)str_replace('%', '', $scorm->height);
 123  
 124      if (!isset($scorm->whatgrade)) {
 125          $scorm->whatgrade = 0;
 126      }
 127  
 128      $id = $DB->insert_record('scorm', $scorm);
 129  
 130      // Update course module record - from now on this instance properly exists and all function may be used.
 131      $DB->set_field('course_modules', 'instance', $id, array('id' => $cmid));
 132  
 133      // Reload scorm instance.
 134      $record = $DB->get_record('scorm', array('id' => $id));
 135  
 136      // Store the package and verify.
 137      if ($record->scormtype === SCORM_TYPE_LOCAL) {
 138          if (!empty($scorm->packagefile)) {
 139              $fs = get_file_storage();
 140              $fs->delete_area_files($context->id, 'mod_scorm', 'package');
 141              file_save_draft_area_files($scorm->packagefile, $context->id, 'mod_scorm', 'package',
 142                  0, array('subdirs' => 0, 'maxfiles' => 1));
 143              // Get filename of zip that was uploaded.
 144              $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false);
 145              $file = reset($files);
 146              $filename = $file->get_filename();
 147              if ($filename !== false) {
 148                  $record->reference = $filename;
 149              }
 150          }
 151  
 152      } else if ($record->scormtype === SCORM_TYPE_LOCALSYNC) {
 153          $record->reference = $scorm->packageurl;
 154      } else if ($record->scormtype === SCORM_TYPE_EXTERNAL) {
 155          $record->reference = $scorm->packageurl;
 156      } else if ($record->scormtype === SCORM_TYPE_AICCURL) {
 157          $record->reference = $scorm->packageurl;
 158          $record->hidetoc = SCORM_TOC_DISABLED; // TOC is useless for direct AICCURL so disable it.
 159      } else {
 160          return false;
 161      }
 162  
 163      // Save reference.
 164      $DB->update_record('scorm', $record);
 165  
 166      // Extra fields required in grade related functions.
 167      $record->course     = $courseid;
 168      $record->cmidnumber = $cmidnumber;
 169      $record->cmid       = $cmid;
 170  
 171      scorm_parse($record, true);
 172  
 173      scorm_grade_item_update($record);
 174      scorm_update_calendar($record, $cmid);
 175      if (!empty($scorm->completionexpected)) {
 176          \core_completion\api::update_completion_date_event($cmid, 'scorm', $record, $scorm->completionexpected);
 177      }
 178  
 179      return $record->id;
 180  }
 181  
 182  /**
 183   * Given an object containing all the necessary data,
 184   * (defined by the form in mod_form.php) this function
 185   * will update an existing instance with new data.
 186   *
 187   * @global stdClass
 188   * @global object
 189   * @uses CONTEXT_MODULE
 190   * @uses SCORM_TYPE_LOCAL
 191   * @uses SCORM_TYPE_LOCALSYNC
 192   * @uses SCORM_TYPE_EXTERNAL
 193   * @param object $scorm Form data
 194   * @param object $mform
 195   * @return bool
 196   */
 197  function scorm_update_instance($scorm, $mform=null) {
 198      global $CFG, $DB;
 199  
 200      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 201  
 202      if (empty($scorm->timeopen)) {
 203          $scorm->timeopen = 0;
 204      }
 205      if (empty($scorm->timeclose)) {
 206          $scorm->timeclose = 0;
 207      }
 208      if (empty($scorm->completionstatusallscos)) {
 209          $scorm->completionstatusallscos = 0;
 210      }
 211  
 212      $cmid       = $scorm->coursemodule;
 213      $cmidnumber = $scorm->cmidnumber;
 214      $courseid   = $scorm->course;
 215  
 216      $scorm->id = $scorm->instance;
 217  
 218      $context = context_module::instance($cmid);
 219  
 220      if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
 221          if (!empty($scorm->packagefile)) {
 222              $fs = get_file_storage();
 223              $fs->delete_area_files($context->id, 'mod_scorm', 'package');
 224              file_save_draft_area_files($scorm->packagefile, $context->id, 'mod_scorm', 'package',
 225                  0, array('subdirs' => 0, 'maxfiles' => 1));
 226              // Get filename of zip that was uploaded.
 227              $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false);
 228              $file = reset($files);
 229              $filename = $file->get_filename();
 230              if ($filename !== false) {
 231                  $scorm->reference = $filename;
 232              }
 233          }
 234  
 235      } else if ($scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
 236          $scorm->reference = $scorm->packageurl;
 237      } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) {
 238          $scorm->reference = $scorm->packageurl;
 239      } else if ($scorm->scormtype === SCORM_TYPE_AICCURL) {
 240          $scorm->reference = $scorm->packageurl;
 241          $scorm->hidetoc = SCORM_TOC_DISABLED; // TOC is useless for direct AICCURL so disable it.
 242      } else {
 243          return false;
 244      }
 245  
 246      $scorm = scorm_option2text($scorm);
 247      $scorm->width        = (int)str_replace('%', '', $scorm->width);
 248      $scorm->height       = (int)str_replace('%', '', $scorm->height);
 249      $scorm->timemodified = time();
 250  
 251      if (!isset($scorm->whatgrade)) {
 252          $scorm->whatgrade = 0;
 253      }
 254  
 255      $DB->update_record('scorm', $scorm);
 256      // We need to find this out before we blow away the form data.
 257      $completionexpected = (!empty($scorm->completionexpected)) ? $scorm->completionexpected : null;
 258  
 259      $scorm = $DB->get_record('scorm', array('id' => $scorm->id));
 260  
 261      // Extra fields required in grade related functions.
 262      $scorm->course   = $courseid;
 263      $scorm->idnumber = $cmidnumber;
 264      $scorm->cmid     = $cmid;
 265  
 266      scorm_parse($scorm, (bool)$scorm->updatefreq);
 267  
 268      scorm_grade_item_update($scorm);
 269      scorm_update_grades($scorm);
 270      scorm_update_calendar($scorm, $cmid);
 271      \core_completion\api::update_completion_date_event($cmid, 'scorm', $scorm, $completionexpected);
 272  
 273      return true;
 274  }
 275  
 276  /**
 277   * Given an ID of an instance of this module,
 278   * this function will permanently delete the instance
 279   * and any data that depends on it.
 280   *
 281   * @global stdClass
 282   * @global object
 283   * @param int $id Scorm instance id
 284   * @return boolean
 285   */
 286  function scorm_delete_instance($id) {
 287      global $CFG, $DB;
 288  
 289      if (! $scorm = $DB->get_record('scorm', array('id' => $id))) {
 290          return false;
 291      }
 292  
 293      $result = true;
 294  
 295      require_once($CFG->dirroot . '/mod/scorm/locallib.php');
 296      // Delete any dependent records.
 297      scorm_delete_tracks($scorm->id);
 298      if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id))) {
 299          foreach ($scoes as $sco) {
 300              if (! $DB->delete_records('scorm_scoes_data', array('scoid' => $sco->id))) {
 301                  $result = false;
 302              }
 303          }
 304          $DB->delete_records('scorm_scoes', array('scorm' => $scorm->id));
 305      }
 306  
 307      scorm_grade_item_delete($scorm);
 308  
 309      // We must delete the module record after we delete the grade item.
 310      if (! $DB->delete_records('scorm', array('id' => $scorm->id))) {
 311          $result = false;
 312      }
 313  
 314      /*if (! $DB->delete_records('scorm_sequencing_controlmode', array('scormid'=>$scorm->id))) {
 315          $result = false;
 316      }
 317      if (! $DB->delete_records('scorm_sequencing_rolluprules', array('scormid'=>$scorm->id))) {
 318          $result = false;
 319      }
 320      if (! $DB->delete_records('scorm_sequencing_rolluprule', array('scormid'=>$scorm->id))) {
 321          $result = false;
 322      }
 323      if (! $DB->delete_records('scorm_sequencing_rollupruleconditions', array('scormid'=>$scorm->id))) {
 324          $result = false;
 325      }
 326      if (! $DB->delete_records('scorm_sequencing_rolluprulecondition', array('scormid'=>$scorm->id))) {
 327          $result = false;
 328      }
 329      if (! $DB->delete_records('scorm_sequencing_rulecondition', array('scormid'=>$scorm->id))) {
 330          $result = false;
 331      }
 332      if (! $DB->delete_records('scorm_sequencing_ruleconditions', array('scormid'=>$scorm->id))) {
 333          $result = false;
 334      }*/
 335  
 336      return $result;
 337  }
 338  
 339  /**
 340   * Return a small object with summary information about what a
 341   * user has done with a given particular instance of this module
 342   * Used for user activity reports.
 343   *
 344   * @param stdClass $course Course object
 345   * @param stdClass $user User
 346   * @param stdClass $mod
 347   * @param stdClass $scorm The scorm
 348   * @return mixed
 349   */
 350  function scorm_user_outline($course, $user, $mod, $scorm) {
 351      global $CFG;
 352      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 353  
 354      require_once("$CFG->libdir/gradelib.php");
 355      $grades = grade_get_grades($course->id, 'mod', 'scorm', $scorm->id, $user->id);
 356      if (!empty($grades->items[0]->grades)) {
 357          $grade = reset($grades->items[0]->grades);
 358          $result = (object) [
 359              'time' => grade_get_date_for_user_grade($grade, $user),
 360          ];
 361          if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
 362              $result->info = get_string('gradenoun') . ': '. $grade->str_long_grade;
 363          } else {
 364              $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades');
 365          }
 366  
 367          return $result;
 368      }
 369      return null;
 370  }
 371  
 372  /**
 373   * Print a detailed representation of what a user has done with
 374   * a given particular instance of this module, for user activity reports.
 375   *
 376   * @global stdClass
 377   * @global object
 378   * @param object $course
 379   * @param object $user
 380   * @param object $mod
 381   * @param object $scorm
 382   * @return boolean
 383   */
 384  function scorm_user_complete($course, $user, $mod, $scorm) {
 385      global $CFG, $DB, $OUTPUT;
 386      require_once("$CFG->libdir/gradelib.php");
 387  
 388      $liststyle = 'structlist';
 389      $now = time();
 390      $firstmodify = $now;
 391      $lastmodify = 0;
 392      $sometoreport = false;
 393      $report = '';
 394  
 395      // First Access and Last Access dates for SCOs.
 396      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 397      $timetracks = scorm_get_sco_runtime($scorm->id, false, $user->id);
 398      $firstmodify = $timetracks->start;
 399      $lastmodify = $timetracks->finish;
 400  
 401      $grades = grade_get_grades($course->id, 'mod', 'scorm', $scorm->id, $user->id);
 402      if (!empty($grades->items[0]->grades)) {
 403          $grade = reset($grades->items[0]->grades);
 404          if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
 405              echo $OUTPUT->container(get_string('gradenoun').': '.$grade->str_long_grade);
 406              if ($grade->str_feedback) {
 407                  echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
 408              }
 409          } else {
 410              echo $OUTPUT->container(get_string('gradenoun') . ': ' . get_string('hidden', 'grades'));
 411          }
 412      }
 413  
 414      if ($orgs = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.
 415                                           $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
 416                                           $DB->sql_isempty('scorm_scoes', 'organization', false, false),
 417                                           array($scorm->id), 'sortorder, id', 'id, identifier, title')) {
 418          if (count($orgs) <= 1) {
 419              unset($orgs);
 420              $orgs = array();
 421              $org = new stdClass();
 422              $org->identifier = '';
 423              $orgs[] = $org;
 424          }
 425          $report .= html_writer::start_div('mod-scorm');
 426          foreach ($orgs as $org) {
 427              $conditions = array();
 428              $currentorg = '';
 429              if (!empty($org->identifier)) {
 430                  $report .= html_writer::div($org->title, 'orgtitle');
 431                  $currentorg = $org->identifier;
 432                  $conditions['organization'] = $currentorg;
 433              }
 434              $report .= html_writer::start_tag('ul', array('id' => '0', 'class' => $liststyle));
 435                  $conditions['scorm'] = $scorm->id;
 436              if ($scoes = $DB->get_records('scorm_scoes', $conditions, "sortorder, id")) {
 437                  // Drop keys so that we can access array sequentially.
 438                  $scoes = array_values($scoes);
 439                  $level = 0;
 440                  $sublist = 1;
 441                  $parents[$level] = '/';
 442                  foreach ($scoes as $pos => $sco) {
 443                      if ($parents[$level] != $sco->parent) {
 444                          if ($level > 0 && $parents[$level - 1] == $sco->parent) {
 445                              $report .= html_writer::end_tag('ul').html_writer::end_tag('li');
 446                              $level--;
 447                          } else {
 448                              $i = $level;
 449                              $closelist = '';
 450                              while (($i > 0) && ($parents[$level] != $sco->parent)) {
 451                                  $closelist .= html_writer::end_tag('ul').html_writer::end_tag('li');
 452                                  $i--;
 453                              }
 454                              if (($i == 0) && ($sco->parent != $currentorg)) {
 455                                  $report .= html_writer::start_tag('li');
 456                                  $report .= html_writer::start_tag('ul', array('id' => $sublist, 'class' => $liststyle));
 457                                  $level++;
 458                              } else {
 459                                  $report .= $closelist;
 460                                  $level = $i;
 461                              }
 462                              $parents[$level] = $sco->parent;
 463                          }
 464                      }
 465                      $report .= html_writer::start_tag('li');
 466                      if (isset($scoes[$pos + 1])) {
 467                          $nextsco = $scoes[$pos + 1];
 468                      } else {
 469                          $nextsco = false;
 470                      }
 471                      if (($nextsco !== false) && ($sco->parent != $nextsco->parent) &&
 472                              (($level == 0) || (($level > 0) && ($nextsco->parent == $sco->identifier)))) {
 473                          $sublist++;
 474                      } else {
 475                          $report .= $OUTPUT->spacer(array("height" => "12", "width" => "13"));
 476                      }
 477  
 478                      if ($sco->launch) {
 479                          $score = '';
 480                          $totaltime = '';
 481                          if ($usertrack = scorm_get_tracks($sco->id, $user->id)) {
 482                              if ($usertrack->status == '') {
 483                                  $usertrack->status = 'notattempted';
 484                              }
 485                              $strstatus = get_string($usertrack->status, 'scorm');
 486                              $report .= $OUTPUT->pix_icon($usertrack->status, $strstatus, 'scorm');
 487                          } else {
 488                              if ($sco->scormtype == 'sco') {
 489                                  $report .= $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm');
 490                              } else {
 491                                  $report .= $OUTPUT->pix_icon('asset', get_string('asset', 'scorm'), 'scorm');
 492                              }
 493                          }
 494                          $report .= "&nbsp;$sco->title $score$totaltime".html_writer::end_tag('li');
 495                          if ($usertrack !== false) {
 496                              $sometoreport = true;
 497                              $report .= html_writer::start_tag('li').html_writer::start_tag('ul', array('class' => $liststyle));
 498                              foreach ($usertrack as $element => $value) {
 499                                  if (substr($element, 0, 3) == 'cmi') {
 500                                      $report .= html_writer::tag('li', s($element) . ' => ' . s($value));
 501                                  }
 502                              }
 503                              $report .= html_writer::end_tag('ul').html_writer::end_tag('li');
 504                          }
 505                      } else {
 506                          $report .= "&nbsp;$sco->title".html_writer::end_tag('li');
 507                      }
 508                  }
 509                  for ($i = 0; $i < $level; $i++) {
 510                      $report .= html_writer::end_tag('ul').html_writer::end_tag('li');
 511                  }
 512              }
 513              $report .= html_writer::end_tag('ul').html_writer::empty_tag('br');
 514          }
 515          $report .= html_writer::end_div();
 516      }
 517      if ($sometoreport) {
 518          if ($firstmodify < $now) {
 519              $timeago = format_time($now - $firstmodify);
 520              echo get_string('firstaccess', 'scorm').': '.userdate($firstmodify).' ('.$timeago.")".html_writer::empty_tag('br');
 521          }
 522          if ($lastmodify > 0) {
 523              $timeago = format_time($now - $lastmodify);
 524              echo get_string('lastaccess', 'scorm').': '.userdate($lastmodify).' ('.$timeago.")".html_writer::empty_tag('br');
 525          }
 526          echo get_string('report', 'scorm').":".html_writer::empty_tag('br');
 527          echo $report;
 528      } else {
 529          print_string('noactivity', 'scorm');
 530      }
 531  
 532      return true;
 533  }
 534  
 535  /**
 536   * Function to be run periodically according to the moodle Tasks API
 537   * This function searches for things that need to be done, such
 538   * as sending out mail, toggling flags etc ...
 539   *
 540   * @global stdClass
 541   * @global object
 542   * @return boolean
 543   */
 544  function scorm_cron_scheduled_task () {
 545      global $CFG, $DB;
 546  
 547      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 548  
 549      $sitetimezone = core_date::get_server_timezone();
 550      // Now see if there are any scorm updates to be done.
 551  
 552      if (!isset($CFG->scorm_updatetimelast)) {    // To catch the first time.
 553          set_config('scorm_updatetimelast', 0);
 554      }
 555  
 556      $timenow = time();
 557      $updatetime = usergetmidnight($timenow, $sitetimezone);
 558  
 559      if ($CFG->scorm_updatetimelast < $updatetime and $timenow > $updatetime) {
 560  
 561          set_config('scorm_updatetimelast', $timenow);
 562  
 563          mtrace('Updating scorm packages which require daily update');// We are updating.
 564  
 565          $scormsupdate = $DB->get_records('scorm', array('updatefreq' => SCORM_UPDATE_EVERYDAY));
 566          foreach ($scormsupdate as $scormupdate) {
 567              scorm_parse($scormupdate, true);
 568          }
 569  
 570          // Now clear out AICC session table with old session data.
 571          $cfgscorm = get_config('scorm');
 572          if (!empty($cfgscorm->allowaicchacp)) {
 573              $expiretime = time() - ($cfgscorm->aicchacpkeepsessiondata * 24 * 60 * 60);
 574              $DB->delete_records_select('scorm_aicc_session', 'timemodified < ?', array($expiretime));
 575          }
 576      }
 577  
 578      return true;
 579  }
 580  
 581  /**
 582   * Return grade for given user or all users.
 583   *
 584   * @global stdClass
 585   * @global object
 586   * @param int $scormid id of scorm
 587   * @param int $userid optional user id, 0 means all users
 588   * @return array array of grades, false if none
 589   */
 590  function scorm_get_user_grades($scorm, $userid=0) {
 591      global $CFG, $DB;
 592      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 593  
 594      $grades = array();
 595      if (empty($userid)) {
 596          $sql = "SELECT DISTINCT userid
 597                    FROM {scorm_attempt}
 598                   WHERE scormid = ?";
 599          $scousers = $DB->get_recordset_sql($sql, [$scorm->id]);
 600  
 601          foreach ($scousers as $scouser) {
 602              $grades[$scouser->userid] = new stdClass();
 603              $grades[$scouser->userid]->id = $scouser->userid;
 604              $grades[$scouser->userid]->userid = $scouser->userid;
 605              $grades[$scouser->userid]->rawgrade = scorm_grade_user($scorm, $scouser->userid);
 606          }
 607          $scousers->close();
 608      } else {
 609          $preattempt = $DB->record_exists('scorm_attempt', ['scormid' => $scorm->id, 'userid' => $userid]);
 610          if (!$preattempt) {
 611              return false; // No attempt yet.
 612          }
 613          $grades[$userid] = new stdClass();
 614          $grades[$userid]->id = $userid;
 615          $grades[$userid]->userid = $userid;
 616          $grades[$userid]->rawgrade = scorm_grade_user($scorm, $userid);
 617      }
 618  
 619      if (empty($grades)) {
 620          return false;
 621      }
 622  
 623      return $grades;
 624  }
 625  
 626  /**
 627   * Update grades in central gradebook
 628   *
 629   * @category grade
 630   * @param object $scorm
 631   * @param int $userid specific user only, 0 mean all
 632   * @param bool $nullifnone
 633   */
 634  function scorm_update_grades($scorm, $userid=0, $nullifnone=true) {
 635      global $CFG;
 636      require_once($CFG->libdir.'/gradelib.php');
 637      require_once($CFG->libdir.'/completionlib.php');
 638  
 639      if ($grades = scorm_get_user_grades($scorm, $userid)) {
 640          scorm_grade_item_update($scorm, $grades);
 641          // Set complete.
 642          scorm_set_completion($scorm, $userid, COMPLETION_COMPLETE, $grades);
 643      } else if ($userid and $nullifnone) {
 644          $grade = new stdClass();
 645          $grade->userid   = $userid;
 646          $grade->rawgrade = null;
 647          scorm_grade_item_update($scorm, $grade);
 648          // Set incomplete.
 649          scorm_set_completion($scorm, $userid, COMPLETION_INCOMPLETE);
 650      } else {
 651          scorm_grade_item_update($scorm);
 652      }
 653  }
 654  
 655  /**
 656   * Update/create grade item for given scorm
 657   *
 658   * @category grade
 659   * @uses GRADE_TYPE_VALUE
 660   * @uses GRADE_TYPE_NONE
 661   * @param object $scorm object with extra cmidnumber
 662   * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
 663   * @return object grade_item
 664   */
 665  function scorm_grade_item_update($scorm, $grades=null) {
 666      global $CFG, $DB;
 667      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 668      if (!function_exists('grade_update')) { // Workaround for buggy PHP versions.
 669          require_once($CFG->libdir.'/gradelib.php');
 670      }
 671  
 672      $params = array('itemname' => $scorm->name);
 673      if (isset($scorm->cmidnumber)) {
 674          $params['idnumber'] = $scorm->cmidnumber;
 675      }
 676  
 677      if ($scorm->grademethod == GRADESCOES) {
 678          $maxgrade = $DB->count_records_select('scorm_scoes', 'scorm = ? AND '.
 679                                                  $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id));
 680          if ($maxgrade) {
 681              $params['gradetype'] = GRADE_TYPE_VALUE;
 682              $params['grademax']  = $maxgrade;
 683              $params['grademin']  = 0;
 684          } else {
 685              $params['gradetype'] = GRADE_TYPE_NONE;
 686          }
 687      } else {
 688          $params['gradetype'] = GRADE_TYPE_VALUE;
 689          $params['grademax']  = $scorm->maxgrade;
 690          $params['grademin']  = 0;
 691      }
 692  
 693      if ($grades === 'reset') {
 694          $params['reset'] = true;
 695          $grades = null;
 696      }
 697  
 698      return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, $grades, $params);
 699  }
 700  
 701  /**
 702   * Delete grade item for given scorm
 703   *
 704   * @category grade
 705   * @param object $scorm object
 706   * @return object grade_item
 707   */
 708  function scorm_grade_item_delete($scorm) {
 709      global $CFG;
 710      require_once($CFG->libdir.'/gradelib.php');
 711  
 712      return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, null, array('deleted' => 1));
 713  }
 714  
 715  /**
 716   * List the actions that correspond to a view of this module.
 717   * This is used by the participation report.
 718   *
 719   * Note: This is not used by new logging system. Event with
 720   *       crud = 'r' and edulevel = LEVEL_PARTICIPATING will
 721   *       be considered as view action.
 722   *
 723   * @return array
 724   */
 725  function scorm_get_view_actions() {
 726      return array('pre-view', 'view', 'view all', 'report');
 727  }
 728  
 729  /**
 730   * List the actions that correspond to a post of this module.
 731   * This is used by the participation report.
 732   *
 733   * Note: This is not used by new logging system. Event with
 734   *       crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
 735   *       will be considered as post action.
 736   *
 737   * @return array
 738   */
 739  function scorm_get_post_actions() {
 740      return array();
 741  }
 742  
 743  /**
 744   * @param object $scorm
 745   * @return object $scorm
 746   */
 747  function scorm_option2text($scorm) {
 748      $scormpopoupoptions = scorm_get_popup_options_array();
 749  
 750      if (isset($scorm->popup)) {
 751          if ($scorm->popup == 1) {
 752              $optionlist = array();
 753              foreach ($scormpopoupoptions as $name => $option) {
 754                  if (isset($scorm->$name)) {
 755                      $optionlist[] = $name.'='.$scorm->$name;
 756                  } else {
 757                      $optionlist[] = $name.'=0';
 758                  }
 759              }
 760              $scorm->options = implode(',', $optionlist);
 761          } else {
 762              $scorm->options = '';
 763          }
 764      } else {
 765          $scorm->popup = 0;
 766          $scorm->options = '';
 767      }
 768      return $scorm;
 769  }
 770  
 771  /**
 772   * Implementation of the function for printing the form elements that control
 773   * whether the course reset functionality affects the scorm.
 774   *
 775   * @param MoodleQuickForm $mform form passed by reference
 776   */
 777  function scorm_reset_course_form_definition(&$mform) {
 778      $mform->addElement('header', 'scormheader', get_string('modulenameplural', 'scorm'));
 779      $mform->addElement('advcheckbox', 'reset_scorm', get_string('deleteallattempts', 'scorm'));
 780  }
 781  
 782  /**
 783   * Course reset form defaults.
 784   *
 785   * @return array
 786   */
 787  function scorm_reset_course_form_defaults($course) {
 788      return array('reset_scorm' => 1);
 789  }
 790  
 791  /**
 792   * Removes all grades from gradebook
 793   *
 794   * @global stdClass
 795   * @global object
 796   * @param int $courseid
 797   * @param string optional type
 798   */
 799  function scorm_reset_gradebook($courseid, $type='') {
 800      global $CFG, $DB;
 801  
 802      $sql = "SELECT s.*, cm.idnumber as cmidnumber, s.course as courseid
 803                FROM {scorm} s, {course_modules} cm, {modules} m
 804               WHERE m.name='scorm' AND m.id=cm.module AND cm.instance=s.id AND s.course=?";
 805  
 806      if ($scorms = $DB->get_records_sql($sql, array($courseid))) {
 807          foreach ($scorms as $scorm) {
 808              scorm_grade_item_update($scorm, 'reset');
 809          }
 810      }
 811  }
 812  
 813  /**
 814   * Actual implementation of the reset course functionality, delete all the
 815   * scorm attempts for course $data->courseid.
 816   *
 817   * @global stdClass
 818   * @global object
 819   * @param object $data the data submitted from the reset course.
 820   * @return array status array
 821   */
 822  function scorm_reset_userdata($data) {
 823      global $DB, $CFG;
 824      require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 825  
 826      $componentstr = get_string('modulenameplural', 'scorm');
 827      $status = [];
 828  
 829      if (!empty($data->reset_scorm)) {
 830  
 831          $scorms = $DB->get_recordset('scorm', ['course' => $data->courseid]);
 832          foreach ($scorms as $scorm) {
 833              scorm_delete_tracks($scorm->id);
 834          }
 835          $scorms->close();
 836  
 837          // Remove all grades from gradebook.
 838          if (empty($data->reset_gradebook_grades)) {
 839              scorm_reset_gradebook($data->courseid);
 840          }
 841  
 842          $status[] = ['component' => $componentstr, 'item' => get_string('deleteallattempts', 'scorm'), 'error' => false];
 843      }
 844  
 845      // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
 846      // See MDL-9367.
 847      shift_course_mod_dates('scorm', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
 848      $status[] = ['component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false];
 849  
 850      return $status;
 851  }
 852  
 853  /**
 854   * Lists all file areas current user may browse
 855   *
 856   * @param object $course
 857   * @param object $cm
 858   * @param object $context
 859   * @return array
 860   */
 861  function scorm_get_file_areas($course, $cm, $context) {
 862      $areas = array();
 863      $areas['content'] = get_string('areacontent', 'scorm');
 864      $areas['package'] = get_string('areapackage', 'scorm');
 865      return $areas;
 866  }
 867  
 868  /**
 869   * File browsing support for SCORM file areas
 870   *
 871   * @package  mod_scorm
 872   * @category files
 873   * @param file_browser $browser file browser instance
 874   * @param array $areas file areas
 875   * @param stdClass $course course object
 876   * @param stdClass $cm course module object
 877   * @param stdClass $context context object
 878   * @param string $filearea file area
 879   * @param int $itemid item ID
 880   * @param string $filepath file path
 881   * @param string $filename file name
 882   * @return file_info instance or null if not found
 883   */
 884  function scorm_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
 885      global $CFG;
 886  
 887      if (!has_capability('moodle/course:managefiles', $context)) {
 888          return null;
 889      }
 890  
 891      // No writing for now!
 892  
 893      $fs = get_file_storage();
 894  
 895      if ($filearea === 'content') {
 896  
 897          $filepath = is_null($filepath) ? '/' : $filepath;
 898          $filename = is_null($filename) ? '.' : $filename;
 899  
 900          $urlbase = $CFG->wwwroot.'/pluginfile.php';
 901          if (!$storedfile = $fs->get_file($context->id, 'mod_scorm', 'content', 0, $filepath, $filename)) {
 902              if ($filepath === '/' and $filename === '.') {
 903                  $storedfile = new virtual_root_file($context->id, 'mod_scorm', 'content', 0);
 904              } else {
 905                  // Not found.
 906                  return null;
 907              }
 908          }
 909          require_once("$CFG->dirroot/mod/scorm/locallib.php");
 910          return new scorm_package_file_info($browser, $context, $storedfile, $urlbase, $areas[$filearea], true, true, false, false);
 911  
 912      } else if ($filearea === 'package') {
 913          $filepath = is_null($filepath) ? '/' : $filepath;
 914          $filename = is_null($filename) ? '.' : $filename;
 915  
 916          $urlbase = $CFG->wwwroot.'/pluginfile.php';
 917          if (!$storedfile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, $filepath, $filename)) {
 918              if ($filepath === '/' and $filename === '.') {
 919                  $storedfile = new virtual_root_file($context->id, 'mod_scorm', 'package', 0);
 920              } else {
 921                  // Not found.
 922                  return null;
 923              }
 924          }
 925          return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false);
 926      }
 927  
 928      // Scorm_intro handled in file_browser.
 929  
 930      return false;
 931  }
 932  
 933  /**
 934   * Serves scorm content, introduction images and packages. Implements needed access control ;-)
 935   *
 936   * @package  mod_scorm
 937   * @category files
 938   * @param stdClass $course course object
 939   * @param stdClass $cm course module object
 940   * @param stdClass $context context object
 941   * @param string $filearea file area
 942   * @param array $args extra arguments
 943   * @param bool $forcedownload whether or not force download
 944   * @param array $options additional options affecting the file serving
 945   * @return bool false if file not found, does not return if found - just send the file
 946   */
 947  function scorm_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
 948      global $CFG, $DB;
 949  
 950      if ($context->contextlevel != CONTEXT_MODULE) {
 951          return false;
 952      }
 953  
 954      require_login($course, true, $cm);
 955  
 956      $canmanageactivity = has_capability('moodle/course:manageactivities', $context);
 957      $lifetime = null;
 958  
 959      // Check SCORM availability.
 960      if (!$canmanageactivity) {
 961          require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 962  
 963          $scorm = $DB->get_record('scorm', array('id' => $cm->instance), 'id, timeopen, timeclose', MUST_EXIST);
 964          list($available, $warnings) = scorm_get_availability_status($scorm);
 965          if (!$available) {
 966              return false;
 967          }
 968      }
 969  
 970      if ($filearea === 'content') {
 971          $revision = (int)array_shift($args); // Prevents caching problems - ignored here.
 972          $relativepath = implode('/', $args);
 973          $fullpath = "/$context->id/mod_scorm/content/0/$relativepath";
 974          $options['immutable'] = true; // Add immutable option, $relativepath changes on file update.
 975  
 976      } else if ($filearea === 'package') {
 977          // Check if the global setting for disabling package downloads is enabled.
 978          $protectpackagedownloads = get_config('scorm', 'protectpackagedownloads');
 979          if ($protectpackagedownloads and !$canmanageactivity) {
 980              return false;
 981          }
 982          $revision = (int)array_shift($args); // Prevents caching problems - ignored here.
 983          $relativepath = implode('/', $args);
 984          $fullpath = "/$context->id/mod_scorm/package/0/$relativepath";
 985          $lifetime = 0; // No caching here.
 986  
 987      } else if ($filearea === 'imsmanifest') { // This isn't a real filearea, it's a url parameter for this type of package.
 988          $revision = (int)array_shift($args); // Prevents caching problems - ignored here.
 989          $relativepath = implode('/', $args);
 990  
 991          // Get imsmanifest file.
 992          $fs = get_file_storage();
 993          $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false);
 994          $file = reset($files);
 995  
 996          // Check that the package file is an imsmanifest.xml file - if not then this method is not allowed.
 997          $packagefilename = $file->get_filename();
 998          if (strtolower($packagefilename) !== 'imsmanifest.xml') {
 999              return false;
1000          }
1001  
1002          $file->send_relative_file($relativepath);
1003      } else {
1004          return false;
1005      }
1006  
1007      $fs = get_file_storage();
1008      if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1009          if ($filearea === 'content') { // Return file not found straight away to improve performance.
1010              send_header_404();
1011              die;
1012          }
1013          return false;
1014      }
1015  
1016      // Allow SVG files to be loaded within SCORM content, instead of forcing download.
1017      $options['dontforcesvgdownload'] = true;
1018  
1019      // Finally send the file.
1020      send_stored_file($file, $lifetime, 0, false, $options);
1021  }
1022  
1023  /**
1024   * @uses FEATURE_GROUPS
1025   * @uses FEATURE_GROUPINGS
1026   * @uses FEATURE_MOD_INTRO
1027   * @uses FEATURE_COMPLETION_TRACKS_VIEWS
1028   * @uses FEATURE_COMPLETION_HAS_RULES
1029   * @uses FEATURE_GRADE_HAS_GRADE
1030   * @uses FEATURE_GRADE_OUTCOMES
1031   * @param string $feature FEATURE_xx constant for requested feature
1032   * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose.
1033   */
1034  function scorm_supports($feature) {
1035      switch($feature) {
1036          case FEATURE_GROUPS:                  return true;
1037          case FEATURE_GROUPINGS:               return true;
1038          case FEATURE_MOD_INTRO:               return true;
1039          case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
1040          case FEATURE_COMPLETION_HAS_RULES:    return true;
1041          case FEATURE_GRADE_HAS_GRADE:         return true;
1042          case FEATURE_GRADE_OUTCOMES:          return true;
1043          case FEATURE_BACKUP_MOODLE2:          return true;
1044          case FEATURE_SHOW_DESCRIPTION:        return true;
1045          case FEATURE_MOD_PURPOSE:             return MOD_PURPOSE_CONTENT;
1046  
1047          default: return null;
1048      }
1049  }
1050  
1051  /**
1052   * Get the filename for a temp log file
1053   *
1054   * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
1055   * @param integer $scoid - scoid of object this log entry is for
1056   * @return string The filename as an absolute path
1057   */
1058  function scorm_debug_log_filename($type, $scoid) {
1059      global $CFG, $USER;
1060  
1061      $logpath = $CFG->tempdir.'/scormlogs';
1062      $logfile = $logpath.'/'.$type.'debug_'.$USER->id.'_'.$scoid.'.log';
1063      return $logfile;
1064  }
1065  
1066  /**
1067   * writes log output to a temp log file
1068   *
1069   * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
1070   * @param string $text - text to be written to file.
1071   * @param integer $scoid - scoid of object this log entry is for.
1072   */
1073  function scorm_debug_log_write($type, $text, $scoid) {
1074      global $CFG;
1075  
1076      $debugenablelog = get_config('scorm', 'allowapidebug');
1077      if (!$debugenablelog || empty($text)) {
1078          return;
1079      }
1080      if (make_temp_directory('scormlogs/')) {
1081          $logfile = scorm_debug_log_filename($type, $scoid);
1082          @file_put_contents($logfile, date('Y/m/d H:i:s O')." DEBUG $text\r\n", FILE_APPEND);
1083          @chmod($logfile, $CFG->filepermissions);
1084      }
1085  }
1086  
1087  /**
1088   * Remove debug log file
1089   *
1090   * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
1091   * @param integer $scoid - scoid of object this log entry is for
1092   * @return boolean True if the file is successfully deleted, false otherwise
1093   */
1094  function scorm_debug_log_remove($type, $scoid) {
1095  
1096      $debugenablelog = get_config('scorm', 'allowapidebug');
1097      $logfile = scorm_debug_log_filename($type, $scoid);
1098      if (!$debugenablelog || !file_exists($logfile)) {
1099          return false;
1100      }
1101  
1102      return @unlink($logfile);
1103  }
1104  
1105  /**
1106   * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
1107   */
1108  function scorm_print_overview() {
1109      throw new coding_exception('scorm_print_overview() can not be used any more and is obsolete.');
1110  }
1111  
1112  /**
1113   * Return a list of page types
1114   * @param string $pagetype current page type
1115   * @param stdClass $parentcontext Block's parent context
1116   * @param stdClass $currentcontext Current context of block
1117   */
1118  function scorm_page_type_list($pagetype, $parentcontext, $currentcontext) {
1119      $modulepagetype = array('mod-scorm-*' => get_string('page-mod-scorm-x', 'scorm'));
1120      return $modulepagetype;
1121  }
1122  
1123  /**
1124   * Returns the SCORM version used.
1125   * @param string $scormversion comes from $scorm->version
1126   * @param string $version one of the defined vars SCORM_12, SCORM_13, SCORM_AICC (or empty)
1127   * @return Scorm version.
1128   */
1129  function scorm_version_check($scormversion, $version='') {
1130      $scormversion = trim(strtolower($scormversion));
1131      if (empty($version) || $version == SCORM_12) {
1132          if ($scormversion == 'scorm_12' || $scormversion == 'scorm_1.2') {
1133              return SCORM_12;
1134          }
1135          if (!empty($version)) {
1136              return false;
1137          }
1138      }
1139      if (empty($version) || $version == SCORM_13) {
1140          if ($scormversion == 'scorm_13' || $scormversion == 'scorm_1.3') {
1141              return SCORM_13;
1142          }
1143          if (!empty($version)) {
1144              return false;
1145          }
1146      }
1147      if (empty($version) || $version == SCORM_AICC) {
1148          if (strpos($scormversion, 'aicc')) {
1149              return SCORM_AICC;
1150          }
1151          if (!empty($version)) {
1152              return false;
1153          }
1154      }
1155      return false;
1156  }
1157  
1158  /**
1159   * Register the ability to handle drag and drop file uploads
1160   * @return array containing details of the files / types the mod can handle
1161   */
1162  function scorm_dndupload_register() {
1163      return array('files' => array(
1164          array('extension' => 'zip', 'message' => get_string('dnduploadscorm', 'scorm'))
1165      ));
1166  }
1167  
1168  /**
1169   * Handle a file that has been uploaded
1170   * @param object $uploadinfo details of the file / content that has been uploaded
1171   * @return int instance id of the newly created mod
1172   */
1173  function scorm_dndupload_handle($uploadinfo) {
1174  
1175      $context = context_module::instance($uploadinfo->coursemodule);
1176      file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_scorm', 'package', 0);
1177      $fs = get_file_storage();
1178      $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, 'sortorder, itemid, filepath, filename', false);
1179      $file = reset($files);
1180  
1181      // Validate the file, make sure it's a valid SCORM package!
1182      $errors = scorm_validate_package($file);
1183      if (!empty($errors)) {
1184          return false;
1185      }
1186      // Create a default scorm object to pass to scorm_add_instance()!
1187      $scorm = get_config('scorm');
1188      $scorm->course = $uploadinfo->course->id;
1189      $scorm->coursemodule = $uploadinfo->coursemodule;
1190      $scorm->cmidnumber = '';
1191      $scorm->name = $uploadinfo->displayname;
1192      $scorm->scormtype = SCORM_TYPE_LOCAL;
1193      $scorm->reference = $file->get_filename();
1194      $scorm->intro = '';
1195      $scorm->width = $scorm->framewidth;
1196      $scorm->height = $scorm->frameheight;
1197  
1198      return scorm_add_instance($scorm, null);
1199  }
1200  
1201  /**
1202   * Sets activity completion state
1203   *
1204   * @param object $scorm object
1205   * @param int $userid User ID
1206   * @param int $completionstate Completion state
1207   * @param array $grades grades array of users with grades - used when $userid = 0
1208   */
1209  function scorm_set_completion($scorm, $userid, $completionstate = COMPLETION_COMPLETE, $grades = array()) {
1210      $course = new stdClass();
1211      $course->id = $scorm->course;
1212      $completion = new completion_info($course);
1213  
1214      // Check if completion is enabled site-wide, or for the course.
1215      if (!$completion->is_enabled()) {
1216          return;
1217      }
1218  
1219      $cm = get_coursemodule_from_instance('scorm', $scorm->id, $scorm->course);
1220      if (empty($cm) || !$completion->is_enabled($cm)) {
1221              return;
1222      }
1223  
1224      if (empty($userid)) { // We need to get all the relevant users from $grades param.
1225          foreach ($grades as $grade) {
1226              $completion->update_state($cm, $completionstate, $grade->userid);
1227          }
1228      } else {
1229          $completion->update_state($cm, $completionstate, $userid);
1230      }
1231  }
1232  
1233  /**
1234   * Check that a Zip file contains a valid SCORM package
1235   *
1236   * @param $file stored_file a Zip file.
1237   * @return array empty if no issue is found. Array of error message otherwise
1238   */
1239  function scorm_validate_package($file) {
1240      $packer = get_file_packer('application/zip');
1241      $errors = array();
1242      if ($file->is_external_file()) { // Get zip file so we can check it is correct.
1243          $file->import_external_file_contents();
1244      }
1245      $filelist = $file->list_files($packer);
1246  
1247      if (!is_array($filelist)) {
1248          $errors['packagefile'] = get_string('badarchive', 'scorm');
1249      } else {
1250          $aiccfound = false;
1251          $badmanifestpresent = false;
1252          foreach ($filelist as $info) {
1253              if ($info->pathname == 'imsmanifest.xml') {
1254                  return array();
1255              } else if (strpos($info->pathname, 'imsmanifest.xml') !== false) {
1256                  // This package has an imsmanifest file inside a folder of the package.
1257                  $badmanifestpresent = true;
1258              }
1259              if (preg_match('/\.cst$/', $info->pathname)) {
1260                  return array();
1261              }
1262          }
1263          if (!$aiccfound) {
1264              if ($badmanifestpresent) {
1265                  $errors['packagefile'] = get_string('badimsmanifestlocation', 'scorm');
1266              } else {
1267                  $errors['packagefile'] = get_string('nomanifest', 'scorm');
1268              }
1269          }
1270      }
1271      return $errors;
1272  }
1273  
1274  /**
1275   * Check and set the correct mode and attempt when entering a SCORM package.
1276   *
1277   * @param object $scorm object
1278   * @param string $newattempt should a new attempt be generated here.
1279   * @param int $attempt the attempt number this is for.
1280   * @param int $userid the userid of the user.
1281   * @param string $mode the current mode that has been selected.
1282   */
1283  function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) {
1284      global $DB;
1285  
1286      if (($mode == 'browse')) {
1287          if ($scorm->hidebrowse == 1) {
1288              // Prevent Browse mode if hidebrowse is set.
1289              $mode = 'normal';
1290          } else {
1291              // We don't need to check attempts as browse mode is set.
1292              return;
1293          }
1294      }
1295  
1296      if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS) {
1297          // This SCORM is configured to force a new attempt on every re-entry.
1298          $newattempt = 'on';
1299          $mode = 'normal';
1300          if ($attempt == 1) {
1301              // Check if the user has any existing data or if this is really the first attempt.
1302              $exists = $DB->record_exists('scorm_attempt', ['userid' => $userid, 'scormid' => $scorm->id]);
1303              if (!$exists) {
1304                  // No records yet - Attempt should == 1.
1305                  return;
1306              }
1307          }
1308          $attempt++;
1309  
1310          return;
1311      }
1312      // Check if the scorm module is incomplete (used to validate user request to start a new attempt).
1313      $incomplete = true;
1314  
1315      // Note - in SCORM_13 the cmi-core.lesson_status field was split into
1316      // 'cmi.completion_status' and 'cmi.success_status'.
1317      // 'cmi.completion_status' can only contain values 'completed', 'incomplete', 'not attempted' or 'unknown'.
1318      // This means the values 'passed' or 'failed' will never be reported for a track in SCORM_13 and
1319      // the only status that will be treated as complete is 'completed'.
1320  
1321      $completionelements = array(
1322          SCORM_12 => 'cmi.core.lesson_status',
1323          SCORM_13 => 'cmi.completion_status',
1324          SCORM_AICC => 'cmi.core.lesson_status'
1325      );
1326      $scormversion = scorm_version_check($scorm->version);
1327      if($scormversion===false) {
1328          $scormversion = SCORM_12;
1329      }
1330      $completionelement = $completionelements[$scormversion];
1331  
1332      $sql = "SELECT sc.id, sub.value
1333                FROM {scorm_scoes} sc
1334           LEFT JOIN (SELECT v.scoid, v.value
1335                        FROM {scorm_attempt} a
1336                        JOIN {scorm_scoes_value} v ON a.id = v.attemptid
1337                        JOIN {scorm_element} e on e.id = v.elementid AND e.element = :element
1338                       WHERE a.userid = :userid AND a.attempt = :attempt AND a.scormid = :scormid) sub ON sub.scoid = sc.id
1339               WHERE sc.scormtype = 'sco' AND sc.scorm = :scormid2";
1340      $tracks = $DB->get_recordset_sql($sql, ['userid' => $userid, 'attempt' => $attempt,
1341                                              'element' => $completionelement, 'scormid' => $scorm->id,
1342                                              'scormid2' => $scorm->id]);
1343  
1344      foreach ($tracks as $track) {
1345          if (($track->value == 'completed') || ($track->value == 'passed') || ($track->value == 'failed')) {
1346              $incomplete = false;
1347          } else {
1348              $incomplete = true;
1349              break; // Found an incomplete sco, so the result as a whole is incomplete.
1350          }
1351      }
1352      $tracks->close();
1353  
1354      // Validate user request to start a new attempt.
1355      if ($incomplete === true) {
1356          // The option to start a new attempt should never have been presented. Force false.
1357          $newattempt = 'off';
1358      } else if (!empty($scorm->forcenewattempt)) {
1359          // A new attempt should be forced for already completed attempts.
1360          $newattempt = 'on';
1361      }
1362  
1363      if (($newattempt == 'on') && (($attempt < $scorm->maxattempt) || ($scorm->maxattempt == 0))) {
1364          $attempt++;
1365          $mode = 'normal';
1366      } else { // Check if review mode should be set.
1367          if ($incomplete === true) {
1368              $mode = 'normal';
1369          } else {
1370              $mode = 'review';
1371          }
1372      }
1373  }
1374  
1375  /**
1376   * Trigger the course_module_viewed event.
1377   *
1378   * @param  stdClass $scorm        scorm object
1379   * @param  stdClass $course     course object
1380   * @param  stdClass $cm         course module object
1381   * @param  stdClass $context    context object
1382   * @since Moodle 3.0
1383   */
1384  function scorm_view($scorm, $course, $cm, $context) {
1385  
1386      // Trigger course_module_viewed event.
1387      $params = array(
1388          'context' => $context,
1389          'objectid' => $scorm->id
1390      );
1391  
1392      $event = \mod_scorm\event\course_module_viewed::create($params);
1393      $event->add_record_snapshot('course_modules', $cm);
1394      $event->add_record_snapshot('course', $course);
1395      $event->add_record_snapshot('scorm', $scorm);
1396      $event->trigger();
1397  }
1398  
1399  /**
1400   * Check if the module has any update that affects the current user since a given time.
1401   *
1402   * @param  cm_info $cm course module data
1403   * @param  int $from the time to check updates from
1404   * @param  array $filter  if we need to check only specific updates
1405   * @return stdClass an object with the different type of areas indicating if they were updated or not
1406   * @since Moodle 3.2
1407   */
1408  function scorm_check_updates_since(cm_info $cm, $from, $filter = array()) {
1409      global $DB, $USER, $CFG;
1410      require_once($CFG->dirroot . '/mod/scorm/locallib.php');
1411  
1412      $scorm = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
1413      $updates = new stdClass();
1414      list($available, $warnings) = scorm_get_availability_status($scorm, true, $cm->context);
1415      if (!$available) {
1416          return $updates;
1417      }
1418      $updates = course_check_module_updates_since($cm, $from, array('package'), $filter);
1419  
1420      $updates->tracks = (object) array('updated' => false);
1421      $sql = "SELECT v.id
1422                FROM {scorm_scoes_value} v
1423                JOIN {scorm_attempt} a ON a.id = v.attemptid
1424               WHERE a.scormid = :scormid AND v.timemodified > :timemodified";
1425      $params = ['scormid' => $scorm->id, 'timemodified' => $from, 'userid' => $USER->id];
1426      $tracks = $DB->get_records_sql($sql ." AND a.userid = :userid", $params);
1427      if (!empty($tracks)) {
1428          $updates->tracks->updated = true;
1429          $updates->tracks->itemids = array_keys($tracks);
1430      }
1431  
1432      // Now, teachers should see other students updates.
1433      if (has_capability('mod/scorm:viewreport', $cm->context)) {
1434          $params = ['scormid' => $scorm->id, 'timemodified' => $from];
1435  
1436          if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
1437              $groupusers = array_keys(groups_get_activity_shared_group_members($cm));
1438              if (empty($groupusers)) {
1439                  return $updates;
1440              }
1441              list($insql, $inparams) = $DB->get_in_or_equal($groupusers, SQL_PARAMS_NAMED);
1442              $sql .= ' AND userid ' . $insql;
1443              $params = array_merge($params, $inparams);
1444          }
1445  
1446          $updates->usertracks = (object) array('updated' => false);
1447  
1448          $tracks = $DB->get_records_sql($sql, $params);
1449          if (!empty($tracks)) {
1450              $updates->usertracks->updated = true;
1451              $updates->usertracks->itemids = array_keys($tracks);
1452          }
1453      }
1454      return $updates;
1455  }
1456  
1457  /**
1458   * Get icon mapping for font-awesome.
1459   */
1460  function mod_scorm_get_fontawesome_icon_map() {
1461      return [
1462          'mod_scorm:assetc' => 'fa-file-archive-o',
1463          'mod_scorm:asset' => 'fa-file-archive-o',
1464          'mod_scorm:browsed' => 'fa-book',
1465          'mod_scorm:completed' => 'fa-check-square-o',
1466          'mod_scorm:failed' => 'fa-times',
1467          'mod_scorm:incomplete' => 'fa-pencil-square-o',
1468          'mod_scorm:minus' => 'fa-minus',
1469          'mod_scorm:notattempted' => 'fa-square-o',
1470          'mod_scorm:passed' => 'fa-check',
1471          'mod_scorm:plus' => 'fa-plus',
1472          'mod_scorm:popdown' => 'fa-window-close-o',
1473          'mod_scorm:popup' => 'fa-window-restore',
1474          'mod_scorm:suspend' => 'fa-pause',
1475          'mod_scorm:wait' => 'fa-clock-o',
1476      ];
1477  }
1478  
1479  /**
1480   * This standard function will check all instances of this module
1481   * and make sure there are up-to-date events created for each of them.
1482   * If courseid = 0, then every scorm event in the site is checked, else
1483   * only scorm events belonging to the course specified are checked.
1484   *
1485   * @param int $courseid
1486   * @param int|stdClass $instance scorm module instance or ID.
1487   * @param int|stdClass $cm Course module object or ID.
1488   * @return bool
1489   */
1490  function scorm_refresh_events($courseid = 0, $instance = null, $cm = null) {
1491      global $CFG, $DB;
1492  
1493      require_once($CFG->dirroot . '/mod/scorm/locallib.php');
1494  
1495      // If we have instance information then we can just update the one event instead of updating all events.
1496      if (isset($instance)) {
1497          if (!is_object($instance)) {
1498              $instance = $DB->get_record('scorm', array('id' => $instance), '*', MUST_EXIST);
1499          }
1500          if (isset($cm)) {
1501              if (!is_object($cm)) {
1502                  $cm = (object)array('id' => $cm);
1503              }
1504          } else {
1505              $cm = get_coursemodule_from_instance('scorm', $instance->id);
1506          }
1507          scorm_update_calendar($instance, $cm->id);
1508          return true;
1509      }
1510  
1511      if ($courseid) {
1512          // Make sure that the course id is numeric.
1513          if (!is_numeric($courseid)) {
1514              return false;
1515          }
1516          if (!$scorms = $DB->get_records('scorm', array('course' => $courseid))) {
1517              return false;
1518          }
1519      } else {
1520          if (!$scorms = $DB->get_records('scorm')) {
1521              return false;
1522          }
1523      }
1524  
1525      foreach ($scorms as $scorm) {
1526          $cm = get_coursemodule_from_instance('scorm', $scorm->id);
1527          scorm_update_calendar($scorm, $cm->id);
1528      }
1529  
1530      return true;
1531  }
1532  
1533  /**
1534   * This function receives a calendar event and returns the action associated with it, or null if there is none.
1535   *
1536   * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
1537   * is not displayed on the block.
1538   *
1539   * @param calendar_event $event
1540   * @param \core_calendar\action_factory $factory
1541   * @param int $userid User id override
1542   * @return \core_calendar\local\event\entities\action_interface|null
1543   */
1544  function mod_scorm_core_calendar_provide_event_action(calendar_event $event,
1545                                                        \core_calendar\action_factory $factory, $userid = null) {
1546      global $CFG, $USER;
1547  
1548      require_once($CFG->dirroot . '/mod/scorm/locallib.php');
1549  
1550      if (empty($userid)) {
1551          $userid = $USER->id;
1552      }
1553  
1554      $cm = get_fast_modinfo($event->courseid, $userid)->instances['scorm'][$event->instance];
1555  
1556      if (has_capability('mod/scorm:viewreport', $cm->context, $userid)) {
1557          // Teachers do not need to be reminded to complete a scorm.
1558          return null;
1559      }
1560  
1561      $completion = new \completion_info($cm->get_course());
1562  
1563      $completiondata = $completion->get_data($cm, false, $userid);
1564  
1565      if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
1566          return null;
1567      }
1568  
1569      if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
1570          // The scorm has closed so the user can no longer submit anything.
1571          return null;
1572      }
1573  
1574      // Restore scorm object from cached values in $cm, we only need id, timeclose and timeopen.
1575      $customdata = $cm->customdata ?: [];
1576      $customdata['id'] = $cm->instance;
1577      $scorm = (object)($customdata + ['timeclose' => 0, 'timeopen' => 0]);
1578  
1579      // Check that the SCORM activity is open.
1580      list($actionable, $warnings) = scorm_get_availability_status($scorm, false, null, $userid);
1581  
1582      return $factory->create_instance(
1583          get_string('enter', 'scorm'),
1584          new \moodle_url('/mod/scorm/view.php', array('id' => $cm->id)),
1585          1,
1586          $actionable
1587      );
1588  }
1589  
1590  /**
1591   * Add a get_coursemodule_info function in case any SCORM type wants to add 'extra' information
1592   * for the course (see resource).
1593   *
1594   * Given a course_module object, this function returns any "extra" information that may be needed
1595   * when printing this activity in a course listing.  See get_array_of_activities() in course/lib.php.
1596   *
1597   * @param stdClass $coursemodule The coursemodule object (record).
1598   * @return cached_cm_info An object on information that the courses
1599   *                        will know about (most noticeably, an icon).
1600   */
1601  function scorm_get_coursemodule_info($coursemodule) {
1602      global $DB;
1603  
1604      $dbparams = ['id' => $coursemodule->instance];
1605      $fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos, '.
1606          'timeopen, timeclose';
1607      if (!$scorm = $DB->get_record('scorm', $dbparams, $fields)) {
1608          return false;
1609      }
1610  
1611      $result = new cached_cm_info();
1612      $result->name = $scorm->name;
1613  
1614      if ($coursemodule->showdescription) {
1615          // Convert intro to html. Do not filter cached version, filters run at display time.
1616          $result->content = format_module_intro('scorm', $scorm, $coursemodule->id, false);
1617      }
1618  
1619      // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
1620      if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
1621          $result->customdata['customcompletionrules']['completionstatusrequired'] = $scorm->completionstatusrequired;
1622          $result->customdata['customcompletionrules']['completionscorerequired'] = $scorm->completionscorerequired;
1623          $result->customdata['customcompletionrules']['completionstatusallscos'] = $scorm->completionstatusallscos;
1624      }
1625      // Populate some other values that can be used in calendar or on dashboard.
1626      if ($scorm->timeopen) {
1627          $result->customdata['timeopen'] = $scorm->timeopen;
1628      }
1629      if ($scorm->timeclose) {
1630          $result->customdata['timeclose'] = $scorm->timeclose;
1631      }
1632  
1633      return $result;
1634  }
1635  
1636  /**
1637   * Callback which returns human-readable strings describing the active completion custom rules for the module instance.
1638   *
1639   * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules']
1640   * @return array $descriptions the array of descriptions for the custom rules.
1641   */
1642  function mod_scorm_get_completion_active_rule_descriptions($cm) {
1643      // Values will be present in cm_info, and we assume these are up to date.
1644      if (empty($cm->customdata['customcompletionrules'])
1645          || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
1646          return [];
1647      }
1648  
1649      $descriptions = [];
1650      foreach ($cm->customdata['customcompletionrules'] as $key => $val) {
1651          switch ($key) {
1652              case 'completionstatusrequired':
1653                  if (!is_null($val)) {
1654                      // Determine the selected statuses using a bitwise operation.
1655                      $cvalues = array();
1656                      foreach (scorm_status_options(true) as $bit => $string) {
1657                          if (($val & $bit) == $bit) {
1658                              $cvalues[] = $string;
1659                          }
1660                      }
1661                      $statusstring = implode(', ', $cvalues);
1662                      $descriptions[] = get_string('completionstatusrequireddesc', 'scorm', $statusstring);
1663                  }
1664                  break;
1665              case 'completionscorerequired':
1666                  if (!is_null($val)) {
1667                      $descriptions[] = get_string('completionscorerequireddesc', 'scorm', $val);
1668                  }
1669                  break;
1670              case 'completionstatusallscos':
1671                  if (!empty($val)) {
1672                      $descriptions[] = get_string('completionstatusallscos', 'scorm');
1673                  }
1674                  break;
1675              default:
1676                  break;
1677          }
1678      }
1679      return $descriptions;
1680  }
1681  
1682  /**
1683   * This function will update the scorm module according to the
1684   * event that has been modified.
1685   *
1686   * It will set the timeopen or timeclose value of the scorm instance
1687   * according to the type of event provided.
1688   *
1689   * @throws \moodle_exception
1690   * @param \calendar_event $event
1691   * @param stdClass $scorm The module instance to get the range from
1692   */
1693  function mod_scorm_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $scorm) {
1694      global $DB;
1695  
1696      if (empty($event->instance) || $event->modulename != 'scorm') {
1697          return;
1698      }
1699  
1700      if ($event->instance != $scorm->id) {
1701          return;
1702      }
1703  
1704      if (!in_array($event->eventtype, [SCORM_EVENT_TYPE_OPEN, SCORM_EVENT_TYPE_CLOSE])) {
1705          return;
1706      }
1707  
1708      $courseid = $event->courseid;
1709      $modulename = $event->modulename;
1710      $instanceid = $event->instance;
1711      $modified = false;
1712  
1713      $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
1714      $context = context_module::instance($coursemodule->id);
1715  
1716      // The user does not have the capability to modify this activity.
1717      if (!has_capability('moodle/course:manageactivities', $context)) {
1718          return;
1719      }
1720  
1721      if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) {
1722          // If the event is for the scorm activity opening then we should
1723          // set the start time of the scorm activity to be the new start
1724          // time of the event.
1725          if ($scorm->timeopen != $event->timestart) {
1726              $scorm->timeopen = $event->timestart;
1727              $scorm->timemodified = time();
1728              $modified = true;
1729          }
1730      } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) {
1731          // If the event is for the scorm activity closing then we should
1732          // set the end time of the scorm activity to be the new start
1733          // time of the event.
1734          if ($scorm->timeclose != $event->timestart) {
1735              $scorm->timeclose = $event->timestart;
1736              $modified = true;
1737          }
1738      }
1739  
1740      if ($modified) {
1741          $scorm->timemodified = time();
1742          $DB->update_record('scorm', $scorm);
1743          $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
1744          $event->trigger();
1745      }
1746  }
1747  
1748  /**
1749   * This function calculates the minimum and maximum cutoff values for the timestart of
1750   * the given event.
1751   *
1752   * It will return an array with two values, the first being the minimum cutoff value and
1753   * the second being the maximum cutoff value. Either or both values can be null, which
1754   * indicates there is no minimum or maximum, respectively.
1755   *
1756   * If a cutoff is required then the function must return an array containing the cutoff
1757   * timestamp and error string to display to the user if the cutoff value is violated.
1758   *
1759   * A minimum and maximum cutoff return value will look like:
1760   * [
1761   *     [1505704373, 'The date must be after this date'],
1762   *     [1506741172, 'The date must be before this date']
1763   * ]
1764   *
1765   * @param \calendar_event $event The calendar event to get the time range for
1766   * @param \stdClass $instance The module instance to get the range from
1767   * @return array Returns an array with min and max date.
1768   */
1769  function mod_scorm_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
1770      $mindate = null;
1771      $maxdate = null;
1772  
1773      if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) {
1774          // The start time of the open event can't be equal to or after the
1775          // close time of the scorm activity.
1776          if (!empty($instance->timeclose)) {
1777              $maxdate = [
1778                  $instance->timeclose,
1779                  get_string('openafterclose', 'scorm')
1780              ];
1781          }
1782      } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) {
1783          // The start time of the close event can't be equal to or earlier than the
1784          // open time of the scorm activity.
1785          if (!empty($instance->timeopen)) {
1786              $mindate = [
1787                  $instance->timeopen,
1788                  get_string('closebeforeopen', 'scorm')
1789              ];
1790          }
1791      }
1792  
1793      return [$mindate, $maxdate];
1794  }
1795  
1796  /**
1797   * Given an array with a file path, it returns the itemid and the filepath for the defined filearea.
1798   *
1799   * @param  string $filearea The filearea.
1800   * @param  array  $args The path (the part after the filearea and before the filename).
1801   * @return array The itemid and the filepath inside the $args path, for the defined filearea.
1802   */
1803  function mod_scorm_get_path_from_pluginfile(string $filearea, array $args) : array {
1804      // SCORM never has an itemid (the number represents the revision but it's not stored in database).
1805      array_shift($args);
1806  
1807      // Get the filepath.
1808      if (empty($args)) {
1809          $filepath = '/';
1810      } else {
1811          $filepath = '/' . implode('/', $args) . '/';
1812      }
1813  
1814      return [
1815          'itemid' => 0,
1816          'filepath' => $filepath,
1817      ];
1818  }
1819  
1820  /**
1821   * Callback to fetch the activity event type lang string.
1822   *
1823   * @param string $eventtype The event type.
1824   * @return lang_string The event type lang string.
1825   */
1826  function mod_scorm_core_calendar_get_event_action_string(string $eventtype): string {
1827      $modulename = get_string('modulename', 'scorm');
1828  
1829      switch ($eventtype) {
1830          case SCORM_EVENT_TYPE_OPEN:
1831              $identifier = 'calendarstart';
1832              break;
1833          case SCORM_EVENT_TYPE_CLOSE:
1834              $identifier = 'calendarend';
1835              break;
1836          default:
1837              return get_string('requiresaction', 'calendar', $modulename);
1838      }
1839  
1840      return get_string($identifier, 'scorm', $modulename);
1841  }
1842  
1843  /**
1844   * This function extends the settings navigation block for the site.
1845   *
1846   * It is safe to rely on PAGE here as we will only ever be within the module
1847   * context when this is called
1848   *
1849   * @param settings_navigation $settings navigation_node object.
1850   * @param navigation_node $scormnode navigation_node object.
1851   * @return void
1852   */
1853  function scorm_extend_settings_navigation(settings_navigation $settings, navigation_node $scormnode): void {
1854      if (has_capability('mod/scorm:viewreport', $settings->get_page()->cm->context)) {
1855          $url = new moodle_url('/mod/scorm/report.php', ['id' => $settings->get_page()->cm->id]);
1856          $scormnode->add(get_string("reports", "scorm"), $url, navigation_node::TYPE_CUSTOM, null, 'scormreport');
1857      }
1858  }