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