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