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');
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');
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(){
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
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'));
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'));
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'));
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'));
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'));
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);
125 * This function will permanently delete the given
126 * directory and all files and subdirectories.
128 * @param string $directory The directory to remove
131 function scorm_delete_files($directory) {
132 if (is_dir($directory)) {
133 $files=scorm_scandir($directory);
135 foreach($files as $file) {
136 if (($file != '.') && ($file != '..')) {
137 if (!is_dir($directory.'/'.$file)) {
138 unlink($directory.'/'.$file);
140 scorm_delete_files($directory.'/'.$file);
151 * Given a diretory path returns the file list
153 * @param string $directory
156 function scorm_scandir($directory) {
157 if (version_compare(phpversion(),'5.0.0','>=')) {
158 return scandir($directory);
161 if ($dh = opendir($directory)) {
162 while (($file = readdir($dh)) !== false) {
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)
181 if (is_dir($strPath)) {
183 // Create a random string of 8 chars
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;
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;
206 function scorm_array_search($item, $needle, $haystacks, $strict=false) {
207 if (!empty($haystacks)) {
208 foreach ($haystacks as $key => $element) {
210 if ($element->{$item} === $needle) {
214 if ($element->{$item} == $needle) {
223 function scorm_repeater($what, $times) {
228 for ($i=0; $i<$times;$i++
) {
234 function scorm_external_link($link) {
235 // check if a link is external
237 $link = strtolower($link);
238 if (substr($link,0,7) == 'http://') {
240 } else if (substr($link,0,8) == 'https://') {
242 } else if (substr($link,0,4) == 'www.') {
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
= '';
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
);
300 function scorm_insert_track($userid,$scormid,$scoid,$attempt,$element,$value) {
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);
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);
330 function scorm_get_tracks($scoid,$userid,$attempt='') {
331 /// Gets all tracks of specified sco and user
334 if (empty($attempt)) {
335 if ($scormid = get_field('scorm_scoes','scorm','id',$scoid)) {
336 $attempt = scorm_get_last_attempt($scormid,$userid);
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
;
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
;
363 case 'cmi.core.score.raw':
364 case 'cmi.score.raw':
365 $usertrack->score_raw
= (float) sprintf('%2.2f', $track->value
);
367 case 'cmi.core.session_time':
368 case 'cmi.session_time':
369 $usertrack->session_time
= $track->value
;
371 case 'cmi.core.total_time':
372 case 'cmi.total_time':
373 $usertrack->total_time
= $track->value
;
376 if (isset($track->timemodified
) && ($track->timemodified
> $usertrack->timemodified
)) {
377 $usertrack->timemodified
= $track->timemodified
;
380 if (is_array($usertrack)) {
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");
405 $tracks = array_values($tracks);
409 $timedata->start
= $tracks[0]->timemodified
;
412 $timedata->start
= false;
414 if ($tracks && $track = array_pop($tracks)) {
415 $timedata->finish
= $track->timemodified
;
418 $timedata->finish
= $timedata->start
;
424 function scorm_get_user_data($userid) {
425 /// Gets user info required to display the table of scorm results
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
)) {
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
;
455 $attemptscore->lastmodify
= 0;
460 switch ($scorm->grademethod
) {
462 $score = (float) $attemptscore->max
;
465 if ($attemptscore->values
> 0) {
466 $score = $attemptscore->sum
/$attemptscore->values
;
472 $score = $attemptscore->sum
;
475 $score = $attemptscore->scoes
;
478 $score = $attemptscore->max
; // Remote Learner GRADEHIGHEST is default
482 $result = new stdClass();
483 $result->score
= $score;
484 $result->time
= $attemptscore->lastmodify
;
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
) {
502 return scorm_grade_user_attempt($scorm, $userid, 1, $time);
505 return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id
, $userid), $time);
510 for ($attempt = 1; $attempt <= $lastattempt; $attempt++
) {
511 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt, $time);
513 if ($attemptscore->score
> $maxscore) {
514 $maxscore = $attemptscore->score
;
515 $attempttime = $attemptscore->time
;
518 $maxscore = $attemptscore > $maxscore ?
$attemptscore: $maxscore;
522 $result = new stdClass();
523 $result->score
= $maxscore;
524 $result->time
= $attempttime;
531 $lastattempt = scorm_get_last_attempt($scorm->id
, $userid);
533 for ($attempt = 1; $attempt <= $lastattempt; $attempt++
) {
534 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt, $time);
536 $sumscore +
= $attemptscore->score
;
538 $sumscore +
= $attemptscore;
542 if ($lastattempt > 0) {
543 $score = $sumscore / $lastattempt;
549 $result = new stdClass();
550 $result->score
= $score;
551 $result->time
= $attemptscore->time
;
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
)) {
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
)) {
585 return $lastattempt->a
;
590 function scorm_course_format_display($user,$course) {
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');
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
.'&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>';
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%');
630 if (has_capability('moodle/course:update', $context)) {
631 // Create a new activity
632 redirect($CFG->wwwroot
.'/course/mod.php?id='.$course->id
.'&section=0&sesskey='.sesskey().'&add=scorm');
634 notify('Could not find a scorm course here');
640 function scorm_view_display ($user, $scorm, $action, $cm, $boxwidth='') {
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
>
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()') ?
>
672 if ($sco = scorm_get_sco($organization, SCO_ONLY
)) {
673 if (($sco->organization
== '') && ($sco->launch
== '')) {
674 $orgidentifier = $sco->identifier
;
676 $orgidentifier = $sco->organization
;
682 if ($org = get_record('scorm_scoes','id',$organization)) {
683 if (($org->organization == '') && ($org->launch == '')) {
684 $orgidentifier = $org->identifier;
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
;
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">
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";
710 echo '<input type="hidden" name="mode" value="normal" />'."\n";
712 if (($incomplete === false) && (($result->attemptleft
> 0)||
($scorm->maxattempt
== 0))) {
715 <input type
="checkbox" id
="a" name
="newattempt" />
716 <label
for="a"><?php
print_string('newattempt','scorm') ?
></label
>
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') ?>" />
729 function scorm_simple_play($scorm,$user, $context) {
732 if ($scorm->updatefreq
== UPDATE_EVERYTIME
) {
735 if (has_capability('mod/scorm:viewreport', $context)) { //if this user can view reports, don't skipview so they can see links to reports.
739 $scoes = get_records_select('scorm_scoes','scorm='.$scorm->id
.' AND launch<>\''.sql_empty().'\'', 'id', 'id');
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
);
747 } else if ($scorm->skipview
== 2) {
748 header('Location: player.php?a='.$scorm->id
.'&scoid='.$sco->id
);
756 function scorm_parse($scorm) {
759 if ($scorm->reference
[0] == '#') {
760 if (isset($CFG->repositoryactivate
) && $CFG->repositoryactivate
) {
761 $referencedir = $CFG->repository
.substr($scorm->reference
,1);
764 if ((!scorm_external_link($scorm->reference
)) && (basename($scorm->reference
) == 'imsmanifest.xml')) {
765 $referencedir = $CFG->dataroot
.'/'.$scorm->course
.'/'.$scorm->datadir
;
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
);
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
788 function scorm_validate_manifest($manifest) {
789 $validation = new stdClass();
790 if (is_file($manifest)) {
791 $validation->result
= true;
793 $validation->result
= false;
794 $validation->errors
['reference'] = get_string('nomanifest','scorm');
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
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;
820 if ($validation->result
== false) {
821 $validation->errors
['reference'] = get_string('nomanifest','scorm');
827 function scorm_validate($data) {
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;
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;
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;
857 if ($reference[0] == '#') {
858 if (isset($CFG->repositoryactivate
) && $CFG->repositoryactivate
) {
859 $reference = $CFG->repository
.substr($reference,1).'/imsmanifest.xml';
861 $validation->errors
['reference'] = get_string('badpackage','scorm');
862 $validation->result
= false;
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
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;
880 $ext = strtolower(substr(basename($localreference),strrpos(basename($localreference),'.')));
884 if (!unzip_file($localreference, $tempdir, false)) {
885 $validation->errors
['reference'] = get_string('unziperror','scorm');
886 $validation->result
= false;
888 unlink ($localreference);
889 if (is_file($tempdir.'/imsmanifest.xml')) {
890 $validation = scorm_validate_manifest($tempdir.'/imsmanifest.xml');
891 $validation->pkgtype
= 'SCORM';
893 $validation = scorm_validate_aicc($tempdir);
894 if (($validation->result
== 'regular') ||
($validation->result
== 'found')) {
895 $validation->pkgtype
= 'AICC';
897 $validation->errors
['reference'] = get_string('nomanifest','scorm');
898 $validation->result
= false;
904 if (basename($localreference) == 'imsmanifest.xml') {
905 $validation = scorm_validate_manifest($localreference);
907 $validation->errors
['reference'] = get_string('nomanifest','scorm');
908 $validation->result
= false;
912 $validation->errors
['reference'] = get_string('badpackage','scorm');
913 $validation->result
= false;
917 if (is_dir($tempdir)) {
918 // Delete files and temporary directory
919 scorm_delete_files($tempdir);
922 $validation->errors
['reference'] = get_string('packagedir','scorm');
923 $validation->result
= false;
926 $validation->errors
['reference'] = get_string('datadir','scorm');
927 $validation->result
= false;
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)) {
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';
957 } else if (!$externalpackage) {
958 $reference = $CFG->dataroot
.'/'.$courseid.'/'.$reference;
961 if (!empty($scormid)) {
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);
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';
987 $oldreference = $scorm->reference
;
989 } else if (!scorm_external_link($scorm->reference
)) {
990 $oldreference = $CFG->dataroot
.'/'.$courseid.'/'.$scorm->reference
;
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;
999 // Old package already validated
1000 if (strpos($scorm->version
,'AICC') !== false) {
1001 $validation->pkgtype
= 'AICC';
1003 $validation->pkgtype
= 'SCORM';
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),'.')));
1023 // Create a temporary directory to unzip package and validate package
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);
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';
1043 $validation = scorm_validate_aicc($tempdir);
1044 $validation->pkgtype
= 'AICC';
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);
1062 if (is_file($tempdir.'/'.basename($reference))) {
1063 $validation = scorm_validate_manifest($tempdir.'/'.basename($reference));
1070 $validation = scorm_validate_manifest($reference);
1072 $validation->pkgtype
= 'SCORM';
1081 if ($validation == null) {
1082 if (is_dir($tempdir)) {
1083 // Delete files and temporary directory
1084 scorm_delete_files($tempdir);
1087 if (($ext == '.xml') && (!$externalpackage)) {
1088 $validation->datadir
= dirname($referencefield);
1090 $validation->datadir
= substr($tempdir,strlen($scormdir));
1092 $validation->launch
= 0;
1102 function scorm_get_count_users($scormid, $groupingid=null) {
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
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
1132 function scorm_reconstitute_array_element($sversion, $userdata, $element_name, $children) {
1133 // reconstitute comments_from_learner and comments_from_lms
1135 $current_subelement = '';
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);
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];
1169 $current_subelement = '';
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);
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];
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];
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";
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) {
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) {
1252 if ($left < $right) {
1253 return -1; // smaller
1254 } elseif ($left > $right) {
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)) {
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)) {
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);
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, '');
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, '' );
1348 $result = preg_replace($pattern, $replace, $datetime);