Updated the 19 build version to 20101023
[moodle.git] / mod / scorm / locallib.php
blob4ea6429e0b44c573c6c7707a415436da8a6c0236
1 <?php // $Id$
3 /// Constants and settings for module scorm
4 define('UPDATE_NEVER', '0');
5 define('UPDATE_ONCHANGE', '1');
6 define('UPDATE_EVERYDAY', '2');
7 define('UPDATE_EVERYTIME', '3');
9 define('SCO_ALL', 0);
10 define('SCO_DATA', 1);
11 define('SCO_ONLY', 2);
13 define('GRADESCOES', '0');
14 define('GRADEHIGHEST', '1');
15 define('GRADEAVERAGE', '2');
16 define('GRADESUM', '3');
18 define('HIGHESTATTEMPT', '0');
19 define('AVERAGEATTEMPT', '1');
20 define('FIRSTATTEMPT', '2');
21 define('LASTATTEMPT', '3');
23 /**
24 * Returns an array of the popup options for SCORM and each options default value
26 * @return array an array of popup options as the key and their defaults as the value
28 function scorm_get_popup_options_array(){
29 global $CFG;
30 return array('resizable'=> isset($CFG->scorm_resizable) ? $CFG->scorm_resizable : 0,
31 'scrollbars'=> isset($CFG->scorm_scrollbars) ? $CFG->scorm_scrollbars : 0,
32 'directories'=> isset($CFG->scorm_directories) ? $CFG->scorm_directories : 0,
33 'location'=> isset($CFG->scorm_location) ? $CFG->scorm_location : 0,
34 'menubar'=> isset($CFG->scorm_menubar) ? $CFG->scorm_menubar : 0,
35 'toolbar'=> isset($CFG->scorm_toolbar) ? $CFG->scorm_toolbar : 0,
36 'status'=> isset($CFG->scorm_status) ? $CFG->scorm_status : 0);
39 /// Local Library of functions for module scorm
40 /**
41 * Returns an array of the array of what grade options
43 * @return array an array of what grade options
45 function scorm_get_grade_method_array(){
46 return array (GRADESCOES => get_string('gradescoes', 'scorm'),
47 GRADEHIGHEST => get_string('gradehighest', 'scorm'),
48 GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
49 GRADESUM => get_string('gradesum', 'scorm'));
52 /**
53 * Returns an array of the array of what grade options
55 * @return array an array of what grade options
57 function scorm_get_what_grade_array(){
58 return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
59 AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
60 FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
61 LASTATTEMPT => get_string('lastattempt', 'scorm'));
64 /**
65 * Returns an array of the array of skip view options
67 * @return array an array of skip view options
69 function scorm_get_skip_view_array(){
70 return array(0 => get_string('never'),
71 1 => get_string('firstaccess','scorm'),
72 2 => get_string('always'));
75 /**
76 * Returns an array of the array of hide table of contents options
78 * @return array an array of hide table of contents options
80 function scorm_get_hidetoc_array(){
81 return array(0 =>get_string('sided','scorm'),
82 1 => get_string('hidden','scorm'),
83 2 => get_string('popupmenu','scorm'));
86 /**
87 * Returns an array of the array of update frequency options
89 * @return array an array of update frequency options
91 function scorm_get_updatefreq_array(){
92 return array(0 => get_string('never'),
93 //1 => get_string('onchanges','scorm'),
94 2 => get_string('everyday','scorm'),
95 3 => get_string('everytime','scorm'));
98 /**
99 * Returns an array of the array of popup display options
101 * @return array an array of popup display options
103 function scorm_get_popup_display_array(){
104 return array(0 => get_string('iframe', 'scorm'),
105 1 => get_string('popup', 'scorm'));
109 * Returns an array of the array of attempt options
111 * @return array an array of attempt options
113 function scorm_get_attempts_array(){
114 $attempts = array(0 => get_string('nolimit','scorm'),
115 1 => get_string('attempt1','scorm'));
117 for ($i=2; $i<=6; $i++) {
118 $attempts[$i] = get_string('attemptsx','scorm', $i);
121 return $attempts;
125 * This function will permanently delete the given
126 * directory and all files and subdirectories.
128 * @param string $directory The directory to remove
129 * @return boolean
131 function scorm_delete_files($directory) {
132 if (is_dir($directory)) {
133 $files=scorm_scandir($directory);
134 set_time_limit(0);
135 foreach($files as $file) {
136 if (($file != '.') && ($file != '..')) {
137 if (!is_dir($directory.'/'.$file)) {
138 unlink($directory.'/'.$file);
139 } else {
140 scorm_delete_files($directory.'/'.$file);
144 rmdir($directory);
145 return true;
147 return false;
151 * Given a diretory path returns the file list
153 * @param string $directory
154 * @return array
156 function scorm_scandir($directory) {
157 if (version_compare(phpversion(),'5.0.0','>=')) {
158 return scandir($directory);
159 } else {
160 $files = array();
161 if ($dh = opendir($directory)) {
162 while (($file = readdir($dh)) !== false) {
163 $files[] = $file;
165 closedir($dh);
167 return $files;
172 * Create a new temporary subdirectory with a random name in the given path
174 * @param string $strpath The scorm data directory
175 * @return string/boolean
177 function scorm_tempdir($strPath)
179 global $CFG;
181 if (is_dir($strPath)) {
182 do {
183 // Create a random string of 8 chars
184 $randstring = NULL;
185 $lchar = '';
186 $len = 8;
187 for ($i=0; $i<$len; $i++) {
188 $char = chr(rand(48,122));
189 while (!ereg('[a-zA-Z0-9]', $char)){
190 if ($char == $lchar) continue;
191 $char = chr(rand(48,90));
193 $randstring .= $char;
194 $lchar = $char;
196 $datadir='/'.$randstring;
197 } while (file_exists($strPath.$datadir));
198 mkdir($strPath.$datadir, $CFG->directorypermissions);
199 @chmod($strPath.$datadir, $CFG->directorypermissions); // Just in case mkdir didn't do it
200 return $strPath.$datadir;
201 } else {
202 return false;
206 function scorm_array_search($item, $needle, $haystacks, $strict=false) {
207 if (!empty($haystacks)) {
208 foreach ($haystacks as $key => $element) {
209 if ($strict) {
210 if ($element->{$item} === $needle) {
211 return $key;
213 } else {
214 if ($element->{$item} == $needle) {
215 return $key;
220 return false;
223 function scorm_repeater($what, $times) {
224 if ($times <= 0) {
225 return null;
227 $return = '';
228 for ($i=0; $i<$times;$i++) {
229 $return .= $what;
231 return $return;
234 function scorm_external_link($link) {
235 // check if a link is external
236 $result = false;
237 $link = strtolower($link);
238 if (substr($link,0,7) == 'http://') {
239 $result = true;
240 } else if (substr($link,0,8) == 'https://') {
241 $result = true;
242 } else if (substr($link,0,4) == 'www.') {
243 $result = true;
245 return $result;
249 * Returns an object containing all datas relative to the given sco ID
251 * @param integer $id The sco ID
252 * @return mixed (false if sco id does not exists)
255 function scorm_get_sco($id,$what=SCO_ALL) {
256 if ($sco = get_record('scorm_scoes','id',$id)) {
257 $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
258 if (($what != SCO_ONLY) && ($scodatas = get_records('scorm_scoes_data','scoid',$id))) {
259 foreach ($scodatas as $scodata) {
260 $sco->{$scodata->name} = $scodata->value;
262 } else if (($what != SCO_ONLY) && (!($scodatas = get_records('scorm_scoes_data','scoid',$id)))) {
263 $sco->parameters = '';
265 return $sco;
266 } else {
267 return false;
272 * Returns an object (array) containing all the scoes data related to the given sco ID
274 * @param integer $id The sco ID
275 * @param integer $organisation an organisation ID - defaults to false if not required
276 * @return mixed (false if there are no scoes or an array)
279 function scorm_get_scoes($id,$organisation=false) {
280 $organizationsql = '';
281 if (!empty($organisation)) {
282 $organizationsql = "AND organization='$organisation'";
284 if ($scoes = get_records_select('scorm_scoes',"scorm='$id' $organizationsql order by id ASC")) {
285 // drop keys so that it is a simple array as expected
286 $scoes = array_values($scoes);
287 foreach ($scoes as $sco) {
288 if ($scodatas = get_records('scorm_scoes_data','scoid',$sco->id)) {
289 foreach ($scodatas as $scodata) {
290 $sco->{$scodata->name} = stripslashes_safe($scodata->value);
294 return $scoes;
295 } else {
296 return false;
300 function scorm_insert_track($userid,$scormid,$scoid,$attempt,$element,$value) {
301 global $CFG;
302 $id = null;
303 if ($track = get_record_select('scorm_scoes_track',"userid='$userid' AND scormid='$scormid' AND scoid='$scoid' AND attempt='$attempt' AND element='$element'")) {
304 if ($element != 'x.start.time' ) { //don't update x.start.time - keep the original value.
305 $track->value = addslashes_js($value);
306 $track->timemodified = time();
307 $id = update_record('scorm_scoes_track',$track);
309 } else {
310 $track->userid = $userid;
311 $track->scormid = $scormid;
312 $track->scoid = $scoid;
313 $track->attempt = $attempt;
314 $track->element = $element;
315 $track->value = addslashes_js($value);
316 $track->timemodified = time();
317 $id = insert_record('scorm_scoes_track',$track);
320 if (strstr($element, '.score.raw') ||
321 (($element == 'cmi.core.lesson_status' || $element == 'cmi.completion_status') && ($track->value == 'completed' || $track->value == 'passed'))) {
322 $scorm = get_record('scorm', 'id', $scormid);
323 include_once($CFG->dirroot.'/mod/scorm/lib.php');
324 scorm_update_grades($scorm, $userid);
327 return $id;
330 function scorm_get_tracks($scoid,$userid,$attempt='') {
331 /// Gets all tracks of specified sco and user
332 global $CFG;
334 if (empty($attempt)) {
335 if ($scormid = get_field('scorm_scoes','scorm','id',$scoid)) {
336 $attempt = scorm_get_last_attempt($scormid,$userid);
337 } else {
338 $attempt = 1;
341 $attemptsql = ' AND attempt=' . $attempt;
342 if ($tracks = get_records_select('scorm_scoes_track',"userid=$userid AND scoid=$scoid".$attemptsql,'element ASC')) {
343 $usertrack->userid = $userid;
344 $usertrack->scoid = $scoid;
345 // Defined in order to unify scorm1.2 and scorm2004
346 $usertrack->score_raw = '';
347 $usertrack->status = '';
348 $usertrack->total_time = '00:00:00';
349 $usertrack->session_time = '00:00:00';
350 $usertrack->timemodified = 0;
351 foreach ($tracks as $track) {
352 $element = $track->element;
353 $track->value = stripslashes_safe($track->value);
354 $usertrack->{$element} = $track->value;
355 switch ($element) {
356 case 'cmi.core.lesson_status':
357 case 'cmi.completion_status':
358 if ($track->value == 'not attempted') {
359 $track->value = 'notattempted';
361 $usertrack->status = $track->value;
362 break;
363 case 'cmi.core.score.raw':
364 case 'cmi.score.raw':
365 $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
366 break;
367 case 'cmi.core.session_time':
368 case 'cmi.session_time':
369 $usertrack->session_time = $track->value;
370 break;
371 case 'cmi.core.total_time':
372 case 'cmi.total_time':
373 $usertrack->total_time = $track->value;
374 break;
376 if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
377 $usertrack->timemodified = $track->timemodified;
380 if (is_array($usertrack)) {
381 ksort($usertrack);
383 return $usertrack;
384 } else {
385 return false;
389 /* Find the start and finsh time for a a given SCO attempt
391 * @param int $scormid SCORM Id
392 * @param int $scoid SCO Id
393 * @param int $userid User Id
394 * @param int $attemt Attempt Id
396 * @return object start and finsh time EPOC secods
399 function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
401 $timedata = new object();
402 $sql = !empty($scoid) ? "userid=$userid AND scormid=$scormid AND scoid=$scoid AND attempt=$attempt" : "userid=$userid AND scormid=$scormid AND attempt=$attempt";
403 $tracks = get_records_select('scorm_scoes_track',"$sql ORDER BY timemodified ASC");
404 if ($tracks) {
405 $tracks = array_values($tracks);
408 if ($tracks) {
409 $timedata->start = $tracks[0]->timemodified;
411 else {
412 $timedata->start = false;
414 if ($tracks && $track = array_pop($tracks)) {
415 $timedata->finish = $track->timemodified;
417 else {
418 $timedata->finish = $timedata->start;
420 return $timedata;
424 function scorm_get_user_data($userid) {
425 /// Gets user info required to display the table of scorm results
426 /// for report.php
428 return get_record('user','id',$userid,'','','','','firstname, lastname, picture');
431 function scorm_grade_user_attempt($scorm, $userid, $attempt=1, $time=false) {
432 $attemptscore = NULL;
433 $attemptscore->scoes = 0;
434 $attemptscore->values = 0;
435 $attemptscore->max = 0;
436 $attemptscore->sum = 0;
437 $attemptscore->lastmodify = 0;
439 if (!$scoes = get_records('scorm_scoes','scorm',$scorm->id)) {
440 return NULL;
443 foreach ($scoes as $sco) {
444 if ($userdata = scorm_get_tracks($sco->id, $userid, $attempt)) {
445 if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
446 $attemptscore->scoes++;
448 if (!empty($userdata->score_raw) || ($scorm->type=='sco' && isset($userdata->score_raw))) {
449 $attemptscore->values++;
450 $attemptscore->sum += $userdata->score_raw;
451 $attemptscore->max = ($userdata->score_raw > $attemptscore->max)?$userdata->score_raw:$attemptscore->max;
452 if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
453 $attemptscore->lastmodify = $userdata->timemodified;
454 } else {
455 $attemptscore->lastmodify = 0;
460 switch ($scorm->grademethod) {
461 case GRADEHIGHEST:
462 $score = (float) $attemptscore->max;
463 break;
464 case GRADEAVERAGE:
465 if ($attemptscore->values > 0) {
466 $score = $attemptscore->sum/$attemptscore->values;
467 } else {
468 $score = 0;
470 break;
471 case GRADESUM:
472 $score = $attemptscore->sum;
473 break;
474 case GRADESCOES:
475 $score = $attemptscore->scoes;
476 break;
477 default:
478 $score = $attemptscore->max; // Remote Learner GRADEHIGHEST is default
481 if ($time) {
482 $result = new stdClass();
483 $result->score = $score;
484 $result->time = $attemptscore->lastmodify;
485 } else {
486 $result = $score;
489 return $result;
492 function scorm_grade_user($scorm, $userid, $time=false) {
494 // insure we dont grade user beyond $scorm->maxattempt settings
495 $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
496 if($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt){
497 $lastattempt = $scorm->maxattempt;
500 switch ($scorm->whatgrade) {
501 case FIRSTATTEMPT:
502 return scorm_grade_user_attempt($scorm, $userid, 1, $time);
503 break;
504 case LASTATTEMPT:
505 return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid), $time);
506 break;
507 case HIGHESTATTEMPT:
508 $maxscore = 0;
509 $attempttime = 0;
510 for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
511 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt, $time);
512 if ($time) {
513 if ($attemptscore->score > $maxscore) {
514 $maxscore = $attemptscore->score;
515 $attempttime = $attemptscore->time;
517 } else {
518 $maxscore = $attemptscore > $maxscore ? $attemptscore: $maxscore;
521 if ($time) {
522 $result = new stdClass();
523 $result->score = $maxscore;
524 $result->time = $attempttime;
525 return $result;
526 } else {
527 return $maxscore;
529 break;
530 case AVERAGEATTEMPT:
531 $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
532 $sumscore = 0;
533 for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
534 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt, $time);
535 if ($time) {
536 $sumscore += $attemptscore->score;
537 } else {
538 $sumscore += $attemptscore;
542 if ($lastattempt > 0) {
543 $score = $sumscore / $lastattempt;
544 } else {
545 $score = 0;
548 if ($time) {
549 $result = new stdClass();
550 $result->score = $score;
551 $result->time = $attemptscore->time;
552 return $result;
553 } else {
554 return $score;
556 break;
560 function scorm_count_launchable($scormid,$organization='') {
561 $strorganization = '';
562 if (!empty($organization)) {
563 $strorganization = " AND organization='$organization'";
565 return count_records_select('scorm_scoes',"scorm=$scormid$strorganization AND launch<>'".sql_empty()."'");
568 function scorm_get_last_attempt($scormid, $userid) {
569 /// Find the last attempt number for the given user id and scorm id
570 if ($lastattempt = get_record('scorm_scoes_track', 'userid', $userid, 'scormid', $scormid, '', '', 'max(attempt) as a')) {
571 if (empty($lastattempt->a)) {
572 return '1';
573 } else {
574 return $lastattempt->a;
579 function scorm_get_last_completed_attempt($scormid, $userid) {
580 /// Find the last attempt number for the given user id and scorm id
581 if ($lastattempt = get_record('scorm_scoes_track', 'userid', $userid, 'scormid', $scormid, 'value', 'completed', 'max(attempt) as a')) {
582 if (empty($lastattempt->a)) {
583 return '1';
584 } else {
585 return $lastattempt->a;
590 function scorm_course_format_display($user,$course) {
591 global $CFG;
593 $strupdate = get_string('update');
594 $strmodule = get_string('modulename','scorm');
595 $context = get_context_instance(CONTEXT_COURSE,$course->id);
597 echo '<div class="mod-scorm">';
598 if ($scorms = get_all_instances_in_course('scorm', $course)) {
599 // The module SCORM activity with the least id is the course
600 $scorm = current($scorms);
601 if (! $cm = get_coursemodule_from_instance('scorm', $scorm->id, $course->id)) {
602 error('Course Module ID was incorrect');
604 $colspan = '';
605 $headertext = '<table width="100%"><tr><td class="title">'.get_string('name').': <b>'.format_string($scorm->name).'</b>';
606 if (has_capability('moodle/course:manageactivities', $context)) {
607 if (isediting($course->id)) {
608 // Display update icon
609 $path = $CFG->wwwroot.'/course';
610 $headertext .= '<span class="commands">'.
611 '<a title="'.$strupdate.'" href="'.$path.'/mod.php?update='.$cm->id.'&amp;sesskey='.sesskey().'">'.
612 '<img src="'.$CFG->pixpath.'/t/edit.gif" class="iconsmall" alt="'.$strupdate.'" /></a></span>';
614 $headertext .= '</td>';
615 // Display report link
616 $trackedusers = get_record('scorm_scoes_track', 'scormid', $scorm->id, '', '', '', '', 'count(distinct(userid)) as c');
617 if ($trackedusers->c > 0) {
618 $headertext .= '<td class="reportlink">'.
619 '<a '.$CFG->frametarget.'" href="'.$CFG->wwwroot.'/mod/scorm/report.php?id='.$cm->id.'">'.
620 get_string('viewallreports','scorm',$trackedusers->c).'</a>';
621 } else {
622 $headertext .= '<td class="reportlink">'.get_string('noreports','scorm');
624 $colspan = ' colspan="2"';
626 $headertext .= '</td></tr><tr><td'.$colspan.'>'.format_text(get_string('summary').':<br />'.$scorm->summary).'</td></tr></table>';
627 print_simple_box($headertext,'','100%');
628 scorm_view_display($user, $scorm, 'view.php?id='.$course->id, $cm, '100%');
629 } else {
630 if (has_capability('moodle/course:update', $context)) {
631 // Create a new activity
632 redirect($CFG->wwwroot.'/course/mod.php?id='.$course->id.'&amp;section=0&sesskey='.sesskey().'&amp;add=scorm');
633 } else {
634 notify('Could not find a scorm course here');
637 echo '</div>';
640 function scorm_view_display ($user, $scorm, $action, $cm, $boxwidth='') {
641 global $CFG;
643 if ($scorm->updatefreq == UPDATE_EVERYTIME){
644 require_once($CFG->dirroot.'/mod/scorm/lib.php');
646 $scorm->instance = $scorm->id;
647 scorm_update_instance($scorm);
650 $organization = optional_param('organization', '', PARAM_INT);
652 print_simple_box_start('center',$boxwidth);
654 <div class="structurehead"><?php print_string('contents','scorm') ?></div>
655 <?php
656 if (empty($organization)) {
657 $organization = $scorm->launch;
659 if ($orgs = get_records_select_menu('scorm_scoes',"scorm='$scorm->id' AND organization='' AND launch=''",'id','id,title')) {
660 if (count($orgs) > 1) {
662 <div class='scorm-center'>
663 <?php print_string('organizations','scorm') ?>
664 <form id='changeorg' method='post' action='<?php echo $action ?>'>
665 <?php choose_from_menu($orgs, 'organization', "$organization", '','submit()') ?>
666 </form>
667 </div>
668 <?php
671 $orgidentifier = '';
672 if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
673 if (($sco->organization == '') && ($sco->launch == '')) {
674 $orgidentifier = $sco->identifier;
675 } else {
676 $orgidentifier = $sco->organization;
681 $orgidentifier = '';
682 if ($org = get_record('scorm_scoes','id',$organization)) {
683 if (($org->organization == '') && ($org->launch == '')) {
684 $orgidentifier = $org->identifier;
685 } else {
686 $orgidentifier = $org->organization;
690 $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR)); // Just to be safe
691 if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
692 $scorm->version = 'scorm_12';
694 require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
696 $result = scorm_get_toc($user,$scorm,'structlist',$orgidentifier);
697 $incomplete = $result->incomplete;
698 echo $result->toc;
699 print_simple_box_end();
702 <div class="scorm-center">
703 <form id="theform" method="post" action="<?php echo $CFG->wwwroot ?>/mod/scorm/player.php">
704 <?php
705 if ($scorm->hidebrowse == 0) {
706 print_string('mode','scorm');
707 echo ': <input type="radio" id="b" name="mode" value="browse" /><label for="b">'.get_string('browse','scorm').'</label>'."\n";
708 echo '<input type="radio" id="n" name="mode" value="normal" checked="checked" /><label for="n">'.get_string('normal','scorm')."</label>\n";
709 } else {
710 echo '<input type="hidden" name="mode" value="normal" />'."\n";
712 if (($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
714 <br />
715 <input type="checkbox" id="a" name="newattempt" />
716 <label for="a"><?php print_string('newattempt','scorm') ?></label>
717 <?php
720 <br />
721 <input type="hidden" name="scoid"/>
722 <input type="hidden" name="id" value="<?php echo $cm->id ?>"/>
723 <input type="hidden" name="currentorg" value="<?php echo $orgidentifier ?>" />
724 <input type="submit" value="<?php print_string('enter','scorm') ?>" />
725 </form>
726 </div>
727 <?php
729 function scorm_simple_play($scorm,$user, $context) {
730 $result = false;
732 if ($scorm->updatefreq == UPDATE_EVERYTIME) {
733 scorm_parse($scorm);
735 if (has_capability('mod/scorm:viewreport', $context)) { //if this user can view reports, don't skipview so they can see links to reports.
736 return $result;
739 $scoes = get_records_select('scorm_scoes','scorm='.$scorm->id.' AND launch<>\''.sql_empty().'\'', 'id', 'id');
741 if ($scoes) {
742 if ($scorm->skipview >= 1) {
743 $sco = current($scoes);
744 if (scorm_get_tracks($sco->id,$user->id) === false) {
745 header('Location: player.php?a='.$scorm->id.'&scoid='.$sco->id);
746 $result = true;
747 } else if ($scorm->skipview == 2) {
748 header('Location: player.php?a='.$scorm->id.'&scoid='.$sco->id);
749 $result = true;
753 return $result;
756 function scorm_parse($scorm) {
757 global $CFG;
759 if ($scorm->reference[0] == '#') {
760 if (isset($CFG->repositoryactivate) && $CFG->repositoryactivate) {
761 $referencedir = $CFG->repository.substr($scorm->reference,1);
763 } else {
764 if ((!scorm_external_link($scorm->reference)) && (basename($scorm->reference) == 'imsmanifest.xml')) {
765 $referencedir = $CFG->dataroot.'/'.$scorm->course.'/'.$scorm->datadir;
766 } else {
767 $referencedir = $CFG->dataroot.'/'.$scorm->course.'/moddata/scorm/'.$scorm->id;
771 // Parse scorm manifest
772 if ($scorm->pkgtype == 'AICC') {
773 require_once('datamodels/aicclib.php');
774 $scorm->launch = scorm_parse_aicc($referencedir, $scorm->id);
775 } else {
776 require_once('datamodels/scormlib.php');
777 $scorm->launch = scorm_parse_scorm($referencedir,$scorm->id);
779 return $scorm->launch;
783 * Given a manifest path, this function will check if the manifest is valid
785 * @param string $manifest The manifest file
786 * @return object
788 function scorm_validate_manifest($manifest) {
789 $validation = new stdClass();
790 if (is_file($manifest)) {
791 $validation->result = true;
792 } else {
793 $validation->result = false;
794 $validation->errors['reference'] = get_string('nomanifest','scorm');
796 return $validation;
800 * Given a aicc package directory, this function will check if the course structure is valid
802 * @param string $packagedir The aicc package directory path
803 * @return object
805 function scorm_validate_aicc($packagedir) {
806 $validation = new stdClass();
807 $validation->result = false;
808 if (is_dir($packagedir)) {
809 if ($handle = opendir($packagedir)) {
810 while (($file = readdir($handle)) !== false) {
811 $ext = substr($file,strrpos($file,'.'));
812 if (strtolower($ext) == '.cst') {
813 $validation->result = true;
814 break;
817 closedir($handle);
820 if ($validation->result == false) {
821 $validation->errors['reference'] = get_string('nomanifest','scorm');
823 return $validation;
827 function scorm_validate($data) {
828 global $CFG;
830 $validation = new stdClass();
831 $validation->errors = array();
833 if (!isset($data['course']) || empty($data['course'])) {
834 $validation->errors['reference'] = get_string('missingparam','scorm');
835 $validation->result = false;
836 return $validation;
838 $courseid = $data['course']; // Course Module ID
840 if (!isset($data['reference']) || empty($data['reference'])) {
841 $validation->errors['reference'] = get_string('packagefile','scorm');
842 $validation->result = false;
843 return $validation;
845 $reference = $data['reference']; // Package/manifest path/location
847 $scormid = $data['instance']; // scorm ID
848 $scorm = new stdClass();
849 if (!empty($scormid)) {
850 if (!$scorm = get_record('scorm','id',$scormid)) {
851 $validation->errors['reference'] = get_string('missingparam','scorm');
852 $validation->result = false;
853 return $validation;
857 if ($reference[0] == '#') {
858 if (isset($CFG->repositoryactivate) && $CFG->repositoryactivate) {
859 $reference = $CFG->repository.substr($reference,1).'/imsmanifest.xml';
860 } else {
861 $validation->errors['reference'] = get_string('badpackage','scorm');
862 $validation->result = false;
863 return $validation;
865 } else if (!scorm_external_link($reference)) {
866 $reference = $CFG->dataroot.'/'.$courseid.'/'.$reference;
869 // Create a temporary directory to unzip package or copy manifest and validate package
870 $tempdir = '';
871 $scormdir = '';
872 if ($scormdir = make_upload_directory("$courseid/$CFG->moddata/scorm")) {
873 if ($tempdir = scorm_tempdir($scormdir)) {
874 $localreference = $tempdir.'/'.basename($reference);
875 copy ("$reference", $localreference);
876 if (!is_file($localreference)) {
877 $validation->errors['reference'] = get_string('badpackage','scorm');
878 $validation->result = false;
879 } else {
880 $ext = strtolower(substr(basename($localreference),strrpos(basename($localreference),'.')));
881 switch ($ext) {
882 case '.pif':
883 case '.zip':
884 if (!unzip_file($localreference, $tempdir, false)) {
885 $validation->errors['reference'] = get_string('unziperror','scorm');
886 $validation->result = false;
887 } else {
888 unlink ($localreference);
889 if (is_file($tempdir.'/imsmanifest.xml')) {
890 $validation = scorm_validate_manifest($tempdir.'/imsmanifest.xml');
891 $validation->pkgtype = 'SCORM';
892 } else {
893 $validation = scorm_validate_aicc($tempdir);
894 if (($validation->result == 'regular') || ($validation->result == 'found')) {
895 $validation->pkgtype = 'AICC';
896 } else {
897 $validation->errors['reference'] = get_string('nomanifest','scorm');
898 $validation->result = false;
902 break;
903 case '.xml':
904 if (basename($localreference) == 'imsmanifest.xml') {
905 $validation = scorm_validate_manifest($localreference);
906 } else {
907 $validation->errors['reference'] = get_string('nomanifest','scorm');
908 $validation->result = false;
910 break;
911 default:
912 $validation->errors['reference'] = get_string('badpackage','scorm');
913 $validation->result = false;
914 break;
917 if (is_dir($tempdir)) {
918 // Delete files and temporary directory
919 scorm_delete_files($tempdir);
921 } else {
922 $validation->errors['reference'] = get_string('packagedir','scorm');
923 $validation->result = false;
925 } else {
926 $validation->errors['reference'] = get_string('datadir','scorm');
927 $validation->result = false;
929 return $validation;
932 function scorm_check_package($data) {
933 global $CFG, $COURSE;
935 require_once($CFG->libdir.'/filelib.php');
937 $courseid = $data->course; // Course Module ID
938 $reference = $data->reference; // Package path
939 $scormid = $data->instance; // scorm ID
941 $validation = new stdClass();
943 if (!empty($courseid) && !empty($reference)) {
944 $externalpackage = scorm_external_link($reference);
946 $validation->launch = 0;
947 $referencefield = $reference;
948 if (empty($reference)) {
949 $validation = null;
950 } else if ($reference[0] == '#') {
951 if (isset($CFG->repositoryactivate) && $CFG->repositoryactivate) {
952 $referencefield = $reference.'/imsmanifest.xml';
953 $reference = $CFG->repository.substr($reference,1).'/imsmanifest.xml';
954 } else {
955 $validation = null;
957 } else if (!$externalpackage) {
958 $reference = $CFG->dataroot.'/'.$courseid.'/'.$reference;
961 if (!empty($scormid)) {
963 // SCORM Update
965 if ((!empty($validation)) && (is_file($reference) || $externalpackage)){
967 if (!$externalpackage) {
968 $mdcheck = md5_file($reference);
969 } else if ($externalpackage){
970 if ($scormdir = make_upload_directory("$courseid/$CFG->moddata/scorm")) {
971 if ($tempdir = scorm_tempdir($scormdir)) {
972 $content = download_file_content($reference);
973 $file = fopen($tempdir.'/'.basename($reference), 'x');
974 fwrite($file, $content);
975 fclose($file);
976 $mdcheck = md5_file($tempdir.'/'.basename($reference));
977 scorm_delete_files($tempdir);
982 if ($scorm = get_record('scorm','id',$scormid)) {
983 if ($scorm->reference[0] == '#') {
984 if (isset($CFG->repositoryactivate) && $CFG->repositoryactivate) {
985 $oldreference = $CFG->repository.substr($scorm->reference,1).'/imsmanifest.xml';
986 } else {
987 $oldreference = $scorm->reference;
989 } else if (!scorm_external_link($scorm->reference)) {
990 $oldreference = $CFG->dataroot.'/'.$courseid.'/'.$scorm->reference;
991 } else {
992 $oldreference = $scorm->reference;
994 $validation->launch = $scorm->launch;
995 if ((($oldreference == $reference) && ($mdcheck != $scorm->md5hash)) || ($oldreference != $reference)) {
996 // This is a new or a modified package
997 $validation->launch = 0;
998 } else {
999 // Old package already validated
1000 if (strpos($scorm->version,'AICC') !== false) {
1001 $validation->pkgtype = 'AICC';
1002 } else {
1003 $validation->pkgtype = 'SCORM';
1006 } else {
1007 $validation = null;
1009 } else {
1010 $validation = null;
1013 //$validation->launch = 0;
1014 if (($validation != null) && ($validation->launch == 0)) {
1016 // Package must be validated
1018 $ext = strtolower(substr(basename($reference),strrpos(basename($reference),'.')));
1019 $tempdir = '';
1020 switch ($ext) {
1021 case '.pif':
1022 case '.zip':
1023 // Create a temporary directory to unzip package and validate package
1024 $scormdir = '';
1025 if ($scormdir = make_upload_directory("$courseid/$CFG->moddata/scorm")) {
1026 if ($tempdir = scorm_tempdir($scormdir)) {
1027 if ($externalpackage){
1028 $content = download_file_content($reference);
1029 $file = fopen($tempdir.'/'.basename($reference), 'x');
1030 fwrite($file, $content);
1031 fclose($file);
1032 } else {
1033 copy ("$reference", $tempdir.'/'.basename($reference));
1035 unzip_file($tempdir.'/'.basename($reference), $tempdir, false);
1036 if (!$externalpackage) {
1037 unlink ($tempdir.'/'.basename($reference));
1039 if (is_file($tempdir.'/imsmanifest.xml')) {
1040 $validation = scorm_validate_manifest($tempdir.'/imsmanifest.xml');
1041 $validation->pkgtype = 'SCORM';
1042 } else {
1043 $validation = scorm_validate_aicc($tempdir);
1044 $validation->pkgtype = 'AICC';
1046 } else {
1047 $validation = null;
1049 } else {
1050 $validation = null;
1052 break;
1053 case '.xml':
1054 if (basename($reference) == 'imsmanifest.xml') {
1055 if ($externalpackage) {
1056 if ($scormdir = make_upload_directory("$courseid/$CFG->moddata/scorm")) {
1057 if ($tempdir = scorm_tempdir($scormdir)) {
1058 $content = download_file_content($reference);
1059 $file = fopen($tempdir.'/'.basename($reference), 'x');
1060 fwrite($file, $content);
1061 fclose($file);
1062 if (is_file($tempdir.'/'.basename($reference))) {
1063 $validation = scorm_validate_manifest($tempdir.'/'.basename($reference));
1064 } else {
1065 $validation = null;
1069 } else {
1070 $validation = scorm_validate_manifest($reference);
1072 $validation->pkgtype = 'SCORM';
1073 } else {
1074 $validation = null;
1076 break;
1077 default:
1078 $validation = null;
1079 break;
1081 if ($validation == null) {
1082 if (is_dir($tempdir)) {
1083 // Delete files and temporary directory
1084 scorm_delete_files($tempdir);
1086 } else {
1087 if (($ext == '.xml') && (!$externalpackage)) {
1088 $validation->datadir = dirname($referencefield);
1089 } else {
1090 $validation->datadir = substr($tempdir,strlen($scormdir));
1092 $validation->launch = 0;
1095 } else {
1096 $validation = null;
1098 return $validation;
1102 function scorm_get_count_users($scormid, $groupingid=null) {
1104 global $CFG;
1106 if (!empty($CFG->enablegroupings) && !empty($groupingid)) {
1107 $sql = "SELECT COUNT(DISTINCT st.userid)
1108 FROM {$CFG->prefix}scorm_scoes_track st
1109 INNER JOIN {$CFG->prefix}groups_members gm ON st.userid = gm.userid
1110 INNER JOIN {$CFG->prefix}groupings_groups gg ON gm.groupid = gg.groupid
1111 WHERE st.scormid = $scormid AND gg.groupingid = $groupingid
1113 } else {
1114 $sql = "SELECT COUNT(DISTINCT st.userid)
1115 FROM {$CFG->prefix}scorm_scoes_track st
1116 WHERE st.scormid = $scormid
1120 return(count_records_sql($sql));
1124 * Build up the JavaScript representation of an array element
1126 * @param string $sversion SCORM API version
1127 * @param array $userdata User track data
1128 * @param string $element_name Name of array element to get values for
1129 * @param array $children list of sub elements of this array element that also need instantiating
1130 * @return None
1132 function scorm_reconstitute_array_element($sversion, $userdata, $element_name, $children) {
1133 // reconstitute comments_from_learner and comments_from_lms
1134 $current = '';
1135 $current_subelement = '';
1136 $current_sub = '';
1137 $count = 0;
1138 $count_sub = 0;
1139 $scormseperator = '_';
1140 if ($sversion == 'scorm_13') { //scorm 1.3 elements use a . instead of an _
1141 $scormseperator = '.';
1143 // filter out the ones we want
1144 $element_list = array();
1145 foreach($userdata as $element => $value){
1146 if (substr($element,0,strlen($element_name)) == $element_name) {
1147 $element_list[$element] = $value;
1151 // sort elements in .n array order
1152 uksort($element_list, "scorm_element_cmp");
1154 // generate JavaScript
1155 foreach($element_list as $element => $value){
1156 if ($sversion == 'scorm_13') {
1157 $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
1158 preg_match('/\.(N\d+)\./', $element, $matches);
1159 } else {
1160 $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
1161 preg_match('/\_(\d+)\./', $element, $matches);
1163 if (count($matches) > 0 && $current != $matches[1]) {
1164 if ($count_sub > 0) {
1165 echo ' '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
1167 $current = $matches[1];
1168 $count++;
1169 $current_subelement = '';
1170 $current_sub = '';
1171 $count_sub = 0;
1172 $end = strpos($element,$matches[1])+strlen($matches[1]);
1173 $subelement = substr($element,0,$end);
1174 echo ' '.$subelement." = new Object();\n";
1175 // now add the children
1176 foreach ($children as $child) {
1177 echo ' '.$subelement.".".$child." = new Object();\n";
1178 echo ' '.$subelement.".".$child."._children = ".$child."_children;\n";
1182 // now - flesh out the second level elements if there are any
1183 if ($sversion == 'scorm_13') {
1184 $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
1185 preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
1186 } else {
1187 $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
1188 preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
1191 // check the sub element type
1192 if (count($matches) > 0 && $current_subelement != $matches[1]) {
1193 if ($count_sub > 0) {
1194 echo ' '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
1196 $current_subelement = $matches[1];
1197 $current_sub = '';
1198 $count_sub = 0;
1199 $end = strpos($element,$matches[1])+strlen($matches[1]);
1200 $subelement = substr($element,0,$end);
1201 echo ' '.$subelement." = new Object();\n";
1204 // now check the subelement subscript
1205 if (count($matches) > 0 && $current_sub != $matches[2]) {
1206 $current_sub = $matches[2];
1207 $count_sub++;
1208 $end = strrpos($element,$matches[2])+strlen($matches[2]);
1209 $subelement = substr($element,0,$end);
1210 echo ' '.$subelement." = new Object();\n";
1213 echo ' '.$element.' = \''.$value."';\n";
1215 if ($count_sub > 0) {
1216 echo ' '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
1218 if ($count > 0) {
1219 echo ' '.$element_name.'._count = '.$count.";\n";
1224 * Build up the JavaScript representation of an array element
1226 * @param string $a left array element
1227 * @param string $b right array element
1228 * @return comparator - 0,1,-1
1230 function scorm_element_cmp($a, $b) {
1231 preg_match('/.*?(\d+)\./', $a, $matches);
1232 $left = intval($matches[1]);
1233 preg_match('/.?(\d+)\./', $b, $matches);
1234 $right = intval($matches[1]);
1235 if ($left < $right) {
1236 return -1; // smaller
1237 } elseif ($left > $right) {
1238 return 1; // bigger
1239 } else {
1240 // look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern
1241 if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
1242 $leftterm = intval($matches[2]);
1243 $left = intval($matches[3]);
1244 if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
1245 $rightterm = intval($matches[2]);
1246 $right = intval($matches[3]);
1247 if ($leftterm < $rightterm) {
1248 return -1; // smaller
1249 } elseif ($leftterm > $rightterm) {
1250 return 1; // bigger
1251 } else {
1252 if ($left < $right) {
1253 return -1; // smaller
1254 } elseif ($left > $right) {
1255 return 1; // bigger
1260 // fall back for no second level matches or second level matches are equal
1261 return 0; // equal to
1266 * Delete Scorm tracks for selected users
1268 * @param array $attemptids list of attempts that need to be deleted
1269 * @param int $scormid ID of Scorm
1271 * return bool true deleted all responses, false failed deleting an attempt - stopped here
1273 function scorm_delete_responses($attemptids, $scormid) {
1274 if(!is_array($attemptids) || empty($attemptids)) {
1275 return false;
1278 foreach($attemptids as $num => $attemptid) {
1279 if(empty($attemptid)) {
1280 unset($attemptids[$num]);
1284 foreach($attemptids as $attempt) {
1285 $keys = explode(':', $attempt);
1286 if (count($keys) == 2) {
1287 $userid = clean_param($keys[0], PARAM_INT);
1288 $attemptid = clean_param($keys[1], PARAM_INT);
1289 if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scormid, $attemptid)) {
1290 return false;
1292 } else {
1293 return false;
1296 return true;
1300 * Delete Scorm tracks for selected users
1302 * @param int $userid ID of User
1303 * @param int $scormid ID of Scorm
1304 * @param int $attemptid user attempt that need to be deleted
1306 * return bool true suceeded
1308 function scorm_delete_attempt($userid, $scormid, $attemptid) {
1309 delete_records('scorm_scoes_track', 'userid', $userid, 'scormid', $scormid, 'attempt', $attemptid);
1310 return true;
1314 * Converts SCORM date/time notation to human-readable format
1315 * The function works with both SCORM 1.2 and SCORM 2004 time formats
1316 * @param $datetime string SCORM date/time
1317 * @return string human-readable date/time
1319 function scorm_format_date_time($datetime) {
1320 // fetch date/time strings
1321 $stryears = get_string('numyears');
1322 $strmonths = get_string('nummonths');
1323 $strdays = get_string('numdays');
1324 $strhours = get_string('numhours');
1325 $strminutes = get_string('numminutes');
1326 $strseconds = get_string('numseconds');
1328 if ($datetime[0] == 'P') {
1329 // if timestamp starts with 'P' - it's a SCORM 2004 format
1330 // this regexp discards empty sections, takes Month/Minute ambiguity into consideration,
1331 // and outputs filled sections, discarding leading zeroes and any format literals
1332 // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero
1333 $pattern = array( '#([A-Z])0+Y#', '#([A-Z])0+M#', '#([A-Z])0+D#', '#P(|\d+Y)0*(\d+)M#', '#0*(\d+)Y#', '#0*(\d+)D#', '#P#',
1334 '#([A-Z])0+H#', '#([A-Z])[0.]+S#', '#\.0+S#', '#T(|\d+H)0*(\d+)M#', '#0*(\d+)H#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' );
1335 $replace = array( '$1', '$1', '$1', '$1$2'.$strmonths.' ', '$1'.$stryears.' ', '$1'.$strdays.' ', '',
1336 '$1', '$1', 'S', '$1$2'.$strminutes.' ', '$1'.$strhours.' ', '0.$1'.$strseconds, '$1'.$strseconds, '');
1337 } else {
1338 // else we have SCORM 1.2 format there
1339 // first convert the timestamp to some SCORM 2004-like format for conveniency
1340 $datetime = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $datetime);
1341 // then convert in the same way as SCORM 2004
1342 $pattern = array( '#T0+H#', '#([A-Z])0+M#', '#([A-Z])[0.]+S#', '#\.0+S#', '#0*(\d+)H#', '#0*(\d+)M#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' );
1343 $replace = array( 'T', '$1', '$1', 'S', '$1'.$strhours.' ', '$1'.$strminutes.' ', '0.$1'.$strseconds, '$1'.$strseconds, '' );
1344 //$pattern = '##';
1345 //$replace = '';
1348 $result = preg_replace($pattern, $replace, $datetime);
1350 return $result;