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