[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/scorm/ -> locallib.php (source)

   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   * Library of internal classes and functions for module SCORM
  19   *
  20   * @package    mod_scorm
  21   * @copyright  1999 onwards Roberto Pinna
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  require_once("$CFG->dirroot/mod/scorm/lib.php");
  26  require_once("$CFG->libdir/filelib.php");
  27  
  28  // Constants and settings for module scorm.
  29  define('SCORM_UPDATE_NEVER', '0');
  30  define('SCORM_UPDATE_EVERYDAY', '2');
  31  define('SCORM_UPDATE_EVERYTIME', '3');
  32  
  33  define('SCORM_SKIPVIEW_NEVER', '0');
  34  define('SCORM_SKIPVIEW_FIRST', '1');
  35  define('SCORM_SKIPVIEW_ALWAYS', '2');
  36  
  37  define('SCO_ALL', 0);
  38  define('SCO_DATA', 1);
  39  define('SCO_ONLY', 2);
  40  
  41  define('GRADESCOES', '0');
  42  define('GRADEHIGHEST', '1');
  43  define('GRADEAVERAGE', '2');
  44  define('GRADESUM', '3');
  45  
  46  define('HIGHESTATTEMPT', '0');
  47  define('AVERAGEATTEMPT', '1');
  48  define('FIRSTATTEMPT', '2');
  49  define('LASTATTEMPT', '3');
  50  
  51  define('TOCJSLINK', 1);
  52  define('TOCFULLURL', 2);
  53  
  54  // Local Library of functions for module scorm.
  55  
  56  /**
  57   * @package   mod_scorm
  58   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  59   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  60   */
  61  class scorm_package_file_info extends file_info_stored {
  62      public function get_parent() {
  63          if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
  64              return $this->browser->get_file_info($this->context);
  65          }
  66          return parent::get_parent();
  67      }
  68      public function get_visible_name() {
  69          if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
  70              return $this->topvisiblename;
  71          }
  72          return parent::get_visible_name();
  73      }
  74  }
  75  
  76  /**
  77   * Returns an array of the popup options for SCORM and each options default value
  78   *
  79   * @return array an array of popup options as the key and their defaults as the value
  80   */
  81  function scorm_get_popup_options_array() {
  82      $cfgscorm = get_config('scorm');
  83  
  84      return array('scrollbars' => isset($cfgscorm->scrollbars) ? $cfgscorm->scrollbars : 0,
  85                   'directories' => isset($cfgscorm->directories) ? $cfgscorm->directories : 0,
  86                   'location' => isset($cfgscorm->location) ? $cfgscorm->location : 0,
  87                   'menubar' => isset($cfgscorm->menubar) ? $cfgscorm->menubar : 0,
  88                   'toolbar' => isset($cfgscorm->toolbar) ? $cfgscorm->toolbar : 0,
  89                   'status' => isset($cfgscorm->status) ? $cfgscorm->status : 0);
  90  }
  91  
  92  /**
  93   * Returns an array of the array of what grade options
  94   *
  95   * @return array an array of what grade options
  96   */
  97  function scorm_get_grade_method_array() {
  98      return array (GRADESCOES => get_string('gradescoes', 'scorm'),
  99                    GRADEHIGHEST => get_string('gradehighest', 'scorm'),
 100                    GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
 101                    GRADESUM => get_string('gradesum', 'scorm'));
 102  }
 103  
 104  /**
 105   * Returns an array of the array of what grade options
 106   *
 107   * @return array an array of what grade options
 108   */
 109  function scorm_get_what_grade_array() {
 110      return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
 111                    AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
 112                    FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
 113                    LASTATTEMPT => get_string('lastattempt', 'scorm'));
 114  }
 115  
 116  /**
 117   * Returns an array of the array of skip view options
 118   *
 119   * @return array an array of skip view options
 120   */
 121  function scorm_get_skip_view_array() {
 122      return array(SCORM_SKIPVIEW_NEVER => get_string('never'),
 123                   SCORM_SKIPVIEW_FIRST => get_string('firstaccess', 'scorm'),
 124                   SCORM_SKIPVIEW_ALWAYS => get_string('always'));
 125  }
 126  
 127  /**
 128   * Returns an array of the array of hide table of contents options
 129   *
 130   * @return array an array of hide table of contents options
 131   */
 132  function scorm_get_hidetoc_array() {
 133       return array(SCORM_TOC_SIDE => get_string('sided', 'scorm'),
 134                    SCORM_TOC_HIDDEN => get_string('hidden', 'scorm'),
 135                    SCORM_TOC_POPUP => get_string('popupmenu', 'scorm'),
 136                    SCORM_TOC_DISABLED => get_string('disabled', 'scorm'));
 137  }
 138  
 139  /**
 140   * Returns an array of the array of update frequency options
 141   *
 142   * @return array an array of update frequency options
 143   */
 144  function scorm_get_updatefreq_array() {
 145      return array(SCORM_UPDATE_NEVER => get_string('never'),
 146                   SCORM_UPDATE_EVERYDAY => get_string('everyday', 'scorm'),
 147                   SCORM_UPDATE_EVERYTIME => get_string('everytime', 'scorm'));
 148  }
 149  
 150  /**
 151   * Returns an array of the array of popup display options
 152   *
 153   * @return array an array of popup display options
 154   */
 155  function scorm_get_popup_display_array() {
 156      return array(0 => get_string('currentwindow', 'scorm'),
 157                   1 => get_string('popup', 'scorm'));
 158  }
 159  
 160  /**
 161   * Returns an array of the array of navigation buttons display options
 162   *
 163   * @return array an array of navigation buttons display options
 164   */
 165  function scorm_get_navigation_display_array() {
 166      return array(SCORM_NAV_DISABLED => get_string('no'),
 167                   SCORM_NAV_UNDER_CONTENT => get_string('undercontent', 'scorm'),
 168                   SCORM_NAV_FLOATING => get_string('floating', 'scorm'));
 169  }
 170  
 171  /**
 172   * Returns an array of the array of attempt options
 173   *
 174   * @return array an array of attempt options
 175   */
 176  function scorm_get_attempts_array() {
 177      $attempts = array(0 => get_string('nolimit', 'scorm'),
 178                        1 => get_string('attempt1', 'scorm'));
 179  
 180      for ($i = 2; $i <= 6; $i++) {
 181          $attempts[$i] = get_string('attemptsx', 'scorm', $i);
 182      }
 183  
 184      return $attempts;
 185  }
 186  
 187  /**
 188   * Returns an array of the attempt status options
 189   *
 190   * @return array an array of attempt status options
 191   */
 192  function scorm_get_attemptstatus_array() {
 193      return array(SCORM_DISPLAY_ATTEMPTSTATUS_NO => get_string('no'),
 194                   SCORM_DISPLAY_ATTEMPTSTATUS_ALL => get_string('attemptstatusall', 'scorm'),
 195                   SCORM_DISPLAY_ATTEMPTSTATUS_MY => get_string('attemptstatusmy', 'scorm'),
 196                   SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY => get_string('attemptstatusentry', 'scorm'));
 197  }
 198  
 199  /**
 200   * Extracts scrom package, sets up all variables.
 201   * Called whenever scorm changes
 202   * @param object $scorm instance - fields are updated and changes saved into database
 203   * @param bool $full force full update if true
 204   * @return void
 205   */
 206  function scorm_parse($scorm, $full) {
 207      global $CFG, $DB;
 208      $cfgscorm = get_config('scorm');
 209  
 210      if (!isset($scorm->cmid)) {
 211          $cm = get_coursemodule_from_instance('scorm', $scorm->id);
 212          $scorm->cmid = $cm->id;
 213      }
 214      $context = context_module::instance($scorm->cmid);
 215      $newhash = $scorm->sha1hash;
 216  
 217      if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
 218  
 219          $fs = get_file_storage();
 220          $packagefile = false;
 221          $packagefileimsmanifest = false;
 222  
 223          if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
 224              if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
 225                  if ($packagefile->is_external_file()) { // Get zip file so we can check it is correct.
 226                      $packagefile->import_external_file_contents();
 227                  }
 228                  $newhash = $packagefile->get_contenthash();
 229                  if (strtolower($packagefile->get_filename()) == 'imsmanifest.xml') {
 230                      $packagefileimsmanifest = true;
 231                  }
 232              } else {
 233                  $newhash = null;
 234              }
 235          } else {
 236              if (!$cfgscorm->allowtypelocalsync) {
 237                  // Sorry - localsync disabled.
 238                  return;
 239              }
 240              if ($scorm->reference !== '') {
 241                  $fs->delete_area_files($context->id, 'mod_scorm', 'package');
 242                  $filerecord = array('contextid' => $context->id, 'component' => 'mod_scorm', 'filearea' => 'package',
 243                                      'itemid' => 0, 'filepath' => '/');
 244                  if ($packagefile = $fs->create_file_from_url($filerecord, $scorm->reference, array('calctimeout' => true), true)) {
 245                      $newhash = $packagefile->get_contenthash();
 246                  } else {
 247                      $newhash = null;
 248                  }
 249              }
 250          }
 251  
 252          if ($packagefile) {
 253              if (!$full and $packagefile and $scorm->sha1hash === $newhash) {
 254                  if (strpos($scorm->version, 'SCORM') !== false) {
 255                      if ($packagefileimsmanifest || $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
 256                          // No need to update.
 257                          return;
 258                      }
 259                  } else if (strpos($scorm->version, 'AICC') !== false) {
 260                      // TODO: add more sanity checks - something really exists in scorm_content area.
 261                      return;
 262                  }
 263              }
 264              if (!$packagefileimsmanifest) {
 265                  // Now extract files.
 266                  $fs->delete_area_files($context->id, 'mod_scorm', 'content');
 267  
 268                  $packer = get_file_packer('application/zip');
 269                  $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/');
 270              }
 271  
 272          } else if (!$full) {
 273              return;
 274          }
 275          if ($packagefileimsmanifest) {
 276              require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
 277              // Direct link to imsmanifest.xml file.
 278              if (!scorm_parse_scorm($scorm, $packagefile)) {
 279                  $scorm->version = 'ERROR';
 280              }
 281  
 282          } else if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
 283              require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
 284              // SCORM.
 285              if (!scorm_parse_scorm($scorm, $manifest)) {
 286                  $scorm->version = 'ERROR';
 287              }
 288          } else {
 289              require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
 290              // AICC.
 291              $result = scorm_parse_aicc($scorm);
 292              if (!$result) {
 293                  $scorm->version = 'ERROR';
 294              } else {
 295                  $scorm->version = 'AICC';
 296              }
 297          }
 298  
 299      } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfgscorm->allowtypeexternal) {
 300          require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
 301          // SCORM only, AICC can not be external.
 302          if (!scorm_parse_scorm($scorm, $scorm->reference)) {
 303              $scorm->version = 'ERROR';
 304          }
 305          $newhash = sha1($scorm->reference);
 306  
 307      } else if ($scorm->scormtype === SCORM_TYPE_AICCURL  and $cfgscorm->allowtypeexternalaicc) {
 308          require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
 309          // AICC.
 310          $result = scorm_parse_aicc($scorm);
 311          if (!$result) {
 312              $scorm->version = 'ERROR';
 313          } else {
 314              $scorm->version = 'AICC';
 315          }
 316  
 317      } else {
 318          // Sorry, disabled type.
 319          return;
 320      }
 321  
 322      $scorm->revision++;
 323      $scorm->sha1hash = $newhash;
 324      $DB->update_record('scorm', $scorm);
 325  }
 326  
 327  
 328  function scorm_array_search($item, $needle, $haystacks, $strict=false) {
 329      if (!empty($haystacks)) {
 330          foreach ($haystacks as $key => $element) {
 331              if ($strict) {
 332                  if ($element->{$item} === $needle) {
 333                      return $key;
 334                  }
 335              } else {
 336                  if ($element->{$item} == $needle) {
 337                      return $key;
 338                  }
 339              }
 340          }
 341      }
 342      return false;
 343  }
 344  
 345  function scorm_repeater($what, $times) {
 346      if ($times <= 0) {
 347          return null;
 348      }
 349      $return = '';
 350      for ($i = 0; $i < $times; $i++) {
 351          $return .= $what;
 352      }
 353      return $return;
 354  }
 355  
 356  function scorm_external_link($link) {
 357      // Check if a link is external.
 358      $result = false;
 359      $link = strtolower($link);
 360      if (substr($link, 0, 7) == 'http://') {
 361          $result = true;
 362      } else if (substr($link, 0, 8) == 'https://') {
 363          $result = true;
 364      } else if (substr($link, 0, 4) == 'www.') {
 365          $result = true;
 366      }
 367      return $result;
 368  }
 369  
 370  /**
 371   * Returns an object containing all datas relative to the given sco ID
 372   *
 373   * @param integer $id The sco ID
 374   * @return mixed (false if sco id does not exists)
 375   */
 376  function scorm_get_sco($id, $what=SCO_ALL) {
 377      global $DB;
 378  
 379      if ($sco = $DB->get_record('scorm_scoes', array('id' => $id))) {
 380          $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
 381          if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id)))) {
 382              foreach ($scodatas as $scodata) {
 383                  $sco->{$scodata->name} = $scodata->value;
 384              }
 385          } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id))))) {
 386              $sco->parameters = '';
 387          }
 388          return $sco;
 389      } else {
 390          return false;
 391      }
 392  }
 393  
 394  /**
 395   * Returns an object (array) containing all the scoes data related to the given sco ID
 396   *
 397   * @param integer $id The sco ID
 398   * @param integer $organisation an organisation ID - defaults to false if not required
 399   * @return mixed (false if there are no scoes or an array)
 400   */
 401  function scorm_get_scoes($id, $organisation=false) {
 402      global $DB;
 403  
 404      $queryarray = array('scorm' => $id);
 405      if (!empty($organisation)) {
 406          $queryarray['organization'] = $organisation;
 407      }
 408      if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'sortorder, id')) {
 409          // Drop keys so that it is a simple array as expected.
 410          $scoes = array_values($scoes);
 411          foreach ($scoes as $sco) {
 412              if ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $sco->id))) {
 413                  foreach ($scodatas as $scodata) {
 414                      $sco->{$scodata->name} = $scodata->value;
 415                  }
 416              }
 417          }
 418          return $scoes;
 419      } else {
 420          return false;
 421      }
 422  }
 423  
 424  function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $value, $forcecompleted=false, $trackdata = null) {
 425      global $DB, $CFG;
 426  
 427      $id = null;
 428  
 429      if ($forcecompleted) {
 430          // TODO - this could be broadened to encompass SCORM 2004 in future.
 431          if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) {
 432              if ($track = $DB->get_record_select('scorm_scoes_track',
 433                                                  'userid=? AND scormid=? AND scoid=? AND attempt=? '.
 434                                                  'AND element=\'cmi.core.score.raw\'',
 435                                                  array($userid, $scormid, $scoid, $attempt))) {
 436                  $value = 'completed';
 437              }
 438          }
 439          if ($element == 'cmi.core.score.raw') {
 440              if ($tracktest = $DB->get_record_select('scorm_scoes_track',
 441                                                      'userid=? AND scormid=? AND scoid=? AND attempt=? '.
 442                                                      'AND element=\'cmi.core.lesson_status\'',
 443                                                      array($userid, $scormid, $scoid, $attempt))) {
 444                  if ($tracktest->value == "incomplete") {
 445                      $tracktest->value = "completed";
 446                      $DB->update_record('scorm_scoes_track', $tracktest);
 447                  }
 448              }
 449          }
 450          if (($element == 'cmi.success_status') && ($value == 'passed' || $value == 'failed')) {
 451              if ($DB->get_record('scorm_scoes_data', array('scoid' => $scoid, 'name' => 'objectivesetbycontent'))) {
 452                  $objectiveprogressstatus = true;
 453                  $objectivesatisfiedstatus = false;
 454                  if ($value == 'passed') {
 455                      $objectivesatisfiedstatus = true;
 456                  }
 457  
 458                  if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
 459                                                                          'scormid' => $scormid,
 460                                                                          'scoid' => $scoid,
 461                                                                          'attempt' => $attempt,
 462                                                                          'element' => 'objectiveprogressstatus'))) {
 463                      $track->value = $objectiveprogressstatus;
 464                      $track->timemodified = time();
 465                      $DB->update_record('scorm_scoes_track', $track);
 466                      $id = $track->id;
 467                  } else {
 468                      $track = new stdClass();
 469                      $track->userid = $userid;
 470                      $track->scormid = $scormid;
 471                      $track->scoid = $scoid;
 472                      $track->attempt = $attempt;
 473                      $track->element = 'objectiveprogressstatus';
 474                      $track->value = $objectiveprogressstatus;
 475                      $track->timemodified = time();
 476                      $id = $DB->insert_record('scorm_scoes_track', $track);
 477                  }
 478                  if ($objectivesatisfiedstatus) {
 479                      if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
 480                                                                              'scormid' => $scormid,
 481                                                                              'scoid' => $scoid,
 482                                                                              'attempt' => $attempt,
 483                                                                              'element' => 'objectivesatisfiedstatus'))) {
 484                          $track->value = $objectivesatisfiedstatus;
 485                          $track->timemodified = time();
 486                          $DB->update_record('scorm_scoes_track', $track);
 487                          $id = $track->id;
 488                      } else {
 489                          $track = new stdClass();
 490                          $track->userid = $userid;
 491                          $track->scormid = $scormid;
 492                          $track->scoid = $scoid;
 493                          $track->attempt = $attempt;
 494                          $track->element = 'objectivesatisfiedstatus';
 495                          $track->value = $objectivesatisfiedstatus;
 496                          $track->timemodified = time();
 497                          $id = $DB->insert_record('scorm_scoes_track', $track);
 498                      }
 499                  }
 500              }
 501          }
 502  
 503      }
 504  
 505      $track = null;
 506      if ($trackdata !== null) {
 507          if (isset($trackdata[$element])) {
 508              $track = $trackdata[$element];
 509          }
 510      } else {
 511          $track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
 512                                                              'scormid' => $scormid,
 513                                                              'scoid' => $scoid,
 514                                                              'attempt' => $attempt,
 515                                                              'element' => $element));
 516      }
 517      if ($track) {
 518          if ($element != 'x.start.time' ) { // Don't update x.start.time - keep the original value.
 519              if ($track->value != $value) {
 520                  $track->value = $value;
 521                  $track->timemodified = time();
 522                  $DB->update_record('scorm_scoes_track', $track);
 523              }
 524              $id = $track->id;
 525          }
 526      } else {
 527          $track = new stdClass();
 528          $track->userid = $userid;
 529          $track->scormid = $scormid;
 530          $track->scoid = $scoid;
 531          $track->attempt = $attempt;
 532          $track->element = $element;
 533          $track->value = $value;
 534          $track->timemodified = time();
 535          $id = $DB->insert_record('scorm_scoes_track', $track);
 536          $track->id = $id;
 537      }
 538  
 539      // Trigger updating grades based on a given set of SCORM CMI elements.
 540      $scorm = false;
 541      if (strstr($element, '.score.raw') ||
 542          (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
 543           && in_array($track->value, array('completed', 'passed')))) {
 544          $scorm = $DB->get_record('scorm', array('id' => $scormid));
 545          include_once($CFG->dirroot.'/mod/scorm/lib.php');
 546          scorm_update_grades($scorm, $userid);
 547      }
 548  
 549      // Trigger CMI element events.
 550      if (strstr($element, '.score.raw') ||
 551          (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
 552          && in_array($track->value, array('completed', 'failed', 'passed')))) {
 553          if (!$scorm) {
 554              $scorm = $DB->get_record('scorm', array('id' => $scormid));
 555          }
 556          $cm = get_coursemodule_from_instance('scorm', $scormid);
 557          $data = array(
 558              'other' => array('attemptid' => $attempt, 'cmielement' => $element, 'cmivalue' => $track->value),
 559              'objectid' => $scorm->id,
 560              'context' => context_module::instance($cm->id),
 561              'relateduserid' => $userid
 562          );
 563          if (strstr($element, '.score.raw')) {
 564              // Create score submitted event.
 565              $event = \mod_scorm\event\scoreraw_submitted::create($data);
 566          } else {
 567              // Create status submitted event.
 568              $event = \mod_scorm\event\status_submitted::create($data);
 569          }
 570          // Fix the missing track keys when the SCORM track record already exists, see $trackdata in datamodel.php.
 571          // There, for performances reasons, columns are limited to: element, id, value, timemodified.
 572          // Missing fields are: userid, scormid, scoid, attempt.
 573          $track->userid = $userid;
 574          $track->scormid = $scormid;
 575          $track->scoid = $scoid;
 576          $track->attempt = $attempt;
 577          // Trigger submitted event.
 578          $event->add_record_snapshot('scorm_scoes_track', $track);
 579          $event->add_record_snapshot('course_modules', $cm);
 580          $event->add_record_snapshot('scorm', $scorm);
 581          $event->trigger();
 582      }
 583  
 584      return $id;
 585  }
 586  
 587  /**
 588   * simple quick function to return true/false if this user has tracks in this scorm
 589   *
 590   * @param integer $scormid The scorm ID
 591   * @param integer $userid the users id
 592   * @return boolean (false if there are no tracks)
 593   */
 594  function scorm_has_tracks($scormid, $userid) {
 595      global $DB;
 596      return $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scormid));
 597  }
 598  
 599  function scorm_get_tracks($scoid, $userid, $attempt='') {
 600      // Gets all tracks of specified sco and user.
 601      global $DB;
 602  
 603      if (empty($attempt)) {
 604          if ($scormid = $DB->get_field('scorm_scoes', 'scorm', array('id' => $scoid))) {
 605              $attempt = scorm_get_last_attempt($scormid, $userid);
 606          } else {
 607              $attempt = 1;
 608          }
 609      }
 610      if ($tracks = $DB->get_records('scorm_scoes_track', array('userid' => $userid, 'scoid' => $scoid,
 611                                                                'attempt' => $attempt), 'element ASC')) {
 612          $usertrack = scorm_format_interactions($tracks);
 613          $usertrack->userid = $userid;
 614          $usertrack->scoid = $scoid;
 615  
 616          return $usertrack;
 617      } else {
 618          return false;
 619      }
 620  }
 621  /**
 622   * helper function to return a formatted list of interactions for reports.
 623   *
 624   * @param array $trackdata the records from scorm_scoes_track table
 625   * @return object formatted list of interactions
 626   */
 627  function scorm_format_interactions($trackdata) {
 628      $usertrack = new stdClass();
 629  
 630      // Defined in order to unify scorm1.2 and scorm2004.
 631      $usertrack->score_raw = '';
 632      $usertrack->status = '';
 633      $usertrack->total_time = '00:00:00';
 634      $usertrack->session_time = '00:00:00';
 635      $usertrack->timemodified = 0;
 636  
 637      foreach ($trackdata as $track) {
 638          $element = $track->element;
 639          $usertrack->{$element} = $track->value;
 640          switch ($element) {
 641              case 'cmi.core.lesson_status':
 642              case 'cmi.completion_status':
 643                  if ($track->value == 'not attempted') {
 644                      $track->value = 'notattempted';
 645                  }
 646                  $usertrack->status = $track->value;
 647                  break;
 648              case 'cmi.core.score.raw':
 649              case 'cmi.score.raw':
 650                  $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
 651                  break;
 652              case 'cmi.core.session_time':
 653              case 'cmi.session_time':
 654                  $usertrack->session_time = $track->value;
 655                  break;
 656              case 'cmi.core.total_time':
 657              case 'cmi.total_time':
 658                  $usertrack->total_time = $track->value;
 659                  break;
 660          }
 661          if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
 662              $usertrack->timemodified = $track->timemodified;
 663          }
 664      }
 665  
 666      return $usertrack;
 667  }
 668  /* Find the start and finsh time for a a given SCO attempt
 669   *
 670   * @param int $scormid SCORM Id
 671   * @param int $scoid SCO Id
 672   * @param int $userid User Id
 673   * @param int $attemt Attempt Id
 674   *
 675   * @return object start and finsh time EPOC secods
 676   *
 677   */
 678  function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
 679      global $DB;
 680  
 681      $timedata = new stdClass();
 682      $params = array('userid' => $userid, 'scormid' => $scormid, 'attempt' => $attempt);
 683      if (!empty($scoid)) {
 684          $params['scoid'] = $scoid;
 685      }
 686      $tracks = $DB->get_records('scorm_scoes_track', $params, "timemodified ASC");
 687      if ($tracks) {
 688          $tracks = array_values($tracks);
 689      }
 690  
 691      if ($tracks) {
 692          $timedata->start = $tracks[0]->timemodified;
 693      } else {
 694          $timedata->start = false;
 695      }
 696      if ($tracks && $track = array_pop($tracks)) {
 697          $timedata->finish = $track->timemodified;
 698      } else {
 699          $timedata->finish = $timedata->start;
 700      }
 701      return $timedata;
 702  }
 703  
 704  function scorm_grade_user_attempt($scorm, $userid, $attempt=1) {
 705      global $DB;
 706      $attemptscore = new stdClass();
 707      $attemptscore->scoes = 0;
 708      $attemptscore->values = 0;
 709      $attemptscore->max = 0;
 710      $attemptscore->sum = 0;
 711      $attemptscore->lastmodify = 0;
 712  
 713      if (!$scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) {
 714          return null;
 715      }
 716  
 717      foreach ($scoes as $sco) {
 718          if ($userdata = scorm_get_tracks($sco->id, $userid, $attempt)) {
 719              if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
 720                  $attemptscore->scoes++;
 721              }
 722              if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type == 'sco' && isset($userdata->score_raw))) {
 723                  $attemptscore->values++;
 724                  $attemptscore->sum += $userdata->score_raw;
 725                  $attemptscore->max = ($userdata->score_raw > $attemptscore->max) ? $userdata->score_raw : $attemptscore->max;
 726                  if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
 727                      $attemptscore->lastmodify = $userdata->timemodified;
 728                  } else {
 729                      $attemptscore->lastmodify = 0;
 730                  }
 731              }
 732          }
 733      }
 734      switch ($scorm->grademethod) {
 735          case GRADEHIGHEST:
 736              $score = (float) $attemptscore->max;
 737          break;
 738          case GRADEAVERAGE:
 739              if ($attemptscore->values > 0) {
 740                  $score = $attemptscore->sum / $attemptscore->values;
 741              } else {
 742                  $score = 0;
 743              }
 744          break;
 745          case GRADESUM:
 746              $score = $attemptscore->sum;
 747          break;
 748          case GRADESCOES:
 749              $score = $attemptscore->scoes;
 750          break;
 751          default:
 752              $score = $attemptscore->max;   // Remote Learner GRADEHIGHEST is default.
 753      }
 754  
 755      return $score;
 756  }
 757  
 758  function scorm_grade_user($scorm, $userid) {
 759  
 760      // Ensure we dont grade user beyond $scorm->maxattempt settings.
 761      $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
 762      if ($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt) {
 763          $lastattempt = $scorm->maxattempt;
 764      }
 765  
 766      switch ($scorm->whatgrade) {
 767          case FIRSTATTEMPT:
 768              return scorm_grade_user_attempt($scorm, $userid, 1);
 769          break;
 770          case LASTATTEMPT:
 771              return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
 772          break;
 773          case HIGHESTATTEMPT:
 774              $maxscore = 0;
 775              for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
 776                  $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
 777                  $maxscore = $attemptscore > $maxscore ? $attemptscore : $maxscore;
 778              }
 779              return $maxscore;
 780  
 781          break;
 782          case AVERAGEATTEMPT:
 783              $attemptcount = scorm_get_attempt_count($userid, $scorm, true, true);
 784              if (empty($attemptcount)) {
 785                  return 0;
 786              } else {
 787                  $attemptcount = count($attemptcount);
 788              }
 789              $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
 790              $sumscore = 0;
 791              for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
 792                  $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
 793                  $sumscore += $attemptscore;
 794              }
 795  
 796              return round($sumscore / $attemptcount);
 797          break;
 798      }
 799  }
 800  
 801  function scorm_count_launchable($scormid, $organization='') {
 802      global $DB;
 803  
 804      $sqlorganization = '';
 805      $params = array($scormid);
 806      if (!empty($organization)) {
 807          $sqlorganization = " AND organization=?";
 808          $params[] = $organization;
 809      }
 810      return $DB->count_records_select('scorm_scoes', "scorm = ? $sqlorganization AND ".
 811                                          $DB->sql_isnotempty('scorm_scoes', 'launch', false, true),
 812                                          $params);
 813  }
 814  
 815  /**
 816   * Returns the last attempt used - if no attempts yet, returns 1 for first attempt
 817   *
 818   * @param int $scormid the id of the scorm.
 819   * @param int $userid the id of the user.
 820   *
 821   * @return int The attempt number to use.
 822   */
 823  function scorm_get_last_attempt($scormid, $userid) {
 824      global $DB;
 825  
 826      // Find the last attempt number for the given user id and scorm id.
 827      $sql = "SELECT MAX(attempt)
 828                FROM {scorm_scoes_track}
 829               WHERE userid = ? AND scormid = ?";
 830      $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
 831      if (empty($lastattempt)) {
 832          return '1';
 833      } else {
 834          return $lastattempt;
 835      }
 836  }
 837  
 838  /**
 839   * Returns the last completed attempt used - if no completed attempts yet, returns 1 for first attempt
 840   *
 841   * @param int $scormid the id of the scorm.
 842   * @param int $userid the id of the user.
 843   *
 844   * @return int The attempt number to use.
 845   */
 846  function scorm_get_last_completed_attempt($scormid, $userid) {
 847      global $DB;
 848  
 849      // Find the last completed attempt number for the given user id and scorm id.
 850      $sql = "SELECT MAX(attempt)
 851                FROM {scorm_scoes_track}
 852               WHERE userid = ? AND scormid = ?
 853                 AND (".$DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?')." OR ".
 854                        $DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?').")";
 855      $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid, 'completed', 'passed'));
 856      if (empty($lastattempt)) {
 857          return '1';
 858      } else {
 859          return $lastattempt;
 860      }
 861  }
 862  
 863  /**
 864   * Returns the full list of attempts a user has made.
 865   *
 866   * @param int $scormid the id of the scorm.
 867   * @param int $userid the id of the user.
 868   *
 869   * @return array array of attemptids
 870   */
 871  function scorm_get_all_attempts($scormid, $userid) {
 872      global $DB;
 873      $attemptids = array();
 874      $sql = "SELECT DISTINCT attempt FROM {scorm_scoes_track} WHERE userid = ? AND scormid = ? ORDER BY attempt";
 875      $attempts = $DB->get_records_sql($sql, array($userid, $scormid));
 876      foreach ($attempts as $attempt) {
 877          $attemptids[] = $attempt->attempt;
 878      }
 879      return $attemptids;
 880  }
 881  
 882  /**
 883   * Displays the entry form and toc if required.
 884   *
 885   * @param  stdClass $user   user object
 886   * @param  stdClass $scorm  scorm object
 887   * @param  string   $action base URL for the organizations select box
 888   * @param  stdClass $cm     course module object
 889   */
 890  function scorm_print_launch ($user, $scorm, $action, $cm) {
 891      global $CFG, $DB, $PAGE, $OUTPUT, $COURSE;
 892  
 893      if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
 894          scorm_parse($scorm, false);
 895      }
 896  
 897      $organization = optional_param('organization', '', PARAM_INT);
 898  
 899      if ($scorm->displaycoursestructure == 1) {
 900          echo $OUTPUT->box_start('generalbox boxaligncenter toc', 'toc');
 901          echo html_writer::div(get_string('contents', 'scorm'), 'structurehead');
 902      }
 903      if (empty($organization)) {
 904          $organization = $scorm->launch;
 905      }
 906      if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '.
 907                                           $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
 908                                           $DB->sql_isempty('scorm_scoes', 'organization', false, false),
 909                                           array($scorm->id), 'sortorder, id', 'id,title')) {
 910          if (count($orgs) > 1) {
 911              $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null);
 912              $select->label = get_string('organizations', 'scorm');
 913              $select->class = 'scorm-center';
 914              echo $OUTPUT->render($select);
 915          }
 916      }
 917      $orgidentifier = '';
 918      if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
 919          if (($sco->organization == '') && ($sco->launch == '')) {
 920              $orgidentifier = $sco->identifier;
 921          } else {
 922              $orgidentifier = $sco->organization;
 923          }
 924      }
 925  
 926      $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR));   // Just to be safe.
 927      if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
 928          $scorm->version = 'scorm_12';
 929      }
 930      require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
 931  
 932      $result = scorm_get_toc($user, $scorm, $cm->id, TOCFULLURL, $orgidentifier);
 933      $incomplete = $result->incomplete;
 934  
 935      // Do we want the TOC to be displayed?
 936      if ($scorm->displaycoursestructure == 1) {
 937          echo $result->toc;
 938          echo $OUTPUT->box_end();
 939      }
 940  
 941      // Is this the first attempt ?
 942      $attemptcount = scorm_get_attempt_count($user->id, $scorm);
 943  
 944      // Do not give the player launch FORM if the SCORM object is locked after the final attempt.
 945      if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) {
 946              echo html_writer::start_div('scorm-center');
 947              echo html_writer::start_tag('form', array('id' => 'scormviewform',
 948                                                          'method' => 'post',
 949                                                          'action' => $CFG->wwwroot.'/mod/scorm/player.php'));
 950          if ($scorm->hidebrowse == 0) {
 951              print_string('mode', 'scorm');
 952              echo ': '.html_writer::empty_tag('input', array('type' => 'radio', 'id' => 'b', 'name' => 'mode', 'value' => 'browse')).
 953                          html_writer::label(get_string('browse', 'scorm'), 'b');
 954              echo html_writer::empty_tag('input', array('type' => 'radio',
 955                                                          'id' => 'n', 'name' => 'mode',
 956                                                          'value' => 'normal', 'checked' => 'checked')).
 957                      html_writer::label(get_string('normal', 'scorm'), 'n');
 958  
 959          } else {
 960              echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'mode', 'value' => 'normal'));
 961          }
 962          if ($scorm->forcenewattempt == 1) {
 963              if ($incomplete === false) {
 964                  echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'newattempt', 'value' => 'on'));
 965              }
 966          } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
 967                  echo html_writer::empty_tag('br');
 968                  echo html_writer::checkbox('newattempt', 'on', false, '', array('id' => 'a'));
 969                  echo html_writer::label(get_string('newattempt', 'scorm'), 'a');
 970          }
 971          if (!empty($scorm->popup)) {
 972              echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'display', 'value' => 'popup'));
 973          }
 974  
 975          echo html_writer::empty_tag('br');
 976          echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scoid', 'value' => $scorm->launch));
 977          echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cm', 'value' => $cm->id));
 978          echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentorg', 'value' => $orgidentifier));
 979          echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('enter', 'scorm')));
 980          echo html_writer::end_tag('form');
 981          echo html_writer::end_div();
 982      }
 983  }
 984  
 985  function scorm_simple_play($scorm, $user, $context, $cmid) {
 986      global $DB;
 987  
 988      $result = false;
 989  
 990      if (has_capability('mod/scorm:viewreport', $context)) {
 991          // If this user can view reports, don't skipview so they can see links to reports.
 992          return $result;
 993      }
 994  
 995      if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
 996          scorm_parse($scorm, false);
 997      }
 998      $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.
 999          $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'sortorder, id', 'id');
