Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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', $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 // Allow SVG files to be loaded within SCORM content, instead of forcing download. 1012 $options['dontforcesvgdownload'] = true; 1013 1014 // Finally send the file. 1015 send_stored_file($file, $lifetime, 0, false, $options); 1016 } 1017 1018 /** 1019 * @uses FEATURE_GROUPS 1020 * @uses FEATURE_GROUPINGS 1021 * @uses FEATURE_MOD_INTRO 1022 * @uses FEATURE_COMPLETION_TRACKS_VIEWS 1023 * @uses FEATURE_COMPLETION_HAS_RULES 1024 * @uses FEATURE_GRADE_HAS_GRADE 1025 * @uses FEATURE_GRADE_OUTCOMES 1026 * @param string $feature FEATURE_xx constant for requested feature 1027 * @return mixed True if module supports feature, false if not, null if doesn't know 1028 */ 1029 function scorm_supports($feature) { 1030 switch($feature) { 1031 case FEATURE_GROUPS: return true; 1032 case FEATURE_GROUPINGS: return true; 1033 case FEATURE_MOD_INTRO: return true; 1034 case FEATURE_COMPLETION_TRACKS_VIEWS: return true; 1035 case FEATURE_COMPLETION_HAS_RULES: return true; 1036 case FEATURE_GRADE_HAS_GRADE: return true; 1037 case FEATURE_GRADE_OUTCOMES: return true; 1038 case FEATURE_BACKUP_MOODLE2: return true; 1039 case FEATURE_SHOW_DESCRIPTION: return true; 1040 1041 default: return null; 1042 } 1043 } 1044 1045 /** 1046 * Get the filename for a temp log file 1047 * 1048 * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename 1049 * @param integer $scoid - scoid of object this log entry is for 1050 * @return string The filename as an absolute path 1051 */ 1052 function scorm_debug_log_filename($type, $scoid) { 1053 global $CFG, $USER; 1054 1055 $logpath = $CFG->tempdir.'/scormlogs'; 1056 $logfile = $logpath.'/'.$type.'debug_'.$USER->id.'_'.$scoid.'.log'; 1057 return $logfile; 1058 } 1059 1060 /** 1061 * writes log output to a temp log file 1062 * 1063 * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename 1064 * @param string $text - text to be written to file. 1065 * @param integer $scoid - scoid of object this log entry is for. 1066 */ 1067 function scorm_debug_log_write($type, $text, $scoid) { 1068 global $CFG; 1069 1070 $debugenablelog = get_config('scorm', 'allowapidebug'); 1071 if (!$debugenablelog || empty($text)) { 1072 return; 1073 } 1074 if (make_temp_directory('scormlogs/')) { 1075 $logfile = scorm_debug_log_filename($type, $scoid); 1076 @file_put_contents($logfile, date('Y/m/d H:i:s O')." DEBUG $text\r\n", FILE_APPEND); 1077 @chmod($logfile, $CFG->filepermissions); 1078 } 1079 } 1080 1081 /** 1082 * Remove debug log file 1083 * 1084 * @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename 1085 * @param integer $scoid - scoid of object this log entry is for 1086 * @return boolean True if the file is successfully deleted, false otherwise 1087 */ 1088 function scorm_debug_log_remove($type, $scoid) { 1089 1090 $debugenablelog = get_config('scorm', 'allowapidebug'); 1091 $logfile = scorm_debug_log_filename($type, $scoid); 1092 if (!$debugenablelog || !file_exists($logfile)) { 1093 return false; 1094 } 1095 1096 return @unlink($logfile); 1097 } 1098 1099 /** 1100 * @deprecated since Moodle 3.3, when the block_course_overview block was removed. 1101 */ 1102 function scorm_print_overview() { 1103 throw new coding_exception('scorm_print_overview() can not be used any more and is obsolete.'); 1104 } 1105 1106 /** 1107 * Return a list of page types 1108 * @param string $pagetype current page type 1109 * @param stdClass $parentcontext Block's parent context 1110 * @param stdClass $currentcontext Current context of block 1111 */ 1112 function scorm_page_type_list($pagetype, $parentcontext, $currentcontext) { 1113 $modulepagetype = array('mod-scorm-*' => get_string('page-mod-scorm-x', 'scorm')); 1114 return $modulepagetype; 1115 } 1116 1117 /** 1118 * Returns the SCORM version used. 1119 * @param string $scormversion comes from $scorm->version 1120 * @param string $version one of the defined vars SCORM_12, SCORM_13, SCORM_AICC (or empty) 1121 * @return Scorm version. 1122 */ 1123 function scorm_version_check($scormversion, $version='') { 1124 $scormversion = trim(strtolower($scormversion)); 1125 if (empty($version) || $version == SCORM_12) { 1126 if ($scormversion == 'scorm_12' || $scormversion == 'scorm_1.2') { 1127 return SCORM_12; 1128 } 1129 if (!empty($version)) { 1130 return false; 1131 } 1132 } 1133 if (empty($version) || $version == SCORM_13) { 1134 if ($scormversion == 'scorm_13' || $scormversion == 'scorm_1.3') { 1135 return SCORM_13; 1136 } 1137 if (!empty($version)) { 1138 return false; 1139 } 1140 } 1141 if (empty($version) || $version == SCORM_AICC) { 1142 if (strpos($scormversion, 'aicc')) { 1143 return SCORM_AICC; 1144 } 1145 if (!empty($version)) { 1146 return false; 1147 } 1148 } 1149 return false; 1150 } 1151 1152 /** 1153 * Obtains the automatic completion state for this scorm based on any conditions 1154 * in scorm settings. 1155 * 1156 * @param object $course Course 1157 * @param object $cm Course-module 1158 * @param int $userid User ID 1159 * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) 1160 * @return bool True if completed, false if not. (If no conditions, then return 1161 * value depends on comparison type) 1162 */ 1163 function scorm_get_completion_state($course, $cm, $userid, $type) { 1164 global $DB; 1165 1166 $result = $type; 1167 1168 // Get scorm. 1169 if (!$scorm = $DB->get_record('scorm', array('id' => $cm->instance))) { 1170 print_error('cannotfindscorm'); 1171 } 1172 // Only check for existence of tracks and return false if completionstatusrequired or completionscorerequired 1173 // this means that if only view is required we don't end up with a false state. 1174 if ($scorm->completionstatusrequired !== null || 1175 $scorm->completionscorerequired !== null) { 1176 // Get user's tracks data. 1177 $tracks = $DB->get_records_sql( 1178 " 1179 SELECT 1180 id, 1181 scoid, 1182 element, 1183 value 1184 FROM 1185 {scorm_scoes_track} 1186 WHERE 1187 scormid = ? 1188 AND userid = ? 1189 AND element IN 1190 ( 1191 'cmi.core.lesson_status', 1192 'cmi.completion_status', 1193 'cmi.success_status', 1194 'cmi.core.score.raw', 1195 'cmi.score.raw' 1196 ) 1197 ", 1198 array($scorm->id, $userid) 1199 ); 1200 1201 if (!$tracks) { 1202 return completion_info::aggregate_completion_states($type, $result, false); 1203 } 1204 } 1205 1206 // Check for status. 1207 if ($scorm->completionstatusrequired !== null) { 1208 1209 // Get status. 1210 $statuses = array_flip(scorm_status_options()); 1211 $nstatus = 0; 1212 // Check any track for these values. 1213 $scostatus = array(); 1214 foreach ($tracks as $track) { 1215 if (!in_array($track->element, array('cmi.core.lesson_status', 'cmi.completion_status', 'cmi.success_status'))) { 1216 continue; 1217 } 1218 if (array_key_exists($track->value, $statuses)) { 1219 $scostatus[$track->scoid] = true; 1220 $nstatus |= $statuses[$track->value]; 1221 } 1222 } 1223 1224 if (!empty($scorm->completionstatusallscos)) { 1225 // Iterate over all scos and make sure each has a lesson_status. 1226 $scos = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id, 'scormtype' => 'sco')); 1227 foreach ($scos as $sco) { 1228 if (empty($scostatus[$sco->id])) { 1229 return completion_info::aggregate_completion_states($type, $result, false); 1230 } 1231 } 1232 return completion_info::aggregate_completion_states($type, $result, true); 1233 } else if ($scorm->completionstatusrequired & $nstatus) { 1234 return completion_info::aggregate_completion_states($type, $result, true); 1235 } else { 1236 return completion_info::aggregate_completion_states($type, $result, false); 1237 } 1238 } 1239 1240 // Check for score. 1241 if ($scorm->completionscorerequired !== null) { 1242 $maxscore = -1; 1243 1244 foreach ($tracks as $track) { 1245 if (!in_array($track->element, array('cmi.core.score.raw', 'cmi.score.raw'))) { 1246 continue; 1247 } 1248 1249 if (strlen($track->value) && floatval($track->value) >= $maxscore) { 1250 $maxscore = floatval($track->value); 1251 } 1252 } 1253 1254 if ($scorm->completionscorerequired <= $maxscore) { 1255 return completion_info::aggregate_completion_states($type, $result, true); 1256 } else { 1257 return completion_info::aggregate_completion_states($type, $result, false); 1258 } 1259 } 1260 1261 return $result; 1262 } 1263 1264 /** 1265 * Register the ability to handle drag and drop file uploads 1266 * @return array containing details of the files / types the mod can handle 1267 */ 1268 function scorm_dndupload_register() { 1269 return array('files' => array( 1270 array('extension' => 'zip', 'message' => get_string('dnduploadscorm', 'scorm')) 1271 )); 1272 } 1273 1274 /** 1275 * Handle a file that has been uploaded 1276 * @param object $uploadinfo details of the file / content that has been uploaded 1277 * @return int instance id of the newly created mod 1278 */ 1279 function scorm_dndupload_handle($uploadinfo) { 1280 1281 $context = context_module::instance($uploadinfo->coursemodule); 1282 file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_scorm', 'package', 0); 1283 $fs = get_file_storage(); 1284 $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, 'sortorder, itemid, filepath, filename', false); 1285 $file = reset($files); 1286 1287 // Validate the file, make sure it's a valid SCORM package! 1288 $errors = scorm_validate_package($file); 1289 if (!empty($errors)) { 1290 return false; 1291 } 1292 // Create a default scorm object to pass to scorm_add_instance()! 1293 $scorm = get_config('scorm'); 1294 $scorm->course = $uploadinfo->course->id; 1295 $scorm->coursemodule = $uploadinfo->coursemodule; 1296 $scorm->cmidnumber = ''; 1297 $scorm->name = $uploadinfo->displayname; 1298 $scorm->scormtype = SCORM_TYPE_LOCAL; 1299 $scorm->reference = $file->get_filename(); 1300 $scorm->intro = ''; 1301 $scorm->width = $scorm->framewidth; 1302 $scorm->height = $scorm->frameheight; 1303 1304 return scorm_add_instance($scorm, null); 1305 } 1306 1307 /** 1308 * Sets activity completion state 1309 * 1310 * @param object $scorm object 1311 * @param int $userid User ID 1312 * @param int $completionstate Completion state 1313 * @param array $grades grades array of users with grades - used when $userid = 0 1314 */ 1315 function scorm_set_completion($scorm, $userid, $completionstate = COMPLETION_COMPLETE, $grades = array()) { 1316 $course = new stdClass(); 1317 $course->id = $scorm->course; 1318 $completion = new completion_info($course); 1319 1320 // Check if completion is enabled site-wide, or for the course. 1321 if (!$completion->is_enabled()) { 1322 return; 1323 } 1324 1325 $cm = get_coursemodule_from_instance('scorm', $scorm->id, $scorm->course); 1326 if (empty($cm) || !$completion->is_enabled($cm)) { 1327 return; 1328 } 1329 1330 if (empty($userid)) { // We need to get all the relevant users from $grades param. 1331 foreach ($grades as $grade) { 1332 $completion->update_state($cm, $completionstate, $grade->userid); 1333 } 1334 } else { 1335 $completion->update_state($cm, $completionstate, $userid); 1336 } 1337 } 1338 1339 /** 1340 * Check that a Zip file contains a valid SCORM package 1341 * 1342 * @param $file stored_file a Zip file. 1343 * @return array empty if no issue is found. Array of error message otherwise 1344 */ 1345 function scorm_validate_package($file) { 1346 $packer = get_file_packer('application/zip'); 1347 $errors = array(); 1348 if ($file->is_external_file()) { // Get zip file so we can check it is correct. 1349 $file->import_external_file_contents(); 1350 } 1351 $filelist = $file->list_files($packer); 1352 1353 if (!is_array($filelist)) { 1354 $errors['packagefile'] = get_string('badarchive', 'scorm'); 1355 } else { 1356 $aiccfound = false; 1357 $badmanifestpresent = false; 1358 foreach ($filelist as $info) { 1359 if ($info->pathname == 'imsmanifest.xml') { 1360 return array(); 1361 } else if (strpos($info->pathname, 'imsmanifest.xml') !== false) { 1362 // This package has an imsmanifest file inside a folder of the package. 1363 $badmanifestpresent = true; 1364 } 1365 if (preg_match('/\.cst$/', $info->pathname)) { 1366 return array(); 1367 } 1368 } 1369 if (!$aiccfound) { 1370 if ($badmanifestpresent) { 1371 $errors['packagefile'] = get_string('badimsmanifestlocation', 'scorm'); 1372 } else { 1373 $errors['packagefile'] = get_string('nomanifest', 'scorm'); 1374 } 1375 } 1376 } 1377 return $errors; 1378 } 1379 1380 /** 1381 * Check and set the correct mode and attempt when entering a SCORM package. 1382 * 1383 * @param object $scorm object 1384 * @param string $newattempt should a new attempt be generated here. 1385 * @param int $attempt the attempt number this is for. 1386 * @param int $userid the userid of the user. 1387 * @param string $mode the current mode that has been selected. 1388 */ 1389 function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) { 1390 global $DB; 1391 1392 if (($mode == 'browse')) { 1393 if ($scorm->hidebrowse == 1) { 1394 // Prevent Browse mode if hidebrowse is set. 1395 $mode = 'normal'; 1396 } else { 1397 // We don't need to check attempts as browse mode is set. 1398 return; 1399 } 1400 } 1401 1402 if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS) { 1403 // This SCORM is configured to force a new attempt on every re-entry. 1404 $newattempt = 'on'; 1405 $mode = 'normal'; 1406 if ($attempt == 1) { 1407 // Check if the user has any existing data or if this is really the first attempt. 1408 $exists = $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id)); 1409 if (!$exists) { 1410 // No records yet - Attempt should == 1. 1411 return; 1412 } 1413 } 1414 $attempt++; 1415 1416 return; 1417 } 1418 // Check if the scorm module is incomplete (used to validate user request to start a new attempt). 1419 $incomplete = true; 1420 1421 // Note - in SCORM_13 the cmi-core.lesson_status field was split into 1422 // 'cmi.completion_status' and 'cmi.success_status'. 1423 // 'cmi.completion_status' can only contain values 'completed', 'incomplete', 'not attempted' or 'unknown'. 1424 // This means the values 'passed' or 'failed' will never be reported for a track in SCORM_13 and 1425 // the only status that will be treated as complete is 'completed'. 1426 1427 $completionelements = array( 1428 SCORM_12 => 'cmi.core.lesson_status', 1429 SCORM_13 => 'cmi.completion_status', 1430 SCORM_AICC => 'cmi.core.lesson_status' 1431 ); 1432 $scormversion = scorm_version_check($scorm->version); 1433 if($scormversion===false) { 1434 $scormversion = SCORM_12; 1435 } 1436 $completionelement = $completionelements[$scormversion]; 1437 1438 $sql = "SELECT sc.id, t.value 1439 FROM {scorm_scoes} sc 1440 LEFT JOIN {scorm_scoes_track} t ON sc.scorm = t.scormid AND sc.id = t.scoid 1441 AND t.element = ? AND t.userid = ? AND t.attempt = ? 1442 WHERE sc.scormtype = 'sco' AND sc.scorm = ?"; 1443 $tracks = $DB->get_recordset_sql($sql, array($completionelement, $userid, $attempt, $scorm->id)); 1444 1445 foreach ($tracks as $track) { 1446 if (($track->value == 'completed') || ($track->value == 'passed') || ($track->value == 'failed')) { 1447 $incomplete = false; 1448 } else { 1449 $incomplete = true; 1450 break; // Found an incomplete sco, so the result as a whole is incomplete. 1451 } 1452 } 1453 $tracks->close(); 1454 1455 // Validate user request to start a new attempt. 1456 if ($incomplete === true) { 1457 // The option to start a new attempt should never have been presented. Force false. 1458 $newattempt = 'off'; 1459 } else if (!empty($scorm->forcenewattempt)) { 1460 // A new attempt should be forced for already completed attempts. 1461 $newattempt = 'on'; 1462 } 1463 1464 if (($newattempt == 'on') && (($attempt < $scorm->maxattempt) || ($scorm->maxattempt == 0))) { 1465 $attempt++; 1466 $mode = 'normal'; 1467 } else { // Check if review mode should be set. 1468 if ($incomplete === true) { 1469 $mode = 'normal'; 1470 } else { 1471 $mode = 'review'; 1472 } 1473 } 1474 } 1475 1476 /** 1477 * Trigger the course_module_viewed event. 1478 * 1479 * @param stdClass $scorm scorm object 1480 * @param stdClass $course course object 1481 * @param stdClass $cm course module object 1482 * @param stdClass $context context object 1483 * @since Moodle 3.0 1484 */ 1485 function scorm_view($scorm, $course, $cm, $context) { 1486 1487 // Trigger course_module_viewed event. 1488 $params = array( 1489 'context' => $context, 1490 'objectid' => $scorm->id 1491 ); 1492 1493 $event = \mod_scorm\event\course_module_viewed::create($params); 1494 $event->add_record_snapshot('course_modules', $cm); 1495 $event->add_record_snapshot('course', $course); 1496 $event->add_record_snapshot('scorm', $scorm); 1497 $event->trigger(); 1498 } 1499 1500 /** 1501 * Check if the module has any update that affects the current user since a given time. 1502 * 1503 * @param cm_info $cm course module data 1504 * @param int $from the time to check updates from 1505 * @param array $filter if we need to check only specific updates 1506 * @return stdClass an object with the different type of areas indicating if they were updated or not 1507 * @since Moodle 3.2 1508 */ 1509 function scorm_check_updates_since(cm_info $cm, $from, $filter = array()) { 1510 global $DB, $USER, $CFG; 1511 require_once($CFG->dirroot . '/mod/scorm/locallib.php'); 1512 1513 $scorm = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); 1514 $updates = new stdClass(); 1515 list($available, $warnings) = scorm_get_availability_status($scorm, true, $cm->context); 1516 if (!$available) { 1517 return $updates; 1518 } 1519 $updates = course_check_module_updates_since($cm, $from, array('package'), $filter); 1520 1521 $updates->tracks = (object) array('updated' => false); 1522 $select = 'scormid = ? AND userid = ? AND timemodified > ?'; 1523 $params = array($scorm->id, $USER->id, $from); 1524 $tracks = $DB->get_records_select('scorm_scoes_track', $select, $params, '', 'id'); 1525 if (!empty($tracks)) { 1526 $updates->tracks->updated = true; 1527 $updates->tracks->itemids = array_keys($tracks); 1528 } 1529 1530 // Now, teachers should see other students updates. 1531 if (has_capability('mod/scorm:viewreport', $cm->context)) { 1532 $select = 'scormid = ? AND timemodified > ?'; 1533 $params = array($scorm->id, $from); 1534 1535 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { 1536 $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); 1537 if (empty($groupusers)) { 1538 return $updates; 1539 } 1540 list($insql, $inparams) = $DB->get_in_or_equal($groupusers); 1541 $select .= ' AND userid ' . $insql; 1542 $params = array_merge($params, $inparams); 1543 } 1544 1545 $updates->usertracks = (object) array('updated' => false); 1546 $tracks = $DB->get_records_select('scorm_scoes_track', $select, $params, '', 'id'); 1547 if (!empty($tracks)) { 1548 $updates->usertracks->updated = true; 1549 $updates->usertracks->itemids = array_keys($tracks); 1550 } 1551 } 1552 return $updates; 1553 } 1554 1555 /** 1556 * Get icon mapping for font-awesome. 1557 */ 1558 function mod_scorm_get_fontawesome_icon_map() { 1559 return [ 1560 'mod_scorm:assetc' => 'fa-file-archive-o', 1561 'mod_scorm:asset' => 'fa-file-archive-o', 1562 'mod_scorm:browsed' => 'fa-book', 1563 'mod_scorm:completed' => 'fa-check-square-o', 1564 'mod_scorm:failed' => 'fa-times', 1565 'mod_scorm:incomplete' => 'fa-pencil-square-o', 1566 'mod_scorm:minus' => 'fa-minus', 1567 'mod_scorm:notattempted' => 'fa-square-o', 1568 'mod_scorm:passed' => 'fa-check', 1569 'mod_scorm:plus' => 'fa-plus', 1570 'mod_scorm:popdown' => 'fa-window-close-o', 1571 'mod_scorm:popup' => 'fa-window-restore', 1572 'mod_scorm:suspend' => 'fa-pause', 1573 'mod_scorm:wait' => 'fa-clock-o', 1574 ]; 1575 } 1576 1577 /** 1578 * This standard function will check all instances of this module 1579 * and make sure there are up-to-date events created for each of them. 1580 * If courseid = 0, then every scorm event in the site is checked, else 1581 * only scorm events belonging to the course specified are checked. 1582 * 1583 * @param int $courseid 1584 * @param int|stdClass $instance scorm module instance or ID. 1585 * @param int|stdClass $cm Course module object or ID. 1586 * @return bool 1587 */ 1588 function scorm_refresh_events($courseid = 0, $instance = null, $cm = null) { 1589 global $CFG, $DB; 1590 1591 require_once($CFG->dirroot . '/mod/scorm/locallib.php'); 1592 1593 // If we have instance information then we can just update the one event instead of updating all events. 1594 if (isset($instance)) { 1595 if (!is_object($instance)) { 1596 $instance = $DB->get_record('scorm', array('id' => $instance), '*', MUST_EXIST); 1597 } 1598 if (isset($cm)) { 1599 if (!is_object($cm)) { 1600 $cm = (object)array('id' => $cm); 1601 } 1602 } else { 1603 $cm = get_coursemodule_from_instance('scorm', $instance->id); 1604 } 1605 scorm_update_calendar($instance, $cm->id); 1606 return true; 1607 } 1608 1609 if ($courseid) { 1610 // Make sure that the course id is numeric. 1611 if (!is_numeric($courseid)) { 1612 return false; 1613 } 1614 if (!$scorms = $DB->get_records('scorm', array('course' => $courseid))) { 1615 return false; 1616 } 1617 } else { 1618 if (!$scorms = $DB->get_records('scorm')) { 1619 return false; 1620 } 1621 } 1622 1623 foreach ($scorms as $scorm) { 1624 $cm = get_coursemodule_from_instance('scorm', $scorm->id); 1625 scorm_update_calendar($scorm, $cm->id); 1626 } 1627 1628 return true; 1629 } 1630 1631 /** 1632 * This function receives a calendar event and returns the action associated with it, or null if there is none. 1633 * 1634 * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event 1635 * is not displayed on the block. 1636 * 1637 * @param calendar_event $event 1638 * @param \core_calendar\action_factory $factory 1639 * @param int $userid User id override 1640 * @return \core_calendar\local\event\entities\action_interface|null 1641 */ 1642 function mod_scorm_core_calendar_provide_event_action(calendar_event $event, 1643 \core_calendar\action_factory $factory, $userid = null) { 1644 global $CFG, $USER; 1645 1646 require_once($CFG->dirroot . '/mod/scorm/locallib.php'); 1647 1648 if (empty($userid)) { 1649 $userid = $USER->id; 1650 } 1651 1652 $cm = get_fast_modinfo($event->courseid, $userid)->instances['scorm'][$event->instance]; 1653 1654 if (has_capability('mod/scorm:viewreport', $cm->context, $userid)) { 1655 // Teachers do not need to be reminded to complete a scorm. 1656 return null; 1657 } 1658 1659 $completion = new \completion_info($cm->get_course()); 1660 1661 $completiondata = $completion->get_data($cm, false, $userid); 1662 1663 if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { 1664 return null; 1665 } 1666 1667 if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) { 1668 // The scorm has closed so the user can no longer submit anything. 1669 return null; 1670 } 1671 1672 // Restore scorm object from cached values in $cm, we only need id, timeclose and timeopen. 1673 $customdata = $cm->customdata ?: []; 1674 $customdata['id'] = $cm->instance; 1675 $scorm = (object)($customdata + ['timeclose' => 0, 'timeopen' => 0]); 1676 1677 // Check that the SCORM activity is open. 1678 list($actionable, $warnings) = scorm_get_availability_status($scorm, false, null, $userid); 1679 1680 return $factory->create_instance( 1681 get_string('enter', 'scorm'), 1682 new \moodle_url('/mod/scorm/view.php', array('id' => $cm->id)), 1683 1, 1684 $actionable 1685 ); 1686 } 1687 1688 /** 1689 * Add a get_coursemodule_info function in case any SCORM type wants to add 'extra' information 1690 * for the course (see resource). 1691 * 1692 * Given a course_module object, this function returns any "extra" information that may be needed 1693 * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. 1694 * 1695 * @param stdClass $coursemodule The coursemodule object (record). 1696 * @return cached_cm_info An object on information that the courses 1697 * will know about (most noticeably, an icon). 1698 */ 1699 function scorm_get_coursemodule_info($coursemodule) { 1700 global $DB; 1701 1702 $dbparams = ['id' => $coursemodule->instance]; 1703 $fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos, '. 1704 'timeopen, timeclose'; 1705 if (!$scorm = $DB->get_record('scorm', $dbparams, $fields)) { 1706 return false; 1707 } 1708 1709 $result = new cached_cm_info(); 1710 $result->name = $scorm->name; 1711 1712 if ($coursemodule->showdescription) { 1713 // Convert intro to html. Do not filter cached version, filters run at display time. 1714 $result->content = format_module_intro('scorm', $scorm, $coursemodule->id, false); 1715 } 1716 1717 // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. 1718 if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { 1719 $result->customdata['customcompletionrules']['completionstatusrequired'] = $scorm->completionstatusrequired; 1720 $result->customdata['customcompletionrules']['completionscorerequired'] = $scorm->completionscorerequired; 1721 $result->customdata['customcompletionrules']['completionstatusallscos'] = $scorm->completionstatusallscos; 1722 } 1723 // Populate some other values that can be used in calendar or on dashboard. 1724 if ($scorm->timeopen) { 1725 $result->customdata['timeopen'] = $scorm->timeopen; 1726 } 1727 if ($scorm->timeclose) { 1728 $result->customdata['timeclose'] = $scorm->timeclose; 1729 } 1730 1731 return $result; 1732 } 1733 1734 /** 1735 * Callback which returns human-readable strings describing the active completion custom rules for the module instance. 1736 * 1737 * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] 1738 * @return array $descriptions the array of descriptions for the custom rules. 1739 */ 1740 function mod_scorm_get_completion_active_rule_descriptions($cm) { 1741 // Values will be present in cm_info, and we assume these are up to date. 1742 if (empty($cm->customdata['customcompletionrules']) 1743 || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { 1744 return []; 1745 } 1746 1747 $descriptions = []; 1748 foreach ($cm->customdata['customcompletionrules'] as $key => $val) { 1749 switch ($key) { 1750 case 'completionstatusrequired': 1751 if (!is_null($val)) { 1752 // Determine the selected statuses using a bitwise operation. 1753 $cvalues = array(); 1754 foreach (scorm_status_options(true) as $bit => $string) { 1755 if (($val & $bit) == $bit) { 1756 $cvalues[] = $string; 1757 } 1758 } 1759 $statusstring = implode(', ', $cvalues); 1760 $descriptions[] = get_string('completionstatusrequireddesc', 'scorm', $statusstring); 1761 } 1762 break; 1763 case 'completionscorerequired': 1764 if (!is_null($val)) { 1765 $descriptions[] = get_string('completionscorerequireddesc', 'scorm', $val); 1766 } 1767 break; 1768 case 'completionstatusallscos': 1769 if (!empty($val)) { 1770 $descriptions[] = get_string('completionstatusallscos', 'scorm'); 1771 } 1772 break; 1773 default: 1774 break; 1775 } 1776 } 1777 return $descriptions; 1778 } 1779 1780 /** 1781 * This function will update the scorm module according to the 1782 * event that has been modified. 1783 * 1784 * It will set the timeopen or timeclose value of the scorm instance 1785 * according to the type of event provided. 1786 * 1787 * @throws \moodle_exception 1788 * @param \calendar_event $event 1789 * @param stdClass $scorm The module instance to get the range from 1790 */ 1791 function mod_scorm_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $scorm) { 1792 global $DB; 1793 1794 if (empty($event->instance) || $event->modulename != 'scorm') { 1795 return; 1796 } 1797 1798 if ($event->instance != $scorm->id) { 1799 return; 1800 } 1801 1802 if (!in_array($event->eventtype, [SCORM_EVENT_TYPE_OPEN, SCORM_EVENT_TYPE_CLOSE])) { 1803 return; 1804 } 1805 1806 $courseid = $event->courseid; 1807 $modulename = $event->modulename; 1808 $instanceid = $event->instance; 1809 $modified = false; 1810 1811 $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; 1812 $context = context_module::instance($coursemodule->id); 1813 1814 // The user does not have the capability to modify this activity. 1815 if (!has_capability('moodle/course:manageactivities', $context)) { 1816 return; 1817 } 1818 1819 if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) { 1820 // If the event is for the scorm activity opening then we should 1821 // set the start time of the scorm activity to be the new start 1822 // time of the event. 1823 if ($scorm->timeopen != $event->timestart) { 1824 $scorm->timeopen = $event->timestart; 1825 $scorm->timemodified = time(); 1826 $modified = true; 1827 } 1828 } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) { 1829 // If the event is for the scorm activity closing then we should 1830 // set the end time of the scorm activity to be the new start 1831 // time of the event. 1832 if ($scorm->timeclose != $event->timestart) { 1833 $scorm->timeclose = $event->timestart; 1834 $modified = true; 1835 } 1836 } 1837 1838 if ($modified) { 1839 $scorm->timemodified = time(); 1840 $DB->update_record('scorm', $scorm); 1841 $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); 1842 $event->trigger(); 1843 } 1844 } 1845 1846 /** 1847 * This function calculates the minimum and maximum cutoff values for the timestart of 1848 * the given event. 1849 * 1850 * It will return an array with two values, the first being the minimum cutoff value and 1851 * the second being the maximum cutoff value. Either or both values can be null, which 1852 * indicates there is no minimum or maximum, respectively. 1853 * 1854 * If a cutoff is required then the function must return an array containing the cutoff 1855 * timestamp and error string to display to the user if the cutoff value is violated. 1856 * 1857 * A minimum and maximum cutoff return value will look like: 1858 * [ 1859 * [1505704373, 'The date must be after this date'], 1860 * [1506741172, 'The date must be before this date'] 1861 * ] 1862 * 1863 * @param \calendar_event $event The calendar event to get the time range for 1864 * @param \stdClass $instance The module instance to get the range from 1865 * @return array Returns an array with min and max date. 1866 */ 1867 function mod_scorm_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) { 1868 $mindate = null; 1869 $maxdate = null; 1870 1871 if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) { 1872 // The start time of the open event can't be equal to or after the 1873 // close time of the scorm activity. 1874 if (!empty($instance->timeclose)) { 1875 $maxdate = [ 1876 $instance->timeclose, 1877 get_string('openafterclose', 'scorm') 1878 ]; 1879 } 1880 } else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) { 1881 // The start time of the close event can't be equal to or earlier than the 1882 // open time of the scorm activity. 1883 if (!empty($instance->timeopen)) { 1884 $mindate = [ 1885 $instance->timeopen, 1886 get_string('closebeforeopen', 'scorm') 1887 ]; 1888 } 1889 } 1890 1891 return [$mindate, $maxdate]; 1892 } 1893 1894 /** 1895 * Given an array with a file path, it returns the itemid and the filepath for the defined filearea. 1896 * 1897 * @param string $filearea The filearea. 1898 * @param array $args The path (the part after the filearea and before the filename). 1899 * @return array The itemid and the filepath inside the $args path, for the defined filearea. 1900 */ 1901 function mod_scorm_get_path_from_pluginfile(string $filearea, array $args) : array { 1902 // SCORM never has an itemid (the number represents the revision but it's not stored in database). 1903 array_shift($args); 1904 1905 // Get the filepath. 1906 if (empty($args)) { 1907 $filepath = '/'; 1908 } else { 1909 $filepath = '/' . implode('/', $args) . '/'; 1910 } 1911 1912 return [ 1913 'itemid' => 0, 1914 'filepath' => $filepath, 1915 ]; 1916 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body