Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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 container', '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 'class' => 'container')); 997 if ($scorm->hidebrowse == 0) { 998 print_string('mode', 'scorm'); 999 echo ': '.html_writer::empty_tag('input', array('type' => 'radio', 'id' => 'b', 'name' => 'mode', 1000 'value' => 'browse', 'class' => 'mr-1')). 1001 html_writer::label(get_string('browse', 'scorm'), 'b'); 1002 echo html_writer::empty_tag('input', array('type' => 'radio', 1003 'id' => 'n', 'name' => 'mode', 1004 'value' => 'normal', 'checked' => 'checked', 1005 'class' => 'mx-1')). 1006 html_writer::label(get_string('normal', 'scorm'), 'n'); 1007 1008 } else { 1009 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'mode', 'value' => 'normal')); 1010 } 1011 if (!empty($scorm->forcenewattempt)) { 1012 if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS || 1013 ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE && $incomplete === false)) { 1014 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'newattempt', 'value' => 'on')); 1015 } 1016 } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) { 1017 echo html_writer::empty_tag('br'); 1018 echo html_writer::checkbox('newattempt', 'on', false, '', array('id' => 'a')); 1019 echo html_writer::label(get_string('newattempt', 'scorm'), 'a'); 1020 } 1021 if (!empty($scorm->popup)) { 1022 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'display', 'value' => 'popup')); 1023 } 1024 1025 echo html_writer::empty_tag('br'); 1026 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scoid', 'value' => $launchsco)); 1027 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cm', 'value' => $cm->id)); 1028 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentorg', 'value' => $orgidentifier)); 1029 echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('enter', 'scorm'), 1030 'class' => 'btn btn-primary')); 1031 echo html_writer::end_tag('form'); 1032 echo html_writer::end_div(); 1033 } 1034 } 1035 1036 function scorm_simple_play($scorm, $user, $context, $cmid) { 1037 global $DB; 1038 1039 $result = false; 1040 1041 if (has_capability('mod/scorm:viewreport', $context)) { 1042 // If this user can view reports, don't skipview so they can see links to reports. 1043 return $result; 1044 } 1045 1046 if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) { 1047 scorm_parse($scorm, false); 1048 } 1049 $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '. 1050 $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'sortorder, id', 'id'); 1051 1052 if ($scoes) { 1053 $orgidentifier = ''; 1054 if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) { 1055 if (($sco->organization == '') && ($sco->launch == '')) { 1056 $orgidentifier = $sco->identifier; 1057 } else { 1058 $orgidentifier = $sco->organization; 1059 } 1060 } 1061 if ($scorm->skipview >= SCORM_SKIPVIEW_FIRST) { 1062 $sco = current($scoes); 1063 $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier); 1064 $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id, 'currentorg' => $orgidentifier)); 1065 1066 // Set last incomplete sco to launch first if forcenewattempt not set to always. 1067 if (!empty($result->sco->id) && $scorm->forcenewattempt != SCORM_FORCEATTEMPT_ALWAYS) { 1068 $url->param('scoid', $result->sco->id); 1069 } else { 1070 $url->param('scoid', $sco->id); 1071 } 1072 1073 if ($scorm->skipview == SCORM_SKIPVIEW_ALWAYS || !scorm_has_tracks($scorm->id, $user->id)) { 1074 if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS || 1075 ($result->incomplete === false && $scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE)) { 1076 1077 $url->param('newattempt', 'on'); 1078 } 1079 redirect($url); 1080 } 1081 } 1082 } 1083 return $result; 1084 } 1085 1086 function scorm_get_count_users($scormid, $groupingid=null) { 1087 global $CFG, $DB; 1088 1089 if (!empty($groupingid)) { 1090 $sql = "SELECT COUNT(DISTINCT st.userid) 1091 FROM {scorm_scoes_track} st 1092 INNER JOIN {groups_members} gm ON st.userid = gm.userid 1093 INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid 1094 WHERE st.scormid = ? AND gg.groupingid = ? 1095 "; 1096 $params = array($scormid, $groupingid); 1097 } else { 1098 $sql = "SELECT COUNT(DISTINCT st.userid) 1099 FROM {scorm_scoes_track} st 1100 WHERE st.scormid = ? 1101 "; 1102 $params = array($scormid); 1103 } 1104 1105 return ($DB->count_records_sql($sql, $params)); 1106 } 1107 1108 /** 1109 * Build up the JavaScript representation of an array element 1110 * 1111 * @param string $sversion SCORM API version 1112 * @param array $userdata User track data 1113 * @param string $elementname Name of array element to get values for 1114 * @param array $children list of sub elements of this array element that also need instantiating 1115 * @return Javascript array elements 1116 */ 1117 function scorm_reconstitute_array_element($sversion, $userdata, $elementname, $children) { 1118 // Reconstitute comments_from_learner and comments_from_lms. 1119 $current = ''; 1120 $currentsubelement = ''; 1121 $currentsub = ''; 1122 $count = 0; 1123 $countsub = 0; 1124 $scormseperator = '_'; 1125 $return = ''; 1126 if (scorm_version_check($sversion, SCORM_13)) { // Scorm 1.3 elements use a . instead of an _ . 1127 $scormseperator = '.'; 1128 } 1129 // Filter out the ones we want. 1130 $elementlist = array(); 1131 foreach ($userdata as $element => $value) { 1132 if (substr($element, 0, strlen($elementname)) == $elementname) { 1133 $elementlist[$element] = $value; 1134 } 1135 } 1136 1137 // Sort elements in .n array order. 1138 uksort($elementlist, "scorm_element_cmp"); 1139 1140 // Generate JavaScript. 1141 foreach ($elementlist as $element => $value) { 1142 if (scorm_version_check($sversion, SCORM_13)) { 1143 $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element); 1144 preg_match('/\.(N\d+)\./', $element, $matches); 1145 } else { 1146 $element = preg_replace('/\.(\d+)\./', "_\$1.", $element); 1147 preg_match('/\_(\d+)\./', $element, $matches); 1148 } 1149 if (count($matches) > 0 && $current != $matches[1]) { 1150 if ($countsub > 0) { 1151 $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n"; 1152 } 1153 $current = $matches[1]; 1154 $count++; 1155 $currentsubelement = ''; 1156 $currentsub = ''; 1157 $countsub = 0; 1158 $end = strpos($element, $matches[1]) + strlen($matches[1]); 1159 $subelement = substr($element, 0, $end); 1160 $return .= ' '.$subelement." = new Object();\n"; 1161 // Now add the children. 1162 foreach ($children as $child) { 1163 $return .= ' '.$subelement.".".$child." = new Object();\n"; 1164 $return .= ' '.$subelement.".".$child."._children = ".$child."_children;\n"; 1165 } 1166 } 1167 1168 // Now - flesh out the second level elements if there are any. 1169 if (scorm_version_check($sversion, SCORM_13)) { 1170 $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element); 1171 preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches); 1172 } else { 1173 $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element); 1174 preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches); 1175 } 1176 1177 // Check the sub element type. 1178 if (count($matches) > 0 && $currentsubelement != $matches[1]) { 1179 if ($countsub > 0) { 1180 $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n"; 1181 } 1182 $currentsubelement = $matches[1]; 1183 $currentsub = ''; 1184 $countsub = 0; 1185 $end = strpos($element, $matches[1]) + strlen($matches[1]); 1186 $subelement = substr($element, 0, $end); 1187 $return .= ' '.$subelement." = new Object();\n"; 1188 } 1189 1190 // Now check the subelement subscript. 1191 if (count($matches) > 0 && $currentsub != $matches[2]) { 1192 $currentsub = $matches[2]; 1193 $countsub++; 1194 $end = strrpos($element, $matches[2]) + strlen($matches[2]); 1195 $subelement = substr($element, 0, $end); 1196 $return .= ' '.$subelement." = new Object();\n"; 1197 } 1198 1199 $return .= ' '.$element.' = '.json_encode($value).";\n"; 1200 } 1201 if ($countsub > 0) { 1202 $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n"; 1203 } 1204 if ($count > 0) { 1205 $return .= ' '.$elementname.'._count = '.$count.";\n"; 1206 } 1207 return $return; 1208 } 1209 1210 /** 1211 * Build up the JavaScript representation of an array element 1212 * 1213 * @param string $a left array element 1214 * @param string $b right array element 1215 * @return comparator - 0,1,-1 1216 */ 1217 function scorm_element_cmp($a, $b) { 1218 preg_match('/.*?(\d+)\./', $a, $matches); 1219 $left = intval($matches[1]); 1220 preg_match('/.?(\d+)\./', $b, $matches); 1221 $right = intval($matches[1]); 1222 if ($left < $right) { 1223 return -1; // Smaller. 1224 } else if ($left > $right) { 1225 return 1; // Bigger. 1226 } else { 1227 // Look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern. 1228 if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) { 1229 $leftterm = intval($matches[2]); 1230 $left = intval($matches[3]); 1231 if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) { 1232 $rightterm = intval($matches[2]); 1233 $right = intval($matches[3]); 1234 if ($leftterm < $rightterm) { 1235 return -1; // Smaller. 1236 } else if ($leftterm > $rightterm) { 1237 return 1; // Bigger. 1238 } else { 1239 if ($left < $right) { 1240 return -1; // Smaller. 1241 } else if ($left > $right) { 1242 return 1; // Bigger. 1243 } 1244 } 1245 } 1246 } 1247 // Fall back for no second level matches or second level matches are equal. 1248 return 0; // Equal to. 1249 } 1250 } 1251 1252 /** 1253 * Generate the user attempt status string 1254 * 1255 * @param object $user Current context user 1256 * @param object $scorm a moodle scrom object - mdl_scorm 1257 * @return string - Attempt status string 1258 */ 1259 function scorm_get_attempt_status($user, $scorm, $cm='') { 1260 global $DB, $PAGE, $OUTPUT; 1261 1262 $attempts = scorm_get_attempt_count($user->id, $scorm, true); 1263 if (empty($attempts)) { 1264 $attemptcount = 0; 1265 } else { 1266 $attemptcount = count($attempts); 1267 } 1268 1269 $result = html_writer::start_tag('p').get_string('noattemptsallowed', 'scorm').': '; 1270 if ($scorm->maxattempt > 0) { 1271 $result .= $scorm->maxattempt . html_writer::empty_tag('br'); 1272 } else { 1273 $result .= get_string('unlimited').html_writer::empty_tag('br'); 1274 } 1275 $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . html_writer::empty_tag('br'); 1276 1277 if ($scorm->maxattempt == 1) { 1278 switch ($scorm->grademethod) { 1279 case GRADEHIGHEST: 1280 $grademethod = get_string('gradehighest', 'scorm'); 1281 break; 1282 case GRADEAVERAGE: 1283 $grademethod = get_string('gradeaverage', 'scorm'); 1284 break; 1285 case GRADESUM: 1286 $grademethod = get_string('gradesum', 'scorm'); 1287 break; 1288 case GRADESCOES: 1289 $grademethod = get_string('gradescoes', 'scorm'); 1290 break; 1291 } 1292 } else { 1293 switch ($scorm->whatgrade) { 1294 case HIGHESTATTEMPT: 1295 $grademethod = get_string('highestattempt', 'scorm'); 1296 break; 1297 case AVERAGEATTEMPT: 1298 $grademethod = get_string('averageattempt', 'scorm'); 1299 break; 1300 case FIRSTATTEMPT: 1301 $grademethod = get_string('firstattempt', 'scorm'); 1302 break; 1303 case LASTATTEMPT: 1304 $grademethod = get_string('lastattempt', 'scorm'); 1305 break; 1306 } 1307 } 1308 1309 if (!empty($attempts)) { 1310 $i = 1; 1311 foreach ($attempts as $attempt) { 1312 $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber); 1313 if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) { 1314 $gradereported = $gradereported / $scorm->maxgrade; 1315 $gradereported = number_format($gradereported * 100, 0) .'%'; 1316 } 1317 $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .html_writer::empty_tag('br'); 1318 $i++; 1319 } 1320 } 1321 $calculatedgrade = scorm_grade_user($scorm, $user->id); 1322 if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) { 1323 $calculatedgrade = $calculatedgrade / $scorm->maxgrade; 1324 $calculatedgrade = number_format($calculatedgrade * 100, 0) .'%'; 1325 } 1326 $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod; 1327 if (empty($attempts)) { 1328 $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm'). 1329 ': '.get_string('none').html_writer::empty_tag('br'); 1330 } else { 1331 $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm'). 1332 ': '.$calculatedgrade.html_writer::empty_tag('br'); 1333 } 1334 $result .= html_writer::end_tag('p'); 1335 if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) { 1336 $result .= html_writer::tag('p', get_string('exceededmaxattempts', 'scorm'), array('class' => 'exceededmaxattempts')); 1337 } 1338 if (!empty($cm)) { 1339 $context = context_module::instance($cm->id); 1340 if (has_capability('mod/scorm:deleteownresponses', $context) && 1341 $DB->record_exists('scorm_scoes_track', array('userid' => $user->id, 'scormid' => $scorm->id))) { 1342 // Check to see if any data is stored for this user. 1343 $deleteurl = new moodle_url($PAGE->url, array('action' => 'delete', 'sesskey' => sesskey())); 1344 $result .= $OUTPUT->single_button($deleteurl, get_string('deleteallattempts', 'scorm')); 1345 } 1346 } 1347 1348 return $result; 1349 } 1350 1351 /** 1352 * Get SCORM attempt count 1353 * 1354 * @param object $user Current context user 1355 * @param object $scorm a moodle scrom object - mdl_scorm 1356 * @param bool $returnobjects if true returns a object with attempts, if false returns count of attempts. 1357 * @param bool $ignoremissingcompletion - ignores attempts that haven't reported a grade/completion. 1358 * @return int - no. of attempts so far 1359 */ 1360 function scorm_get_attempt_count($userid, $scorm, $returnobjects = false, $ignoremissingcompletion = false) { 1361 global $DB; 1362 1363 // Historically attempts that don't report these elements haven't been included in the average attempts grading method 1364 // we may want to change this in future, but to avoid unexpected grade decreases we're leaving this in. MDL-43222 . 1365 if (scorm_version_check($scorm->version, SCORM_13)) { 1366 $element = 'cmi.score.raw'; 1367 } else if ($scorm->grademethod == GRADESCOES) { 1368 $element = 'cmi.core.lesson_status'; 1369 } else { 1370 $element = 'cmi.core.score.raw'; 1371 } 1372 1373 if ($returnobjects) { 1374 $params = array('userid' => $userid, 'scormid' => $scorm->id); 1375 if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested. 1376 $params['element'] = $element; 1377 } 1378 $attempts = $DB->get_records('scorm_scoes_track', $params, 'attempt', 'DISTINCT attempt AS attemptnumber'); 1379 return $attempts; 1380 } else { 1381 $params = array($userid, $scorm->id); 1382 $sql = "SELECT COUNT(DISTINCT attempt) 1383 FROM {scorm_scoes_track} 1384 WHERE userid = ? AND scormid = ?"; 1385 if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested. 1386 $sql .= ' AND element = ?'; 1387 $params[] = $element; 1388 } 1389 1390 $attemptscount = $DB->count_records_sql($sql, $params); 1391 return $attemptscount; 1392 } 1393 } 1394 1395 /** 1396 * Figure out with this is a debug situation 1397 * 1398 * @param object $scorm a moodle scrom object - mdl_scorm 1399 * @return boolean - debugging true/false 1400 */ 1401 function scorm_debugging($scorm) { 1402 global $USER; 1403 $cfgscorm = get_config('scorm'); 1404 1405 if (!$cfgscorm->allowapidebug) { 1406 return false; 1407 } 1408 $identifier = $USER->username.':'.$scorm->name; 1409 $test = $cfgscorm->apidebugmask; 1410 // Check the regex is only a short list of safe characters. 1411 if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) { 1412 return false; 1413 } 1414 1415 if (preg_match('/^'.$test.'/', $identifier)) { 1416 return true; 1417 } 1418 return false; 1419 } 1420 1421 /** 1422 * Delete Scorm tracks for selected users 1423 * 1424 * @param array $attemptids list of attempts that need to be deleted 1425 * @param stdClass $scorm instance 1426 * 1427 * @return bool true deleted all responses, false failed deleting an attempt - stopped here 1428 */ 1429 function scorm_delete_responses($attemptids, $scorm) { 1430 if (!is_array($attemptids) || empty($attemptids)) { 1431 return false; 1432 } 1433 1434 foreach ($attemptids as $num => $attemptid) { 1435 if (empty($attemptid)) { 1436 unset($attemptids[$num]); 1437 } 1438 } 1439 1440 foreach ($attemptids as $attempt) { 1441 $keys = explode(':', $attempt); 1442 if (count($keys) == 2) { 1443 $userid = clean_param($keys[0], PARAM_INT); 1444 $attemptid = clean_param($keys[1], PARAM_INT); 1445 if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) { 1446 return false; 1447 } 1448 } else { 1449 return false; 1450 } 1451 } 1452 return true; 1453 } 1454 1455 /** 1456 * Delete Scorm tracks for selected users 1457 * 1458 * @param int $userid ID of User 1459 * @param stdClass $scorm Scorm object 1460 * @param int $attemptid user attempt that need to be deleted 1461 * 1462 * @return bool true suceeded 1463 */ 1464 function scorm_delete_attempt($userid, $scorm, $attemptid) { 1465 global $DB; 1466 1467 $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid)); 1468 $cm = get_coursemodule_from_instance('scorm', $scorm->id); 1469 1470 // Trigger instances list viewed event. 1471 $event = \mod_scorm\event\attempt_deleted::create(array( 1472 'other' => array('attemptid' => $attemptid), 1473 'context' => context_module::instance($cm->id), 1474 'relateduserid' => $userid 1475 )); 1476 $event->add_record_snapshot('course_modules', $cm); 1477 $event->add_record_snapshot('scorm', $scorm); 1478 $event->trigger(); 1479 1480 include_once ('lib.php'); 1481 scorm_update_grades($scorm, $userid, true); 1482 return true; 1483 } 1484 1485 /** 1486 * Converts SCORM duration notation to human-readable format 1487 * The function works with both SCORM 1.2 and SCORM 2004 time formats 1488 * @param $duration string SCORM duration 1489 * @return string human-readable date/time 1490 */ 1491 function scorm_format_duration($duration) { 1492 // Fetch date/time strings. 1493 $stryears = get_string('years'); 1494 $strmonths = get_string('nummonths'); 1495 $strdays = get_string('days'); 1496 $strhours = get_string('hours'); 1497 $strminutes = get_string('minutes'); 1498 $strseconds = get_string('seconds'); 1499 1500 if ($duration[0] == 'P') { 1501 // If timestamp starts with 'P' - it's a SCORM 2004 format 1502 // this regexp discards empty sections, takes Month/Minute ambiguity into consideration, 1503 // and outputs filled sections, discarding leading zeroes and any format literals 1504 // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero. 1505 $pattern = array( '#([A-Z])0+Y#', '#([A-Z])0+M#', '#([A-Z])0+D#', '#P(|\d+Y)0*(\d+)M#', 1506 '#0*(\d+)Y#', '#0*(\d+)D#', '#P#', '#([A-Z])0+H#', '#([A-Z])[0.]+S#', 1507 '#\.0+S#', '#T(|\d+H)0*(\d+)M#', '#0*(\d+)H#', '#0+\.(\d+)S#', 1508 '#0*([\d.]+)S#', '#T#' ); 1509 $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ', 1510 '', '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ', 1511 '0.$1 '.$strseconds, '$1 '.$strseconds, ''); 1512 } else { 1513 // Else we have SCORM 1.2 format there 1514 // first convert the timestamp to some SCORM 2004-like format for conveniency. 1515 $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration); 1516 // Then convert in the same way as SCORM 2004. 1517 $pattern = array( '#T0+H#', '#([A-Z])0+M#', '#([A-Z])[0.]+S#', '#\.0+S#', '#0*(\d+)H#', 1518 '#0*(\d+)M#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' ); 1519 $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ', 1520 '0.$1 '.$strseconds, '$1 '.$strseconds, '' ); 1521 } 1522 1523 $result = preg_replace($pattern, $replace, $duration); 1524 1525 return $result; 1526 } 1527 1528 function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='normal', $attempt='', 1529 $play=false, $organizationsco=null) { 1530 global $CFG, $DB, $PAGE, $OUTPUT; 1531 1532 // Always pass the mode even if empty as that is what is done elsewhere and the urls have to match. 1533 $modestr = '&mode='; 1534 if ($mode != 'normal') { 1535 $modestr = '&mode='.$mode; 1536 } 1537 1538 $result = array(); 1539 $incomplete = false; 1540 1541 if (!empty($organizationsco)) { 1542 $result[0] = $organizationsco; 1543 $result[0]->isvisible = 'true'; 1544 $result[0]->statusicon = ''; 1545 $result[0]->url = ''; 1546 } 1547 1548 if ($scoes = scorm_get_scoes($scorm->id, $currentorg)) { 1549 // Retrieve user tracking data for each learning object. 1550 $usertracks = array(); 1551 foreach ($scoes as $sco) { 1552 if (!empty($sco->launch)) { 1553 if ($usertrack = scorm_get_tracks($sco->id, $user->id, $attempt)) { 1554 if ($usertrack->status == '') { 1555 $usertrack->status = 'notattempted'; 1556 } 1557 $usertracks[$sco->identifier] = $usertrack; 1558 } 1559 } 1560 } 1561 foreach ($scoes as $sco) { 1562 if (!isset($sco->isvisible)) { 1563 $sco->isvisible = 'true'; 1564 } 1565 1566 if (empty($sco->title)) { 1567 $sco->title = $sco->identifier; 1568 } 1569 1570 if (scorm_version_check($scorm->version, SCORM_13)) { 1571 $sco->prereq = true; 1572 } else { 1573 $sco->prereq = empty($sco->prerequisites) || scorm_eval_prerequisites($sco->prerequisites, $usertracks); 1574 } 1575 1576 if ($sco->isvisible === 'true') { 1577 if (!empty($sco->launch)) { 1578 // Set first sco to launch if in browse/review mode. 1579 if (empty($scoid) && ($mode != 'normal')) { 1580 $scoid = $sco->id; 1581 } 1582 1583 if (isset($usertracks[$sco->identifier])) { 1584 $usertrack = $usertracks[$sco->identifier]; 1585 1586 // Check we have a valid status string identifier. 1587 if ($statusstringexists = get_string_manager()->string_exists($usertrack->status, 'scorm')) { 1588 $strstatus = get_string($usertrack->status, 'scorm'); 1589 } else { 1590 $strstatus = get_string('invalidstatus', 'scorm'); 1591 } 1592 1593 if ($sco->scormtype == 'sco') { 1594 // Assume if we didn't get a valid status string, we don't have an icon either. 1595 $statusicon = $OUTPUT->pix_icon($statusstringexists ? $usertrack->status : 'incomplete', 1596 $strstatus, 'scorm'); 1597 } else { 1598 $statusicon = $OUTPUT->pix_icon('asset', get_string('assetlaunched', 'scorm'), 'scorm'); 1599 } 1600 1601 if (($usertrack->status == 'notattempted') || 1602 ($usertrack->status == 'incomplete') || 1603 ($usertrack->status == 'browsed')) { 1604 $incomplete = true; 1605 if (empty($scoid)) { 1606 $scoid = $sco->id; 1607 } 1608 } 1609 1610 $strsuspended = get_string('suspended', 'scorm'); 1611 1612 $exitvar = 'cmi.core.exit'; 1613 1614 if (scorm_version_check($scorm->version, SCORM_13)) { 1615 $exitvar = 'cmi.exit'; 1616 } 1617 1618 if ($incomplete && isset($usertrack->{$exitvar}) && ($usertrack->{$exitvar} == 'suspend')) { 1619 $statusicon = $OUTPUT->pix_icon('suspend', $strstatus.' - '.$strsuspended, 'scorm'); 1620 } 1621 1622 } else { 1623 if (empty($scoid)) { 1624 $scoid = $sco->id; 1625 } 1626 1627 $incomplete = true; 1628 1629 if ($sco->scormtype == 'sco') { 1630 $statusicon = $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm'); 1631 } else { 1632 $statusicon = $OUTPUT->pix_icon('asset', get_string('asset', 'scorm'), 'scorm'); 1633 } 1634 } 1635 } 1636 } 1637 1638 if (empty($statusicon)) { 1639 $sco->statusicon = $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm'); 1640 } else { 1641 $sco->statusicon = $statusicon; 1642 } 1643 1644 $sco->url = 'a='.$scorm->id.'&scoid='.$sco->id.'¤torg='.$currentorg.$modestr.'&attempt='.$attempt; 1645 $sco->incomplete = $incomplete; 1646 1647 if (!in_array($sco->id, array_keys($result))) { 1648 $result[$sco->id] = $sco; 1649 } 1650 } 1651 } 1652 1653 // Get the parent scoes! 1654 $result = scorm_get_toc_get_parent_child($result, $currentorg); 1655 1656 // Be safe, prevent warnings from showing up while returning array. 1657 if (!isset($scoid)) { 1658 $scoid = ''; 1659 } 1660 1661 return array('scoes' => $result, 'usertracks' => $usertracks, 'scoid' => $scoid); 1662 } 1663 1664 function scorm_get_toc_get_parent_child(&$result, $currentorg) { 1665 $final = array(); 1666 $level = 0; 1667 // Organization is always the root, prevparent. 1668 if (!empty($currentorg)) { 1669 $prevparent = $currentorg; 1670 } else { 1671 $prevparent = '/'; 1672 } 1673 1674 foreach ($result as $sco) { 1675 if ($sco->parent == '/') { 1676 $final[$level][$sco->identifier] = $sco; 1677 $prevparent = $sco->identifier; 1678 unset($result[$sco->id]); 1679 } else { 1680 if ($sco->parent == $prevparent) { 1681 $final[$level][$sco->identifier] = $sco; 1682 $prevparent = $sco->identifier; 1683 unset($result[$sco->id]); 1684 } else { 1685 if (!empty($final[$level])) { 1686 $found = false; 1687 foreach ($final[$level] as $fin) { 1688 if ($sco->parent == $fin->identifier) { 1689 $found = true; 1690 } 1691 } 1692 1693 if ($found) { 1694 $final[$level][$sco->identifier] = $sco; 1695 unset($result[$sco->id]); 1696 $found = false; 1697 } else { 1698 $level++; 1699 $final[$level][$sco->identifier] = $sco; 1700 unset($result[$sco->id]); 1701 } 1702 } 1703 } 1704 } 1705 } 1706 1707 for ($i = 0; $i <= $level; $i++) { 1708 $prevparent = ''; 1709 foreach ($final[$i] as $ident => $sco) { 1710 if (empty($prevparent)) { 1711 $prevparent = $ident; 1712 } 1713 if (!isset($final[$i][$prevparent]->children)) { 1714 $final[$i][$prevparent]->children = array(); 1715 } 1716 if ($sco->parent == $prevparent) { 1717 $final[$i][$prevparent]->children[] = $sco; 1718 $prevparent = $ident; 1719 } else { 1720 $parent = false; 1721 foreach ($final[$i] as $identifier => $scoobj) { 1722 if ($identifier == $sco->parent) { 1723 $parent = $identifier; 1724 } 1725 } 1726 1727 if ($parent !== false) { 1728 $final[$i][$parent]->children[] = $sco; 1729 } 1730 } 1731 } 1732 } 1733 1734 $results = array(); 1735 for ($i = 0; $i <= $level; $i++) { 1736 $keys = array_keys($final[$i]); 1737 $results[] = $final[$i][$keys[0]]; 1738 } 1739 1740 return $results; 1741 } 1742 1743 function scorm_format_toc_for_treeview($user, $scorm, $scoes, $usertracks, $cmid, $toclink=TOCJSLINK, $currentorg='', 1744 $attempt='', $play=false, $organizationsco=null, $children=false) { 1745 global $CFG; 1746 1747 $result = new stdClass(); 1748 $result->prerequisites = true; 1749 $result->incomplete = true; 1750 $result->toc = ''; 1751 1752 if (!$children) { 1753 $attemptsmade = scorm_get_attempt_count($user->id, $scorm); 1754 $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attemptsmade; 1755 } 1756 1757 if (!$children) { 1758 $result->toc = html_writer::start_tag('ul'); 1759 1760 if (!$play && !empty($organizationsco)) { 1761 $result->toc .= html_writer::start_tag('li').$organizationsco->title.html_writer::end_tag('li'); 1762 } 1763 } 1764 1765 $prevsco = ''; 1766 if (!empty($scoes)) { 1767 foreach ($scoes as $sco) { 1768 1769 if ($sco->isvisible === 'false') { 1770 continue; 1771 } 1772 1773 $result->toc .= html_writer::start_tag('li'); 1774 $scoid = $sco->id; 1775 1776 $score = ''; 1777 1778 if (isset($usertracks[$sco->identifier])) { 1779 $viewscore = has_capability('mod/scorm:viewscores', context_module::instance($cmid)); 1780 if (isset($usertracks[$sco->identifier]->score_raw) && $viewscore) { 1781 if ($usertracks[$sco->identifier]->score_raw != '') { 1782 $score = '('.get_string('score', 'scorm').': '.$usertracks[$sco->identifier]->score_raw.')'; 1783 } 1784 } 1785 } 1786 1787 if (!empty($sco->prereq)) { 1788 if ($sco->id == $scoid) { 1789 $result->prerequisites = true; 1790 } 1791 1792 if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) { 1793 if ($sco->scormtype == 'sco') { 1794 $result->toc .= html_writer::span($sco->statusicon.' '.format_string($sco->title)); 1795 } else { 1796 $result->toc .= html_writer::span(' '.format_string($sco->title)); 1797 } 1798 } else if ($toclink == TOCFULLURL) { 1799 $url = $CFG->wwwroot.'/mod/scorm/player.php?'.$sco->url; 1800 if (!empty($sco->launch)) { 1801 if ($sco->scormtype == 'sco') { 1802 $result->toc .= $sco->statusicon.' '; 1803 $result->toc .= html_writer::link($url, format_string($sco->title)).$score; 1804 } else { 1805 $result->toc .= ' '.html_writer::link($url, format_string($sco->title), 1806 array('data-scoid' => $sco->id)).$score; 1807 } 1808 } else { 1809 if ($sco->scormtype == 'sco') { 1810 $result->toc .= $sco->statusicon.' '.format_string($sco->title).$score; 1811 } else { 1812 $result->toc .= ' '.format_string($sco->title).$score; 1813 } 1814 } 1815 } else { 1816 if (!empty($sco->launch)) { 1817 if ($sco->scormtype == 'sco') { 1818 $result->toc .= html_writer::tag('a', $sco->statusicon.' '. 1819 format_string($sco->title).' '.$score, 1820 array('data-scoid' => $sco->id, 'title' => $sco->url)); 1821 } else { 1822 $result->toc .= html_writer::tag('a', ' '.format_string($sco->title).' '.$score, 1823 array('data-scoid' => $sco->id, 'title' => $sco->url)); 1824 } 1825 } else { 1826 if ($sco->scormtype == 'sco') { 1827 $result->toc .= html_writer::span($sco->statusicon.' '.format_string($sco->title)); 1828 } else { 1829 $result->toc .= html_writer::span(' '.format_string($sco->title)); 1830 } 1831 } 1832 } 1833 1834 } else { 1835 if ($play) { 1836 if ($sco->scormtype == 'sco') { 1837 $result->toc .= html_writer::span($sco->statusicon.' '.format_string($sco->title)); 1838 } else { 1839 $result->toc .= ' '.format_string($sco->title).html_writer::end_span(); 1840 } 1841 } else { 1842 if ($sco->scormtype == 'sco') { 1843 $result->toc .= $sco->statusicon.' '.format_string($sco->title); 1844 } else { 1845 $result->toc .= ' '.format_string($sco->title); 1846 } 1847 } 1848 } 1849 1850 if (!empty($sco->children)) { 1851 $result->toc .= html_writer::start_tag('ul'); 1852 $childresult = scorm_format_toc_for_treeview($user, $scorm, $sco->children, $usertracks, $cmid, 1853 $toclink, $currentorg, $attempt, $play, $organizationsco, true); 1854 1855 // Is any of the children incomplete? 1856 $sco->incomplete = $childresult->incomplete; 1857 $result->toc .= $childresult->toc; 1858 $result->toc .= html_writer::end_tag('ul'); 1859 $result->toc .= html_writer::end_tag('li'); 1860 } else { 1861 $result->toc .= html_writer::end_tag('li'); 1862 } 1863 $prevsco = $sco; 1864 } 1865 $result->incomplete = $sco->incomplete; 1866 } 1867 1868 if (!$children) { 1869 $result->toc .= html_writer::end_tag('ul'); 1870 } 1871 1872 return $result; 1873 } 1874 1875 function scorm_format_toc_for_droplist($scorm, $scoes, $usertracks, $currentorg='', $organizationsco=null, 1876 $children=false, $level=0, $tocmenus=array()) { 1877 if (!empty($scoes)) { 1878 if (!empty($organizationsco) && !$children) { 1879 $tocmenus[$organizationsco->id] = $organizationsco->title; 1880 } 1881 1882 $parents[$level] = '/'; 1883 foreach ($scoes as $sco) { 1884 if ($parents[$level] != $sco->parent) { 1885 if ($newlevel = array_search($sco->parent, $parents)) { 1886 $level = $newlevel; 1887 } else { 1888 $i = $level; 1889 while (($i > 0) && ($parents[$level] != $sco->parent)) { 1890 $i--; 1891 } 1892 1893 if (($i == 0) && ($sco->parent != $currentorg)) { 1894 $level++; 1895 } else { 1896 $level = $i; 1897 } 1898 1899 $parents[$level] = $sco->parent; 1900 } 1901 } 1902 1903 if ($sco->scormtype == 'sco') { 1904 $tocmenus[$sco->id] = scorm_repeater('−', $level) . '>' . format_string($sco->title); 1905 } 1906 1907 if (!empty($sco->children)) { 1908 $tocmenus = scorm_format_toc_for_droplist($scorm, $sco->children, $usertracks, $currentorg, 1909 $organizationsco, true, $level, $tocmenus); 1910 } 1911 } 1912 } 1913 1914 return $tocmenus; 1915 } 1916 1917 function scorm_get_toc($user, $scorm, $cmid, $toclink=TOCJSLINK, $currentorg='', $scoid='', $mode='normal', 1918 $attempt='', $play=false, $tocheader=false) { 1919 global $CFG, $DB, $OUTPUT; 1920 1921 if (empty($attempt)) { 1922 $attempt = scorm_get_last_attempt($scorm->id, $user->id); 1923 } 1924 1925 $result = new stdClass(); 1926 $organizationsco = null; 1927 1928 if ($tocheader) { 1929 $result->toc = html_writer::start_div('yui3-g-r', array('id' => 'scorm_layout')); 1930 $result->toc .= html_writer::start_div('yui3-u-1-5 loading', array('id' => 'scorm_toc')); 1931 $result->toc .= html_writer::div('', '', array('id' => 'scorm_toc_title')); 1932 $result->toc .= html_writer::start_div('', array('id' => 'scorm_tree')); 1933 } 1934 1935 if (!empty($currentorg)) { 1936 $organizationsco = $DB->get_record('scorm_scoes', array('scorm' => $scorm->id, 'identifier' => $currentorg)); 1937 if (!empty($organizationsco->title)) { 1938 if ($play) { 1939 $result->toctitle = $organizationsco->title; 1940 } 1941 } 1942 } 1943 1944 $scoes = scorm_get_toc_object($user, $scorm, $currentorg, $scoid, $mode, $attempt, $play, $organizationsco); 1945 1946 $treeview = scorm_format_toc_for_treeview($user, $scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], $cmid, 1947 $toclink, $currentorg, $attempt, $play, $organizationsco, false); 1948 1949 if ($tocheader) { 1950 $result->toc .= $treeview->toc; 1951 } else { 1952 $result->toc = $treeview->toc; 1953 } 1954 1955 if (!empty($scoes['scoid'])) { 1956 $scoid = $scoes['scoid']; 1957 } 1958 1959 if (empty($scoid)) { 1960 // If this is a normal package with an org sco and child scos get the first child. 1961 if (!empty($scoes['scoes'][0]->children)) { 1962 $result->sco = $scoes['scoes'][0]->children[0]; 1963 } else { // This package only has one sco - it may be a simple external AICC package. 1964 $result->sco = $scoes['scoes'][0]; 1965 } 1966 1967 } else { 1968 $result->sco = scorm_get_sco($scoid); 1969 } 1970 1971 if ($scorm->hidetoc == SCORM_TOC_POPUP) { 1972 $tocmenu = scorm_format_toc_for_droplist($scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], 1973 $currentorg, $organizationsco); 1974 1975 $modestr = ''; 1976 if ($mode != 'normal') { 1977 $modestr = '&mode='.$mode; 1978 } 1979 1980 $url = new moodle_url('/mod/scorm/player.php?a='.$scorm->id.'¤torg='.$currentorg.$modestr); 1981 $result->tocmenu = $OUTPUT->single_select($url, 'scoid', $tocmenu, $result->sco->id, null, "tocmenu"); 1982 } 1983 1984 $result->prerequisites = $treeview->prerequisites; 1985 $result->incomplete = $treeview->incomplete; 1986 $result->attemptleft = $treeview->attemptleft; 1987 1988 if ($tocheader) { 1989 $result->toc .= html_writer::end_div().html_writer::end_div(); 1990 $result->toc .= html_writer::start_div('loading', array('id' => 'scorm_toc_toggle')); 1991 $result->toc .= html_writer::tag('button', '', array('id' => 'scorm_toc_toggle_btn')).html_writer::end_div(); 1992 $result->toc .= html_writer::start_div('', array('id' => 'scorm_content')); 1993 $result->toc .= html_writer::div('', '', array('id' => 'scorm_navpanel')); 1994 $result->toc .= html_writer::end_div().html_writer::end_div(); 1995 } 1996 1997 return $result; 1998 } 1999 2000 function scorm_get_adlnav_json ($scoes, &$adlnav = array(), $parentscoid = null) { 2001 if (is_object($scoes)) { 2002 $sco = $scoes; 2003 if (isset($sco->url)) { 2004 $adlnav[$sco->id]['identifier'] = $sco->identifier; 2005 $adlnav[$sco->id]['launch'] = $sco->launch; 2006 $adlnav[$sco->id]['title'] = $sco->title; 2007 $adlnav[$sco->id]['url'] = $sco->url; 2008 $adlnav[$sco->id]['parent'] = $sco->parent; 2009 if (isset($sco->choice)) { 2010 $adlnav[$sco->id]['choice'] = $sco->choice; 2011 } 2012 if (isset($sco->flow)) { 2013 $adlnav[$sco->id]['flow'] = $sco->flow; 2014 } else if (isset($parentscoid) && isset($adlnav[$parentscoid]['flow'])) { 2015 $adlnav[$sco->id]['flow'] = $adlnav[$parentscoid]['flow']; 2016 } 2017 if (isset($sco->isvisible)) { 2018 $adlnav[$sco->id]['isvisible'] = $sco->isvisible; 2019 } 2020 if (isset($sco->parameters)) { 2021 $adlnav[$sco->id]['parameters'] = $sco->parameters; 2022 } 2023 if (isset($sco->hidecontinue)) { 2024 $adlnav[$sco->id]['hidecontinue'] = $sco->hidecontinue; 2025 } 2026 if (isset($sco->hideprevious)) { 2027 $adlnav[$sco->id]['hideprevious'] = $sco->hideprevious; 2028 } 2029 if (isset($sco->hidesuspendall)) { 2030 $adlnav[$sco->id]['hidesuspendall'] = $sco->hidesuspendall; 2031 } 2032 if (!empty($parentscoid)) { 2033 $adlnav[$sco->id]['parentscoid'] = $parentscoid; 2034 } 2035 if (isset($adlnav['prevscoid'])) { 2036 $adlnav[$sco->id]['prevscoid'] = $adlnav['prevscoid']; 2037 $adlnav[$adlnav['prevscoid']]['nextscoid'] = $sco->id; 2038 if (isset($adlnav['prevparent']) && $adlnav['prevparent'] == $sco->parent) { 2039 $adlnav[$sco->id]['prevsibling'] = $adlnav['prevscoid']; 2040 $adlnav[$adlnav['prevscoid']]['nextsibling'] = $sco->id; 2041 } 2042 } 2043 $adlnav['prevscoid'] = $sco->id; 2044 $adlnav['prevparent'] = $sco->parent; 2045 } 2046 if (isset($sco->children)) { 2047 foreach ($sco->children as $children) { 2048 scorm_get_adlnav_json($children, $adlnav, $sco->id); 2049 } 2050 } 2051 } else { 2052 foreach ($scoes as $sco) { 2053 scorm_get_adlnav_json ($sco, $adlnav); 2054 } 2055 unset($adlnav['prevscoid']); 2056 unset($adlnav['prevparent']); 2057 } 2058 return json_encode($adlnav); 2059 } 2060 2061 /** 2062 * Check for the availability of a resource by URL. 2063 * 2064 * Check is performed using an HTTP HEAD call. 2065 * 2066 * @param $url string A valid URL 2067 * @return bool|string True if no issue is found. The error string message, otherwise 2068 */ 2069 function scorm_check_url($url) { 2070 $curl = new curl; 2071 // Same options as in {@link download_file_content()}, used in {@link scorm_parse_scorm()}. 2072 $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 5)); 2073 $cmsg = $curl->head($url); 2074 $info = $curl->get_info(); 2075 if (empty($info['http_code']) || $info['http_code'] != 200) { 2076 return get_string('invalidurlhttpcheck', 'scorm', array('cmsg' => $cmsg)); 2077 } 2078 2079 return true; 2080 } 2081 2082 /** 2083 * Check for a parameter in userdata and return it if it's set 2084 * or return the value from $ifempty if its empty 2085 * 2086 * @param stdClass $userdata Contains user's data 2087 * @param string $param parameter that should be checked 2088 * @param string $ifempty value to be replaced with if $param is not set 2089 * @return string value from $userdata->$param if its not empty, or $ifempty 2090 */ 2091 function scorm_isset($userdata, $param, $ifempty = '') { 2092 if (isset($userdata->$param)) { 2093 return $userdata->$param; 2094 } else { 2095 return $ifempty; 2096 } 2097 } 2098 2099 /** 2100 * Check if the current sco is launchable 2101 * If not, find the next launchable sco 2102 * 2103 * @param stdClass $scorm Scorm object 2104 * @param integer $scoid id of scorm_scoes record. 2105 * @return integer scoid of correct sco to launch or empty if one cannot be found, which will trigger first sco. 2106 */ 2107 function scorm_check_launchable_sco($scorm, $scoid) { 2108 global $DB; 2109 if ($sco = scorm_get_sco($scoid, SCO_ONLY)) { 2110 if ($sco->launch == '') { 2111 // This scoid might be a top level org that can't be launched, find the first launchable sco after this sco. 2112 $scoes = $DB->get_records_select('scorm_scoes', 2113 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true). 2114 ' AND id > ?', array($scorm->id, $sco->id), 'sortorder, id', 'id', 0, 1); 2115 if (!empty($scoes)) { 2116 $sco = reset($scoes); // Get first item from the list. 2117 return $sco->id; 2118 } 2119 } else { 2120 return $sco->id; 2121 } 2122 } 2123 // Returning 0 will cause default behaviour which will find the first launchable sco in the package. 2124 return 0; 2125 } 2126 2127 /** 2128 * Check if a SCORM is available for the current user. 2129 * 2130 * @param stdClass $scorm SCORM record 2131 * @param boolean $checkviewreportcap Check the scorm:viewreport cap 2132 * @param stdClass $context Module context, required if $checkviewreportcap is set to true 2133 * @param int $userid User id override 2134 * @return array status (available or not and possible warnings) 2135 * @since Moodle 3.0 2136 */ 2137 function scorm_get_availability_status($scorm, $checkviewreportcap = false, $context = null, $userid = null) { 2138 $open = true; 2139 $closed = false; 2140 $warnings = array(); 2141 2142 $timenow = time(); 2143 if (!empty($scorm->timeopen) and $scorm->timeopen > $timenow) { 2144 $open = false; 2145 } 2146 if (!empty($scorm->timeclose) and $timenow > $scorm->timeclose) { 2147 $closed = true; 2148 } 2149 2150 if (!$open or $closed) { 2151 if ($checkviewreportcap and !empty($context) and has_capability('mod/scorm:viewreport', $context, $userid)) { 2152 return array(true, $warnings); 2153 } 2154 2155 if (!$open) { 2156 $warnings['notopenyet'] = userdate($scorm->timeopen); 2157 } 2158 if ($closed) { 2159 $warnings['expired'] = userdate($scorm->timeclose); 2160 } 2161 return array(false, $warnings); 2162 } 2163 2164 // Scorm is available. 2165 return array(true, $warnings); 2166 } 2167 2168 /** 2169 * Requires a SCORM package to be available for the current user. 2170 * 2171 * @param stdClass $scorm SCORM record 2172 * @param boolean $checkviewreportcap Check the scorm:viewreport cap 2173 * @param stdClass $context Module context, required if $checkviewreportcap is set to true 2174 * @throws moodle_exception 2175 * @since Moodle 3.0 2176 */ 2177 function scorm_require_available($scorm, $checkviewreportcap = false, $context = null) { 2178 2179 list($available, $warnings) = scorm_get_availability_status($scorm, $checkviewreportcap, $context); 2180 2181 if (!$available) { 2182 $reason = current(array_keys($warnings)); 2183 throw new moodle_exception($reason, 'scorm', '', $warnings[$reason]); 2184 } 2185 2186 } 2187 2188 /** 2189 * Return a SCO object and the SCO launch URL 2190 * 2191 * @param stdClass $scorm SCORM object 2192 * @param int $scoid The SCO id in database 2193 * @param stdClass $context context object 2194 * @return array the SCO object and URL 2195 * @since Moodle 3.1 2196 */ 2197 function scorm_get_sco_and_launch_url($scorm, $scoid, $context) { 2198 global $CFG, $DB; 2199 2200 if (!empty($scoid)) { 2201 // Direct SCO request. 2202 if ($sco = scorm_get_sco($scoid)) { 2203 if ($sco->launch == '') { 2204 // Search for the next launchable sco. 2205 if ($scoes = $DB->get_records_select( 2206 'scorm_scoes', 2207 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).' AND id > ?', 2208 array($scorm->id, $sco->id), 2209 'sortorder, id')) { 2210 $sco = current($scoes); 2211 } 2212 } 2213 } 2214 } 2215 2216 // If no sco was found get the first of SCORM package. 2217 if (!isset($sco)) { 2218 $scoes = $DB->get_records_select( 2219 'scorm_scoes', 2220 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), 2221 array($scorm->id), 2222 'sortorder, id' 2223 ); 2224 $sco = current($scoes); 2225 } 2226 2227 $connector = ''; 2228 $version = substr($scorm->version, 0, 4); 2229 if ((isset($sco->parameters) && (!empty($sco->parameters))) || ($version == 'AICC')) { 2230 if (stripos($sco->launch, '?') !== false) { 2231 $connector = '&'; 2232 } else { 2233 $connector = '?'; 2234 } 2235 if ((isset($sco->parameters) && (!empty($sco->parameters))) && ($sco->parameters[0] == '?')) { 2236 $sco->parameters = substr($sco->parameters, 1); 2237 } 2238 } 2239 2240 if ($version == 'AICC') { 2241 require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php"); 2242 $aiccsid = scorm_aicc_get_hacp_session($scorm->id); 2243 if (empty($aiccsid)) { 2244 $aiccsid = sesskey(); 2245 } 2246 $scoparams = ''; 2247 if (isset($sco->parameters) && (!empty($sco->parameters))) { 2248 $scoparams = '&'. $sco->parameters; 2249 } 2250 $launcher = $sco->launch.$connector.'aicc_sid='.$aiccsid.'&aicc_url='.$CFG->wwwroot.'/mod/scorm/aicc.php'.$scoparams; 2251 } else { 2252 if (isset($sco->parameters) && (!empty($sco->parameters))) { 2253 $launcher = $sco->launch.$connector.$sco->parameters; 2254 } else { 2255 $launcher = $sco->launch; 2256 } 2257 } 2258 2259 if (scorm_external_link($sco->launch)) { 2260 // TODO: does this happen? 2261 $scolaunchurl = $launcher; 2262 } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) { 2263 // Remote learning activity. 2264 $scolaunchurl = dirname($scorm->reference).'/'.$launcher; 2265 } else if ($scorm->scormtype === SCORM_TYPE_LOCAL && strtolower($scorm->reference) == 'imsmanifest.xml') { 2266 // This SCORM content sits in a repository that allows relative links. 2267 $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher"; 2268 } else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) { 2269 // Note: do not convert this to use moodle_url(). 2270 // SCORM does not work without slasharguments and moodle_url() encodes querystring vars. 2271 $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/content/$scorm->revision/$launcher"; 2272 } 2273 return array($sco, $scolaunchurl); 2274 } 2275 2276 /** 2277 * Trigger the scorm_launched event. 2278 * 2279 * @param stdClass $scorm scorm object 2280 * @param stdClass $sco sco object 2281 * @param stdClass $cm course module object 2282 * @param stdClass $context context object 2283 * @param string $scourl SCO URL 2284 * @since Moodle 3.1 2285 */ 2286 function scorm_launch_sco($scorm, $sco, $cm, $context, $scourl) { 2287 2288 $event = \mod_scorm\event\sco_launched::create(array( 2289 'objectid' => $sco->id, 2290 'context' => $context, 2291 'other' => array('instanceid' => $scorm->id, 'loadedcontent' => $scourl) 2292 )); 2293 $event->add_record_snapshot('course_modules', $cm); 2294 $event->add_record_snapshot('scorm', $scorm); 2295 $event->add_record_snapshot('scorm_scoes', $sco); 2296 $event->trigger(); 2297 } 2298 2299 /** 2300 * This is really a little language parser for AICC_SCRIPT 2301 * evaluates the expression and returns a boolean answer 2302 * see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec (CAM). 2303 * Also used by AICC packages. 2304 * 2305 * @param string $prerequisites the aicc_script prerequisites expression 2306 * @param array $usertracks the tracked user data of each SCO visited 2307 * @return boolean 2308 */ 2309 function scorm_eval_prerequisites($prerequisites, $usertracks) { 2310 2311 // This is really a little language parser - AICC_SCRIPT is the reference 2312 // see 2.3.2.5.1. Sequencing/Navigation Today - from the SCORM 1.2 spec. 2313 $element = ''; 2314 $stack = array(); 2315 $statuses = array( 2316 'passed' => 'passed', 2317 'completed' => 'completed', 2318 'failed' => 'failed', 2319 'incomplete' => 'incomplete', 2320 'browsed' => 'browsed', 2321 'not attempted' => 'notattempted', 2322 'p' => 'passed', 2323 'c' => 'completed', 2324 'f' => 'failed', 2325 'i' => 'incomplete', 2326 'b' => 'browsed', 2327 'n' => 'notattempted' 2328 ); 2329 $i = 0; 2330 2331 // Expand the amp entities. 2332 $prerequisites = preg_replace('/&/', '&', $prerequisites); 2333 // Find all my parsable tokens. 2334 $prerequisites = preg_replace('/(&|\||\(|\)|\~)/', '\t$1\t', $prerequisites); 2335 // Expand operators. 2336 $prerequisites = preg_replace('/&/', '&&', $prerequisites); 2337 $prerequisites = preg_replace('/\|/', '||', $prerequisites); 2338 // Now - grab all the tokens. 2339 $elements = explode('\t', trim($prerequisites)); 2340 2341 // Process each token to build an expression to be evaluated. 2342 $stack = array(); 2343 foreach ($elements as $element) { 2344 $element = trim($element); 2345 if (empty($element)) { 2346 continue; 2347 } 2348 if (!preg_match('/^(&&|\|\||\(|\))$/', $element)) { 2349 // Create each individual expression. 2350 // Search for ~ = <> X*{} . 2351 2352 // Sets like 3*{S34, S36, S37, S39}. 2353 if (preg_match('/^(\d+)\*\{(.+)\}$/', $element, $matches)) { 2354 $repeat = $matches[1]; 2355 $set = explode(',', $matches[2]); 2356 $count = 0; 2357 foreach ($set as $setelement) { 2358 if (isset($usertracks[$setelement]) && 2359 ($usertracks[$setelement]->status == 'completed' || $usertracks[$setelement]->status == 'passed')) { 2360 $count++; 2361 } 2362 } 2363 if ($count >= $repeat) { 2364 $element = 'true'; 2365 } else { 2366 $element = 'false'; 2367 } 2368 } else if ($element == '~') { 2369 // Not maps ~. 2370 $element = '!'; 2371 } else if (preg_match('/^(.+)(\=|\<\>)(.+)$/', $element, $matches)) { 2372 // Other symbols = | <> . 2373 $element = trim($matches[1]); 2374 if (isset($usertracks[$element])) { 2375 $value = trim(preg_replace('/(\'|\")/', '', $matches[3])); 2376 if (isset($statuses[$value])) { 2377 $value = $statuses[$value]; 2378 } 2379 2380 $elementprerequisitematch = (strcmp($usertracks[$element]->status, $value) == 0); 2381 if ($matches[2] == '<>') { 2382 $element = $elementprerequisitematch ? 'false' : 'true'; 2383 } else { 2384 $element = $elementprerequisitematch ? 'true' : 'false'; 2385 } 2386 } else { 2387 $element = 'false'; 2388 } 2389 } else { 2390 // Everything else must be an element defined like S45 ... 2391 if (isset($usertracks[$element]) && 2392 ($usertracks[$element]->status == 'completed' || $usertracks[$element]->status == 'passed')) { 2393 $element = 'true'; 2394 } else { 2395 $element = 'false'; 2396 } 2397 } 2398 2399 } 2400 $stack[] = ' '.$element.' '; 2401 } 2402 return eval('return '.implode($stack).';'); 2403 } 2404 2405 /** 2406 * Update the calendar entries for this scorm activity. 2407 * 2408 * @param stdClass $scorm the row from the database table scorm. 2409 * @param int $cmid The coursemodule id 2410 * @return bool 2411 */ 2412 function scorm_update_calendar(stdClass $scorm, $cmid) { 2413 global $DB, $CFG; 2414 2415 require_once($CFG->dirroot.'/calendar/lib.php'); 2416 2417 // Scorm start calendar events. 2418 $event = new stdClass(); 2419 $event->eventtype = SCORM_EVENT_TYPE_OPEN; 2420 // The SCORM_EVENT_TYPE_OPEN event should only be an action event if no close time is specified. 2421 $event->type = empty($scorm->timeclose) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; 2422 if ($event->id = $DB->get_field('event', 'id', 2423 array('modulename' => 'scorm', 'instance' => $scorm->id, 'eventtype' => $event->eventtype))) { 2424 if ((!empty($scorm->timeopen)) && ($scorm->timeopen > 0)) { 2425 // Calendar event exists so update it. 2426 $event->name = get_string('calendarstart', 'scorm', $scorm->name); 2427 $event->description = format_module_intro('scorm', $scorm, $cmid, false); 2428 $event->format = FORMAT_HTML; 2429 $event->timestart = $scorm->timeopen; 2430 $event->timesort = $scorm->timeopen; 2431 $event->visible = instance_is_visible('scorm', $scorm); 2432 $event->timeduration = 0; 2433 2434 $calendarevent = calendar_event::load($event->id); 2435 $calendarevent->update($event, false); 2436 } else { 2437 // Calendar event is on longer needed. 2438 $calendarevent = calendar_event::load($event->id); 2439 $calendarevent->delete(); 2440 } 2441 } else { 2442 // Event doesn't exist so create one. 2443 if ((!empty($scorm->timeopen)) && ($scorm->timeopen > 0)) { 2444 $event->name = get_string('calendarstart', 'scorm', $scorm->name); 2445 $event->description = format_module_intro('scorm', $scorm, $cmid, false); 2446 $event->format = FORMAT_HTML; 2447 $event->courseid = $scorm->course; 2448 $event->groupid = 0; 2449 $event->userid = 0; 2450 $event->modulename = 'scorm'; 2451 $event->instance = $scorm->id; 2452 $event->timestart = $scorm->timeopen; 2453 $event->timesort = $scorm->timeopen; 2454 $event->visible = instance_is_visible('scorm', $scorm); 2455 $event->timeduration = 0; 2456 2457 calendar_event::create($event, false); 2458 } 2459 } 2460 2461 // Scorm end calendar events. 2462 $event = new stdClass(); 2463 $event->type = CALENDAR_EVENT_TYPE_ACTION; 2464 $event->eventtype = SCORM_EVENT_TYPE_CLOSE; 2465 if ($event->id = $DB->get_field('event', 'id', 2466 array('modulename' => 'scorm', 'instance' => $scorm->id, 'eventtype' => $event->eventtype))) { 2467 if ((!empty($scorm->timeclose)) && ($scorm->timeclose > 0)) { 2468 // Calendar event exists so update it. 2469 $event->name = get_string('calendarend', 'scorm', $scorm->name); 2470 $event->description = format_module_intro('scorm', $scorm, $cmid, false); 2471 $event->format = FORMAT_HTML; 2472 $event->timestart = $scorm->timeclose; 2473 $event->timesort = $scorm->timeclose; 2474 $event->visible = instance_is_visible('scorm', $scorm); 2475 $event->timeduration = 0; 2476 2477 $calendarevent = calendar_event::load($event->id); 2478 $calendarevent->update($event, false); 2479 } else { 2480 // Calendar event is on longer needed. 2481 $calendarevent = calendar_event::load($event->id); 2482 $calendarevent->delete(); 2483 } 2484 } else { 2485 // Event doesn't exist so create one. 2486 if ((!empty($scorm->timeclose)) && ($scorm->timeclose > 0)) { 2487 $event->name = get_string('calendarend', 'scorm', $scorm->name); 2488 $event->description = format_module_intro('scorm', $scorm, $cmid, false); 2489 $event->format = FORMAT_HTML; 2490 $event->courseid = $scorm->course; 2491 $event->groupid = 0; 2492 $event->userid = 0; 2493 $event->modulename = 'scorm'; 2494 $event->instance = $scorm->id; 2495 $event->timestart = $scorm->timeclose; 2496 $event->timesort = $scorm->timeclose; 2497 $event->visible = instance_is_visible('scorm', $scorm); 2498 $event->timeduration = 0; 2499 2500 calendar_event::create($event, false); 2501 } 2502 } 2503 2504 return true; 2505 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body