MDL-25708 recordsets - found one more recent use. fixed.
[moodle.git] / mod / scorm / locallib.php
1 <?php
3 require_once("$CFG->dirroot/mod/scorm/lib.php");
4 require_once("$CFG->libdir/filelib.php");
6 /// Constants and settings for module scorm
7 define('UPDATE_NEVER', '0');
8 define('UPDATE_ONCHANGE', '1');
9 define('UPDATE_EVERYDAY', '2');
10 define('UPDATE_EVERYTIME', '3');
12 define('SCO_ALL', 0);
13 define('SCO_DATA', 1);
14 define('SCO_ONLY', 2);
16 define('GRADESCOES', '0');
17 define('GRADEHIGHEST', '1');
18 define('GRADEAVERAGE', '2');
19 define('GRADESUM', '3');
21 define('HIGHESTATTEMPT', '0');
22 define('AVERAGEATTEMPT', '1');
23 define('FIRSTATTEMPT', '2');
24 define('LASTATTEMPT', '3');
26 /// Local Library of functions for module scorm
28 /**
29  * @package   mod-scorm
30  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
31  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32  */
33 class scorm_package_file_info extends file_info_stored {
34     public function get_parent() {
35         if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
36             return $this->browser->get_file_info($this->context);
37         }
38         return parent::get_parent();
39     }
40     public function get_visible_name() {
41         if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
42             return $this->topvisiblename;
43         }
44         return parent::get_visible_name();
45     }
46 }
48 /**
49  * Returns an array of the popup options for SCORM and each options default value
50  *
51  * @return array an array of popup options as the key and their defaults as the value
52  */
53 function scorm_get_popup_options_array(){
54     global $CFG;
55     $cfg_scorm = get_config('scorm');
57     return array('resizable'=> isset($cfg_scorm->resizable) ? $cfg_scorm->resizable : 0,
58                  'scrollbars'=> isset($cfg_scorm->scrollbars) ? $cfg_scorm->scrollbars : 0,
59                  'directories'=> isset($cfg_scorm->directories) ? $cfg_scorm->directories : 0,
60                  'location'=> isset($cfg_scorm->location) ? $cfg_scorm->location : 0,
61                  'menubar'=> isset($cfg_scorm->menubar) ? $cfg_scorm->menubar : 0,
62                  'toolbar'=> isset($cfg_scorm->toolbar) ? $cfg_scorm->toolbar : 0,
63                  'status'=> isset($cfg_scorm->status) ? $cfg_scorm->status : 0);
64 }
66 /**
67  * Returns an array of the array of what grade options
68  *
69  * @return array an array of what grade options
70  */
71 function scorm_get_grade_method_array(){
72     return array (GRADESCOES => get_string('gradescoes', 'scorm'),
73                   GRADEHIGHEST => get_string('gradehighest', 'scorm'),
74                   GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
75                   GRADESUM => get_string('gradesum', 'scorm'));
76 }
78 /**
79  * Returns an array of the array of what grade options
80  *
81  * @return array an array of what grade options
82  */
83 function scorm_get_what_grade_array(){
84     return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
85                   AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
86                   FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
87                   LASTATTEMPT => get_string('lastattempt', 'scorm'));
88 }
90 /**
91  * Returns an array of the array of skip view options
92  *
93  * @return array an array of skip view options
94  */
95 function scorm_get_skip_view_array(){
96    return array(0 => get_string('never'),
97                  1 => get_string('firstaccess','scorm'),
98                  2 => get_string('always'));
99 }
101 /**
102  * Returns an array of the array of hide table of contents options
103  *
104  * @return array an array of hide table of contents options
105  */
106 function scorm_get_hidetoc_array(){
107      return array(0 =>get_string('sided','scorm'),
108                   1 => get_string('hidden','scorm'),
109                   2 => get_string('popupmenu','scorm'));
112 /**
113  * Returns an array of the array of update frequency options
114  *
115  * @return array an array of update frequency options
116  */
117 function scorm_get_updatefreq_array(){
118     return array(0 => get_string('never'),
119                  1 => get_string('onchanges','scorm'),
120                  2 => get_string('everyday','scorm'),
121                  3 => get_string('everytime','scorm'));
124 /**
125  * Returns an array of the array of popup display options
126  *
127  * @return array an array of popup display options
128  */
129 function scorm_get_popup_display_array(){
130     return array(0 => get_string('currentwindow', 'scorm'),
131                  1 => get_string('popup', 'scorm'));
134 /**
135  * Returns an array of the array of attempt options
136  *
137  * @return array an array of attempt options
138  */
139 function scorm_get_attempts_array(){
140     $attempts = array(0 => get_string('nolimit','scorm'),
141                       1 => get_string('attempt1','scorm'));
143     for ($i=2; $i<=6; $i++) {
144         $attempts[$i] = get_string('attemptsx','scorm', $i);
145     }
147     return $attempts;
149 /**
150  * Extracts scrom package, sets up all variables.
151  * Called whenever scorm changes
152  * @param object $scorm instance - fields are updated and changes saved into database
153  * @param bool $full force full update if true
154  * @return void
155  */
156 function scorm_parse($scorm, $full) {
157     global $CFG, $DB;
158     $cfg_scorm = get_config('scorm');
160     if (!isset($scorm->cmid)) {
161         $cm = get_coursemodule_from_instance('scorm', $scorm->id);
162         $scorm->cmid = $cm->id;
163     }
164     $context = get_context_instance(CONTEXT_MODULE, $scorm->cmid);
165     $newhash = $scorm->sha1hash;
167     if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
169         $fs = get_file_storage();
170         $packagefile = false;
172         if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
173             if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
174                 $newhash = $packagefile->get_contenthash();
175             } else {
176                 $newhash = null;
177             }
178         } else {
179             if (!$cfg_scorm->allowtypelocalsync) {
180                 // sorry - localsync disabled
181                 return;
182             }
183             if ($scorm->reference !== '' and (!$full or $scorm->sha1hash !== sha1($scorm->reference))) {
184                 $fs->delete_area_files($context->id, 'mod_scorm', 'package');
185                 $file_record = array('contextid'=>$context->id, 'component'=>'mod_scorm', 'filearea'=>'package', 'itemid'=>0, 'filepath'=>'/');
186                 if ($packagefile = $fs->create_file_from_url($file_record, $scorm->reference)) {
187                     $newhash = sha1($scorm->reference);
188                 } else {
189                     $newhash = null;
190                 }
191             }
192         }
194         if ($packagefile) {
195             if (!$full and $packagefile and $scorm->sha1hash === $newhash) {
196                 if (strpos($scorm->version, 'SCORM') !== false) {
197                     if ($fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
198                         // no need to update
199                         return;
200                     }
201                 } else if (strpos($scorm->version, 'AICC') !== false) {
202                     // TODO: add more sanity checks - something really exists in scorm_content area
203                     return;
204                 }
205             }
207             // now extract files
208             $fs->delete_area_files($context->id, 'mod_scorm', 'content');
210             $packer = get_file_packer('application/zip');
211             $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/');
213         } else if (!$full) {
214             return;
215         }
218         if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
219             require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
220             // SCORM
221             if (!scorm_parse_scorm($scorm, $manifest)) {
222                 $scorm->version = 'ERROR';
223             }
224         } else {
225             require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
226             // AICC
227             if (!scorm_parse_aicc($scorm)) {
228                 $scorm->version = 'ERROR';
229             }
230         }
232     } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfg_scorm->allowtypeexternal) {
233         if (!$full and $scorm->sha1hash === sha1($scorm->reference)) {
234             return;
235         }
236         require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
237         // SCORM only, AICC can not be external
238         if (!scorm_parse_scorm($scorm, $scorm->reference)) {
239             $scorm->version = 'ERROR';
240         }
241         $newhash = sha1($scorm->reference);
243     } else if ($scorm->scormtype === SCORM_TYPE_IMSREPOSITORY and !empty($CFG->repositoryactivate) and $cfg_scorm->allowtypeimsrepository) {
244         if (!$full and $scorm->sha1hash === sha1($scorm->reference)) {
245             return;
246         }
247         require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
248         if (!scorm_parse_scorm($scorm, $CFG->repository.substr($scorm->reference,1).'/imsmanifest.xml')) {
249             $scorm->version = 'ERROR';
250         }
251         $newhash = sha1($scorm->reference);
253     } else {
254         // sorry, disabled type
255         return;
256     }
258     $scorm->revision++;
259     $scorm->sha1hash = $newhash;
260     $DB->update_record('scorm', $scorm);
264 function scorm_array_search($item, $needle, $haystacks, $strict=false) {
265     if (!empty($haystacks)) {
266         foreach ($haystacks as $key => $element) {
267             if ($strict) {
268                 if ($element->{$item} === $needle) {
269                     return $key;
270                 }
271             } else {
272                 if ($element->{$item} == $needle) {
273                     return $key;
274                 }
275             }
276         }
277     }
278     return false;
281 function scorm_repeater($what, $times) {
282     if ($times <= 0) {
283         return null;
284     }
285     $return = '';
286     for ($i=0; $i<$times;$i++) {
287         $return .= $what;
288     }
289     return $return;
292 function scorm_external_link($link) {
293 // check if a link is external
294     $result = false;
295     $link = strtolower($link);
296     if (substr($link,0,7) == 'http://') {
297         $result = true;
298     } else if (substr($link,0,8) == 'https://') {
299         $result = true;
300     } else if (substr($link,0,4) == 'www.') {
301         $result = true;
302     }
303     return $result;
306 /**
307 * Returns an object containing all datas relative to the given sco ID
309 * @param integer $id The sco ID
310 * @return mixed (false if sco id does not exists)
311 */
313 function scorm_get_sco($id,$what=SCO_ALL) {
314     global $DB;
316     if ($sco = $DB->get_record('scorm_scoes', array('id'=>$id))) {
317         $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
318         if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id)))) {
319             foreach ($scodatas as $scodata) {
320                 $sco->{$scodata->name} = $scodata->value;
321             }
322         } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id))))) {
323             $sco->parameters = '';
324         }
325         return $sco;
326     } else {
327         return false;
328     }
331 /**
332 * Returns an object (array) containing all the scoes data related to the given sco ID
334 * @param integer $id The sco ID
335 * @param integer $organisation an organisation ID - defaults to false if not required
336 * @return mixed (false if there are no scoes or an array)
337 */
339 function scorm_get_scoes($id,$organisation=false) {
340     global $DB;
342     $organizationsql = '';
343     $queryarray = array('scorm'=>$id);
344     if (!empty($organisation)) {
345         $queryarray['organization'] = $organisation;
346     }
347     if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'id ASC')) {
348         // drop keys so that it is a simple array as expected
349         $scoes = array_values($scoes);
350         foreach ($scoes as $sco) {
351             if ($scodatas = $DB->get_records('scorm_scoes_data',array('scoid'=>$sco->id))) {
352                 foreach ($scodatas as $scodata) {
353                     $sco->{$scodata->name} = $scodata->value;
354                 }
355             }
356         }
357         return $scoes;
358     } else {
359         return false;
360     }
363 function scorm_insert_track($userid,$scormid,$scoid,$attempt,$element,$value,$forcecompleted=false) {
364     global $DB, $CFG;
366     $id = null;
368     if ($forcecompleted) {
369         //TODO - this could be broadened to encompass SCORM 2004 in future
370         if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) {
371             if ($track = $DB->get_record_select('scorm_scoes_track','userid=? AND scormid=? AND scoid=? AND attempt=? AND element=\'cmi.core.score.raw\'', array($userid, $scormid, $scoid, $attempt))) {
372                 $value = 'completed';
373             }
374         }
375         if ($element == 'cmi.core.score.raw') {
376             if ($tracktest = $DB->get_record_select('scorm_scoes_track','userid=? AND scormid=? AND scoid=? AND attempt=? AND element=\'cmi.core.lesson_status\'', array($userid, $scormid, $scoid, $attempt))) {
377                 if ($tracktest->value == "incomplete") {
378                     $tracktest->value = "completed";
379                     $DB->update_record('scorm_scoes_track',$tracktest);
380                 }
381             }
382         }
383     }
385     if ($track = $DB->get_record('scorm_scoes_track',array('userid'=>$userid, 'scormid'=>$scormid, 'scoid'=>$scoid, 'attempt'=>$attempt, 'element'=>$element))) {
386         if ($element != 'x.start.time' ) { //don't update x.start.time - keep the original value.
387             $track->value = addslashes_js($value);
388             $track->timemodified = time();
389             $DB->update_record('scorm_scoes_track',$track);
390             $id = $track->id;
391         }
392     } else {
393         $track->userid = $userid;
394         $track->scormid = $scormid;
395         $track->scoid = $scoid;
396         $track->attempt = $attempt;
397         $track->element = $element;
398         $track->value = addslashes_js($value);
399         $track->timemodified = time();
400         $id = $DB->insert_record('scorm_scoes_track',$track);
401     }
403     if (strstr($element, '.score.raw') ||
404         (($element == 'cmi.core.lesson_status' || $element == 'cmi.completion_status') && ($track->value == 'completed' || $track->value == 'passed'))) {
405         $scorm = $DB->get_record('scorm', array('id' => $scormid));
406         include_once($CFG->dirroot.'/mod/scorm/lib.php');
407         scorm_update_grades($scorm, $userid);
408     }
410     return $id;
413 function scorm_get_tracks($scoid,$userid,$attempt='') {
414 /// Gets all tracks of specified sco and user
415     global $CFG, $DB;
417     if (empty($attempt)) {
418         if ($scormid = $DB->get_field('scorm_scoes','scorm', array('id'=>$scoid))) {
419             $attempt = scorm_get_last_attempt($scormid,$userid);
420         } else {
421             $attempt = 1;
422         }
423     }
424     if ($tracks = $DB->get_records('scorm_scoes_track', array('userid'=>$userid, 'scoid'=>$scoid, 'attempt'=>$attempt),'element ASC')) {
425         $usertrack = new stdClass();
426         $usertrack->userid = $userid;
427         $usertrack->scoid = $scoid;
428         // Defined in order to unify scorm1.2 and scorm2004
429         $usertrack->score_raw = '';
430         $usertrack->status = '';
431         $usertrack->total_time = '00:00:00';
432         $usertrack->session_time = '00:00:00';
433         $usertrack->timemodified = 0;
434         foreach ($tracks as $track) {
435             $element = $track->element;
436             $track->value = stripslashes($track->value); // TODO: this is probably wrong, the stripslashes() has undefined meaning now; was this related to JS quoting or magic quotes?
437             $usertrack->{$element} = $track->value;
438             switch ($element) {
439                 case 'cmi.core.lesson_status':
440                 case 'cmi.completion_status':
441                     if ($track->value == 'not attempted') {
442                         $track->value = 'notattempted';
443                     }
444                     $usertrack->status = $track->value;
445                 break;
446                 case 'cmi.core.score.raw':
447                 case 'cmi.score.raw':
448                     $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
449                 break;
450                 case 'cmi.core.session_time':
451                 case 'cmi.session_time':
452                     $usertrack->session_time = $track->value;
453                 break;
454                 case 'cmi.core.total_time':
455                 case 'cmi.total_time':
456                     $usertrack->total_time = $track->value;
457                 break;
458             }
459             if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
460                 $usertrack->timemodified = $track->timemodified;
461             }
462         }
463         if (is_array($usertrack)) {
464             ksort($usertrack);
465         }
466         return $usertrack;
467     } else {
468         return false;
469     }
473 /* Find the start and finsh time for a a given SCO attempt
474  *
475  * @param int $scormid SCORM Id
476  * @param int $scoid SCO Id
477  * @param int $userid User Id
478  * @param int $attemt Attempt Id
479  *
480  * @return object start and finsh time EPOC secods
481  *
482  */
483 function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
484     global $DB;
486     $timedata = new stdClass();
487     $sql = !empty($scoid) ? "userid=$userid AND scormid=$scormid AND scoid=$scoid AND attempt=$attempt" : "userid=$userid AND scormid=$scormid AND attempt=$attempt";
488     $tracks = $DB->get_records_select('scorm_scoes_track',"$sql ORDER BY timemodified ASC");
489     if ($tracks) {
490         $tracks = array_values($tracks);
491     }
493     if ($tracks) {
494         $timedata->start = $tracks[0]->timemodified;
495     }
496     else {
497         $timedata->start = false;
498     }
499     if ($tracks && $track = array_pop($tracks)) {
500         $timedata->finish = $track->timemodified;
501     }
502     else {
503         $timedata->finish = $timedata->start;
504     }
505     return $timedata;
509 function scorm_get_user_data($userid) {
510     global $DB;
511 /// Gets user info required to display the table of scorm results
512 /// for report.php
514     return $DB->get_record('user', array('id'=>$userid),'firstname, lastname, picture');
517 function scorm_grade_user_attempt($scorm, $userid, $attempt=1) {
518     global $DB;
519     $attemptscore = NULL;
520     $attemptscore->scoes = 0;
521     $attemptscore->values = 0;
522     $attemptscore->max = 0;
523     $attemptscore->sum = 0;
524     $attemptscore->lastmodify = 0;
526     if (!$scoes = $DB->get_records('scorm_scoes', array('scorm'=>$scorm->id))) {
527         return NULL;
528     }
530     foreach ($scoes as $sco) {
531         if ($userdata=scorm_get_tracks($sco->id, $userid,$attempt)) {
532             if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
533                 $attemptscore->scoes++;
534             }
535             if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type=='sco' && isset($userdata->score_raw))) {
536                 $attemptscore->values++;
537                 $attemptscore->sum += $userdata->score_raw;
538                 $attemptscore->max = ($userdata->score_raw > $attemptscore->max)?$userdata->score_raw:$attemptscore->max;
539                 if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
540                     $attemptscore->lastmodify = $userdata->timemodified;
541                 } else {
542                     $attemptscore->lastmodify = 0;
543                 }
544             }
545         }
546     }
547     switch ($scorm->grademethod) {
548         case GRADEHIGHEST:
549             $score = (float) $attemptscore->max;
550         break;
551         case GRADEAVERAGE:
552             if ($attemptscore->values > 0) {
553                 $score = $attemptscore->sum/$attemptscore->values;
554             } else {
555                 $score = 0;
556             }
557         break;
558         case GRADESUM:
559             $score = $attemptscore->sum;
560         break;
561         case GRADESCOES:
562             $score = $attemptscore->scoes;
563         break;
564         default:
565             $score = $attemptscore->max;   // Remote Learner GRADEHIGHEST is default
566     }
568     return $score;
571 function scorm_grade_user($scorm, $userid) {
573     // ensure we dont grade user beyond $scorm->maxattempt settings
574     $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
575     if($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt){
576         $lastattempt = $scorm->maxattempt;
577     }
579     switch ($scorm->whatgrade) {
580         case FIRSTATTEMPT:
581             return scorm_grade_user_attempt($scorm, $userid, 1);
582         break;
583         case LASTATTEMPT:
584             return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
585         break;
586         case HIGHESTATTEMPT:
587             $maxscore = 0;
588             $attempttime = 0;
589             for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
590                 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
591                 $maxscore = $attemptscore > $maxscore ? $attemptscore: $maxscore;
592             }
593             return $maxscore;
595         break;
596         case AVERAGEATTEMPT:
597             $attemptcount = scorm_get_attempt_count($userid, $scorm, true);
598             if (empty($attemptcount)) {
599                 return 0;
600             } else {
601                 $attemptcount = count($attemptcount);
602             }
603             $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
604             $sumscore = 0;
605             for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
606                 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
607                 $sumscore += $attemptscore;
608             }
610             return round($sumscore / $attemptcount);
611         break;
612     }
615 function scorm_count_launchable($scormid,$organization='') {
616     global $DB;
618     $sqlorganization = '';
619     $params = array($scormid);
620     if (!empty($organization)) {
621         $sqlorganization = " AND organization=?";
622         $params[] = $organization;
623     }
624     return $DB->count_records_select('scorm_scoes',"scorm = ? $sqlorganization AND ".$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), $params);
627 function scorm_get_last_attempt($scormid, $userid) {
628     global $DB;
630 /// Find the last attempt number for the given user id and scorm id
631     if ($lastattempt = $DB->get_record('scorm_scoes_track', array('userid'=>$userid, 'scormid'=>$scormid), 'max(attempt) as a')) {
632         if (empty($lastattempt->a)) {
633             return '1';
634         } else {
635             return $lastattempt->a;
636         }
637     } else {
638         return false;
639     }
642 function scorm_get_last_completed_attempt($scormid, $userid) {
643     global $DB;
645 /// Find the last attempt number for the given user id and scorm id
646     if ($lastattempt = $DB->get_record_select('scorm_scoes_track', "userid = ? AND scormid = ? AND (value='completed' OR value='passed')", array($userid, $scormid), 'max(attempt) as a')) {
647         if (empty($lastattempt->a)) {
648             return '1';
649         } else {
650             return $lastattempt->a;
651         }
652     } else {
653         return false;
654     }
657 function scorm_course_format_display($user,$course) {
658     global $CFG, $DB, $PAGE, $OUTPUT;
660     $strupdate = get_string('update');
661     $strmodule = get_string('modulename','scorm');
662     $context = get_context_instance(CONTEXT_COURSE,$course->id);
664     echo '<div class="mod-scorm">';
665     if ($scorms = get_all_instances_in_course('scorm', $course)) {
666         // The module SCORM activity with the least id is the course
667         $scorm = current($scorms);
668         if (! $cm = get_coursemodule_from_instance('scorm', $scorm->id, $course->id)) {
669             print_error('invalidcoursemodule');
670         }
671         $colspan = '';
672         $headertext = '<table width="100%"><tr><td class="title">'.get_string('name').': <b>'.format_string($scorm->name).'</b>';
673         if (has_capability('moodle/course:manageactivities', $context)) {
674             if ($PAGE->user_is_editing()) {
675                 // Display update icon
676                 $path = $CFG->wwwroot.'/course';
677                 $headertext .= '<span class="commands">'.
678                         '<a title="'.$strupdate.'" href="'.$path.'/mod.php?update='.$cm->id.'&amp;sesskey='.sesskey().'">'.
679                         '<img src="'.$OUTPUT->pix_url('t/edit') . '" class="iconsmall" alt="'.$strupdate.'" /></a></span>';
680             }
681             $headertext .= '</td>';
682             // Display report link
683             $trackedusers = $DB->get_record('scorm_scoes_track', array('scormid'=>$scorm->id), 'count(distinct(userid)) as c');
684             if ($trackedusers->c > 0) {
685                 $headertext .= '<td class="reportlink">'.
686                               '<a href="'.$CFG->wwwroot.'/mod/scorm/report.php?id='.$cm->id.'">'.
687                                get_string('viewallreports','scorm',$trackedusers->c).'</a>';
688             } else {
689                 $headertext .= '<td class="reportlink">'.get_string('noreports','scorm');
690             }
691             $colspan = ' colspan="2"';
692         }
693         $options = (object)array('noclean'=>true);
694         $headertext .= '</td></tr><tr><td'.$colspan.'>'.get_string('summary').':<br />'.format_module_intro('scorm', $scorm, $scorm->coursemodule).'</td></tr></table>';
695         echo $OUTPUT->box($headertext,'generalbox boxwidthwide');
696         scorm_view_display($user, $scorm, 'view.php?id='.$course->id, $cm, '100%');
697     } else {
698         if (has_capability('moodle/course:update', $context)) {
699             // Create a new activity
700             redirect($CFG->wwwroot.'/course/mod.php?id='.$course->id.'&amp;section=0&sesskey='.sesskey().'&amp;add=scorm');
701         } else {
702             echo $OUTPUT->notification('Could not find a scorm course here');
703         }
704     }
705     echo '</div>';
708 function scorm_view_display ($user, $scorm, $action, $cm, $boxwidth='') {
709     global $CFG, $DB, $PAGE, $OUTPUT;
711     if ($scorm->updatefreq == UPDATE_EVERYTIME) {
712         scorm_parse($scorm, false);
713     }
715     $organization = optional_param('organization', '', PARAM_INT);
717     if($scorm->displaycoursestructure == 1) {
718         echo $OUTPUT->box_start('generalbox boxaligncenter');
719 ?>
720         <div class="structurehead"><?php print_string('contents','scorm') ?></div>
721 <?php
722     }
723     if (empty($organization)) {
724         $organization = $scorm->launch;
725     }
726     if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '.
727                                          $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
728                                          $DB->sql_isempty('scorm_scoes', 'organization', false, false),
729                                          array($scorm->id),'id','id,title')) {
730         if (count($orgs) > 1) {
731             $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null);
732             $select->label = get_string('organizations','scorm');
733             $select->class = 'scorm-center';
734             echo $OUTPUT->render($select);
735         }
736     }
737     $orgidentifier = '';
738     if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
739         if (($sco->organization == '') && ($sco->launch == '')) {
740             $orgidentifier = $sco->identifier;
741         } else {
742             $orgidentifier = $sco->organization;
743         }
744     }
746 /*
747  $orgidentifier = '';
748     if ($org = $DB->get_record('scorm_scoes', array('id'=>$organization))) {
749         if (($org->organization == '') && ($org->launch == '')) {
750             $orgidentifier = $org->identifier;
751         } else {
752             $orgidentifier = $org->organization;
753         }
754     }*/
756     $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR));   // Just to be safe
757     if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
758         $scorm->version = 'scorm_12';
759     }
760     require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
762     $result = scorm_get_toc($user,$scorm,'structlist',$orgidentifier);
763     $incomplete = $result->incomplete;
765     // do we want the TOC to be displayed?
766     if($scorm->displaycoursestructure == 1) {
767         echo $result->toc;
768         echo $OUTPUT->box_end();
769     }
771     // is this the first attempt ?
772     $attemptcount = scorm_get_attempt_count($user->id, $scorm);
774     // do not give the player launch FORM if the SCORM object is locked after the final attempt
775     if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) {
776 ?>
777             <div class="scorm-center">
778                <form id="theform" method="post" action="<?php echo $CFG->wwwroot ?>/mod/scorm/player.php">
779               <?php
780                   if ($scorm->hidebrowse == 0) {
781                       print_string('mode','scorm');
782                       echo ': <input type="radio" id="b" name="mode" value="browse" /><label for="b">'.get_string('browse','scorm').'</label>'."\n";
783                       echo '<input type="radio" id="n" name="mode" value="normal" checked="checked" /><label for="n">'.get_string('normal','scorm')."</label>\n";
784                   } else {
785                       echo '<input type="hidden" name="mode" value="normal" />'."\n";
786                   }
787                   if ($scorm->forcenewattempt == 1) {
788                       if ($incomplete === false) {
789                           echo '<input type="hidden" name="newattempt" value="on" />'."\n";
790                       }
791                   } elseif (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
792 ?>
793                       <br />
794                       <input type="checkbox" id="a" name="newattempt" />
795                       <label for="a"><?php print_string('newattempt','scorm') ?></label>
796 <?php
797                   }
798               ?>
799               <br />
800               <input type="hidden" name="scoid"/>
801               <input type="hidden" name="cm" value="<?php echo $cm->id ?>"/>
802               <input type="hidden" name="currentorg" value="<?php echo $orgidentifier ?>" />
803               <input type="submit" value="<?php print_string('enter','scorm') ?>" />
804               </form>
805           </div>
806 <?php
807     }
810 function scorm_simple_play($scorm,$user, $context) {
811     global $DB;
813     $result = false;
815     if ($scorm->updatefreq == UPDATE_EVERYTIME) {
816         scorm_parse($scorm, false);
817     }
818     if (has_capability('mod/scorm:viewreport', $context)) { //if this user can view reports, don't skipview so they can see links to reports.
819         return $result;
820     }
822     $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'id', 'id');
824     if ($scoes) {
825         if ($scorm->skipview >= 1) {
826             $sco = current($scoes);
827             if (scorm_get_tracks($sco->id,$user->id) === false) {
828                 header('Location: player.php?a='.$scorm->id.'&scoid='.$sco->id);
829                 $result = true;
830             } else if ($scorm->skipview == 2) {
831                 header('Location: player.php?a='.$scorm->id.'&scoid='.$sco->id);
832                 $result = true;
833             }
834         }
835     }
836     return $result;
839 function scorm_get_count_users($scormid, $groupingid=null) {
840     global $CFG, $DB;
842     if (!empty($groupingid)) {
843         $sql = "SELECT COUNT(DISTINCT st.userid)
844                 FROM {scorm_scoes_track} st
845                     INNER JOIN {groups_members} gm ON st.userid = gm.userid
846                     INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
847                 WHERE st.scormid = ? AND gg.groupingid = ?
848                 ";
849         $params = array($scormid, $groupingid);
850     } else {
851         $sql = "SELECT COUNT(DISTINCT st.userid)
852                 FROM {scorm_scoes_track} st
853                 WHERE st.scormid = ?
854                 ";
855         $params = array($scormid);
856     }
858     return ($DB->count_records_sql($sql, $params));
861 /**
862 * Build up the JavaScript representation of an array element
864 * @param string $sversion SCORM API version
865 * @param array $userdata User track data
866 * @param string $element_name Name of array element to get values for
867 * @param array $children list of sub elements of this array element that also need instantiating
868 * @return None
869 */
870 function scorm_reconstitute_array_element($sversion, $userdata, $element_name, $children) {
871     // reconstitute comments_from_learner and comments_from_lms
872     $current = '';
873     $current_subelement = '';
874     $current_sub = '';
875     $count = 0;
876     $count_sub = 0;
877     $scormseperator = '_';
878     if ($sversion == 'scorm_13') { //scorm 1.3 elements use a . instead of an _
879         $scormseperator = '.';
880     }
881     // filter out the ones we want
882     $element_list = array();
883     foreach($userdata as $element => $value){
884         if (substr($element,0,strlen($element_name)) == $element_name) {
885             $element_list[$element] = $value;
886         }
887     }
889     // sort elements in .n array order
890     uksort($element_list, "scorm_element_cmp");
892     // generate JavaScript
893     foreach($element_list as $element => $value){
894         if ($sversion == 'scorm_13') {
895             $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
896             preg_match('/\.(N\d+)\./', $element, $matches);
897         } else {
898             $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
899             preg_match('/\_(\d+)\./', $element, $matches);
900         }
901         if (count($matches) > 0 && $current != $matches[1]) {
902             if ($count_sub > 0) {
903                 echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
904             }
905             $current = $matches[1];
906             $count++;
907             $current_subelement = '';
908             $current_sub = '';
909             $count_sub = 0;
910             $end = strpos($element,$matches[1])+strlen($matches[1]);
911             $subelement = substr($element,0,$end);
912             echo '    '.$subelement." = new Object();\n";
913             // now add the children
914             foreach ($children as $child) {
915                 echo '    '.$subelement.".".$child." = new Object();\n";
916                 echo '    '.$subelement.".".$child."._children = ".$child."_children;\n";
917             }
918         }
920         // now - flesh out the second level elements if there are any
921         if ($sversion == 'scorm_13') {
922             $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
923             preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
924         } else {
925             $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
926             preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
927         }
929         // check the sub element type
930         if (count($matches) > 0 && $current_subelement != $matches[1]) {
931             if ($count_sub > 0) {
932                 echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
933             }
934             $current_subelement = $matches[1];
935             $current_sub = '';
936             $count_sub = 0;
937             $end = strpos($element,$matches[1])+strlen($matches[1]);
938             $subelement = substr($element,0,$end);
939             echo '    '.$subelement." = new Object();\n";
940         }
942         // now check the subelement subscript
943         if (count($matches) > 0 && $current_sub != $matches[2]) {
944             $current_sub = $matches[2];
945             $count_sub++;
946             $end = strrpos($element,$matches[2])+strlen($matches[2]);
947             $subelement = substr($element,0,$end);
948             echo '    '.$subelement." = new Object();\n";
949         }
951         echo '    '.$element.' = \''.$value."';\n";
952     }
953     if ($count_sub > 0) {
954         echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
955     }
956     if ($count > 0) {
957         echo '    '.$element_name.'._count = '.$count.";\n";
958     }
961 /**
962 * Build up the JavaScript representation of an array element
964 * @param string $a left array element
965 * @param string $b right array element
966 * @return comparator - 0,1,-1
967 */
968 function scorm_element_cmp($a, $b) {
969     preg_match('/.*?(\d+)\./', $a, $matches);
970     $left = intval($matches[1]);
971     preg_match('/.?(\d+)\./', $b, $matches);
972     $right = intval($matches[1]);
973     if ($left < $right) {
974         return -1; // smaller
975     } elseif ($left > $right) {
976         return 1;  // bigger
977     } else {
978         // look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern
979         if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
980             $leftterm = intval($matches[2]);
981             $left = intval($matches[3]);
982             if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
983                 $rightterm = intval($matches[2]);
984                 $right = intval($matches[3]);
985                 if ($leftterm < $rightterm) {
986                     return -1; // smaller
987                 } elseif ($leftterm > $rightterm) {
988                     return 1;  // bigger
989                 } else {
990                     if ($left < $right) {
991                         return -1; // smaller
992                     } elseif ($left > $right) {
993                         return 1;  // bigger
994                     }
995                 }
996             }
997         }
998         // fall back for no second level matches or second level matches are equal
999         return 0;  // equal to
1000     }
1003 /**
1004 * Generate the user attempt status string
1006 * @param object $user Current context user
1007 * @param object $scorm a moodle scrom object - mdl_scorm
1008 * @return string - Attempt status string
1009 */
1010 function scorm_get_attempt_status($user, $scorm) {
1011     global $DB;
1013     $attempts = scorm_get_attempt_count($user->id, $scorm, true);
1014     if(empty($attempts)) {
1015         $attemptcount = 0;
1016     } else {
1017         $attemptcount = count($attempts);
1018     }
1020     $result = '<p>'.get_string('noattemptsallowed', 'scorm').': ';
1021     if ($scorm->maxattempt > 0) {
1022         $result .= $scorm->maxattempt . '<BR>';
1023     } else {
1024         $result .= get_string('unlimited').'<BR>';
1025     }
1026     $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . '<BR>';
1028     $gradereported = 0;
1029     $gradesum = 0;
1030     if ($scorm->maxattempt == 1) {
1031         switch ($scorm->grademethod) {
1032             case GRADEHIGHEST:
1033                 $grademethod = get_string('gradehighest', 'scorm');
1034             break;
1035             case GRADEAVERAGE:
1036                 $grademethod = get_string('gradeaverage', 'scorm');
1037             break;
1038             case GRADESUM:
1039                 $grademethod = get_string('gradesum', 'scorm');
1040             break;
1041             case GRADESCOES:
1042                 $grademethod = get_string('gradescoes', 'scorm');
1043             break;
1044         }
1045      } else {
1046          switch ($scorm->whatgrade) {
1047             case HIGHESTATTEMPT:
1048                 $grademethod = get_string('highestattempt', 'scorm');
1049             break;
1050             case AVERAGEATTEMPT:
1051                 $grademethod = get_string('averageattempt', 'scorm');
1052             break;
1053             case FIRSTATTEMPT:
1054                 $grademethod = get_string('firstattempt', 'scorm');
1055             break;
1056             case LASTATTEMPT:
1057                 $grademethod = get_string('lastattempt', 'scorm');
1058             break;
1059         }
1060      }
1062     if(!empty($attempts)) {
1063         $i = 1;
1064         foreach($attempts as $attempt) {
1065             $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber);
1066             $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .'%<BR>';
1067             $i++;
1068         }
1069     }
1070     $calculatedgrade = scorm_grade_user($scorm, $user->id);
1071     $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod;
1072     if(empty($attempts)) {
1073         $result .= '<BR>' . get_string('gradereported','scorm') . ': ' . get_string('none') . '<BR>';
1074     } else {
1075         $result .= '<BR>' . get_string('gradereported','scorm') . ': ' . $calculatedgrade . ($scorm->grademethod == GRADESCOES ? '' : '%') .'<BR>';
1076     }
1077     $result .= '</p>';
1078     if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) {
1079         $result .= '<p><font color="#cc0000">'.get_string('exceededmaxattempts','scorm').'</font></p>';
1080     }
1081     return $result;
1084 /**
1085 * Get SCORM attempt count
1087 * @param object $user Current context user
1088 * @param object $scorm a moodle scrom object - mdl_scorm
1089 * @param bool $attempts return the list of attempts
1090 * @return int - no. of attempts so far
1091 */
1092 function scorm_get_attempt_count($userid, $scorm, $attempts_only=false) {
1093     global $DB;
1094     $attemptcount = 0;
1095     $element = 'cmi.core.score.raw';
1096     if ($scorm->grademethod == GRADESCOES) {
1097         $element = 'cmi.core.lesson_status';
1098     }
1099     if ($scorm->version == 'scorm1_3') {
1100         $element = 'cmi.score.raw';
1101     }
1102     $attempts = $DB->get_records_select('scorm_scoes_track',"element=? AND userid=? AND scormid=?", array($element, $userid, $scorm->id),'attempt','DISTINCT attempt AS attemptnumber');
1103     if ($attempts_only) {
1104         return $attempts;
1105     }
1106     if(!empty($attempts)) {
1107         $attemptcount = count($attempts);
1108     }
1109     return $attemptcount;
1112 /**
1113 * Figure out with this is a debug situation
1115 * @param object $scorm a moodle scrom object - mdl_scorm
1116 * @return boolean - debugging true/false
1117 */
1118 function scorm_debugging($scorm) {
1119     global $CFG, $USER;
1120     $cfg_scorm = get_config('scorm');
1122     if (!$cfg_scorm->allowapidebug) {
1123         return false;
1124     }
1125     $identifier = $USER->username.':'.$scorm->name;
1126     $test = $cfg_scorm->apidebugmask;
1127     // check the regex is only a short list of safe characters
1128     if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) {
1129         return false;
1130     }
1131     $res = false;
1132     eval('$res = preg_match(\'/^'.$test.'/\', $identifier) ? true : false;');
1133     return $res;
1136 /**
1137 * Delete Scorm tracks for selected users
1139 * @param array $attemptids list of attempts that need to be deleted
1140 * @param int $scorm instance
1142 * return bool true deleted all responses, false failed deleting an attempt - stopped here
1143 */
1144 function scorm_delete_responses($attemptids, $scorm) {
1145     if(!is_array($attemptids) || empty($attemptids)) {
1146         return false;
1147     }
1149     foreach($attemptids as $num => $attemptid) {
1150         if(empty($attemptid)) {
1151             unset($attemptids[$num]);
1152         }
1153     }
1155     foreach($attemptids as $attempt) {
1156         $keys = explode(':', $attempt);
1157         if (count($keys) == 2) {
1158             $userid = clean_param($keys[0], PARAM_INT);
1159             $attemptid = clean_param($keys[1], PARAM_INT);
1160             if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) {
1161                     return false;
1162             }
1163         } else {
1164             return false;
1165         }
1166     }
1167     return true;
1170 /**
1171 * Delete Scorm tracks for selected users
1173 * @param int $userid ID of User
1174 * @param int $scormid ID of Scorm
1175 * @param int $attemptid user attempt that need to be deleted
1177 * return bool true suceeded
1178 */
1179 function scorm_delete_attempt($userid, $scorm, $attemptid) {
1180     global $DB;
1182     $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid));
1183     include_once('lib.php');
1184     scorm_update_grades($scorm, $userid, true);
1185     return true;
1188 /**
1189  * Converts SCORM duration notation to human-readable format
1190  * The function works with both SCORM 1.2 and SCORM 2004 time formats
1191  * @param $duration string SCORM duration
1192  * @return string human-readable date/time
1193  */
1194 function scorm_format_duration($duration) {
1195     // fetch date/time strings
1196     $stryears = get_string('years');
1197     $strmonths = get_string('nummonths');
1198     $strdays = get_string('days');
1199     $strhours = get_string('hours');
1200     $strminutes = get_string('minutes');
1201     $strseconds = get_string('seconds');
1203     if ($duration[0] == 'P') {
1204         // if timestamp starts with 'P' - it's a SCORM 2004 format
1205         // this regexp discards empty sections, takes Month/Minute ambiguity into consideration,
1206         // and outputs filled sections, discarding leading zeroes and any format literals
1207         // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero
1208         $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#',
1209                           '#([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#' );
1210         $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ', '',
1211                           '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '');
1212     } else {
1213         // else we have SCORM 1.2 format there
1214         // first convert the timestamp to some SCORM 2004-like format for conveniency
1215         $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration);
1216         // then convert in the same way as SCORM 2004
1217         $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#' );
1218         $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '' );
1219         //$pattern = '##';
1220         //$replace = '';
1221     }
1223     $result = preg_replace($pattern, $replace, $duration);
1225     return $result;