Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * @package mod_scorm 19 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 20 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 21 */ 22 defined('MOODLE_INTERNAL') || die(); 23 24 /** SCORM_TYPE_LOCAL = local */ 25 define('SCORM_TYPE_LOCAL', 'local'); 26 /** SCORM_TYPE_LOCALSYNC = localsync */ 27 define('SCORM_TYPE_LOCALSYNC', 'localsync'); 28 /** SCORM_TYPE_EXTERNAL = external */ 29 define('SCORM_TYPE_EXTERNAL', 'external'); 30 /** SCORM_TYPE_AICCURL = external AICC url */ 31 define('SCORM_TYPE_AICCURL', 'aiccurl'); 32 33 define('SCORM_TOC_SIDE', 0); 34 define('SCORM_TOC_HIDDEN', 1); 35 define('SCORM_TOC_POPUP', 2); 36 define('SCORM_TOC_DISABLED', 3); 37 38 // Used to show/hide navigation buttons and set their position. 39 define('SCORM_NAV_DISABLED', 0); 40 define('SCORM_NAV_UNDER_CONTENT', 1); 41 define('SCORM_NAV_FLOATING', 2); 42 43 // Used to check what SCORM version is being used. 44 define('SCORM_12', 1); 45 define('SCORM_13', 2); 46 define('SCORM_AICC', 3); 47 48 // List of possible attemptstatusdisplay options. 49 define('SCORM_DISPLAY_ATTEMPTSTATUS_NO', 0); 50 define('SCORM_DISPLAY_ATTEMPTSTATUS_ALL', 1); 51 define('SCORM_DISPLAY_ATTEMPTSTATUS_MY', 2); 52 define('SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY', 3); 53 54 define('SCORM_EVENT_TYPE_OPEN', 'open'); 55 define('SCORM_EVENT_TYPE_CLOSE', 'close'); 56 57 require_once (__DIR__ . '/deprecatedlib.php'); 58 59 /** 60 * Return an array of status options 61 * 62 * Optionally with translated strings 63 * 64 * @param bool $with_strings (optional) 65 * @return array 66 */ 67 function scorm_status_options($withstrings = false) { 68 // Id's are important as they are bits. 69 $options = array( 70 2 => 'passed', 71 4 => 'completed' 72 ); 73 74 if ($withstrings) { 75 foreach ($options as $key => $value) { 76 $options[$key] = get_string('completionstatus_'.$value, 'scorm'); 77 } 78 } 79 80 return $options; 81 } 82 83 84 /** 85 * Given an object containing all the necessary data, 86 * (defined by the form in mod_form.php) this function 87 * will create a new instance and return the id number 88 * of the new instance. 89 * 90 * @global stdClass 91 * @global object 92 * @uses CONTEXT_MODULE 93 * @uses SCORM_TYPE_LOCAL 94 * @uses SCORM_TYPE_LOCALSYNC 95 * @uses SCORM_TYPE_EXTERNAL 96 * @param object $scorm Form data 97 * @param object $mform 98 * @return int new instance id 99 */ 100 function scorm_add_instance($scorm, $mform=null) { 101 global $CFG, $DB; 102 103 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 104 105 if (empty($scorm->timeopen)) { 106 $scorm->timeopen = 0; 107 } 108 if (empty($scorm->timeclose)) { 109 $scorm->timeclose = 0; 110 } 111 if (empty($scorm->completionstatusallscos)) { 112 $scorm->completionstatusallscos = 0; 113 } 114 $cmid = $scorm->coursemodule; 115 $cmidnumber = $scorm->cmidnumber; 116 $courseid = $scorm->course; 117 118 $context = context_module::instance($cmid); 119 120 $scorm = scorm_option2text($scorm); 121 $scorm->width = (int)str_replace('%', '', $scorm->width); 122 $scorm->height = (int)str_replace('%', '', $scorm->height); 123 124 if (!isset($scorm->whatgrade)) { 125 $scorm->whatgrade = 0; 126 } 127 128 $id = $DB->insert_record('scorm', $scorm); 129 130 // Update course module record - from now on this instance properly exists and all function may be used. 131 $DB->set_field('course_modules', 'instance', $id, array('id' => $cmid)); 132 133 // Reload scorm instance. 134 $record = $DB->get_record('scorm', array('id' => $id)); 135 136 // Store the package and verify. 137 if ($record->scormtype === SCORM_TYPE_LOCAL) { 138 if (!empty($scorm->packagefile)) { 139 $fs = get_file_storage(); 140 $fs->delete_area_files($context->id, 'mod_scorm', 'package'); 141 file_save_draft_area_files($scorm->packagefile, $context->id, 'mod_scorm', 'package', 142 0, array('subdirs' => 0, 'maxfiles' => 1)); 143 // Get filename of zip that was uploaded. 144 $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false); 145 $file = reset($files); 146 $filename = $file->get_filename(); 147 if ($filename !== false) { 148 $record->reference = $filename; 149 } 150 } 151 152 } else if ($record->scormtype === SCORM_TYPE_LOCALSYNC) { 153 $record->reference = $scorm->packageurl; 154 } else if ($record->scormtype === SCORM_TYPE_EXTERNAL) { 155 $record->reference = $scorm->packageurl; 156 } else if ($record->scormtype === SCORM_TYPE_AICCURL) { 157 $record->reference = $scorm->packageurl; 158 $record->hidetoc = SCORM_TOC_DISABLED; // TOC is useless for direct AICCURL so disable it. 159 } else { 160 return false; 161 } 162 163 // Save reference. 164 $DB->update_record('scorm', $record); 165 166 // Extra fields required in grade related functions. 167 $record->course = $courseid; 168 $record->cmidnumber = $cmidnumber; 169 $record->cmid = $cmid; 170 171 scorm_parse($record, true); 172 173 scorm_grade_item_update($record); 174 scorm_update_calendar($record, $cmid); 175 if (!empty($scorm->completionexpected)) { 176 \core_completion\api::update_completion_date_event($cmid, 'scorm', $record, $scorm->completionexpected); 177 } 178 179 return $record->id; 180 } 181 182 /** 183 * Given an object containing all the necessary data, 184 * (defined by the form in mod_form.php) this function 185 * will update an existing instance with new data. 186 * 187 * @global stdClass 188 * @global object 189 * @uses CONTEXT_MODULE 190 * @uses SCORM_TYPE_LOCAL 191 * @uses SCORM_TYPE_LOCALSYNC 192 * @uses SCORM_TYPE_EXTERNAL 193 * @param object $scorm Form data 194 * @param object $mform 195 * @return bool 196 */ 197 function scorm_update_instance($scorm, $mform=null) { 198 global $CFG, $DB; 199 200 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 201 202 if (empty($scorm->timeopen)) { 203 $scorm->timeopen = 0; 204 } 205 if (empty($scorm->timeclose)) { 206 $scorm->timeclose = 0; 207 } 208 if (empty($scorm->completionstatusallscos)) { 209 $scorm->completionstatusallscos = 0; 210 } 211 212 $cmid = $scorm->coursemodule; 213 $cmidnumber = $scorm->cmidnumber; 214 $courseid = $scorm->course; 215 216 $scorm->id = $scorm->instance; 217 218 $context = context_module::instance($cmid); 219 220 if ($scorm->scormtype === SCORM_TYPE_LOCAL) { 221 if (!empty($scorm->packagefile)) { 222 $fs = get_file_storage(); 223 $fs->delete_area_files($context->id, 'mod_scorm', 'package'); 224 file_save_draft_area_files($scorm->packagefile, $context->id, 'mod_scorm', 'package', 225 0, array('subdirs' => 0, 'maxfiles' => 1)); 226 // Get filename of zip that was uploaded. 227 $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false); 228 $file = reset($files); 229 $filename = $file->get_filename(); 230 if ($filename !== false) { 231 $scorm->reference = $filename; 232 } 233 } 234 235 } else if ($scorm->scormtype === SCORM_TYPE_LOCALSYNC) { 236 $scorm->reference = $scorm->packageurl; 237 } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) { 238 $scorm->reference = $scorm->packageurl; 239 } else if ($scorm->scormtype === SCORM_TYPE_AICCURL) { 240 $scorm->reference = $scorm->packageurl; 241 $scorm->hidetoc = SCORM_TOC_DISABLED; // TOC is useless for direct AICCURL so disable it. 242 } else { 243 return false; 244 } 245 246 $scorm = scorm_option2text($scorm); 247 $scorm->width = (int)str_replace('%', '', $scorm->width); 248 $scorm->height = (int)str_replace('%', '', $scorm->height); 249 $scorm->timemodified = time(); 250 251 if (!isset($scorm->whatgrade)) { 252 $scorm->whatgrade = 0; 253 } 254 255 $DB->update_record('scorm', $scorm); 256 // We need to find this out before we blow away the form data. 257 $completionexpected = (!empty($scorm->completionexpected)) ? $scorm->completionexpected : null; 258 259 $scorm = $DB->get_record('scorm', array('id' => $scorm->id)); 260 261 // Extra fields required in grade related functions. 262 $scorm->course = $courseid; 263 $scorm->idnumber = $cmidnumber; 264 $scorm->cmid = $cmid; 265 266 scorm_parse($scorm, (bool)$scorm->updatefreq); 267 268 scorm_grade_item_update($scorm); 269 scorm_update_grades($scorm); 270 scorm_update_calendar($scorm, $cmid); 271 \core_completion\api::update_completion_date_event($cmid, 'scorm', $scorm, $completionexpected); 272 273 return true; 274 } 275 276 /** 277 * Given an ID of an instance of this module, 278 * this function will permanently delete the instance 279 * and any data that depends on it. 280 * 281 * @global stdClass 282 * @global object 283 * @param int $id Scorm instance id 284 * @return boolean 285 */ 286 function scorm_delete_instance($id) { 287 global $CFG, $DB; 288 289 if (! $scorm = $DB->get_record('scorm', array('id' => $id))) { 290 return false; 291 } 292 293 $result = true; 294 295 // Delete any dependent records. 296 if (! $DB->delete_records('scorm_scoes_track', array('scormid' => $scorm->id))) { 297 $result = false; 298 } 299 if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id))) { 300 foreach ($scoes as $sco) { 301 if (! $DB->delete_records('scorm_scoes_data', array('scoid' => $sco->id))) { 302 $result = false; 303 } 304 } 305 $DB->delete_records('scorm_scoes', array('scorm' => $scorm->id)); 306 } 307 308 scorm_grade_item_delete($scorm); 309 310 // We must delete the module record after we delete the grade item. 311 if (! $DB->delete_records('scorm', array('id' => $scorm->id))) { 312 $result = false; 313 } 314 315 /*if (! $DB->delete_records('scorm_sequencing_controlmode', array('scormid'=>$scorm->id))) { 316 $result = false; 317 } 318 if (! $DB->delete_records('scorm_sequencing_rolluprules', array('scormid'=>$scorm->id))) { 319 $result = false; 320 } 321 if (! $DB->delete_records('scorm_sequencing_rolluprule', array('scormid'=>$scorm->id))) { 322 $result = false; 323 } 324 if (! $DB->delete_records('scorm_sequencing_rollupruleconditions', array('scormid'=>$scorm->id))) { 325 $result = false; 326 } 327 if (! $DB->delete_records('scorm_sequencing_rolluprulecondition', array('scormid'=>$scorm->id))) { 328 $result = false; 329 } 330 if (! $DB->delete_records('scorm_sequencing_rulecondition', array('scormid'=>$scorm->id))) { 331 $result = false; 332 } 333 if (! $DB->delete_records('scorm_sequencing_ruleconditions', array('scormid'=>$scorm->id))) { 334 $result = false; 335 }*/ 336 337 return $result; 338 } 339 340 /** 341 * Return a small object with summary information about what a 342 * user has done with a given particular instance of this module 343 * Used for user activity reports. 344 * 345 * @param stdClass $course Course object 346 * @param stdClass $user User 347 * @param stdClass $mod 348 * @param stdClass $scorm The scorm 349 * @return mixed 350 */ 351 function scorm_user_outline($course, $user, $mod, $scorm) { 352 global $CFG; 353 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 354 355 require_once("$CFG->libdir/gradelib.php"); 356 $grades = grade_get_grades($course->id, 'mod', 'scorm', $scorm->id, $user->id); 357 if (!empty($grades->items[0]->grades)) { 358 $grade = reset($grades->items[0]->grades); 359 $result = (object) [ 360 'time' => grade_get_date_for_user_grade($grade, $user), 361 ]; 362 if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 363 $result->info = get_string('gradenoun') . ': '. $grade->str_long_grade; 364 } else { 365 $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); 366 } 367 368 return $result; 369 } 370 return null; 371 } 372 373 /** 374 * Print a detailed representation of what a user has done with 375 * a given particular instance of this module, for user activity reports. 376 * 377 * @global stdClass 378 * @global object 379 * @param object $course 380 * @param object $user 381 * @param object $mod 382 * @param object $scorm 383 * @return boolean 384 */ 385 function scorm_user_complete($course, $user, $mod, $scorm) { 386 global $CFG, $DB, $OUTPUT; 387 require_once("$CFG->libdir/gradelib.php"); 388 389 $liststyle = 'structlist'; 390 $now = time(); 391 $firstmodify = $now; 392 $lastmodify = 0; 393 $sometoreport = false; 394 $report = ''; 395 396 // First Access and Last Access dates for SCOs. 397 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 398 $timetracks = scorm_get_sco_runtime($scorm->id, false, $user->id); 399 $firstmodify = $timetracks->start; 400 $lastmodify = $timetracks->finish; 401 402 $grades = grade_get_grades($course->id, 'mod', 'scorm', $scorm->id, $user->id); 403 if (!empty($grades->items[0]->grades)) { 404 $grade = reset($grades->items[0]->grades); 405 if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 406 echo $OUTPUT->container(get_string('gradenoun').': '.$grade->str_long_grade); 407 if ($grade->str_feedback) { 408 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); 409 } 410 } else { 411 echo $OUTPUT->container(get_string('gradenoun') . ': ' . get_string('hidden', 'grades')); 412 } 413 } 414 415 if ($orgs = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '. 416 $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '. 417 $DB->sql_isempty('scorm_scoes', 'organization', false, false), 418 array($scorm->id), 'sortorder, id', 'id, identifier, title')) { 419 if (count($orgs) <= 1) { 420 unset($orgs); 421 $orgs = array(); 422 $org = new stdClass(); 423 $org->identifier = ''; 424 $orgs[] = $org; 425 } 426 $report .= html_writer::start_div('mod-scorm'); 427 foreach ($orgs as $org) { 428 $conditions = array(); 429 $currentorg = ''; 430 if (!empty($org->identifier)) { 431 $report .= html_writer::div($org->title, 'orgtitle'); 432 $currentorg = $org->identifier; 433 $conditions['organization'] = $currentorg; 434 } 435 $report .= html_writer::start_tag('ul', array('id' => '0', 'class' => $liststyle)); 436 $conditions['scorm'] = $scorm->id; 437 if ($scoes = $DB->get_records('scorm_scoes', $conditions, "sortorder, id")) { 438 // Drop keys so that we can access array sequentially. 439 $scoes = array_values($scoes); 440 $level = 0; 441 $sublist = 1; 442 $parents[$level] = '/'; 443 foreach ($scoes as $pos => $sco) { 444 if ($parents[$level] != $sco->parent) { 445 if ($level > 0 && $parents[$level - 1] == $sco->parent) { 446 $report .= html_writer::end_tag('ul').html_writer::end_tag('li'); 447 $level--; 448 } else { 449 $i = $level; 450 $closelist = ''; 451 while (($i > 0) && ($parents[$level] != $sco->parent)) { 452 $closelist .= html_writer::end_tag('ul').html_writer::end_tag('li'); 453 $i--; 454 } 455 if (($i == 0) && ($sco->parent != $currentorg)) { 456 $report .= html_writer::start_tag('li'); 457 $report .= html_writer::start_tag('ul', array('id' => $sublist, 'class' => $liststyle)); 458 $level++; 459 } else { 460 $report .= $closelist; 461 $level = $i; 462 } 463 $parents[$level] = $sco->parent; 464 } 465 } 466 $report .= html_writer::start_tag('li'); 467 if (isset($scoes[$pos + 1])) { 468 $nextsco = $scoes[$pos + 1]; 469 } else { 470 $nextsco = false; 471 } 472 if (($nextsco !== false) && ($sco->parent != $nextsco->parent) && 473 (($level == 0) || (($level > 0) && ($nextsco->parent == $sco->identifier)))) { 474 $sublist++; 475 } else { 476 $report .= $OUTPUT->spacer(array("height" => "12", "width" => "13")); 477 } 478 479 if ($sco->launch) { 480 $score = ''; 481 $totaltime = ''; 482 if ($usertrack = scorm_get_tracks($sco->id, $user->id)) { 483 if ($usertrack->status == '') { 484 $usertrack->status = 'notattempted'; 485 } 486 $strstatus = get_string($usertrack->status, 'scorm'); 487 $report .= $OUTPUT->pix_icon($usertrack->status, $strstatus, 'scorm'); 488 } else { 489 if ($sco->scormtype == 'sco') { 490 $report .= $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm'); 491 } else { 492 $report .= $OUTPUT->pix_icon('asset', get_string('asset', 'scorm'), 'scorm'); 493 } 494 } 495 $report .= " $sco->title $score$totaltime".html_writer::end_tag('li'); 496 if ($usertrack !== false) { 497 $sometoreport = true; 498 $report .= html_writer::start_tag('li').html_writer::start_tag('ul', array('class' => $liststyle)); 499 foreach ($usertrack as $element => $value) { 500 if (substr($element, 0, 3) == 'cmi') { 501 $report .= html_writer::tag('li', s($element) . ' => ' . s($value)); 502 } 503 } 504 $report .= html_writer::end_tag('ul').html_writer::end_tag('li'); 505 } 506 } else { 507 $report .= " $sco->title".html_writer::end_tag('li'); 508 } 509 } 510 for ($i = 0; $i < $level; $i++) { 511 $report .= html_writer::end_tag('ul').html_writer::end_tag('li'); 512 } 513 } 514 $report .= html_writer::end_tag('ul').html_writer::empty_tag('br'); 515 } 516 $report .= html_writer::end_div(); 517 } 518 if ($sometoreport) { 519 if ($firstmodify < $now) { 520 $timeago = format_time($now - $firstmodify); 521 echo get_string('firstaccess', 'scorm').': '.userdate($firstmodify).' ('.$timeago.")".html_writer::empty_tag('br'); 522 } 523 if ($lastmodify > 0) { 524 $timeago = format_time($now - $lastmodify); 525 echo get_string('lastaccess', 'scorm').': '.userdate($lastmodify).' ('.$timeago.")".html_writer::empty_tag('br'); 526 } 527 echo get_string('report', 'scorm').":".html_writer::empty_tag('br'); 528 echo $report; 529 } else { 530 print_string('noactivity', 'scorm'); 531 } 532 533 return true; 534 } 535 536 /** 537 * Function to be run periodically according to the moodle Tasks API 538 * This function searches for things that need to be done, such 539 * as sending out mail, toggling flags etc ... 540 * 541 * @global stdClass 542 * @global object 543 * @return boolean 544 */ 545 function scorm_cron_scheduled_task () { 546 global $CFG, $DB; 547 548 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 549 550 $sitetimezone = core_date::get_server_timezone(); 551 // Now see if there are any scorm updates to be done. 552 553 if (!isset($CFG->scorm_updatetimelast)) { // To catch the first time. 554 set_config('scorm_updatetimelast', 0); 555 } 556 557 $timenow = time(); 558 $updatetime = usergetmidnight($timenow, $sitetimezone); 559 560 if ($CFG->scorm_updatetimelast < $updatetime and $timenow > $updatetime) { 561 562 set_config('scorm_updatetimelast', $timenow); 563 564 mtrace('Updating scorm packages which require daily update');// We are updating. 565 566 $scormsupdate = $DB->get_records('scorm', array('updatefreq' => SCORM_UPDATE_EVERYDAY)); 567 foreach ($scormsupdate as $scormupdate) { 568 scorm_parse($scormupdate, true); 569 } 570 571 // Now clear out AICC session table with old session data. 572 $cfgscorm = get_config('scorm'); 573 if (!empty($cfgscorm->allowaicchacp)) { 574 $expiretime = time() - ($cfgscorm->aicchacpkeepsessiondata * 24 * 60 * 60); 575 $DB->delete_records_select('scorm_aicc_session', 'timemodified < ?', array($expiretime)); 576 } 577 } 578 579 return true; 580 } 581 582 /** 583 * Return grade for given user or all users. 584 * 585 * @global stdClass 586 * @global object 587 * @param int $scormid id of scorm 588 * @param int $userid optional user id, 0 means all users 589 * @return array array of grades, false if none 590 */ 591 function scorm_get_user_grades($scorm, $userid=0) { 592 global $CFG, $DB; 593 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 594 595 $grades = array(); 596 if (empty($userid)) { 597 $scousers = $DB->get_records_select('scorm_scoes_track', "scormid=? GROUP BY userid", 598 array($scorm->id), "", "userid,null"); 599 if ($scousers) { 600 foreach ($scousers as $scouser) { 601 $grades[$scouser->userid] = new stdClass(); 602 $grades[$scouser->userid]->id = $scouser->userid; 603 $grades[$scouser->userid]->userid = $scouser->userid; 604 $grades[$scouser->userid]->rawgrade = scorm_grade_user($scorm, $scouser->userid); 605 } 606 } else { 607 return false; 608 } 609 610 } else { 611 $preattempt = $DB->get_records_select('scorm_scoes_track', "scormid=? AND userid=? GROUP BY userid", 612 array($scorm->id, $userid), "", "userid,null"); 613 if (!$preattempt) { 614 return false; // No attempt yet. 615 } 616 $grades[$userid] = new stdClass(); 617 $grades[$userid]->id = $userid; 618 $grades[$userid]->userid = $userid; 619 $grades[$userid]->rawgrade = scorm_grade_user($scorm, $userid); 620 } 621 622 return $grades; 623 } 624 625 /** 626 * Update grades in central gradebook 627 * 628 * @category grade 629 * @param object $scorm 630 * @param int $userid specific user only, 0 mean all 631 * @param bool $nullifnone 632 */ 633 function scorm_update_grades($scorm, $userid=0, $nullifnone=true) { 634 global $CFG; 635 require_once($CFG->libdir.'/gradelib.php'); 636 require_once($CFG->libdir.'/completionlib.php'); 637 638 if ($grades = scorm_get_user_grades($scorm, $userid)) { 639 scorm_grade_item_update($scorm, $grades); 640 // Set complete. 641 scorm_set_completion($scorm, $userid, COMPLETION_COMPLETE, $grades); 642 } else if ($userid and $nullifnone) { 643 $grade = new stdClass(); 644 $grade->userid = $userid; 645 $grade->rawgrade = null; 646 scorm_grade_item_update($scorm, $grade); 647 // Set incomplete. 648 scorm_set_completion($scorm, $userid, COMPLETION_INCOMPLETE); 649 } else { 650 scorm_grade_item_update($scorm); 651 } 652 } 653 654 /** 655 * Update/create grade item for given scorm 656 * 657 * @category grade 658 * @uses GRADE_TYPE_VALUE 659 * @uses GRADE_TYPE_NONE 660 * @param object $scorm object with extra cmidnumber 661 * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook 662 * @return object grade_item 663 */ 664 function scorm_grade_item_update($scorm, $grades=null) { 665 global $CFG, $DB; 666 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 667 if (!function_exists('grade_update')) { // Workaround for buggy PHP versions. 668 require_once($CFG->libdir.'/gradelib.php'); 669 } 670 671 $params = array('itemname' => $scorm->name); 672 if (isset($scorm->cmidnumber)) { 673 $params['idnumber'] = $scorm->cmidnumber; 674 } 675 676 if ($scorm->grademethod == GRADESCOES) { 677 $maxgrade = $DB->count_records_select('scorm_scoes', 'scorm = ? AND '. 678 $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id)); 679 if ($maxgrade) { 680 $params['gradetype'] = GRADE_TYPE_VALUE; 681 $params['grademax'] = $maxgrade; 682 $params['grademin'] = 0; 683 } else { 684 $params['gradetype'] = GRADE_TYPE_NONE; 685 } 686 } else { 687 $params['gradetype'] = GRADE_TYPE_VALUE; 688 $params['grademax'] = $scorm->maxgrade; 689 $params['grademin'] = 0; 690 } 691 692 if ($grades === 'reset') { 693 $params['reset'] = true; 694 $grades = null; 695 } 696 697 return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, $grades, $params); 698 } 699 700 /** 701 * Delete grade item for given scorm 702 * 703 * @category grade 704 * @param object $scorm object 705 * @return object grade_item 706 */ 707 function scorm_grade_item_delete($scorm) { 708 global $CFG; 709 require_once($CFG->libdir.'/gradelib.php'); 710 711 return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, null, array('deleted' => 1)); 712 } 713 714 /** 715 * List the actions that correspond to a view of this module. 716 * This is used by the participation report. 717 * 718 * Note: This is not used by new logging system. Event with 719 * crud = 'r' and edulevel = LEVEL_PARTICIPATING will 720 * be considered as view action. 721 * 722 * @return array 723 */ 724 function scorm_get_view_actions() { 725 return array('pre-view', 'view', 'view all', 'report'); 726 } 727 728 /** 729 * List the actions that correspond to a post of this module. 730 * This is used by the participation report. 731 * 732 * Note: This is not used by new logging system. Event with 733 * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING 734 * will be considered as post action. 735 * 736 * @return array 737 */ 738 function scorm_get_post_actions() { 739 return array(); 740 } 741 742 /** 743 * @param object $scorm 744 * @return object $scorm 745 */ 746 function scorm_option2text($scorm) { 747 $scormpopoupoptions = scorm_get_popup_options_array(); 748 749 if (isset($scorm->popup)) { 750 if ($scorm->popup == 1) { 751 $optionlist = array(); 752 foreach ($scormpopoupoptions as $name => $option) { 753 if (isset($scorm->$name)) { 754 $optionlist[] = $name.'='.$scorm->$name; 755 } else { 756 $optionlist[] = $name.'=0'; 757 } 758 } 759 $scorm->options = implode(',', $optionlist); 760 } else { 761 $scorm->options = ''; 762 } 763 } else { 764 $scorm->popup = 0; 765 $scorm->options = ''; 766 } 767 return $scorm; 768 } 769 770 /** 771 * Implementation of the function for printing the form elements that control 772 * whether the course reset functionality affects the scorm. 773 * 774 * @param MoodleQuickForm $mform form passed by reference 775 */ 776 function scorm_reset_course_form_definition(&$mform) { 777 $mform->addElement('header', 'scormheader', get_string('modulenameplural', 'scorm')); 778 $mform->addElement('advcheckbox', 'reset_scorm', get_string('deleteallattempts', 'scorm')); 779 } 780 781 /** 782 * Course reset form defaults. 783 * 784 * @return array 785 */ 786 function scorm_reset_course_form_defaults($course) { 787 return array('reset_scorm' => 1); 788 } 789 790 /** 791 * Removes all grades from gradebook 792 * 793 * @global stdClass 794 * @global object 795 * @param int $courseid 796 * @param string optional type 797 */ 798 function scorm_reset_gradebook($courseid, $type='') { 799 global $CFG, $DB; 800 801 $sql = "SELECT s.*, cm.idnumber as cmidnumber, s.course as courseid 802 FROM {scorm} s, {course_modules} cm, {modules} m 803 WHERE m.name='scorm' AND m.id=cm.module AND cm.instance=s.id AND s.course=?"; 804 805 if ($scorms = $DB->get_records_sql($sql, array($courseid))) { 806 foreach ($scorms as $scorm) { 807 scorm_grade_item_update($scorm, 'reset'); 808 } 809 } 810 } 811 812 /** 813 * Actual implementation of the reset course functionality, delete all the 814 * scorm attempts for course $data->courseid. 815 * 816 * @global stdClass 817 * @global object 818 * @param object $data the data submitted from the reset course. 819 * @return array status array 820 */ 821 function scorm_reset_userdata($data) { 822 global $CFG, $DB; 823 824 $componentstr = get_string('modulenameplural', 'scorm'); 825 $status = array(); 826 827 if (!empty($data->reset_scorm)) { 828 $scormssql = "SELECT s.id 829 FROM {scorm} s 830 WHERE s.course=?"; 831 832 $DB->delete_records_select('scorm_scoes_track', "scormid IN ($scormssql)", array($data->courseid)); 833 834 // Remove all grades from gradebook. 835 if (empty($data->reset_gradebook_grades)) { 836 scorm_reset_gradebook($data->courseid); 837 } 838 839 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallattempts', 'scorm'), 'error' => false); 840 } 841 842 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. 843 // See MDL-9367. 844 shift_course_mod_dates('scorm', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid); 845 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); 846 847 return $status; 848 } 849 850 /** 851 * Lists all file areas current user may browse 852 * 853 * @param object $course 854 * @param object $cm 855 * @param object $context 856 * @return array 857 */ 858 function scorm_get_file_areas($course, $cm, $context) { 859 $areas = array(); 860 $areas['content'] = get_string('areacontent', 'scorm'); 861 $areas['package'] = get_string('areapackage', 'scorm'); 862 return $areas; 863 } 864 865 /** 866 * File browsing support for SCORM file areas 867 * 868 * @package mod_scorm 869 * @category files 870 * @param file_browser $browser file browser instance 871 * @param array $areas file areas 872 * @param stdClass $course course object 873 * @param stdClass $cm course module object 874 * @param stdClass $context context object 875 * @param string $filearea file area 876 * @param int $itemid item ID 877 * @param string $filepath file path 878 * @param string $filename file name 879 * @return file_info instance or null if not found 880 */ 881 function scorm_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { 882 global $CFG; 883 884 if (!has_capability('moodle/course:managefiles', $context)) { 885 return null; 886 } 887 888 // No writing for now! 889 890 $fs = get_file_storage(); 891 892 if ($filearea === 'content') { 893 894 $filepath = is_null($filepath) ? '/' : $filepath; 895 $filename = is_null($filename) ? '.' : $filename; 896 897 $urlbase = $CFG->wwwroot.'/pluginfile.php'; 898 if (!$storedfile = $fs->get_file($context->id, 'mod_scorm', 'content', 0, $filepath, $filename)) { 899 if ($filepath === '/' and $filename === '.') { 900 $storedfile = new virtual_root_file($context->id, 'mod_scorm', 'content', 0); 901 } else { 902 // Not found. 903 return null; 904 } 905 } 906 require_once("$CFG->dirroot/mod/scorm/locallib.php"); 907 return new scorm_package_file_info($browser, $context, $storedfile, $urlbase, $areas[$filearea], true, true, false, false); 908 909 } else if ($filearea === 'package') { 910 $filepath = is_null($filepath) ? '/' : $filepath; 911 $filename = is_null($filename) ? '.' : $filename; 912 913 $urlbase = $CFG->wwwroot.'/pluginfile.php'; 914 if (!$storedfile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, $filepath, $filename)) { 915 if ($filepath === '/' and $filename === '.') { 916 $storedfile = new virtual_root_file($context->id, 'mod_scorm', 'package', 0); 917 } else { 918 // Not found. 919 return null; 920 } 921 } 922 return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false); 923 } 924 925 // Scorm_intro handled in file_browser. 926 927 return false; 928 } 929 930 /** 931 * Serves scorm content, introduction images and packages. Implements needed access control ;-) 932 * 933 * @package mod_scorm 934 * @category files 935 * @param stdClass $course course object 936 * @param stdClass $cm course module object 937 * @param stdClass $context context object 938 * @param string $filearea file area 939 * @param array $args extra arguments 940 * @param bool $forcedownload whether or not force download 941 * @param array $options additional options affecting the file serving 942 * @return bool false if file not found, does not return if found - just send the file 943 */ 944 function scorm_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { 945 global $CFG, $DB; 946 947 if ($context->contextlevel != CONTEXT_MODULE) { 948 return false; 949 } 950 951 require_login($course, true, $cm); 952 953 $canmanageactivity = has_capability('moodle/course:manageactivities', $context); 954 $lifetime = null; 955 956 // Check SCORM availability. 957 if (!$canmanageactivity) { 958 require_once($CFG->dirroot.'/mod/scorm/locallib.php'); 959 960 $scorm = $DB->get_record('scorm', array('id' => $cm->instance), 'id, timeopen, timeclose', MUST_EXIST); 961 list($available, $warnings) = scorm_get_availability_status($scorm); 962 if (!$available) { 963 return false; 964 } 965 } 966 967 if ($filearea === 'content') { 968 $revision = (int)array_shift($args); // Prevents caching problems - ignored here. 969 $relativepath = implode('/', $args); 970 $fullpath = "/$context->id/mod_scorm/content/0/$relativepath"; 971 $options['immutable'] = true; // Add immutable option, $relativepath changes on file update. 972 973 } else if ($filearea === 'package') { 974 // Check if the global setting for disabling package downloads is enabled. 975 $protectpackagedownloads = get_config('scorm', 'protectpackagedownloads'); 976 if ($protectpackagedownloads and !$canmanageactivity) { 977 return false; 978 } 979 $revision = (int)array_shift($args); // Prevents caching problems - ignored here. 980 $relativepath = implode('/', $args); 981 $fullpath = "/$context->id/mod_scorm/package/0/$relativepath"; 982 $lifetime = 0; // No caching here. 983 984 } else if ($filearea === 'imsmanifest') { // This isn't a real filearea, it's a url parameter for this type of package. 985 $revision = (int)array_shift($args); // Prevents caching problems - ignored here. 986 $relativepath = implode('/', $args); 987 988 // Get imsmanifest file. 989 $fs = get_file_storage(); 990 $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false); 991 $file = reset($files); 992 993 // Check that the package file is an imsmanifest.xml file - if not then this method is not allowed. 994 $packagefilename = $file->get_filename(); 995 if (strtolower($packagefilename) !== 'imsmanifest.xml') { 996 return false; 997 } 998 999 $file->send_relative_file($relativepath); 1000 } else { 1001 return false; 1002 } 1003 1004 $fs = get_file_storage(); 1005 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1006 if ($filearea === 'content') { // Return file not found straight away to improve performance. 1007 send_header_404(); 1008 die; 1009 } 1010 return false; 1011 } 1012 1013 // Allow SVG files to be loaded within SCORM content, instead of forcing download. 1014 $options['dontforcesvgdownload'] = true; 1015 1016 // Finally send the file. 1017 send_stored_file($file, $lifetime, 0, false, $options); 1018 } 1019 1020 /** 1021 * @uses FEATURE_GROUPS 1022 * @uses FEATURE_GROUPINGS 1023 * @uses FEATURE_MOD_INTRO 1024 * @uses FEATURE_COMPLETION_TRACKS_VIEWS 1025 * @uses FEATURE_COMPLETION_HAS_RULES 1026 * @uses FEATURE_GRADE_HAS_GRADE 1027 * @uses FEATURE_GRADE_OUTCOMES 1028 * @param string $feature FEATURE_xx constant for requested feature 1029 * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. 1030 */ 1031 function scorm_supports($feature) { 1032 switch($feature) { 1033 case FEATURE_GROUPS: return true; 1034 case FEATURE_GROUPINGS: return true; 1035 case FEATURE_MOD_INTRO: return true; 1036 case FEATURE_COMPLETION_TRACKS_VIEWS: return true; 1037 case FEATURE_COMPLETION_HAS_RULES: return true; 1038 case FEATURE_GRADE_HAS_GRADE: return true; 1039 case FEATURE_GRADE_OUTCOMES: return true; 1040 case FEATURE_BACKUP_MOODLE2: return true; 1041 case FEATURE_SHOW_DESCRIPTION: return true; 1042 case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_CONTENT; 1043 1044 default: return null; 1045 } 1046 } 1047 1048 /** 1049 * Get the filename for a temp log file 1050 * 1051 * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename 1052 * @param integer $scoid - scoid of object this log entry is for 1053 * @return string The filename as an absolute path 1054 */ 1055 function scorm_debug_log_filename($type, $scoid) { 1056 global $CFG, $USER; 1057 1058 $logpath = $CFG->tempdir.'/scormlogs'; 1059 $logfile = $logpath.'/'.$type.'debug_'.$USER->id.'_'.$scoid.'.log'; 1060 return $logfile; 1061 } 1062 1063 /** 1064 * writes log output to a temp log file 1065 * 1066 * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename 1067 * @param string $text - text to be written to file. 1068 * @param integer $scoid - scoid of object this log entry is for. 1069 */ 1070 function scorm_debug_log_write($type, $text, $scoid) { 1071 global $CFG; 1072 1073 $debugenablelog = get_config('scorm', 'allowapidebug'); 1074 if (!$debugenablelog || empty($text)) { 1075 return; 1076 } 1077 if (make_temp_directory('scormlogs/')) { 1078 $logfile = scorm_debug_log_filename($type, $scoid); 1079 @file_put_contents($logfile, date('Y/m/d H:i:s O')." DEBUG $text\r\n", FILE_APPEND); 1080 @chmod($logfile, $CFG->filepermissions); 1081 } 1082 } 1083 1084 /** 1085 * Remove debug log file 1086 * 1087 * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename 1088 * @param integer $scoid - scoid of object this log entry is for 1089 * @return boolean True if the file is successfully deleted, false otherwise 1090 */ 1091 function scorm_debug_log_remove($type, $scoid) { 1092 1093 $debugenablelog = get_config('scorm', 'allowapidebug'); 1094 $logfile = scorm_debug_log_filename($type, $scoid); 1095 if (!$debugenablelog || !file_exists($logfile)) { 1096 return false; 1097 } 1098 1099 return @unlink($logfile); 1100 } 1101 1102 /** 1103 * @deprecated since Moodle 3.3, when the block_course_overview block was removed. 1104 */ 1105 function scorm_print_overview() { 1106 throw new coding_exception('scorm_print_overview() can not be used any more and is obsolete.'); 1107 } 1108 1109 /** 1110 * Return a list of page types 1111 * @param string $pagetype current page type 1112 * @param stdClass $parentcontext Block's parent context 1113 * @param stdClass $currentcontext Current context of block 1114 */ 1115 function scorm_page_type_list($pagetype, $parentcontext, $currentcontext) { 1116 $modulepagetype = array('mod-scorm-*' => get_string('page-mod-scorm-x', 'scorm')); 1117 return $modulepagetype; 1118 } 1119 1120 /** 1121 * Returns the SCORM version used. 1122 * @param string $scormversion comes from $scorm->version 1123 * @param string $version one of the defined vars SCORM_12, SCORM_13, SCORM_AICC (or empty) 1124 * @return Scorm version. 1125 */ 1126 function scorm_version_check($scormversion, $version='') { 1127 $scormversion = trim(strtolower($scormversion)); 1128 if (empty($version) || $version == SCORM_12) { 1129 if ($scormversion == 'scorm_12' || $scormversion == 'scorm_1.2') { 1130 return SCORM_12; 1131 } 1132 if (!empty($version)) { 1133 return false; 1134 } 1135 } 1136 if (empty($version) || $version == SCORM_13) { 1137 if ($scormversion == 'scorm_13' || $scormversion == 'scorm_1.3') { 1138 return SCORM_13; 1139 } 1140 if (!empty($version)) { 1141 return false; 1142 } 1143 } 1144 if (empty($version) || $version == SCORM_AICC) { 1145 if (strpos($scormversion, 'aicc')) { 1146 return SCORM_AICC; 1147 } 1148 if (!empty($version)) { 1149 return false; 1150 } 1151 } 1152 return false; 1153 } 1154 1155 /** 1156 * Register the ability to handle drag and drop file uploads 1157 * @return array containing details of the files / types the mod can handle 1158 */ 1159 function scorm_dndupload_register() { 1160 return array('files' => array( 1161 array('extension' => 'zip', 'message' => get_string('dnduploadscorm', 'scorm')) 1162 )); 1163 } 1164 1165 /** 1166 * Handle a file that has been uploaded 1167 * @param object $uploadinfo details of the file / content that has been uploaded 1168 * @return int instance id of the newly created mod 1169 */ 1170 function scorm_dndupload_handle($uploadinfo) { 1171 1172 $context = context_module::instance($uploadinfo->coursemodule); 1173 file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_scorm', 'package', 0); 1174 $fs = get_file_storage(); 1175 $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, 'sortorder, itemid, filepath, filename', false); 1176 $file = reset($files); 1177 1178 // Validate the file, make sure it's a valid SCORM package! 1179 $errors = scorm_validate_package($file); 1180 if (!empty($errors)) { 1181 return false; 1182 } 1183 // Create a default scorm object to pass to scorm_add_instance()! 1184 $scorm = get_config('scorm'); 1185 $scorm->course = $uploadinfo->course->id; 1186 $scorm->coursemodule = $uploadinfo->coursemodule; 1187 $scorm->cmidnumber = ''; 1188 $scorm->name = $uploadinfo->displayname; 1189 $scorm->scormtype = SCORM_TYPE_LOCAL; 1190 $scorm->reference = $file->get_filename(); 1191 $scorm->intro = ''; 1192 $scorm->width = $scorm->framewidth; 1193 $scorm->height = $scorm->frameheight; 1194 1195 return scorm_add_instance($scorm, null); 1196 } 1197 1198 /** 1199 * Sets activity completion state 1200 * 1201 * @param object $scorm object 1202 * @param int $userid User ID 1203 * @param int $completionstate Completion state 1204 * @param array $grades grades array of users with grades - used when $userid = 0 1205 */ 1206 function scorm_set_completion($scorm, $userid, $completionstate = COMPLETION_COMPLETE, $grades = array()) { 1207 $course = new stdClass(); 1208 $course->id = $scorm->course; 1209 $completion = new completion_info($course); 1210 1211 // Check if completion is enabled site-wide, or for the course. 1212 if (!$completion->is_enabled()) { 1213 return; 1214 } 1215 1216 $cm = get_coursemodule_from_instance('scorm', $scorm->id, $scorm->course); 1217 if (empty($cm) || !$completion->is_enabled($cm)) { 1218 return; 1219 } 1220 1221 if (empty($userid)) { // We need to get all the relevant users from $grades param. 1222 foreach ($grades as $grade) { 1223 $completion->update_state($cm, $completionstate, $grade->userid); 1224 } 1225 } else { 1226 $completion->update_state($cm, $completionstate, $userid); 1227 } 1228 } 1229 1230 /** 1231 * Check that a Zip file contains a valid SCORM package 1232 * 1233 * @param $file stored_file a Zip file. 1234 * @return array empty if no issue is found. Array of error message otherwise 1235 */ 1236 function scorm_validate_package($file) { 1237 $packer = get_file_packer('application/zip'); 1238 $errors = array(); 1239 if ($file->is_external_file()) { // Get zip file so we can check it is correct. 1240 $file->import_external_file_contents(); 1241 } 1242 $filelist = $file->list_files($packer); 1243 1244 if (!is_array($filelist)) { 1245 $errors['packagefile'] = get_string('badarchive', 'scorm'); 1246 } else { 1247 $aiccfound = false; 1248 $badmanifestpresent = false; 1249 foreach ($filelist as $info) { 1250 if ($info->pathname == 'imsmanifest.xml') { 1251 return array(); 1252 } else if (strpos($info->pathname, 'imsmanifest.xml') !== false) { 1253 // This package has an imsmanifest file inside a folder of the package. 1254 $badmanifestpresent = true; 1255 } 1256 if (preg_match('/\.cst$/', $info->pathname)) { 1257 return array(); 1258 } 1259 } 1260 if (!$aiccfound) { 1261 if ($badmanifestpresent) { 1262 $errors['packagefile'] = get_string('badimsmanifestlocation', 'scorm'); 1263 } else { 1264 $errors['packagefile'] = get_string('nomanifest', 'scorm'); 1265 } 1266 } 1267 } 1268 return $errors; 1269 } 1270 1271 /** 1272 * Check and set the correct mode and attempt when entering a SCORM package. 1273 * 1274 * @param object $scorm object 1275 * @param string $newattempt should a new attempt be generated here. 1276 * @param int $attempt the attempt number this is for. 1277 * @param int $userid the userid of the user. 1278 * @param string $mode the current mode that has been selected. 1279 */ 1280 function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) { 1281 global $DB; 1282 1283 if (($mode == 'browse')) { 1284 if ($scorm->hidebrowse == 1) { 1285 // Prevent Browse mode if hidebrowse is set. 1286 $mode = 'normal'; 1287 } else { 1288 // We don't need to check attempts as browse mode is set. 1289 return; 1290 } 1291 } 1292 1293 if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS) { 1294 // This SCORM is configured to force a new attempt on every re-entry. 1295 $newattempt = 'on'; 1296 $mode = 'normal'; 1297 if ($attempt == 1) { 1298 // Check if the user has any existing data or if this is really the first attempt. 1299 $exists = $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id)); 1300 if (!$exists) { 1301 // No records yet - Attempt should == 1. 1302 return; 1303 } 1304 } 1305 $attempt++; 1306 1307 return; 1308 } 1309 // Check if the scorm module is incomplete (used to validate user request to start a new attempt). 1310 $incomplete = true; 1311 1312 // Note - in SCORM_13 the cmi-core.lesson_status field was split into 1313 // 'cmi.completion_status' and 'cmi.success_status'. 1314 // 'cmi.completion_status' can only contain values 'completed', 'incomplete', 'not attempted' or 'unknown'. 1315 // This means the values 'passed' or 'failed' will never be reported for a track in SCORM_13 and 1316 // the only status that will be treated as complete is 'completed'. 1317 1318 $completionelements = array( 1319 SCORM_12 => 'cmi.core.lesson_status', 1320 SCORM_13 => 'cmi.completion_status', 1321 SCORM_AICC => 'cmi.core.lesson_status' 1322 ); 1323 $scormversion = scorm_version_check($scorm->version); 1324 if($scormversion===false) { 1325 $scormversion = SCORM_12; 1326 } 1327 $completionelement = $completionelements[$scormversion]; 1328 1329 $sql = "SELECT sc.id, t.value 1330 FROM {scorm_scoes} sc 1331 LEFT JOIN {scorm_scoes_track} t ON sc.scorm = t.scormid AND sc.id = t.scoid 1332 AND t.element = ? AND t.userid = ? AND t.attempt = ? 1333 WHERE sc.scormtype = 'sco' AND sc.scorm = ?"; 1334 $tracks = $DB->get_recordset_sql($sql, array($completionelement, $userid, $attempt, $scorm->id)); 1335 1336 foreach ($tracks as $track) { 1337 if (($track->value == 'completed') || ($track->value == 'passed') || ($track->value == 'failed')) { 1338 $incomplete = false; 1339 } else { 1340 $incomplete = true; 1341 break; // Found an incomplete sco, so the result as a whole is incomplete. 1342 } 1343 } 1344 $tracks->close(); 1345 1346 // Validate user request to start a new attempt. 1347 if ($incomplete === true) { 1348 // The option to start a new attempt should never have been presented. Force false. 1349 $newattempt = 'off'; 1350 } else if (!empty($scorm->forcenewattempt)) { 1351 // A new attempt should be forced for already completed attempts. 1352 $newattempt = 'on'; 1353 } 1354 1355 if (($newattempt == 'on') && (($attempt < $scorm->maxattempt) || ($scorm->maxattempt == 0))) { 1356 $attempt++; 1357 $mode = 'normal'; 1358 } else { // Check if review mode should be set. 1359 if ($incomplete === true) { 1360 $mode = 'normal'; 1361 } else { 1362 $mode = 'review'; 1363 } 1364 } 1365 } 1366 1367 /** 1368 * Trigger the course_module_viewed event. 1369 * 1370 * @param stdClass $scorm scorm object 1371 * @param stdClass $course course object 1372 * @param stdClass $cm course module object 1373 * @param stdClass $context context object 1374 * @since Moodle 3.0 1375 */ 1376 function scorm_view($scorm, $course, $cm, $context) { 1377 1378 // Trigger course_module_viewed event. 1379 $params = array( 1380 'context' => $context, 1381 'objectid' => $scorm->id 1382 ); 1383 1384 $event = \mod_scorm\event\course_module_viewed::create($params); 1385 $event->add_record_snapshot('course_modules', $cm); 1386 $event->add_record_snapshot('course', $course); 1387 $event->add_record_snapshot('scorm', $scorm); 1388 $event->trigger(); 1389 } 1390 1391 /** 1392 * Check if the module has any update that affects the current user since a given time. 1393 * 1394 * @param cm_info $cm course module data 1395 * @param int $from the time to check updates from 1396 * @param array $filter if we need to check only specific updates 1397 * @return stdClass an object with the different type of areas indicating if they were updated or not 1398 * @since Moodle 3.2 1399 */ 1400 function scorm_check_updates_since(cm_info $cm, $from, $filter = array()) { 1401 global $DB, $USER, $CFG; 1402 require_once($CFG->dirroot . '/mod/scorm/locallib.php'); 1403 1404 $scorm = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); 1405 $updates = new stdClass(); 1406 list($available, $warnings) = scorm_get_availability_status($scorm, true, $cm->context); 1407 if (!$available) { 1408 return $updates; 1409 } 1410 $updates = course_check_module_updates_since($cm, $from, array('package'), $filter); 1411 1412 $updates->tracks = (object) array('updated' => false); 1413 $select = 'scormid = ? AND userid = ? AND timemodified > ?'; 1414 $params = array($scorm->id, $USER->id, $from); 1415 $tracks = $DB->get_records_select('scorm_scoes_track', $select, $params, '', 'id'); 1416 if (!empty($tracks)) { 1417 $updates->tracks->updated = true; 1418 $updates->tracks->itemids = array_keys($tracks); 1419 } 1420 1421 // Now, teachers should see other students updates. 1422 if (has_capability('mod/scorm:viewreport', $cm->context)) { 1423 $select = 'scormid = ? AND timemodified > ?'; 1424 $params = array($scorm->id, $from); 1425 1426 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { 1427 $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); 1428 if (empty($groupusers)) { 1429 return $updates; 1430 } 1431 list($insql, $inparams) = $DB->get_in_or_equal($groupusers); 1432 $select .= ' AND userid ' . $insql; 1433 $params = array_merge($params, $inparams); 1434 } 1435 1436 $updates->usertracks = (object) array('updated' => false); 1437 $tracks = $DB->get_records_select('scorm_scoes_track', $select, $params, '', 'id'); 1438 if (!empty($tracks)) { 1439 $updates->usertracks->updated = true; 1440 $updates->usertracks->itemids = array_keys($tracks); 1441 } 1442 } 1443 return $updates; 1444 } 1445 1446 /** 1447 * Get icon mapping for font-awesome. 1448 */ 1449 function mod_scorm_get_fontawesome_icon_map() { 1450 return [ 1451 'mod_scorm:assetc' => 'fa-file-archive-o', 1452 'mod_scorm:asset' => 'fa-file-archive-o', 1453 'mod_scorm:browsed' => 'fa-book', 1454 'mod_scorm:completed' => 'fa-check-square-o', 1455 'mod_scorm:failed' => 'fa-times', 1456 'mod_scorm:incomplete' => 'fa-pencil-square-o', 1457 'mod_scorm:minus' => 'fa-minus', 1458 'mod_scorm:notattempted' => 'fa-square-o', 1459 'mod_scorm:passed' => 'fa-check', 1460 'mod_scorm:plus' => 'fa-plus', 1461 'mod_scorm:popdown' => 'fa-window-close-o', 1462 'mod_scorm:popup' => 'fa-window-restore', 1463 'mod_scorm:suspend' => 'fa-pause', 1464 'mod_scorm:wait' => 'fa-clock-o', 1465 ]; 1466 } 1467 1468 /** 1469 * This standard function will check all instances of this module 1470 * and make sure there are up-to-date events created for each of them. 1471 * If courseid = 0, then every scorm event in the site is checked, else 1472 * only scorm events belonging to the course specified are checked. 1473 * 1474 * @param int $courseid 1475 * @param int|stdClass $instance scorm module instance or ID. 1476 * @param int|stdClass $cm Course module object or ID. 1477 * @return bool 1478 */ 1479 function scorm_refresh_events($courseid = 0, $instance = null, $cm = null) { 1480 global $CFG, $DB; 1481 1482 require_once($CFG->dirroot . '/mod/scorm/locallib.php'); 1483 1484 // If we have instance information then we can just update the one event instead of updating all events. 1485 if (isset($instance)) { 1486 if (!is_object($instance)) { 1487 $instance = $DB->get_record('scorm', array('id' => $instance), '*', MUST_EXIST); 1488 } 1489 if (isset($cm)) { 1490 if (!is_object($cm)) { 1491 $cm = (object)array('id' => $cm); 1492 } 1493 } else { 1494 $cm = get_coursemodule_from_instance('scorm', $instance->id); 1495 } 1496 scorm_update_calendar($instance, $cm->id); 1497 return true; 1498 } 1499 1500 if ($courseid) { 1501 // Make sure that the course id is numeric. 1502 if (!is_numeric($courseid)) { 1503 return false; 1504 } 1505 if (!$scorms = $DB->get_records('scorm', array('course' => $courseid))) { 1506 return false; 1507 } 1508 } else { 1509 if (!$scorms = $DB->get_records('scorm')) { 1510 return false; 1511 } 1512 } 1513 1514 foreach ($scorms as $scorm) { 1515 $cm = get_coursemodule_from_instance('scorm', $scorm->id); 1516 scorm_update_calendar($scorm, $cm->id); 1517 } 1518 1519 return true; 1520 } 1521 1522 /** 1523 * This function receives a calendar event and returns the action associated with it, or null if there is none. 1524 * 1525 * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event 1526 * is not displayed on the block. 1527 * 1528 * @param calendar_event $event 1529 * @param \core_calendar\action_factory $factory 1530 * @param int $userid User id override 1531 * @return \core_calendar\local\event\entities\action_interface|null 1532 */ 1533 function mod_scorm_core_calendar_provide_event_action(calendar_event $event, 1534 \core_calendar\action_factory $factory, $userid = null) { 1535 global $CFG, $USER; 1536 1537 require_once($CFG->dirroot . '/mod/scorm/locallib.php'); 1538 1539 if (empty($userid)) { 1540 $userid = $USER->id; 1541 } 1542 1543 $cm = get_fast_modinfo($event->courseid, $userid)->instances['scorm'][$event->instance]; 1544 1545 if (has_capability('mod/scorm:viewreport', $cm->context, $userid)) { 1546 // Teachers do not need to be reminded to complete a scorm. 1547 return null; 1548 } 1549 1550 $completion = new \completion_info($cm->get_course()); 1551 1552 $completiondata = $completion->get_data($cm, false, $userid); 1553 1554 if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { 1555 return null; 1556 } 1557 1558 if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) { 1559 // The scorm has closed so the user can no longer submit anything. 1560 return null; 1561 } 1562 1563 // Restore scorm object from cached values in $cm, we only need id, timeclose and timeopen. 1564 $customdata = $cm->customdata ?: []; 1565 $customdata['id'] = $cm->instance; 1566 $scorm = (object)($customdata + ['timeclose' => 0, 'timeopen' => 0]); 1567 1568 // Check that the SCORM activity is open. 1569 list($actionable, $warnings) = scorm_get_availability_status($scorm, false, null, $userid); 1570 1571 return $factory->create_instance( 1572 get_string('enter', 'scorm'), 1573 new \moodle_url('/mod/scorm/view.php', array('id' => $cm->id)), 1574 1, 1575 $actionable 1576 ); 1577 } 1578 1579 /** 1580 * Add a get_coursemodule_info function in case any SCORM type wants to add 'extra' information 1581 * for the course (see resource). 1582 * 1583 * Given a course_module object, this function returns any "extra" information that may be needed 1584 * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. 1585 * 1586 * @param stdClass $coursemodule The coursemodule object (record). 1587 * @return cached_cm_info An object on information that the courses 1588 * will know about (most noticeably, an icon). 1589 */ 1590 function scorm_get_coursemodule_info($coursemodule) { 1591 global $DB; 1592 1593 $dbparams = ['id' => $coursemodule->instance]; 1594 $fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos, '. 1595 'timeopen, timeclose'; 1596 if (!$scorm = $DB->get_record('scorm', $dbparams, $fields)) { 1597 return false; 1598 } 1599 1600 $result = new cached_cm_info(); 1601 $result->name = $scorm->name; 1602 1603 if ($coursemodule->showdescription) { 1604 // Convert intro to html. Do not filter cached version, filters run at display time. 1605 $result->content = format_module_intro('scorm', $scorm, $coursemodule->id, false); 1606 } 1607 1608 // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. 1609 if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { 1610 $result->customdata['customcompletionrules']['completionstatusrequired'] = $scorm->completionstatusrequired; 1611 $result->customdata['customcompletionrules']['completionscorerequired'] = $scorm->completionscorerequired; 1612 $result->customdata['customcompletionrules']['completionstatusallscos'] = $scorm->completionstatusallscos; 1613 } 1614 // Populate some other values that can be used in calendar or on dashboard. 1615 if ($scorm->timeopen) { 1616 $result->customdata['timeopen'] = $scorm->timeopen; 1617 } 1618 if ($scorm->timeclose) { 1619 $result->customdata['timeclose'] = $scorm->timeclose; 1620 } 1621 1622 return $result; 1623 } 1624 1625 /** 1626 * Callback which returns human-readable strings describing the active completion custom rules for the module instance. 1627 * 1628 * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] 1629 * @return array $descriptions the array of descriptions for the custom rules. 1630 */ 1631 function mod_scorm_get_completion_active_rule_descriptions($cm) { 1632 // Values will be present in cm_info, and we assume these are up to date. 1633 if (empty($cm->customdata['customcompletionrules']) 1634 || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { 1635 return []; 1636 } 1637 1638 $descriptions = []; 1639 foreach ($cm->customdata['customcompletionrules'] as $key => $val) { 1640 switch ($key) { 1641 case 'completionstatusrequired': 1642 if (!is_null($val)) { 1643 // Determine the selected statuses using a bitwise operation. 1644 $cvalues = array(); 1645 foreach (scorm_status_options(true) as $bit => $string) { 1646 if (($val & $bit) == $bit) { 1647 $cvalues[] = $string; 1648 } 1649 } 1650 $statusstring = implode(', ', $cvalues); 1651 $descriptions[] = get_string('completionstatusrequireddesc', 'scorm', $statusstring); 1652 } 1653 break; 1654 case 'completionscorerequired': 1655 if (!is_null($val)) { 1656 $descriptions[] = get_string('completionscorerequireddesc', 'scorm', $val); 1657 } 1658 break; 1659 case 'completionstatusallscos': 1660 if (!empty($val)) { 1661 $descriptions[] = get_string('completionstatusallscos', 'scorm'); 1662 } 1663 break; 1664 default: 1665 break; 1666 } 1667 } 1668 return $descriptions; 1669 } 1670 1671 /** 1672 * This function will update the scorm module according to the 1673 * event that has been modified. 1674 * 1675 * It will set the timeopen or timeclose value of the scorm instance 1676 * according to the type of event provided. 1677 * 1678 * @throws \moodle_exception 1679 * @param \calendar_event $event 1680 * @param stdClass $scorm The module instance to get the range from 1681 */ 1682 function mod_scorm_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $scorm) { 1683 global $DB; 1684 1685 if (empty($event->instance) || $event->modulename != 'scorm') { 1686 return; 1687 } 1688 1689 if ($event->instance != $scorm->id) { 1690 return; 1691 } 1692 1693 if (!in_array($event->eventtype, [SCORM_EVENT_TYPE_OPEN, SCORM_EVENT_TYPE_CLOSE])) { 1694 return; 1695 } 1696 1697 $courseid = $event->courseid; 1698 $modulename = $event->modulename; 1699 $instanceid = $event->instance; 1700 $modified = false; 1701 1702 $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; 1703 $context = context_module::instance($coursemodule->id); 1704 1705 // The user does not have the capability to modify this activity. 1706 if (!has_capability('moodle/course:manageactivities', $context)) { 1707 return; 1708 } 1709 1710 if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) { 1711 // If the event is for the scorm activity opening then we should 1712 // set the start time of the scorm activity to be the new start 1713 // time of the event. 1714 if ($scorm->timeopen != $event->timestart) { 1715 $scorm->timeopen = $event->timestart; 1716 $scorm->timemodified = time(); 1717 $modified = true; 1718 } 1719 } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) { 1720 // If the event is for the scorm activity closing then we should 1721 // set the end time of the scorm activity to be the new start 1722 // time of the event. 1723 if ($scorm->timeclose != $event->timestart) { 1724 $scorm->timeclose = $event->timestart; 1725 $modified = true; 1726 } 1727 } 1728 1729 if ($modified) { 1730 $scorm->timemodified = time(); 1731 $DB->update_record('scorm', $scorm); 1732 $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); 1733 $event->trigger(); 1734 } 1735 } 1736 1737 /** 1738 * This function calculates the minimum and maximum cutoff values for the timestart of 1739 * the given event. 1740 * 1741 * It will return an array with two values, the first being the minimum cutoff value and 1742 * the second being the maximum cutoff value. Either or both values can be null, which 1743 * indicates there is no minimum or maximum, respectively. 1744 * 1745 * If a cutoff is required then the function must return an array containing the cutoff 1746 * timestamp and error string to display to the user if the cutoff value is violated. 1747 * 1748 * A minimum and maximum cutoff return value will look like: 1749 * [ 1750 * [1505704373, 'The date must be after this date'], 1751 * [1506741172, 'The date must be before this date'] 1752 * ] 1753 * 1754 * @param \calendar_event $event The calendar event to get the time range for 1755 * @param \stdClass $instance The module instance to get the range from 1756 * @return array Returns an array with min and max date. 1757 */ 1758 function mod_scorm_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) { 1759 $mindate = null; 1760 $maxdate = null; 1761 1762 if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) { 1763 // The start time of the open event can't be equal to or after the 1764 // close time of the scorm activity. 1765 if (!empty($instance->timeclose)) { 1766 $maxdate = [ 1767 $instance->timeclose, 1768 get_string('openafterclose', 'scorm') 1769 ]; 1770 } 1771 } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) { 1772 // The start time of the close event can't be equal to or earlier than the 1773 // open time of the scorm activity. 1774 if (!empty($instance->timeopen)) { 1775 $mindate = [ 1776 $instance->timeopen, 1777 get_string('closebeforeopen', 'scorm') 1778 ]; 1779 } 1780 } 1781 1782 return [$mindate, $maxdate]; 1783 } 1784 1785 /** 1786 * Given an array with a file path, it returns the itemid and the filepath for the defined filearea. 1787 * 1788 * @param string $filearea The filearea. 1789 * @param array $args The path (the part after the filearea and before the filename). 1790 * @return array The itemid and the filepath inside the $args path, for the defined filearea. 1791 */ 1792 function mod_scorm_get_path_from_pluginfile(string $filearea, array $args) : array { 1793 // SCORM never has an itemid (the number represents the revision but it's not stored in database). 1794 array_shift($args); 1795 1796 // Get the filepath. 1797 if (empty($args)) { 1798 $filepath = '/'; 1799 } else { 1800 $filepath = '/' . implode('/', $args) . '/'; 1801 } 1802 1803 return [ 1804 'itemid' => 0, 1805 'filepath' => $filepath, 1806 ]; 1807 } 1808 1809 /** 1810 * Callback to fetch the activity event type lang string. 1811 * 1812 * @param string $eventtype The event type. 1813 * @return lang_string The event type lang string. 1814 */ 1815 function mod_scorm_core_calendar_get_event_action_string(string $eventtype): string { 1816 $modulename = get_string('modulename', 'scorm'); 1817 1818 switch ($eventtype) { 1819 case SCORM_EVENT_TYPE_OPEN: 1820 $identifier = 'calendarstart'; 1821 break; 1822 case SCORM_EVENT_TYPE_CLOSE: 1823 $identifier = 'calendarend'; 1824 break; 1825 default: 1826 return get_string('requiresaction', 'calendar', $modulename); 1827 } 1828 1829 return get_string($identifier, 'scorm', $modulename); 1830 } 1831 1832 /** 1833 * This function extends the settings navigation block for the site. 1834 * 1835 * It is safe to rely on PAGE here as we will only ever be within the module 1836 * context when this is called 1837 * 1838 * @param settings_navigation $settings navigation_node object. 1839 * @param navigation_node $scormnode navigation_node object. 1840 * @return void 1841 */ 1842 function scorm_extend_settings_navigation(settings_navigation $settings, navigation_node $scormnode): void { 1843 if (has_capability('mod/scorm:viewreport', $settings->get_page()->cm->context)) { 1844 $url = new moodle_url('/mod/scorm/report.php', ['id' => $settings->get_page()->cm->id]); 1845 $scormnode->add(get_string("reports", "scorm"), $url, navigation_node::TYPE_CUSTOM, null, 'scormreport'); 1846 } 1847 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body