1000  
1001      if ($scoes) {
1002          $orgidentifier = '';
1003          if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) {
1004              if (($sco->organization == '') && ($sco->launch == '')) {
1005                  $orgidentifier = $sco->identifier;
1006              } else {
1007                  $orgidentifier = $sco->organization;
1008              }
1009          }
1010          if ($scorm->skipview >= SCORM_SKIPVIEW_FIRST) {
1011              $sco = current($scoes);
1012              $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id,
1013                                                                  'currentorg' => $orgidentifier,
1014                                                                  'scoid' => $sco->id));
1015              if ($scorm->skipview == SCORM_SKIPVIEW_ALWAYS || !scorm_has_tracks($scorm->id, $user->id)) {
1016                  if (!empty($scorm->forcenewattempt)) {
1017                      $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier);
1018                      if ($result->incomplete === false) {
1019                          $url->param('newattempt', 'on');
1020                      }
1021                  }
1022                  redirect($url);
1023              }
1024          }
1025      }
1026      return $result;
1027  }
1028  
1029  function scorm_get_count_users($scormid, $groupingid=null) {
1030      global $CFG, $DB;
1031  
1032      if (!empty($groupingid)) {
1033          $sql = "SELECT COUNT(DISTINCT st.userid)
1034                  FROM {scorm_scoes_track} st
1035                      INNER JOIN {groups_members} gm ON st.userid = gm.userid
1036                      INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
1037                  WHERE st.scormid = ? AND gg.groupingid = ?
1038                  ";
1039          $params = array($scormid, $groupingid);
1040      } else {
1041          $sql = "SELECT COUNT(DISTINCT st.userid)
1042                  FROM {scorm_scoes_track} st
1043                  WHERE st.scormid = ?
1044                  ";
1045          $params = array($scormid);
1046      }
1047  
1048      return ($DB->count_records_sql($sql, $params));
1049  }
1050  
1051  /**
1052   * Build up the JavaScript representation of an array element
1053   *
1054   * @param string $sversion SCORM API version
1055   * @param array $userdata User track data
1056   * @param string $elementname Name of array element to get values for
1057   * @param array $children list of sub elements of this array element that also need instantiating
1058   * @return Javascript array elements
1059   */
1060  function scorm_reconstitute_array_element($sversion, $userdata, $elementname, $children) {
1061      // Reconstitute comments_from_learner and comments_from_lms.
1062      $current = '';
1063      $currentsubelement = '';
1064      $currentsub = '';
1065      $count = 0;
1066      $countsub = 0;
1067      $scormseperator = '_';
1068      $return = '';
1069      if (scorm_version_check($sversion, SCORM_13)) { // Scorm 1.3 elements use a . instead of an _ .
1070          $scormseperator = '.';
1071      }
1072      // Filter out the ones we want.
1073      $elementlist = array();
1074      foreach ($userdata as $element => $value) {
1075          if (substr($element, 0, strlen($elementname)) == $elementname) {
1076              $elementlist[$element] = $value;
1077          }
1078      }
1079  
1080      // Sort elements in .n array order.
1081      uksort($elementlist, "scorm_element_cmp");
1082  
1083      // Generate JavaScript.
1084      foreach ($elementlist as $element => $value) {
1085          if (scorm_version_check($sversion, SCORM_13)) {
1086              $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
1087              preg_match('/\.(N\d+)\./', $element, $matches);
1088          } else {
1089              $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
1090              preg_match('/\_(\d+)\./', $element, $matches);
1091          }
1092          if (count($matches) > 0 && $current != $matches[1]) {
1093              if ($countsub > 0) {
1094                  $return .= '    '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
1095              }
1096              $current = $matches[1];
1097              $count++;
1098              $currentsubelement = '';
1099              $currentsub = '';
1100              $countsub = 0;
1101              $end = strpos($element, $matches[1]) + strlen($matches[1]);
1102              $subelement = substr($element, 0, $end);
1103              $return .= '    '.$subelement." = new Object();\n";
1104              // Now add the children.
1105              foreach ($children as $child) {
1106                  $return .= '    '.$subelement.".".$child." = new Object();\n";
1107                  $return .= '    '.$subelement.".".$child."._children = ".$child."_children;\n";
1108              }
1109          }
1110  
1111          // Now - flesh out the second level elements if there are any.
1112          if (scorm_version_check($sversion, SCORM_13)) {
1113              $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
1114              preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
1115          } else {
1116              $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
1117              preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
1118          }
1119  
1120          // Check the sub element type.
1121          if (count($matches) > 0 && $currentsubelement != $matches[1]) {
1122              if ($countsub > 0) {
1123                  $return .= '    '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
1124              }
1125              $currentsubelement = $matches[1];
1126              $currentsub = '';
1127              $countsub = 0;
1128              $end = strpos($element, $matches[1]) + strlen($matches[1]);
1129              $subelement = substr($element, 0, $end);
1130              $return .= '    '.$subelement." = new Object();\n";
1131          }
1132  
1133          // Now check the subelement subscript.
1134          if (count($matches) > 0 && $currentsub != $matches[2]) {
1135              $currentsub = $matches[2];
1136              $countsub++;
1137              $end = strrpos($element, $matches[2]) + strlen($matches[2]);
1138              $subelement = substr($element, 0, $end);
1139              $return .= '    '.$subelement." = new Object();\n";
1140          }
1141  
1142          $return .= '    '.$element.' = '.json_encode($value).";\n";
1143      }
1144      if ($countsub > 0) {
1145          $return .= '    '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
1146      }
1147      if ($count > 0) {
1148          $return .= '    '.$elementname.'._count = '.$count.";\n";
1149      }
1150      return $return;
1151  }
1152  
1153  /**
1154   * Build up the JavaScript representation of an array element
1155   *
1156   * @param string $a left array element
1157   * @param string $b right array element
1158   * @return comparator - 0,1,-1
1159   */
1160  function scorm_element_cmp($a, $b) {
1161      preg_match('/.*?(\d+)\./', $a, $matches);
1162      $left = intval($matches[1]);
1163      preg_match('/.?(\d+)\./', $b, $matches);
1164      $right = intval($matches[1]);
1165      if ($left < $right) {
1166          return -1; // Smaller.
1167      } else if ($left > $right) {
1168          return 1;  // Bigger.
1169      } else {
1170          // Look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern.
1171          if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
1172              $leftterm = intval($matches[2]);
1173              $left = intval($matches[3]);
1174              if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
1175                  $rightterm = intval($matches[2]);
1176                  $right = intval($matches[3]);
1177                  if ($leftterm < $rightterm) {
1178                      return -1; // Smaller.
1179                  } else if ($leftterm > $rightterm) {
1180                      return 1;  // Bigger.
1181                  } else {
1182                      if ($left < $right) {
1183                          return -1; // Smaller.
1184                      } else if ($left > $right) {
1185                          return 1;  // Bigger.
1186                      }
1187                  }
1188              }
1189          }
1190          // Fall back for no second level matches or second level matches are equal.
1191          return 0;  // Equal to.
1192      }
1193  }
1194  
1195  /**
1196   * Generate the user attempt status string
1197   *
1198   * @param object $user Current context user
1199   * @param object $scorm a moodle scrom object - mdl_scorm
1200   * @return string - Attempt status string
1201   */
1202  function scorm_get_attempt_status($user, $scorm, $cm='') {
1203      global $DB, $PAGE, $OUTPUT;
1204  
1205      $attempts = scorm_get_attempt_count($user->id, $scorm, true);
1206      if (empty($attempts)) {
1207          $attemptcount = 0;
1208      } else {
1209          $attemptcount = count($attempts);
1210      }
1211  
1212      $result = html_writer::start_tag('p').get_string('noattemptsallowed', 'scorm').': ';
1213      if ($scorm->maxattempt > 0) {
1214          $result .= $scorm->maxattempt . html_writer::empty_tag('br');
1215      } else {
1216          $result .= get_string('unlimited').html_writer::empty_tag('br');
1217      }
1218      $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . html_writer::empty_tag('br');
1219  
1220      if ($scorm->maxattempt == 1) {
1221          switch ($scorm->grademethod) {
1222              case GRADEHIGHEST:
1223                  $grademethod = get_string('gradehighest', 'scorm');
1224              break;
1225              case GRADEAVERAGE:
1226                  $grademethod = get_string('gradeaverage', 'scorm');
1227              break;
1228              case GRADESUM:
1229                  $grademethod = get_string('gradesum', 'scorm');
1230              break;
1231              case GRADESCOES:
1232                  $grademethod = get_string('gradescoes', 'scorm');
1233              break;
1234          }
1235      } else {
1236          switch ($scorm->whatgrade) {
1237              case HIGHESTATTEMPT:
1238                  $grademethod = get_string('highestattempt', 'scorm');
1239              break;
1240              case AVERAGEATTEMPT:
1241                  $grademethod = get_string('averageattempt', 'scorm');
1242              break;
1243              case FIRSTATTEMPT:
1244                  $grademethod = get_string('firstattempt', 'scorm');
1245              break;
1246              case LASTATTEMPT:
1247                  $grademethod = get_string('lastattempt', 'scorm');
1248              break;
1249          }
1250      }
1251  
1252      if (!empty($attempts)) {
1253          $i = 1;
1254          foreach ($attempts as $attempt) {
1255              $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber);
1256              if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1257                  $gradereported = $gradereported / $scorm->maxgrade;
1258                  $gradereported = number_format($gradereported * 100, 0) .'%';
1259              }
1260              $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .html_writer::empty_tag('br');
1261              $i++;
1262          }
1263      }
1264      $calculatedgrade = scorm_grade_user($scorm, $user->id);
1265      if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1266          $calculatedgrade = $calculatedgrade / $scorm->maxgrade;
1267          $calculatedgrade = number_format($calculatedgrade * 100, 0) .'%';
1268      }
1269      $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod;
1270      if (empty($attempts)) {
1271          $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm').
1272                      ': '.get_string('none').html_writer::empty_tag('br');
1273      } else {
1274          $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm').
1275                      ': '.$calculatedgrade.html_writer::empty_tag('br');
1276      }
1277      $result .= html_writer::end_tag('p');
1278      if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) {
1279          $result .= html_writer::tag('p', get_string('exceededmaxattempts', 'scorm'), array('class' => 'exceededmaxattempts'));
1280      }
1281      if (!empty($cm)) {
1282          $context = context_module::instance($cm->id);
1283          if (has_capability('mod/scorm:deleteownresponses', $context) &&
1284              $DB->record_exists('scorm_scoes_track', array('userid' => $user->id, 'scormid' => $scorm->id))) {
1285              // Check to see if any data is stored for this user.
1286              $deleteurl = new moodle_url($PAGE->url, array('action' => 'delete', 'sesskey' => sesskey()));
1287              $result .= $OUTPUT->single_button($deleteurl, get_string('deleteallattempts', 'scorm'));
1288          }
1289      }
1290  
1291      return $result;
1292  }
1293  
1294  /**
1295   * Get SCORM attempt count
1296   *
1297   * @param object $user Current context user
1298   * @param object $scorm a moodle scrom object - mdl_scorm
1299   * @param bool $returnobjects if true returns a object with attempts, if false returns count of attempts.
1300   * @param bool $ignoremissingcompletion - ignores attempts that haven't reported a grade/completion.
1301   * @return int - no. of attempts so far
1302   */
1303  function scorm_get_attempt_count($userid, $scorm, $returnobjects = false, $ignoremissingcompletion = false) {
1304      global $DB;
1305  
1306      // Historically attempts that don't report these elements haven't been included in the average attempts grading method
1307      // we may want to change this in future, but to avoid unexpected grade decreases we're leaving this in. MDL-43222 .
1308      if (scorm_version_check($scorm->version, SCORM_13)) {
1309          $element = 'cmi.score.raw';
1310      } else if ($scorm->grademethod == GRADESCOES) {
1311          $element = 'cmi.core.lesson_status';
1312      } else {
1313          $element = 'cmi.core.score.raw';
1314      }
1315  
1316      if ($returnobjects) {
1317          $params = array('userid' => $userid, 'scormid' => $scorm->id);
1318          if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested.
1319              $params['element'] = $element;
1320          }
1321          $attempts = $DB->get_records('scorm_scoes_track', $params, 'attempt', 'DISTINCT attempt AS attemptnumber');
1322          return $attempts;
1323      } else {
1324          $params = array($userid, $scorm->id);
1325          $sql = "SELECT COUNT(DISTINCT attempt)
1326                    FROM {scorm_scoes_track}
1327                   WHERE userid = ? AND scormid = ?";
1328          if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested.
1329              $sql .= ' AND element = ?';
1330              $params[] = $element;
1331          }
1332  
1333          $attemptscount = $DB->count_records_sql($sql, $params);
1334          return $attemptscount;
1335      }
1336  }
1337  
1338  /**
1339   * Figure out with this is a debug situation
1340   *
1341   * @param object $scorm a moodle scrom object - mdl_scorm
1342   * @return boolean - debugging true/false
1343   */
1344  function scorm_debugging($scorm) {
1345      global $USER;
1346      $cfgscorm = get_config('scorm');
1347  
1348      if (!$cfgscorm->allowapidebug) {
1349          return false;
1350      }
1351      $identifier = $USER->username.':'.$scorm->name;
1352      $test = $cfgscorm->apidebugmask;
1353      // Check the regex is only a short list of safe characters.
1354      if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) {
1355          return false;
1356      }
1357  
1358      if (preg_match('/^'.$test.'/', $identifier)) {
1359          return true;
1360      }
1361      return false;
1362  }
1363  
1364  /**
1365   * Delete Scorm tracks for selected users
1366   *
1367   * @param array $attemptids list of attempts that need to be deleted
1368   * @param stdClass $scorm instance
1369   *
1370   * @return bool true deleted all responses, false failed deleting an attempt - stopped here
1371   */
1372  function scorm_delete_responses($attemptids, $scorm) {
1373      if (!is_array($attemptids) || empty($attemptids)) {
1374          return false;
1375      }
1376  
1377      foreach ($attemptids as $num => $attemptid) {
1378          if (empty($attemptid)) {
1379              unset($attemptids[$num]);
1380          }
1381      }
1382  
1383      foreach ($attemptids as $attempt) {
1384          $keys = explode(':', $attempt);
1385          if (count($keys) == 2) {
1386              $userid = clean_param($keys[0], PARAM_INT);
1387              $attemptid = clean_param($keys[1], PARAM_INT);
1388              if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) {
1389                      return false;
1390              }
1391          } else {
1392              return false;
1393          }
1394      }
1395      return true;
1396  }
1397  
1398  /**
1399   * Delete Scorm tracks for selected users
1400   *
1401   * @param int $userid ID of User
1402   * @param stdClass $scorm Scorm object
1403   * @param int $attemptid user attempt that need to be deleted
1404   *
1405   * @return bool true suceeded
1406   */
1407  function scorm_delete_attempt($userid, $scorm, $attemptid) {
1408      global $DB;
1409  
1410      $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid));
1411      $cm = get_coursemodule_from_instance('scorm', $scorm->id);
1412  
1413      // Trigger instances list viewed event.
1414      $event = \mod_scorm\event\attempt_deleted::create(array(
1415           'other' => array('attemptid' => $attemptid),
1416           'context' => context_module::instance($cm->id),
1417           'relateduserid' => $userid
1418      ));
1419      $event->add_record_snapshot('course_modules', $cm);
1420      $event->add_record_snapshot('scorm', $scorm);
1421      $event->trigger();
1422  
1423      include_once ('lib.php');
1424      scorm_update_grades($scorm, $userid, true);
1425      return true;
1426  }
1427  
1428  /**
1429   * Converts SCORM duration notation to human-readable format
1430   * The function works with both SCORM 1.2 and SCORM 2004 time formats
1431   * @param $duration string SCORM duration
1432   * @return string human-readable date/time
1433   */
1434  function scorm_format_duration($duration) {
1435      // Fetch date/time strings.
1436      $stryears = get_string('years');
1437      $strmonths = get_string('nummonths');
1438      $strdays = get_string('days');
1439      $strhours = get_string('hours');
1440      $strminutes = get_string('minutes');
1441      $strseconds = get_string('seconds');
1442  
1443      if ($duration[0] == 'P') {
1444          // If timestamp starts with 'P' - it's a SCORM 2004 format
1445          // this regexp discards empty sections, takes Month/Minute ambiguity into consideration,
1446          // and outputs filled sections, discarding leading zeroes and any format literals
1447          // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero.
1448          $pattern = array( '#([A-Z])0+Y#', '#([A-Z])0+M#', '#([A-Z])0+D#', '#P(|\d+Y)0*(\d+)M#',
1449                              '#0*(\d+)Y#', '#0*(\d+)D#', '#P#', '#([A-Z])0+H#', '#([A-Z])[0.]+S#',
1450                              '#\.0+S#', '#T(|\d+H)0*(\d+)M#', '#0*(\d+)H#', '#0+\.(\d+)S#',
1451                              '#0*([\d.]+)S#', '#T#' );
1452          $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ',
1453                              '', '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ',
1454                              '0.$1 '.$strseconds, '$1 '.$strseconds, '');
1455      } else {
1456          // Else we have SCORM 1.2 format there
1457          // first convert the timestamp to some SCORM 2004-like format for conveniency.
1458          $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration);
1459          // Then convert in the same way as SCORM 2004.
1460          $pattern = array( '#T0+H#', '#([A-Z])0+M#', '#([A-Z])[0.]+S#', '#\.0+S#', '#0*(\d+)H#',
1461                              '#0*(\d+)M#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' );
1462          $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ',
1463                              '0.$1 '.$strseconds, '$1 '.$strseconds, '' );
1464      }
1465  
1466      $result = preg_replace($pattern, $replace, $duration);
1467  
1468      return $result;
1469  }
1470  
1471  function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='normal', $attempt='',
1472                                  $play=false, $organizationsco=null) {
1473      global $CFG, $DB, $PAGE, $OUTPUT;
1474  
1475      // Always pass the mode even if empty as that is what is done elsewhere and the urls have to match.
1476      $modestr = '&mode=';
1477      if ($mode != 'normal') {
1478          $modestr = '&mode='.$mode;
1479      }
1480  
1481      $result = array();
1482      $incomplete = false;
1483  
1484      if (!empty($organizationsco)) {
1485          $result[0] = $organizationsco;
1486          $result[0]->isvisible = 'true';
1487          $result[0]->statusicon = '';
1488          $result[0]->url = '';
1489      }
1490  
1491      if ($scoes = scorm_get_scoes($scorm->id, $currentorg)) {
1492          // Retrieve user tracking data for each learning object.
1493          $usertracks = array();
1494          foreach ($scoes as $sco) {
1495              if (!empty($sco->launch)) {
1496                  if ($usertrack = scorm_get_tracks($sco->id, $user->id, $attempt)) {
1497                      if ($usertrack->status == '') {
1498                          $usertrack->status = 'notattempted';
1499                      }
1500                      $usertracks[$sco->identifier] = $usertrack;
1501                  }
1502              }
1503          }
1504          foreach ($scoes as $sco) {
1505              if (!isset($sco->isvisible)) {
1506                  $sco->isvisible = 'true';
1507              }
1508  
1509              if (empty($sco->title)) {
1510                  $sco->title = $sco->identifier;
1511              }
1512  
1513              if (scorm_version_check($scorm->version, SCORM_13)) {
1514                  $sco->prereq = true;
1515              } else {
1516                  $sco->prereq = empty($sco->prerequisites) || scorm_eval_prerequisites($sco->prerequisites, $usertracks);
1517              }
1518  
1519              if ($sco->isvisible === 'true') {
1520                  if (!empty($sco->launch)) {
1521                      if (empty($scoid) && ($mode != 'normal')) {
1522                          $scoid = $sco->id;
1523                      }
1524  
1525                      if (isset($usertracks[$sco->identifier])) {
1526                          $usertrack = $usertracks[$sco->identifier];
1527                          $strstatus = get_string($usertrack->status, 'scorm');
1528  
1529                          if ($sco->scormtype == 'sco') {
1530                              $statusicon = html_writer::img($OUTPUT->pix_url($usertrack->status, 'scorm'), $strstatus,
1531                                                              array('title' => $strstatus));
1532                          } else {
1533                              $statusicon = html_writer::img($OUTPUT->pix_url('asset', 'scorm'), get_string('assetlaunched', 'scorm'),
1534                                                              array('title' => get_string('assetlaunched', 'scorm')));
1535                          }
1536  
1537                          if (($usertrack->status == 'notattempted') ||
1538                                  ($usertrack->status == 'incomplete') ||
1539                                  ($usertrack->status == 'browsed')) {
1540                              $incomplete = true;
1541                              if ($play && empty($scoid)) {
1542                                  $scoid = $sco->id;
1543                              }
1544                          }
1545  
1546                          $strsuspended = get_string('suspended', 'scorm');
1547  
1548                          $exitvar = 'cmi.core.exit';
1549  
1550                          if (scorm_version_check($scorm->version, SCORM_13)) {
1551                              $exitvar = 'cmi.exit';
1552                          }
1553  
1554                          if ($incomplete && isset($usertrack->{$exitvar}) && ($usertrack->{$exitvar} == 'suspend')) {
1555                              $statusicon = html_writer::img($OUTPUT->pix_url('suspend', 'scorm'), $strstatus.' - '.$strsuspended,
1556                                                              array('title' => $strstatus.' - '.$strsuspended));
1557                          }
1558  
1559                      } else {
1560                          if ($play && empty($scoid)) {
1561                              $scoid = $sco->id;
1562                          }
1563  
1564                          $incomplete = true;
1565  
1566                          if ($sco->scormtype == 'sco') {
1567                              $statusicon = html_writer::img($OUTPUT->pix_url('notattempted', 'scorm'),
1568                                                              get_string('notattempted', 'scorm'),
1569                                                              array('title' => get_string('notattempted', 'scorm')));
1570                          } else {
1571                              $statusicon = html_writer::img($OUTPUT->pix_url('asset', 'scorm'), get_string('asset', 'scorm'),
1572                                                              array('title' => get_string('asset', 'scorm')));
1573                          }
1574                      }
1575                  }
1576              }
1577  
1578              if (empty($statusicon)) {
1579                  $sco->statusicon = html_writer::img($OUTPUT->pix_url('notattempted', 'scorm'), get_string('notattempted', 'scorm'),
1580                                                      array('title' => get_string('notattempted', 'scorm')));
1581              } else {
1582                  $sco->statusicon = $statusicon;
1583              }
1584  
1585              $sco->url = 'a='.$scorm->id.'&scoid='.$sco->id.'&currentorg='.$currentorg.$modestr.'&attempt='.$attempt;
1586              $sco->incomplete = $incomplete;
1587  
1588              if (!in_array($sco->id, array_keys($result))) {
1589                  $result[$sco->id] = $sco;
1590              }
1591          }
1592      }
1593  
1594      // Get the parent scoes!
1595      $result = scorm_get_toc_get_parent_child($result, $currentorg);
1596  
1597      // Be safe, prevent warnings from showing up while returning array.
1598      if (!isset($scoid)) {
1599          $scoid = '';
1600      }
1601  
1602      return array('scoes' => $result, 'usertracks' => $usertracks, 'scoid' => $scoid);
1603  }
1604  
1605  function scorm_get_toc_get_parent_child(&$result, $currentorg) {
1606      $final = array();
1607      $level = 0;
1608      // Organization is always the root, prevparent.
1609      if (!empty($currentorg)) {
1610          $prevparent = $currentorg;
1611      } else {
1612          $prevparent = '/';
1613      }
1614  
1615      foreach ($result as $sco) {
1616          if ($sco->parent == '/') {
1617              $final[$level][$sco->identifier] = $sco;
1618              $prevparent = $sco->identifier;
1619              unset($result[$sco->id]);
1620          } else {
1621              if ($sco->parent == $prevparent) {
1622                  $final[$level][$sco->identifier] = $sco;
1623                  $prevparent = $sco->identifier;
1624                  unset($result[$sco->id]);
1625              } else {
1626                  if (!empty($final[$level])) {
1627                      $found = false;
1628                      foreach ($final[$level] as $fin) {
1629                          if ($sco->parent == $fin->identifier) {
1630                              $found = true;
1631                          }
1632                      }
1633  
1634                      if ($found) {
1635                          $final[$level][$sco->identifier] = $sco;
1636                          unset($result[$sco->id]);
1637                          $found = false;
1638                      } else {
1639                          $level++;
1640                          $final[$level][$sco->identifier] = $sco;
1641                          unset($result[$sco->id]);
1642                      }
1643                  }
1644              }
1645          }
1646      }
1647  
1648      for ($i = 0; $i <= $level; $i++) {
1649          $prevparent = '';
1650          foreach ($final[$i] as $ident => $sco) {
1651              if (empty($prevparent)) {
1652                  $prevparent = $ident;
1653              }
1654              if (!isset($final[$i][$prevparent]->children)) {
1655                  $final[$i][$prevparent]->children = array();
1656              }
1657              if ($sco->parent == $prevparent) {
1658                  $final[$i][$prevparent]->children[] = $sco;
1659                  $prevparent = $ident;
1660              } else {
1661                  $parent = false;
1662                  foreach ($final[$i] as $identifier => $scoobj) {
1663                      if ($identifier == $sco->parent) {
1664                          $parent = $identifier;
1665                      }
1666                  }
1667  
1668                  if ($parent !== false) {
1669                      $final[$i][$parent]->children[] = $sco;
1670                  }
1671              }
1672          }
1673      }
1674  
1675      $results = array();
1676      for ($i = 0; $i <= $level; $i++) {
1677          $keys = array_keys($final[$i]);
1678          $results[] = $final[$i][$keys[0]];
1679      }
1680  
1681      return $results;
1682  }
1683  
1684  function scorm_format_toc_for_treeview($user, $scorm, $scoes, $usertracks, $cmid, $toclink=TOCJSLINK, $currentorg='',
1685                                          $attempt='', $play=false, $organizationsco=null, $children=false) {
1686      global $CFG;
1687  
1688      $result = new stdClass();
1689      $result->prerequisites = true;
1690      $result->incomplete = true;
1691      $result->toc = '';
1692  
1693      if (!$children) {
1694          $attemptsmade = scorm_get_attempt_count($user->id, $scorm);
1695          $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attemptsmade;
1696      }
1697  
1698      if (!$children) {
1699          $result->toc = html_writer::start_tag('ul');
1700  
1701          if (!$play && !empty($organizationsco)) {
1702              $result->toc .= html_writer::start_tag('li').$organizationsco->title.html_writer::end_tag('li');
1703          }
1704      }
1705  
1706      $prevsco = '';
1707      if (!empty($scoes)) {
1708          foreach ($scoes as $sco) {
1709  
1710              if ($sco->isvisible === 'false') {
1711                  continue;
1712              }
1713  
1714              $result->toc .= html_writer::start_tag('li');
1715              $scoid = $sco->id;
1716  
1717              $score = '';
1718  
1719              if (isset($usertracks[$sco->identifier])) {
1720                  $viewscore = has_capability('mod/scorm:viewscores', context_module::instance($cmid));
1721                  if (isset($usertracks[$sco->identifier]->score_raw) && $viewscore) {
1722                      if ($usertracks[$sco->identifier]->score_raw != '') {
1723                          $score = '('.get_string('score', 'scorm').':&nbsp;'.$usertracks[$sco->identifier]->score_raw.')';
1724                      }
1725                  }
1726              }
1727  
1728              if (!empty($sco->prereq)) {
1729                  if ($sco->id == $scoid) {
1730                      $result->prerequisites = true;
1731                  }
1732  
1733                  if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) {
1734                      if ($sco->scormtype == 'sco') {
1735                          $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
1736                      } else {
1737                          $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
1738                      }
1739                  } else if ($toclink == TOCFULLURL) {
1740                      $url = $CFG->wwwroot.'/mod/scorm/player.php?'.$sco->url;
1741                      if (!empty($sco->launch)) {
1742                          if ($sco->scormtype == 'sco') {
1743                              $result->toc .= $sco->statusicon.'&nbsp;';
1744                              $result->toc .= html_writer::link($url, format_string($sco->title)).$score;
1745                          } else {
1746                              $result->toc .= '&nbsp;'.html_writer::link($url, format_string($sco->title),
1747                                                                          array('data-scoid' => $sco->id)).$score;
1748                          }
1749                      } else {
1750                          if ($sco->scormtype == 'sco') {
1751                              $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title).$score;
1752                          } else {
1753                              $result->toc .= '&nbsp;'.format_string($sco->title).$score;
1754                          }
1755                      }
1756                  } else {
1757                      if (!empty($sco->launch)) {
1758                          if ($sco->scormtype == 'sco') {
1759                              $result->toc .= html_writer::tag('a', $sco->statusicon.'&nbsp;'.
1760                                                                  format_string($sco->title).'&nbsp;'.$score,
1761                                                                  array('data-scoid' => $sco->id, 'title' => $sco->url));
1762                          } else {
1763                              $result->toc .= html_writer::tag('a', '&nbsp;'.format_string($sco->title).'&nbsp;'.$score,
1764                                                                  array('data-scoid' => $sco->id, 'title' => $sco->url));
1765                          }
1766                      } else {
1767                          if ($sco->scormtype == 'sco') {
1768                              $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
1769                          } else {
1770                              $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
1771                          }
1772                      }
1773                  }
1774  
1775              } else {
1776                  if ($play) {
1777                      if ($sco->scormtype == 'sco') {
1778                          $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
1779                      } else {
1780                          $result->toc .= '&nbsp;'.format_string($sco->title).html_writer::end_span();
1781                      }
1782                  } else {
1783                      if ($sco->scormtype == 'sco') {
1784                          $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title);
1785                      } else {
1786                          $result->toc .= '&nbsp;'.format_string($sco->title);
1787                      }
1788                  }
1789              }
1790  
1791              if (!empty($sco->children)) {
1792                  $result->toc .= html_writer::start_tag('ul');
1793                  $childresult = scorm_format_toc_for_treeview($user, $scorm, $sco->children, $usertracks, $cmid,
1794                                                                  $toclink, $currentorg, $attempt, $play, $organizationsco, true);
1795  
1796                  // Is any of the children incomplete?
1797                  $sco->incomplete = $childresult->incomplete;
1798                  $result->toc .= $childresult->toc;
1799                  $result->toc .= html_writer::end_tag('ul');
1800                  $result->toc .= html_writer::end_tag('li');
1801              } else {
1802                  $result->toc .= html_writer::end_tag('li');
1803              }
1804              $prevsco = $sco;
1805          }
1806          $result->incomplete = $sco->incomplete;
1807      }
1808  
1809      if (!$children) {
1810          $result->toc .= html_writer::end_tag('ul');
1811      }
1812  
1813      return $result;
1814  }
1815  
1816  function scorm_format_toc_for_droplist($scorm, $scoes, $usertracks, $currentorg='', $organizationsco=null,
1817                                          $children=false, $level=0, $tocmenus=array()) {
1818      if (!empty($scoes)) {
1819          if (!empty($organizationsco) && !$children) {
1820              $tocmenus[$organizationsco->id] = $organizationsco->title;
1821          }
1822  
1823          $parents[$level] = '/';
1824          foreach ($scoes as $sco) {
1825              if ($parents[$level] != $sco->parent) {
1826                  if ($newlevel = array_search($sco->parent, $parents)) {
1827                      $level = $newlevel;
1828                  } else {
1829                      $i = $level;
1830                      while (($i > 0) && ($parents[$level] != $sco->parent)) {
1831                          $i--;
1832                      }
1833  
1834                      if (($i == 0) && ($sco->parent != $currentorg)) {
1835                          $level++;
1836                      } else {
1837                          $level = $i;
1838                      }
1839  
1840                      $parents[$level] = $sco->parent;
1841                  }
1842              }
1843  
1844              if ($sco->scormtype == 'sco') {
1845                  $tocmenus[$sco->id] = scorm_repeater('&minus;', $level) . '&gt;' . format_string($sco->title);
1846              }
1847  
1848              if (!empty($sco->children)) {
1849                  $tocmenus = scorm_format_toc_for_droplist($scorm, $sco->children, $usertracks, $currentorg,
1850                                                              $organizationsco, true, $level, $tocmenus);
1851              }
1852          }
1853      }
1854  
1855      return $tocmenus;
1856  }
1857  
1858  function scorm_get_toc($user, $scorm, $cmid, $toclink=TOCJSLINK, $currentorg='', $scoid='', $mode='normal',
1859                          $attempt='', $play=false, $tocheader=false) {
1860      global $CFG, $DB, $OUTPUT;
1861  
1862      if (empty($attempt)) {
1863          $attempt = scorm_get_last_attempt($scorm->id, $user->id);
1864      }
1865  
1866      $result = new stdClass();
1867      $organizationsco = null;
1868  
1869      if ($tocheader) {
1870          $result->toc = html_writer::start_div('yui3-g-r', array('id' => 'scorm_layout'));
1871          $result->toc .= html_writer::start_div('yui3-u-1-5', array('id' => 'scorm_toc'));
1872          $result->toc .= html_writer::div('', '', array('id' => 'scorm_toc_title'));
1873          $result->toc .= html_writer::start_div('', array('id' => 'scorm_tree'));
1874      }
1875  
1876      if (!empty($currentorg)) {
1877          $organizationsco = $DB->get_record('scorm_scoes', array('scorm' => $scorm->id, 'identifier' => $currentorg));
1878          if (!empty($organizationsco->title)) {
1879              if ($play) {
1880                  $result->toctitle = $organizationsco->title;
1881              }
1882          }
1883      }
1884  
1885      $scoes = scorm_get_toc_object($user, $scorm, $currentorg, $scoid, $mode, $attempt, $play, $organizationsco);
1886  
1887      $treeview = scorm_format_toc_for_treeview($user, $scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], $cmid,
1888                                                  $toclink, $currentorg, $attempt, $play, $organizationsco, false);
1889  
1890      if ($tocheader) {
1891          $result->toc .= $treeview->toc;
1892      } else {
1893          $result->toc = $treeview->toc;
1894      }
1895  
1896      if (!empty($scoes['scoid'])) {
1897          $scoid = $scoes['scoid'];
1898      }
1899  
1900      if (empty($scoid)) {
1901          // If this is a normal package with an org sco and child scos get the first child.
1902          if (!empty($scoes['scoes'][0]->children)) {
1903              $result->sco = $scoes['scoes'][0]->children[0];
1904          } else { // This package only has one sco - it may be a simple external AICC package.
1905              $result->sco = $scoes['scoes'][0];
1906          }
1907  
1908      } else {
1909          $result->sco = scorm_get_sco($scoid);
1910      }
1911  
1912      if ($scorm->hidetoc == SCORM_TOC_POPUP) {
1913          $tocmenu = scorm_format_toc_for_droplist($scorm, $scoes['scoes'][0]->children, $scoes['usertracks'],
1914                                                      $currentorg, $organizationsco);
1915  
1916          $modestr = '';
1917          if ($mode != 'normal') {
1918              $modestr = '&mode='.$mode;
1919          }
1920  
1921          $url = new moodle_url('/mod/scorm/player.php?a='.$scorm->id.'&currentorg='.$currentorg.$modestr);
1922          $result->tocmenu = $OUTPUT->single_select($url, 'scoid', $tocmenu, $result->sco->id, null, "tocmenu");
1923      }
1924  
1925      $result->prerequisites = $treeview->prerequisites;
1926      $result->incomplete = $treeview->incomplete;
1927      $result->attemptleft = $treeview->attemptleft;
1928  
1929      if ($tocheader) {
1930          $result->toc .= html_writer::end_div().html_writer::end_div();
1931          $result->toc .= html_writer::start_div('', array('id' => 'scorm_toc_toggle'));
1932          $result->toc .= html_writer::tag('button', '', array('id' => 'scorm_toc_toggle_btn')).html_writer::end_div();
1933          $result->toc .= html_writer::start_div('', array('id' => 'scorm_content'));
1934          $result->toc .= html_writer::div('', '', array('id' => 'scorm_navpanel'));
1935          $result->toc .= html_writer::end_div().html_writer::end_div();
1936      }
1937  
1938      return $result;
1939  }
1940  
1941  function scorm_get_adlnav_json ($scoes, &$adlnav = array(), $parentscoid = null) {
1942      if (is_object($scoes)) {
1943          $sco = $scoes;
1944          if (isset($sco->url)) {
1945              $adlnav[$sco->id]['identifier'] = $sco->identifier;
1946              $adlnav[$sco->id]['launch'] = $sco->launch;
1947              $adlnav[$sco->id]['title'] = $sco->title;
1948              $adlnav[$sco->id]['url'] = $sco->url;
1949              $adlnav[$sco->id]['parent'] = $sco->parent;
1950              if (isset($sco->choice)) {
1951                  $adlnav[$sco->id]['choice'] = $sco->choice;
1952              }
1953              if (isset($sco->flow)) {
1954                  $adlnav[$sco->id]['flow'] = $sco->flow;
1955              } else if (isset($parentscoid) && isset($adlnav[$parentscoid]['flow'])) {
1956                  $adlnav[$sco->id]['flow'] = $adlnav[$parentscoid]['flow'];
1957              }
1958              if (isset($sco->isvisible)) {
1959                  $adlnav[$sco->id]['isvisible'] = $sco->isvisible;
1960              }
1961              if (isset($sco->parameters)) {
1962                  $adlnav[$sco->id]['parameters'] = $sco->parameters;
1963              }
1964              if (isset($sco->hidecontinue)) {
1965                  $adlnav[$sco->id]['hidecontinue'] = $sco->hidecontinue;
1966              }
1967              if (isset($sco->hideprevious)) {
1968                  $adlnav[$sco->id]['hideprevious'] = $sco->hideprevious;
1969              }
1970              if (isset($sco->hidesuspendall)) {
1971                  $adlnav[$sco->id]['hidesuspendall'] = $sco->hidesuspendall;
1972              }
1973              if (!empty($parentscoid)) {
1974                  $adlnav[$sco->id]['parentscoid'] = $parentscoid;
1975              }
1976              if (isset($adlnav['prevscoid'])) {
1977                  $adlnav[$sco->id]['prevscoid'] = $adlnav['prevscoid'];
1978                  $adlnav[$adlnav['prevscoid']]['nextscoid'] = $sco->id;
1979                  if (isset($adlnav['prevparent']) && $adlnav['prevparent'] == $sco->parent) {
1980                      $adlnav[$sco->id]['prevsibling'] = $adlnav['prevscoid'];
1981                      $adlnav[$adlnav['prevscoid']]['nextsibling'] = $sco->id;
1982                  }
1983              }
1984              $adlnav['prevscoid'] = $sco->id;
1985              $adlnav['prevparent'] = $sco->parent;
1986          }
1987          if (isset($sco->children)) {
1988              foreach ($sco->children as $children) {
1989                  scorm_get_adlnav_json($children, $adlnav, $sco->id);
1990              }
1991          }
1992      } else {
1993          foreach ($scoes as $sco) {
1994              scorm_get_adlnav_json ($sco, $adlnav);
1995          }
1996          unset($adlnav['prevscoid']);
1997          unset($adlnav['prevparent']);
1998      }
1999      return json_encode($adlnav);
2000  }
2001  
2002  /**
2003   * Check for the availability of a resource by URL.
2004   *
2005   * Check is performed using an HTTP HEAD call.
2006   *
2007   * @param $url string A valid URL
2008   * @return bool|string True if no issue is found. The error string message, otherwise
2009   */
2010  function scorm_check_url($url) {
2011      $curl = new curl;
2012      // Same options as in {@link download_file_content()}, used in {@link scorm_parse_scorm()}.
2013      $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 5));
2014      $cmsg = $curl->head($url);
2015      $info = $curl->get_info();
2016      if (empty($info['http_code']) || $info['http_code'] != 200) {
2017          return get_string('invalidurlhttpcheck', 'scorm', array('cmsg' => $cmsg));
2018      }
2019  
2020      return true;
2021  }
2022  
2023  /**
2024   * Check for a parameter in userdata and return it if it's set
2025   * or return the value from $ifempty if its empty
2026   *
2027   * @param stdClass $userdata Contains user's data
2028   * @param string $param parameter that should be checked
2029   * @param string $ifempty value to be replaced with if $param is not set
2030   * @return string value from $userdata->$param if its not empty, or $ifempty
2031   */
2032  function scorm_isset($userdata, $param, $ifempty = '') {
2033      if (isset($userdata->$param)) {
2034          return $userdata->$param;
2035      } else {
2036          return $ifempty;
2037      }
2038  }
2039  
2040  /**
2041   * Check if the current sco is launchable
2042   * If not, find the next launchable sco
2043   *
2044   * @param stdClass $scorm Scorm object
2045   * @param integer $scoid id of scorm_scoes record.
2046   * @return integer scoid of correct sco to launch or empty if one cannot be found, which will trigger first sco.
2047   */
2048  function scorm_check_launchable_sco($scorm, $scoid) {
2049      global $DB;
2050      if ($sco = scorm_get_sco($scoid, SCO_ONLY)) {
2051          if ($sco->launch == '') {
2052              // This scoid might be a top level org that can't be launched, find the first launchable sco after this sco.
2053              $scoes = $DB->get_records_select('scorm_scoes',
2054                                               'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).
2055                                               ' AND id > ?', array($scorm->id, $sco->id), 'sortorder, id', 'id', 0, 1);
2056              if (!empty($scoes)) {
2057                  $sco = reset($scoes); // Get first item from the list.
2058                  return $sco->id;
2059              }
2060          } else {
2061              return $sco->id;
2062          }
2063      }
2064      // Returning 0 will cause default behaviour which will find the first launchable sco in the package.
2065      return 0;
2066  }
2067  
2068  /**
2069   * Check if a SCORM is available for the current user.
2070   *
2071   * @param  stdClass  $scorm            SCORM record
2072   * @param  boolean $checkviewreportcap Check the scorm:viewreport cap
2073   * @param  stdClass  $context          Module context, required if $checkviewreportcap is set to true
2074   * @return array                       status (available or not and possible warnings)
2075   * @since  Moodle 3.0
2076   */
2077  function scorm_get_availability_status($scorm, $checkviewreportcap = false, $context = null) {
2078      $open = true;
2079      $closed = false;
2080      $warnings = array();
2081  
2082      $timenow = time();
2083      if (!empty($scorm->timeopen) and $scorm->timeopen > $timenow) {
2084          $open = false;
2085      }
2086      if (!empty($scorm->timeclose) and $timenow > $scorm->timeclose) {
2087          $closed = true;
2088      }
2089  
2090      if (!$open or $closed) {
2091          if ($checkviewreportcap and !empty($context) and has_capability('mod/scorm:viewreport', $context)) {
2092              return array(true, $warnings);
2093          }
2094  
2095          if (!$open) {
2096              $warnings['notopenyet'] = userdate($scorm->timeopen);
2097          }
2098          if ($closed) {
2099              $warnings['expired'] = userdate($scorm->timeclose);
2100          }
2101          return array(false, $warnings);
2102      }
2103  
2104      // Scorm is available.
2105      return array(true, $warnings);
2106  }
2107  
2108  /**
2109   * Requires a SCORM package to be available for the current user.
2110   *
2111   * @param  stdClass  $scorm            SCORM record
2112   * @param  boolean $checkviewreportcap Check the scorm:viewreport cap
2113   * @param  stdClass  $context          Module context, required if $checkviewreportcap is set to true
2114   * @throws moodle_exception
2115   * @since  Moodle 3.0
2116   */
2117  function scorm_require_available($scorm, $checkviewreportcap = false, $context = null) {
2118  
2119      list($available, $warnings) = scorm_get_availability_status($scorm, $checkviewreportcap, $context);
2120  
2121      if (!$available) {
2122          $reason = current(array_keys($warnings));
2123          throw new moodle_exception($reason, 'scorm', '', $warnings[$reason]);
2124      }
2125  
2126  }
2127  
2128  /**
2129   * Return a SCO object and the SCO launch URL
2130   *
2131   * @param  stdClass $scorm SCORM object
2132   * @param  int $scoid The SCO id in database
2133   * @param  stdClass $context context object
2134   * @return array the SCO object and URL
2135   * @since  Moodle 3.1
2136   */
2137  function scorm_get_sco_and_launch_url($scorm, $scoid, $context) {
2138      global $CFG, $DB;
2139  
2140      if (!empty($scoid)) {
2141          // Direct SCO request.
2142          if ($sco = scorm_get_sco($scoid)) {
2143              if ($sco->launch == '') {
2144                  // Search for the next launchable sco.
2145                  if ($scoes = $DB->get_records_select(
2146                          'scorm_scoes',
2147                          'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).' AND id > ?',
2148                          array($scorm->id, $sco->id),
2149                          'sortorder, id')) {
2150                      $sco = current($scoes);
2151                  }
2152              }
2153          }
2154      }
2155  
2156      // If no sco was found get the first of SCORM package.
2157      if (!isset($sco)) {
2158          $scoes = $DB->get_records_select(
2159              'scorm_scoes',
2160              'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true),
2161              array($scorm->id),
2162              'sortorder, id'
2163          );
2164          $sco = current($scoes);
2165      }
2166  
2167      $connector = '';
2168      $version = substr($scorm->version, 0, 4);
2169      if ((isset($sco->parameters) && (!empty($sco->parameters))) || ($version == 'AICC')) {
2170          if (stripos($sco->launch, '?') !== false) {
2171              $connector = '&';
2172          } else {
2173              $connector = '?';
2174          }
2175          if ((isset($sco->parameters) && (!empty($sco->parameters))) && ($sco->parameters[0] == '?')) {
2176              $sco->parameters = substr($sco->parameters, 1);
2177          }
2178      }
2179  
2180      if ($version == 'AICC') {
2181          require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
2182          $aiccsid = scorm_aicc_get_hacp_session($scorm->id);
2183          if (empty($aiccsid)) {
2184              $aiccsid = sesskey();
2185          }
2186          $scoparams = '';
2187          if (isset($sco->parameters) && (!empty($sco->parameters))) {
2188              $scoparams = '&'. $sco->parameters;
2189          }
2190          $launcher = $sco->launch.$connector.'aicc_sid='.$aiccsid.'&aicc_url='.$CFG->wwwroot.'/mod/scorm/aicc.php'.$scoparams;
2191      } else {
2192          if (isset($sco->parameters) && (!empty($sco->parameters))) {
2193              $launcher = $sco->launch.$connector.$sco->parameters;
2194          } else {
2195              $launcher = $sco->launch;
2196          }
2197      }
2198  
2199      if (scorm_external_link($sco->launch)) {
2200          // TODO: does this happen?
2201          $scolaunchurl = $launcher;
2202      } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) {
2203          // Remote learning activity.
2204          $scolaunchurl = dirname($scorm->reference).'/'.$launcher;
2205      } else if ($scorm->scormtype === SCORM_TYPE_LOCAL && strtolower($scorm->reference) == 'imsmanifest.xml') {
2206          // This SCORM content sits in a repository that allows relative links.
2207          $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher";
2208      } else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
2209          // Note: do not convert this to use moodle_url().
2210          // SCORM does not work without slasharguments and moodle_url() encodes querystring vars.
2211          $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/content/$scorm->revision/$launcher";
2212      }
2213      return array($sco, $scolaunchurl);
2214  }
2215  
2216  /**
2217   * Trigger the scorm_launched event.
2218   *
2219   * @param  stdClass $scorm   scorm object
2220   * @param  stdClass $sco     sco object
2221   * @param  stdClass $cm      course module object
2222   * @param  stdClass $context context object
2223   * @param  string $scourl    SCO URL
2224   * @since Moodle 3.1
2225   */
2226  function scorm_launch_sco($scorm, $sco, $cm, $context, $scourl) {
2227  
2228      $event = \mod_scorm\event\sco_launched::create(array(
2229          'objectid' => $sco->id,
2230          'context' => $context,
2231          'other' => array('instanceid' => $scorm->id, 'loadedcontent' => $scourl)
2232      ));
2233      $event->add_record_snapshot('course_modules', $cm);
2234      $event->add_record_snapshot('scorm', $scorm);
2235      $event->add_record_snapshot('scorm_scoes', $sco);
2236      $event->trigger();
2237  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1