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