3a35b2f4a378ccb833e8e2b2ba692c1b2909b39e
[moodle.git] / mod / scorm / locallib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 require_once("$CFG->dirroot/mod/scorm/lib.php");
18 require_once("$CFG->libdir/filelib.php");
20 /// Constants and settings for module scorm
21 define('SCORM_UPDATE_NEVER', '0');
22 define('SCORM_UPDATE_EVERYDAY', '2');
23 define('SCORM_UPDATE_EVERYTIME', '3');
25 define('SCO_ALL', 0);
26 define('SCO_DATA', 1);
27 define('SCO_ONLY', 2);
29 define('GRADESCOES', '0');
30 define('GRADEHIGHEST', '1');
31 define('GRADEAVERAGE', '2');
32 define('GRADESUM', '3');
34 define('HIGHESTATTEMPT', '0');
35 define('AVERAGEATTEMPT', '1');
36 define('FIRSTATTEMPT', '2');
37 define('LASTATTEMPT', '3');
39 define('TOCJSLINK', 1);
40 define('TOCFULLURL', 2);
42 /// Local Library of functions for module scorm
44 /**
45  * @package   mod-scorm
46  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
47  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48  */
49 class scorm_package_file_info extends file_info_stored {
50     public function get_parent() {
51         if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
52             return $this->browser->get_file_info($this->context);
53         }
54         return parent::get_parent();
55     }
56     public function get_visible_name() {
57         if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
58             return $this->topvisiblename;
59         }
60         return parent::get_visible_name();
61     }
62 }
64 /**
65  * Returns an array of the popup options for SCORM and each options default value
66  *
67  * @return array an array of popup options as the key and their defaults as the value
68  */
69 function scorm_get_popup_options_array() {
70     global $CFG;
71     $cfg_scorm = get_config('scorm');
73     return array('resizable'=> isset($cfg_scorm->resizable) ? $cfg_scorm->resizable : 0,
74                  'scrollbars'=> isset($cfg_scorm->scrollbars) ? $cfg_scorm->scrollbars : 0,
75                  'directories'=> isset($cfg_scorm->directories) ? $cfg_scorm->directories : 0,
76                  'location'=> isset($cfg_scorm->location) ? $cfg_scorm->location : 0,
77                  'menubar'=> isset($cfg_scorm->menubar) ? $cfg_scorm->menubar : 0,
78                  'toolbar'=> isset($cfg_scorm->toolbar) ? $cfg_scorm->toolbar : 0,
79                  'status'=> isset($cfg_scorm->status) ? $cfg_scorm->status : 0);
80 }
82 /**
83  * Returns an array of the array of what grade options
84  *
85  * @return array an array of what grade options
86  */
87 function scorm_get_grade_method_array() {
88     return array (GRADESCOES => get_string('gradescoes', 'scorm'),
89                   GRADEHIGHEST => get_string('gradehighest', 'scorm'),
90                   GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
91                   GRADESUM => get_string('gradesum', 'scorm'));
92 }
94 /**
95  * Returns an array of the array of what grade options
96  *
97  * @return array an array of what grade options
98  */
99 function scorm_get_what_grade_array() {
100     return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
101                   AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
102                   FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
103                   LASTATTEMPT => get_string('lastattempt', 'scorm'));
106 /**
107  * Returns an array of the array of skip view options
108  *
109  * @return array an array of skip view options
110  */
111 function scorm_get_skip_view_array() {
112     return array(0 => get_string('never'),
113                  1 => get_string('firstaccess', 'scorm'),
114                  2 => get_string('always'));
117 /**
118  * Returns an array of the array of hide table of contents options
119  *
120  * @return array an array of hide table of contents options
121  */
122 function scorm_get_hidetoc_array() {
123      return array(SCORM_TOC_SIDE => get_string('sided', 'scorm'),
124                   SCORM_TOC_HIDDEN => get_string('hidden', 'scorm'),
125                   SCORM_TOC_POPUP => get_string('popupmenu', 'scorm'),
126                   SCORM_TOC_DISABLED => get_string('disabled', 'scorm'));
129 /**
130  * Returns an array of the array of update frequency options
131  *
132  * @return array an array of update frequency options
133  */
134 function scorm_get_updatefreq_array() {
135     return array(SCORM_UPDATE_NEVER => get_string('never'),
136                  SCORM_UPDATE_EVERYDAY => get_string('everyday', 'scorm'),
137                  SCORM_UPDATE_EVERYTIME => get_string('everytime', 'scorm'));
140 /**
141  * Returns an array of the array of popup display options
142  *
143  * @return array an array of popup display options
144  */
145 function scorm_get_popup_display_array() {
146     return array(0 => get_string('currentwindow', 'scorm'),
147                  1 => get_string('popup', 'scorm'));
150 /**
151  * Returns an array of the array of attempt options
152  *
153  * @return array an array of attempt options
154  */
155 function scorm_get_attempts_array() {
156     $attempts = array(0 => get_string('nolimit', 'scorm'),
157                       1 => get_string('attempt1', 'scorm'));
159     for ($i=2; $i<=6; $i++) {
160         $attempts[$i] = get_string('attemptsx', 'scorm', $i);
161     }
163     return $attempts;
165 /**
166  * Extracts scrom package, sets up all variables.
167  * Called whenever scorm changes
168  * @param object $scorm instance - fields are updated and changes saved into database
169  * @param bool $full force full update if true
170  * @return void
171  */
172 function scorm_parse($scorm, $full) {
173     global $CFG, $DB;
174     $cfg_scorm = get_config('scorm');
176     if (!isset($scorm->cmid)) {
177         $cm = get_coursemodule_from_instance('scorm', $scorm->id);
178         $scorm->cmid = $cm->id;
179     }
180     $context = get_context_instance(CONTEXT_MODULE, $scorm->cmid);
181     $newhash = $scorm->sha1hash;
183     if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
185         $fs = get_file_storage();
186         $packagefile = false;
188         if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
189             if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
190                 $newhash = $packagefile->get_contenthash();
191             } else {
192                 $newhash = null;
193             }
194         } else {
195             if (!$cfg_scorm->allowtypelocalsync) {
196                 // sorry - localsync disabled
197                 return;
198             }
199             if ($scorm->reference !== '' and (!$full or $scorm->sha1hash !== sha1($scorm->reference))) {
200                 $fs->delete_area_files($context->id, 'mod_scorm', 'package');
201                 $file_record = array('contextid'=>$context->id, 'component'=>'mod_scorm', 'filearea'=>'package', 'itemid'=>0, 'filepath'=>'/');
202                 if ($packagefile = $fs->create_file_from_url($file_record, $scorm->reference, array('calctimeout' => true))) {
203                     $newhash = sha1($scorm->reference);
204                 } else {
205                     $newhash = null;
206                 }
207             }
208         }
210         if ($packagefile) {
211             if (!$full and $packagefile and $scorm->sha1hash === $newhash) {
212                 if (strpos($scorm->version, 'SCORM') !== false) {
213                     if ($fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
214                         // no need to update
215                         return;
216                     }
217                 } else if (strpos($scorm->version, 'AICC') !== false) {
218                     // TODO: add more sanity checks - something really exists in scorm_content area
219                     return;
220                 }
221             }
223             // now extract files
224             $fs->delete_area_files($context->id, 'mod_scorm', 'content');
226             $packer = get_file_packer('application/zip');
227             $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/');
229         } else if (!$full) {
230             return;
231         }
233         if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
234             require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
235             // SCORM
236             if (!scorm_parse_scorm($scorm, $manifest)) {
237                 $scorm->version = 'ERROR';
238             }
239         } else {
240             require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
241             // AICC
242             if (!scorm_parse_aicc($scorm)) {
243                 $scorm->version = 'ERROR';
244             }
245             $scorm->version = 'AICC';
246         }
248     } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfg_scorm->allowtypeexternal) {
249         if (!$full and $scorm->sha1hash === sha1($scorm->reference)) {
250             return;
251         }
252         require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
253         // SCORM only, AICC can not be external
254         if (!scorm_parse_scorm($scorm, $scorm->reference)) {
255             $scorm->version = 'ERROR';
256         }
257         $newhash = sha1($scorm->reference);
259     } else if ($scorm->scormtype === SCORM_TYPE_IMSREPOSITORY and !empty($CFG->repositoryactivate) and $cfg_scorm->allowtypeimsrepository) {
260         if (!$full and $scorm->sha1hash === sha1($scorm->reference)) {
261             return;
262         }
263         require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
264         if (!scorm_parse_scorm($scorm, $CFG->repository.substr($scorm->reference, 1).'/imsmanifest.xml')) {
265             $scorm->version = 'ERROR';
266         }
267         $newhash = sha1($scorm->reference);
268     } else if ($scorm->scormtype === SCORM_TYPE_AICCURL  and $cfg_scorm->allowtypeexternalaicc) {
269         require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
270         // AICC
271         if (!scorm_parse_aicc($scorm)) {
272             $scorm->version = 'ERROR';
273         }
274         $scorm->version = 'AICC';
275     } else {
276         // sorry, disabled type
277         return;
278     }
280     $scorm->revision++;
281     $scorm->sha1hash = $newhash;
282     $DB->update_record('scorm', $scorm);
286 function scorm_array_search($item, $needle, $haystacks, $strict=false) {
287     if (!empty($haystacks)) {
288         foreach ($haystacks as $key => $element) {
289             if ($strict) {
290                 if ($element->{$item} === $needle) {
291                     return $key;
292                 }
293             } else {
294                 if ($element->{$item} == $needle) {
295                     return $key;
296                 }
297             }
298         }
299     }
300     return false;
303 function scorm_repeater($what, $times) {
304     if ($times <= 0) {
305         return null;
306     }
307     $return = '';
308     for ($i=0; $i<$times; $i++) {
309         $return .= $what;
310     }
311     return $return;
314 function scorm_external_link($link) {
315     // check if a link is external
316     $result = false;
317     $link = strtolower($link);
318     if (substr($link, 0, 7) == 'http://') {
319         $result = true;
320     } else if (substr($link, 0, 8) == 'https://') {
321         $result = true;
322     } else if (substr($link, 0, 4) == 'www.') {
323         $result = true;
324     }
325     return $result;
328 /**
329  * Returns an object containing all datas relative to the given sco ID
330  *
331  * @param integer $id The sco ID
332  * @return mixed (false if sco id does not exists)
333  */
334 function scorm_get_sco($id, $what=SCO_ALL) {
335     global $DB;
337     if ($sco = $DB->get_record('scorm_scoes', array('id'=>$id))) {
338         $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
339         if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id)))) {
340             foreach ($scodatas as $scodata) {
341                 $sco->{$scodata->name} = $scodata->value;
342             }
343         } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id))))) {
344             $sco->parameters = '';
345         }
346         return $sco;
347     } else {
348         return false;
349     }
352 /**
353  * Returns an object (array) containing all the scoes data related to the given sco ID
354  *
355  * @param integer $id The sco ID
356  * @param integer $organisation an organisation ID - defaults to false if not required
357  * @return mixed (false if there are no scoes or an array)
358  */
359 function scorm_get_scoes($id, $organisation=false) {
360     global $DB;
362     $organizationsql = '';
363     $queryarray = array('scorm'=>$id);
364     if (!empty($organisation)) {
365         $queryarray['organization'] = $organisation;
366     }
367     if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'id ASC')) {
368         // drop keys so that it is a simple array as expected
369         $scoes = array_values($scoes);
370         foreach ($scoes as $sco) {
371             if ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$sco->id))) {
372                 foreach ($scodatas as $scodata) {
373                     $sco->{$scodata->name} = $scodata->value;
374                 }
375             }
376         }
377         return $scoes;
378     } else {
379         return false;
380     }
383 function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $value, $forcecompleted=false) {
384     global $DB, $CFG;
386     $id = null;
388     if ($forcecompleted) {
389         //TODO - this could be broadened to encompass SCORM 2004 in future
390         if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) {
391             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))) {
392                 $value = 'completed';
393             }
394         }
395         if ($element == 'cmi.core.score.raw') {
396             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))) {
397                 if ($tracktest->value == "incomplete") {
398                     $tracktest->value = "completed";
399                     $DB->update_record('scorm_scoes_track', $tracktest);
400                 }
401             }
402         }
403     }
405     if ($track = $DB->get_record('scorm_scoes_track', array('userid'=>$userid, 'scormid'=>$scormid, 'scoid'=>$scoid, 'attempt'=>$attempt, 'element'=>$element))) {
406         if ($element != 'x.start.time' ) { //don't update x.start.time - keep the original value.
407             $track->value = $value;
408             $track->timemodified = time();
409             $DB->update_record('scorm_scoes_track', $track);
410             $id = $track->id;
411         }
412     } else {
413         $track->userid = $userid;
414         $track->scormid = $scormid;
415         $track->scoid = $scoid;
416         $track->attempt = $attempt;
417         $track->element = $element;
418         $track->value = $value;
419         $track->timemodified = time();
420         $id = $DB->insert_record('scorm_scoes_track', $track);
421     }
423     if (strstr($element, '.score.raw') ||
424         (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
425          && in_array($track->value, array('completed', 'passed')))) {
426         $scorm = $DB->get_record('scorm', array('id' => $scormid));
427         include_once($CFG->dirroot.'/mod/scorm/lib.php');
428         scorm_update_grades($scorm, $userid);
429     }
431     return $id;
434 function scorm_get_tracks($scoid, $userid, $attempt='') {
435     /// Gets all tracks of specified sco and user
436     global $CFG, $DB;
438     if (empty($attempt)) {
439         if ($scormid = $DB->get_field('scorm_scoes', 'scorm', array('id'=>$scoid))) {
440             $attempt = scorm_get_last_attempt($scormid, $userid);
441         } else {
442             $attempt = 1;
443         }
444     }
445     if ($tracks = $DB->get_records('scorm_scoes_track', array('userid'=>$userid, 'scoid'=>$scoid, 'attempt'=>$attempt), 'element ASC')) {
446         $usertrack = new stdClass();
447         $usertrack->userid = $userid;
448         $usertrack->scoid = $scoid;
449         // Defined in order to unify scorm1.2 and scorm2004
450         $usertrack->score_raw = '';
451         $usertrack->status = '';
452         $usertrack->total_time = '00:00:00';
453         $usertrack->session_time = '00:00:00';
454         $usertrack->timemodified = 0;
455         foreach ($tracks as $track) {
456             $element = $track->element;
457             $usertrack->{$element} = $track->value;
458             switch ($element) {
459                 case 'cmi.core.lesson_status':
460                 case 'cmi.completion_status':
461                     if ($track->value == 'not attempted') {
462                         $track->value = 'notattempted';
463                     }
464                     $usertrack->status = $track->value;
465                 break;
466                 case 'cmi.core.score.raw':
467                 case 'cmi.score.raw':
468                     $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
469                 break;
470                 case 'cmi.core.session_time':
471                 case 'cmi.session_time':
472                     $usertrack->session_time = $track->value;
473                 break;
474                 case 'cmi.core.total_time':
475                 case 'cmi.total_time':
476                     $usertrack->total_time = $track->value;
477                 break;
478             }
479             if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
480                 $usertrack->timemodified = $track->timemodified;
481             }
482         }
483         if (is_array($usertrack)) {
484             ksort($usertrack);
485         }
486         return $usertrack;
487     } else {
488         return false;
489     }
493 /* Find the start and finsh time for a a given SCO attempt
494  *
495  * @param int $scormid SCORM Id
496  * @param int $scoid SCO Id
497  * @param int $userid User Id
498  * @param int $attemt Attempt Id
499  *
500  * @return object start and finsh time EPOC secods
501  *
502  */
503 function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
504     global $DB;
506     $timedata = new stdClass();
507     $sql = !empty($scoid) ? "userid=$userid AND scormid=$scormid AND scoid=$scoid AND attempt=$attempt" : "userid=$userid AND scormid=$scormid AND attempt=$attempt";
508     $tracks = $DB->get_records_select('scorm_scoes_track', "$sql ORDER BY timemodified ASC");
509     if ($tracks) {
510         $tracks = array_values($tracks);
511     }
513     if ($tracks) {
514         $timedata->start = $tracks[0]->timemodified;
515     } else {
516         $timedata->start = false;
517     }
518     if ($tracks && $track = array_pop($tracks)) {
519         $timedata->finish = $track->timemodified;
520     } else {
521         $timedata->finish = $timedata->start;
522     }
523     return $timedata;
527 function scorm_get_user_data($userid) {
528     global $DB;
529     /// Gets user info required to display the table of scorm results
530     /// for report.php
532     return $DB->get_record('user', array('id'=>$userid), user_picture::fields());
535 function scorm_grade_user_attempt($scorm, $userid, $attempt=1) {
536     global $DB;
537     $attemptscore = null;
538     $attemptscore->scoes = 0;
539     $attemptscore->values = 0;
540     $attemptscore->max = 0;
541     $attemptscore->sum = 0;
542     $attemptscore->lastmodify = 0;
544     if (!$scoes = $DB->get_records('scorm_scoes', array('scorm'=>$scorm->id))) {
545         return null;
546     }
548     foreach ($scoes as $sco) {
549         if ($userdata=scorm_get_tracks($sco->id, $userid, $attempt)) {
550             if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
551                 $attemptscore->scoes++;
552             }
553             if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type=='sco' && isset($userdata->score_raw))) {
554                 $attemptscore->values++;
555                 $attemptscore->sum += $userdata->score_raw;
556                 $attemptscore->max = ($userdata->score_raw > $attemptscore->max)?$userdata->score_raw:$attemptscore->max;
557                 if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
558                     $attemptscore->lastmodify = $userdata->timemodified;
559                 } else {
560                     $attemptscore->lastmodify = 0;
561                 }
562             }
563         }
564     }
565     switch ($scorm->grademethod) {
566         case GRADEHIGHEST:
567             $score = (float) $attemptscore->max;
568         break;
569         case GRADEAVERAGE:
570             if ($attemptscore->values > 0) {
571                 $score = $attemptscore->sum/$attemptscore->values;
572             } else {
573                 $score = 0;
574             }
575         break;
576         case GRADESUM:
577             $score = $attemptscore->sum;
578         break;
579         case GRADESCOES:
580             $score = $attemptscore->scoes;
581         break;
582         default:
583             $score = $attemptscore->max;   // Remote Learner GRADEHIGHEST is default
584     }
586     return $score;
589 function scorm_grade_user($scorm, $userid) {
591     // ensure we dont grade user beyond $scorm->maxattempt settings
592     $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
593     if ($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt) {
594         $lastattempt = $scorm->maxattempt;
595     }
597     switch ($scorm->whatgrade) {
598         case FIRSTATTEMPT:
599             return scorm_grade_user_attempt($scorm, $userid, 1);
600         break;
601         case LASTATTEMPT:
602             return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
603         break;
604         case HIGHESTATTEMPT:
605             $maxscore = 0;
606             for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
607                 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
608                 $maxscore = $attemptscore > $maxscore ? $attemptscore: $maxscore;
609             }
610             return $maxscore;
612         break;
613         case AVERAGEATTEMPT:
614             $attemptcount = scorm_get_attempt_count($userid, $scorm, true);
615             if (empty($attemptcount)) {
616                 return 0;
617             } else {
618                 $attemptcount = count($attemptcount);
619             }
620             $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
621             $sumscore = 0;
622             for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
623                 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
624                 $sumscore += $attemptscore;
625             }
627             return round($sumscore / $attemptcount);
628         break;
629     }
632 function scorm_count_launchable($scormid, $organization='') {
633     global $DB;
635     $sqlorganization = '';
636     $params = array($scormid);
637     if (!empty($organization)) {
638         $sqlorganization = " AND organization=?";
639         $params[] = $organization;
640     }
641     return $DB->count_records_select('scorm_scoes', "scorm = ? $sqlorganization AND ".$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), $params);
644 function scorm_get_last_attempt($scormid, $userid) {
645     global $DB;
647     /// Find the last attempt number for the given user id and scorm id
648     if ($lastattempt = $DB->get_record('scorm_scoes_track', array('userid'=>$userid, 'scormid'=>$scormid), 'max(attempt) as a')) {
649         if (empty($lastattempt->a)) {
650             return '1';
651         } else {
652             return $lastattempt->a;
653         }
654     } else {
655         return false;
656     }
659 function scorm_get_last_completed_attempt($scormid, $userid) {
660     global $DB;
662     /// Find the last attempt number for the given user id and scorm id
663     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')) {
664         if (empty($lastattempt->a)) {
665             return '1';
666         } else {
667             return $lastattempt->a;
668         }
669     } else {
670         return false;
671     }
674 function scorm_course_format_display($user, $course) {
675     global $CFG, $DB, $PAGE, $OUTPUT;
677     $strupdate = get_string('update');
678     $context = get_context_instance(CONTEXT_COURSE, $course->id);
680     echo '<div class="mod-scorm">';
681     if ($scorms = get_all_instances_in_course('scorm', $course)) {
682         // The module SCORM activity with the least id is the course
683         $scorm = current($scorms);
684         if (! $cm = get_coursemodule_from_instance('scorm', $scorm->id, $course->id)) {
685             print_error('invalidcoursemodule');
686         }
687         $contextmodule = get_context_instance(CONTEXT_MODULE, $cm->id);
688         if ((has_capability('mod/scorm:skipview', $contextmodule))) {
689             scorm_simple_play($scorm, $user, $contextmodule, $cm->id);
690         }
691         $colspan = '';
692         $headertext = '<table width="100%"><tr><td class="title">'.get_string('name').': <b>'.format_string($scorm->name).'</b>';
693         if (has_capability('moodle/course:manageactivities', $context)) {
694             if ($PAGE->user_is_editing()) {
695                 // Display update icon
696                 $path = $CFG->wwwroot.'/course';
697                 $headertext .= '<span class="commands">'.
698                         '<a title="'.$strupdate.'" href="'.$path.'/mod.php?update='.$cm->id.'&amp;sesskey='.sesskey().'">'.
699                         '<img src="'.$OUTPUT->pix_url('t/edit') . '" class="iconsmall" alt="'.$strupdate.'" /></a></span>';
700             }
701             $headertext .= '</td>';
702             // Display report link
703             $trackedusers = $DB->get_record('scorm_scoes_track', array('scormid'=>$scorm->id), 'count(distinct(userid)) as c');
704             if ($trackedusers->c > 0) {
705                 $headertext .= '<td class="reportlink">'.
706                               '<a href="'.$CFG->wwwroot.'/mod/scorm/report.php?id='.$cm->id.'">'.
707                                get_string('viewallreports', 'scorm', $trackedusers->c).'</a>';
708             } else {
709                 $headertext .= '<td class="reportlink">'.get_string('noreports', 'scorm');
710             }
711             $colspan = ' colspan="2"';
712         }
713         $headertext .= '</td></tr><tr><td'.$colspan.'>'.get_string('summary').':<br />'.format_module_intro('scorm', $scorm, $scorm->coursemodule).'</td></tr></table>';
714         echo $OUTPUT->box($headertext, 'generalbox boxwidthwide');
715         scorm_view_display($user, $scorm, 'view.php?id='.$course->id, $cm);
716     } else {
717         if (has_capability('moodle/course:update', $context)) {
718             // Create a new activity
719             $url = new moodle_url('/course/mod.php', array('id'=>$course->id, 'section'=>'0', 'sesskey'=>sesskey(),'add'=>'scorm'));
720             redirect($url);
721         } else {
722             echo $OUTPUT->notification('Could not find a scorm course here');
723         }
724     }
725     echo '</div>';
728 function scorm_view_display ($user, $scorm, $action, $cm) {
729     global $CFG, $DB, $PAGE, $OUTPUT;
731     if ($scorm->scormtype != SCORM_TYPE_LOCAL && $scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
732         scorm_parse($scorm, false);
733     }
735     $organization = optional_param('organization', '', PARAM_INT);
737     if ($scorm->displaycoursestructure == 1) {
738         echo $OUTPUT->box_start('generalbox boxaligncenter toc');
739         ?>
740         <div class="structurehead"><?php print_string('contents', 'scorm') ?></div>
741         <?php
742     }
743     if (empty($organization)) {
744         $organization = $scorm->launch;
745     }
746     if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '.
747                                          $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
748                                          $DB->sql_isempty('scorm_scoes', 'organization', false, false),
749                                          array($scorm->id), 'id', 'id,title')) {
750         if (count($orgs) > 1) {
751             $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null);
752             $select->label = get_string('organizations', 'scorm');
753             $select->class = 'scorm-center';
754             echo $OUTPUT->render($select);
755         }
756     }
757     $orgidentifier = '';
758     if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
759         if (($sco->organization == '') && ($sco->launch == '')) {
760             $orgidentifier = $sco->identifier;
761         } else {
762             $orgidentifier = $sco->organization;
763         }
764     }
766     $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR));   // Just to be safe
767     if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
768         $scorm->version = 'scorm_12';
769     }
770     require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
772     $result = scorm_get_toc($user, $scorm, $cm->id, TOCFULLURL, $orgidentifier);
773     $incomplete = $result->incomplete;
775     // do we want the TOC to be displayed?
776     if ($scorm->displaycoursestructure == 1) {
777         echo $result->toc;
778         echo $OUTPUT->box_end();
779     }
781     // is this the first attempt ?
782     $attemptcount = scorm_get_attempt_count($user->id, $scorm);
784     // do not give the player launch FORM if the SCORM object is locked after the final attempt
785     if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) {
786         ?>
787             <div class="scorm-center">
788                <form id="scormviewform" method="post" action="<?php echo $CFG->wwwroot ?>/mod/scorm/player.php">
789         <?php
790         if ($scorm->hidebrowse == 0) {
791             print_string('mode', 'scorm');
792             echo ': <input type="radio" id="b" name="mode" value="browse" /><label for="b">'.get_string('browse', 'scorm').'</label>'."\n";
793             echo '<input type="radio" id="n" name="mode" value="normal" checked="checked" /><label for="n">'.get_string('normal', 'scorm')."</label>\n";
794         } else {
795             echo '<input type="hidden" name="mode" value="normal" />'."\n";
796         }
797         if ($scorm->forcenewattempt == 1) {
798             if ($incomplete === false) {
799                 echo '<input type="hidden" name="newattempt" value="on" />'."\n";
800             }
801         } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
802             ?>
803                       <br />
804                       <input type="checkbox" id="a" name="newattempt" />
805                       <label for="a"><?php print_string('newattempt', 'scorm') ?></label>
806             <?php
807         }
808         if (!empty($scorm->popup)) {
809             echo '<input type="hidden" name="display" value="popup" />'."\n";
810         }
811         ?>
812               <br />
813               <input type="hidden" name="scoid"/>
814               <input type="hidden" name="cm" value="<?php echo $cm->id ?>"/>
815               <input type="hidden" name="currentorg" value="<?php echo $orgidentifier ?>" />
816               <input type="submit" value="<?php print_string('enter', 'scorm') ?>" />
817               </form>
818           </div>
819         <?php
820     }
823 function scorm_simple_play($scorm, $user, $context, $cmid) {
824     global $DB;
826     $result = false;
828     if ($scorm->scormtype != SCORM_TYPE_LOCAL && $scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
829         scorm_parse($scorm, false);
830     }
831     if (has_capability('mod/scorm:viewreport', $context)) { //if this user can view reports, don't skipview so they can see links to reports.
832         return $result;
833     }
835     $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'id', 'id');
837     if ($scoes) {
838         $orgidentifier = '';
839         if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) {
840             if (($sco->organization == '') && ($sco->launch == '')) {
841                 $orgidentifier = $sco->identifier;
842             } else {
843                 $orgidentifier = $sco->organization;
844             }
845         }
846         if ($scorm->skipview >= 1) {
847             $sco = current($scoes);
848             $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id,
849                                                                 'currentorg'=>$orgidentifier,
850                                                                 'scoid'=>$sco->id));
851             if ($scorm->skipview == 2 || scorm_get_tracks($sco->id, $user->id) === false) {
852                 if (!empty($scorm->forcenewattempt)) {
853                     $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier);
854                     if ($result->incomplete === false) {
855                         $url->param('newattempt','on');
856                     }
857                 }
858                 redirect($url);
859             }
860         }
861     }
862     return $result;
865 function scorm_get_count_users($scormid, $groupingid=null) {
866     global $CFG, $DB;
868     if (!empty($groupingid)) {
869         $sql = "SELECT COUNT(DISTINCT st.userid)
870                 FROM {scorm_scoes_track} st
871                     INNER JOIN {groups_members} gm ON st.userid = gm.userid
872                     INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
873                 WHERE st.scormid = ? AND gg.groupingid = ?
874                 ";
875         $params = array($scormid, $groupingid);
876     } else {
877         $sql = "SELECT COUNT(DISTINCT st.userid)
878                 FROM {scorm_scoes_track} st
879                 WHERE st.scormid = ?
880                 ";
881         $params = array($scormid);
882     }
884     return ($DB->count_records_sql($sql, $params));
887 /**
888  * Build up the JavaScript representation of an array element
889  *
890  * @param string $sversion SCORM API version
891  * @param array $userdata User track data
892  * @param string $element_name Name of array element to get values for
893  * @param array $children list of sub elements of this array element that also need instantiating
894  * @return None
895  */
896 function scorm_reconstitute_array_element($sversion, $userdata, $element_name, $children) {
897     // reconstitute comments_from_learner and comments_from_lms
898     $current = '';
899     $current_subelement = '';
900     $current_sub = '';
901     $count = 0;
902     $count_sub = 0;
903     $scormseperator = '_';
904     if (scorm_version_check($sversion, SCORM_13)) { //scorm 1.3 elements use a . instead of an _
905         $scormseperator = '.';
906     }
907     // filter out the ones we want
908     $element_list = array();
909     foreach ($userdata as $element => $value) {
910         if (substr($element, 0, strlen($element_name)) == $element_name) {
911             $element_list[$element] = $value;
912         }
913     }
915     // sort elements in .n array order
916     uksort($element_list, "scorm_element_cmp");
918     // generate JavaScript
919     foreach ($element_list as $element => $value) {
920         if (scorm_version_check($sversion, SCORM_13)) {
921             $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
922             preg_match('/\.(N\d+)\./', $element, $matches);
923         } else {
924             $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
925             preg_match('/\_(\d+)\./', $element, $matches);
926         }
927         if (count($matches) > 0 && $current != $matches[1]) {
928             if ($count_sub > 0) {
929                 echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
930             }
931             $current = $matches[1];
932             $count++;
933             $current_subelement = '';
934             $current_sub = '';
935             $count_sub = 0;
936             $end = strpos($element, $matches[1])+strlen($matches[1]);
937             $subelement = substr($element, 0, $end);
938             echo '    '.$subelement." = new Object();\n";
939             // now add the children
940             foreach ($children as $child) {
941                 echo '    '.$subelement.".".$child." = new Object();\n";
942                 echo '    '.$subelement.".".$child."._children = ".$child."_children;\n";
943             }
944         }
946         // now - flesh out the second level elements if there are any
947         if (scorm_version_check($sversion, SCORM_13)) {
948             $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
949             preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
950         } else {
951             $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
952             preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
953         }
955         // check the sub element type
956         if (count($matches) > 0 && $current_subelement != $matches[1]) {
957             if ($count_sub > 0) {
958                 echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
959             }
960             $current_subelement = $matches[1];
961             $current_sub = '';
962             $count_sub = 0;
963             $end = strpos($element, $matches[1])+strlen($matches[1]);
964             $subelement = substr($element, 0, $end);
965             echo '    '.$subelement." = new Object();\n";
966         }
968         // now check the subelement subscript
969         if (count($matches) > 0 && $current_sub != $matches[2]) {
970             $current_sub = $matches[2];
971             $count_sub++;
972             $end = strrpos($element, $matches[2])+strlen($matches[2]);
973             $subelement = substr($element, 0, $end);
974             echo '    '.$subelement." = new Object();\n";
975         }
977         echo '    '.$element.' = \''.$value."';\n";
978     }
979     if ($count_sub > 0) {
980         echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
981     }
982     if ($count > 0) {
983         echo '    '.$element_name.'._count = '.$count.";\n";
984     }
987 /**
988  * Build up the JavaScript representation of an array element
989  *
990  * @param string $a left array element
991  * @param string $b right array element
992  * @return comparator - 0,1,-1
993  */
994 function scorm_element_cmp($a, $b) {
995     preg_match('/.*?(\d+)\./', $a, $matches);
996     $left = intval($matches[1]);
997     preg_match('/.?(\d+)\./', $b, $matches);
998     $right = intval($matches[1]);
999     if ($left < $right) {
1000         return -1; // smaller
1001     } else if ($left > $right) {
1002         return 1;  // bigger
1003     } else {
1004         // look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern
1005         if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
1006             $leftterm = intval($matches[2]);
1007             $left = intval($matches[3]);
1008             if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
1009                 $rightterm = intval($matches[2]);
1010                 $right = intval($matches[3]);
1011                 if ($leftterm < $rightterm) {
1012                     return -1; // smaller
1013                 } else if ($leftterm > $rightterm) {
1014                     return 1;  // bigger
1015                 } else {
1016                     if ($left < $right) {
1017                         return -1; // smaller
1018                     } else if ($left > $right) {
1019                         return 1;  // bigger
1020                     }
1021                 }
1022             }
1023         }
1024         // fall back for no second level matches or second level matches are equal
1025         return 0;  // equal to
1026     }
1029 /**
1030  * Generate the user attempt status string
1031  *
1032  * @param object $user Current context user
1033  * @param object $scorm a moodle scrom object - mdl_scorm
1034  * @return string - Attempt status string
1035  */
1036 function scorm_get_attempt_status($user, $scorm, $cm='') {
1037     global $DB, $PAGE, $OUTPUT;
1039     $attempts = scorm_get_attempt_count($user->id, $scorm, true);
1040     if (empty($attempts)) {
1041         $attemptcount = 0;
1042     } else {
1043         $attemptcount = count($attempts);
1044     }
1046     $result = '<p>'.get_string('noattemptsallowed', 'scorm').': ';
1047     if ($scorm->maxattempt > 0) {
1048         $result .= $scorm->maxattempt . '<br />';
1049     } else {
1050         $result .= get_string('unlimited').'<br />';
1051     }
1052     $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . '<br />';
1054     if ($scorm->maxattempt == 1) {
1055         switch ($scorm->grademethod) {
1056             case GRADEHIGHEST:
1057                 $grademethod = get_string('gradehighest', 'scorm');
1058             break;
1059             case GRADEAVERAGE:
1060                 $grademethod = get_string('gradeaverage', 'scorm');
1061             break;
1062             case GRADESUM:
1063                 $grademethod = get_string('gradesum', 'scorm');
1064             break;
1065             case GRADESCOES:
1066                 $grademethod = get_string('gradescoes', 'scorm');
1067             break;
1068         }
1069     } else {
1070         switch ($scorm->whatgrade) {
1071             case HIGHESTATTEMPT:
1072                 $grademethod = get_string('highestattempt', 'scorm');
1073             break;
1074             case AVERAGEATTEMPT:
1075                 $grademethod = get_string('averageattempt', 'scorm');
1076             break;
1077             case FIRSTATTEMPT:
1078                 $grademethod = get_string('firstattempt', 'scorm');
1079             break;
1080             case LASTATTEMPT:
1081                 $grademethod = get_string('lastattempt', 'scorm');
1082             break;
1083         }
1084     }
1086     if (!empty($attempts)) {
1087         $i = 1;
1088         foreach ($attempts as $attempt) {
1089             $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber);
1090             if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1091                 $gradereported = $gradereported/$scorm->maxgrade;
1092                 $gradereported = number_format($gradereported*100, 0) .'%';
1093             }
1094             $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .'<br />';
1095             $i++;
1096         }
1097     }
1098     $calculatedgrade = scorm_grade_user($scorm, $user->id);
1099     if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1100         $calculatedgrade = $calculatedgrade/$scorm->maxgrade;
1101         $calculatedgrade = number_format($calculatedgrade*100, 0) .'%';
1102     }
1103     $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod;
1104     if (empty($attempts)) {
1105         $result .= '<br />' . get_string('gradereported', 'scorm') . ': ' . get_string('none') . '<br />';
1106     } else {
1107         $result .= '<br />' . get_string('gradereported', 'scorm') . ': ' . $calculatedgrade . '<br />';
1108     }
1109     $result .= '</p>';
1110     if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) {
1111         $result .= '<p><font color="#cc0000">'.get_string('exceededmaxattempts', 'scorm').'</font></p>';
1112     }
1113     if (!empty($cm)) {
1114         $context = context_module::instance($cm->id);
1115         if (has_capability('mod/scorm:deleteownresponses', $context) &&
1116             $DB->record_exists('scorm_scoes_track', array('userid' => $user->id, 'scormid' => $scorm->id))) {
1117             //check to see if any data is stored for this user:
1118             $deleteurl = new moodle_url($PAGE->url, array('action'=>'delete', 'sesskey' => sesskey()));
1119             $result .= $OUTPUT->single_button($deleteurl, get_string('deleteallattempts', 'scorm'));
1120         }
1121     }
1124     return $result;
1127 /**
1128  * Get SCORM attempt count
1129  *
1130  * @param object $user Current context user
1131  * @param object $scorm a moodle scrom object - mdl_scorm
1132  * @param bool $attempts return the list of attempts
1133  * @return int - no. of attempts so far
1134  */
1135 function scorm_get_attempt_count($userid, $scorm, $attempts_only=false) {
1136     global $DB;
1137     $attemptcount = 0;
1138     $element = 'cmi.core.score.raw';
1139     if ($scorm->grademethod == GRADESCOES) {
1140         $element = 'cmi.core.lesson_status';
1141     }
1142     if (scorm_version_check($scorm->version, SCORM_13)) {
1143         $element = 'cmi.score.raw';
1144     }
1145     $attempts = $DB->get_records_select('scorm_scoes_track', "element=? AND userid=? AND scormid=?", array($element, $userid, $scorm->id), 'attempt', 'DISTINCT attempt AS attemptnumber');
1146     if ($attempts_only) {
1147         return $attempts;
1148     }
1149     if (!empty($attempts)) {
1150         $attemptcount = count($attempts);
1151     }
1152     return $attemptcount;
1155 /**
1156  * Figure out with this is a debug situation
1157  *
1158  * @param object $scorm a moodle scrom object - mdl_scorm
1159  * @return boolean - debugging true/false
1160  */
1161 function scorm_debugging($scorm) {
1162     global $CFG, $USER;
1163     $cfg_scorm = get_config('scorm');
1165     if (!$cfg_scorm->allowapidebug) {
1166         return false;
1167     }
1168     $identifier = $USER->username.':'.$scorm->name;
1169     $test = $cfg_scorm->apidebugmask;
1170     // check the regex is only a short list of safe characters
1171     if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) {
1172         return false;
1173     }
1174     $res = false;
1175     eval('$res = preg_match(\'/^'.$test.'/\', $identifier) ? true : false;');
1176     return $res;
1179 /**
1180  * Delete Scorm tracks for selected users
1181  *
1182  * @param array $attemptids list of attempts that need to be deleted
1183  * @param int $scorm instance
1184  *
1185  * return bool true deleted all responses, false failed deleting an attempt - stopped here
1186  */
1187 function scorm_delete_responses($attemptids, $scorm) {
1188     if (!is_array($attemptids) || empty($attemptids)) {
1189         return false;
1190     }
1192     foreach ($attemptids as $num => $attemptid) {
1193         if (empty($attemptid)) {
1194             unset($attemptids[$num]);
1195         }
1196     }
1198     foreach ($attemptids as $attempt) {
1199         $keys = explode(':', $attempt);
1200         if (count($keys) == 2) {
1201             $userid = clean_param($keys[0], PARAM_INT);
1202             $attemptid = clean_param($keys[1], PARAM_INT);
1203             if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) {
1204                     return false;
1205             }
1206         } else {
1207             return false;
1208         }
1209     }
1210     return true;
1213 /**
1214  * Delete Scorm tracks for selected users
1215  *
1216  * @param int $userid ID of User
1217  * @param int $scormid ID of Scorm
1218  * @param int $attemptid user attempt that need to be deleted
1219  *
1220  * return bool true suceeded
1221  */
1222 function scorm_delete_attempt($userid, $scorm, $attemptid) {
1223     global $DB;
1225     $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid));
1226     include_once('lib.php');
1227     scorm_update_grades($scorm, $userid, true);
1228     return true;
1231 /**
1232  * Converts SCORM duration notation to human-readable format
1233  * The function works with both SCORM 1.2 and SCORM 2004 time formats
1234  * @param $duration string SCORM duration
1235  * @return string human-readable date/time
1236  */
1237 function scorm_format_duration($duration) {
1238     // fetch date/time strings
1239     $stryears = get_string('years');
1240     $strmonths = get_string('nummonths');
1241     $strdays = get_string('days');
1242     $strhours = get_string('hours');
1243     $strminutes = get_string('minutes');
1244     $strseconds = get_string('seconds');
1246     if ($duration[0] == 'P') {
1247         // if timestamp starts with 'P' - it's a SCORM 2004 format
1248         // this regexp discards empty sections, takes Month/Minute ambiguity into consideration,
1249         // and outputs filled sections, discarding leading zeroes and any format literals
1250         // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero
1251         $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#',
1252                           '#([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#' );
1253         $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ', '',
1254                           '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '');
1255     } else {
1256         // else we have SCORM 1.2 format there
1257         // first convert the timestamp to some SCORM 2004-like format for conveniency
1258         $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration);
1259         // then convert in the same way as SCORM 2004
1260         $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#' );
1261         $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '' );
1262     }
1264     $result = preg_replace($pattern, $replace, $duration);
1266     return $result;
1269 function scorm_get_toc($user,$scorm,$cmid,$toclink=TOCJSLINK,$currentorg='',$scoid='',$mode='normal',$attempt='',$play=false, $tocheader=false) {
1270     global $CFG, $DB, $PAGE, $OUTPUT;
1272     $modestr = '';
1273     if ($mode == 'browse') {
1274         $modestr = '&amp;mode='.$mode;
1275     }
1277     $result = new stdClass();
1278     if ($tocheader) {
1279         $result->toc = '<div id="scorm_layout">';
1280         $result->toc .= '<div id="scorm_toc">';
1281         $result->toc .= '<div id="scorm_tree">';
1282     }
1283     $result->toc .= '<ul>';
1284     $tocmenus = array();
1285     $result->prerequisites = true;
1286     $incomplete = false;
1288     //
1289     // Get the current organization infos
1290     //
1291     if (!empty($currentorg)) {
1292         if (($organizationtitle = $DB->get_field('scorm_scoes','title', array('scorm'=>$scorm->id,'identifier'=>$currentorg))) != '') {
1293             if ($play) {
1294                 $result->toctitle = "$organizationtitle";
1295             }
1296             else {
1297                 $result->toc .= "\t<li>$organizationtitle</li>\n";
1298             }
1299             $tocmenus[] = $organizationtitle;
1300         }
1301     }
1303     //
1304     // If not specified retrieve the last attempt number
1305     //
1306     if (empty($attempt)) {
1307         $attempt = scorm_get_attempt_count($user->id, $scorm);
1308     }
1309     $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attempt;
1310     if ($scoes = scorm_get_scoes($scorm->id, $currentorg)){
1311         //
1312         // Retrieve user tracking data for each learning object
1313         //
1314         $usertracks = array();
1315         foreach ($scoes as $sco) {
1316             if (!empty($sco->launch)) {
1317                 if ($usertrack = scorm_get_tracks($sco->id,$user->id,$attempt)) {
1318                     if ($usertrack->status == '') {
1319                         $usertrack->status = 'notattempted';
1320                     }
1321                     $usertracks[$sco->identifier] = $usertrack;
1322                 }
1323             }
1324         }
1326         $level=0;
1327         $sublist=1;
1328         $previd = 0;
1329         $nextid = 0;
1330         $findnext = false;
1331         $parents[$level]='/';
1332         $prevsco = '';
1333         foreach ($scoes as $pos => $sco) {
1334             $isvisible = false;
1335             $sco->title = $sco->title;
1336             if (!isset($sco->isvisible) || (isset($sco->isvisible) && ($sco->isvisible == 'true'))) {
1337                 $isvisible = true;
1338             }
1339             if ($parents[$level] != $sco->parent) {
1340                 if ($newlevel = array_search($sco->parent,$parents)) {
1341                     for ($i=0; $i<($level-$newlevel); $i++) {
1342                         $result->toc .= "\t\t</li></ul></li>\n";
1343                     }
1344                     $level = $newlevel;
1345                 } else {
1346                     $i = $level;
1347                     $closelist = '';
1348                     while (($i > 0) && ($parents[$level] != $sco->parent)) {
1349                         if ($i === 1 && $level > 1) {
1350                             $closelist .= "\t\t</ul></li>\n";
1351                         } else {
1352                             $closelist .= "\t</li></ul></li>\n";
1353                         }
1354                         $i--;
1355                     }
1356                     if (($i == 0) && ($sco->parent != $currentorg)) {
1357                         $result->toc .= "\n\t<ul>\n";
1358                         $level++;
1359                     } else {
1360                         $result->toc .= $closelist;
1361                         $level = $i;
1362                     }
1363                     $parents[$level] = $sco->parent;
1364                 }
1365             }
1366             if ($isvisible) {
1367                 $result->toc .= "<li>";
1368             }
1369             if (isset($scoes[$pos+1])) {
1370                 $nextsco = $scoes[$pos+1];
1371             } else {
1372                 $nextsco = false;
1373             }
1374             $nextisvisible = false;
1375             if (($nextsco !== false) && (!isset($nextsco->isvisible) || (isset($nextsco->isvisible) && ($nextsco->isvisible == 'true')))) {
1376                 $nextisvisible = true;
1377             }
1378             if ($nextisvisible && ($nextsco !== false) && ($sco->parent != $nextsco->parent) &&
1379                (($level==0) || (($level>0) && ($nextsco->parent == $sco->identifier)))) {
1380                 $sublist++;
1381             }
1382             if (empty($sco->title)) {
1383                 $sco->title = $sco->identifier;
1384             }
1385             if ($isvisible) {
1386                 if (!empty($sco->launch)) {
1387                     $score = '';
1388                     if (empty($scoid) && ($mode != 'normal')) {
1389                         $scoid = $sco->id;
1390                     }
1391                     if (isset($usertracks[$sco->identifier])) {
1392                         $usertrack = $usertracks[$sco->identifier];
1393                         $strstatus = get_string($usertrack->status,'scorm');
1394                         if ($sco->scormtype == 'sco') {
1395                             $statusicon = '<img src="'.$OUTPUT->pix_url($usertrack->status, 'scorm').'" alt="'.$strstatus.'" title="'.$strstatus.'" />';
1396                         } else {
1397                             $statusicon = '<img src="'.$OUTPUT->pix_url('assetc', 'scorm').'" alt="'.get_string('assetlaunched','scorm').'" title="'.get_string('assetlaunched','scorm').'" />';
1398                         }
1400                         if (($usertrack->status == 'notattempted') || ($usertrack->status == 'incomplete') || ($usertrack->status == 'browsed')) {
1401                             $incomplete = true;
1402                             if ($play && empty($scoid)) {
1403                                 $scoid = $sco->id;
1404                             }
1405                         }
1406                         if ($usertrack->score_raw != '' && has_capability('mod/scorm:viewscores', get_context_instance(CONTEXT_MODULE,$cmid))) {
1407                             $score = '('.get_string('score','scorm').':&nbsp;'.$usertrack->score_raw.')';
1408                         }
1409                         $strsuspended = get_string('suspended','scorm');
1410                         $exitvar = 'cmi.core.exit';
1411                         if (scorm_version_check($scorm->version, SCORM_13)) {
1412                             $exitvar = 'cmi.exit';
1413                         }
1414                         if ($incomplete && isset($usertrack->{$exitvar}) && ($usertrack->{$exitvar} == 'suspend')) {
1415                             $statusicon = '<img src="'.$OUTPUT->pix_url('suspend', 'scorm').'" alt="'.$strstatus.' - '.$strsuspended.'" title="'.$strstatus.' - '.$strsuspended.'" />';
1416                         }
1417                     } else {
1418                         if ($play && empty($scoid)) {
1419                             $scoid = $sco->id;
1420                         }
1421                         $incomplete = true;
1422                         if ($sco->scormtype == 'sco') {
1423                             $statusicon = '<img src="'.$OUTPUT->pix_url('notattempted', 'scorm').'" alt="'.get_string('notattempted','scorm').'" title="'.get_string('notattempted','scorm').'" />';
1424                         } else {
1425                             $statusicon = '<img src="'.$OUTPUT->pix_url('asset', 'scorm').'" alt="'.get_string('asset','scorm').'" title="'.get_string('asset','scorm').'" />';
1426                         }
1427                     }
1428                     if ($sco->id == $scoid) {
1429                         $findnext = true;
1430                     }
1432                     if (($nextid == 0) && (scorm_count_launchable($scorm->id,$currentorg) > 1) && ($nextsco!==false) && (!$findnext)) {
1433                         if (!empty($sco->launch)) {
1434                             $previd = $sco->id;
1435                         }
1436                     }
1437                     if (scorm_version_check($scorm->version, SCORM_13)) {
1438                         require_once($CFG->dirroot.'/mod/scorm/datamodels/sequencinglib.php');
1439                         $prereq = scorm_seq_evaluate($sco->id,$usertracks);
1440                     } else {
1441                         //TODO: split check for sco->prerequisites only for AICC as I think that's the only case it's set.
1442                         $prereq = empty($sco->prerequisites) || scorm_eval_prerequisites($sco->prerequisites,$usertracks);
1443                     }
1444                     if ($prereq) {
1445                         if ($sco->id == $scoid) {
1446                             $result->prerequisites = true;
1447                         }
1448                         if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) {
1449                             $result->toc .= '<span>'.$statusicon.'&nbsp;'.format_string($sco->title).'</span>';
1450                         } else if ($toclink == TOCFULLURL) { //display toc with urls for structure page
1451                             $url = $CFG->wwwroot.'/mod/scorm/player.php?a='.$scorm->id.'&amp;currentorg='.$currentorg.$modestr.'&amp;scoid='.$sco->id;
1452                             $result->toc .= $statusicon.'&nbsp;<a href="'.$url.'">'.format_string($sco->title).'</a>'.$score."\n";
1453                         } else { //display toc for inside scorm player
1454                             if ($sco->launch) {
1455                                 $link = 'a='.$scorm->id.'&scoid='.$sco->id.'&currentorg='.$currentorg.$modestr.'&attempt='.$attempt;
1456                                 $result->toc .= '<a title="'.$link.'">'.$statusicon.'&nbsp;'.format_string($sco->title).'&nbsp;'.$score.'</a>';
1457                             } else {
1458                                 $result->toc .= '<span>'.$statusicon.'&nbsp;'.format_string($sco->title).'</span>';
1459                             }
1460                         }
1461                         $tocmenus[$sco->id] = scorm_repeater('&minus;',$level) . '&gt;' . format_string($sco->title);
1462                     } else {
1463                         if ($sco->id == $scoid) {
1464                             $result->prerequisites = false;
1465                         }
1466                         if ($play) {
1467                             // should be disabled
1468                             $result->toc .= '<span>'.$statusicon.'&nbsp;'.format_string($sco->title).'</span>';
1469                         } else {
1470                             $result->toc .= $statusicon.'&nbsp;'.format_string($sco->title)."\n";
1471                         }
1472                     }
1473                 } else {
1474                     $result->toc .= '&nbsp;'.format_string($sco->title);
1475                 }
1476                 if (($nextsco === false) || $nextsco->parent == $sco->parent) {
1477                     $result->toc .= "</li>\n";
1478                 }
1479             }
1480             if (($nextsco !== false) && ($nextid == 0) && ($findnext)) {
1481                 if (!empty($nextsco->launch)) {
1482                     $nextid = $nextsco->id;
1483                 }
1484             }
1485             $prevsco = $sco;
1486         }
1487         for ($i=0;$i<$level;$i++) {
1488             $result->toc .= "\t\t</ul></li>\n";
1489         }
1491         if ($play) {
1492             // it is possible that $scoid is still not set, in this case we don't want an empty object
1493             if ($scoid) {
1494                 $sco = scorm_get_sco($scoid);
1495             }
1496             $sco->previd = $previd;
1497             $sco->nextid = $nextid;
1498             $result->sco = $sco;
1499             $result->incomplete = $incomplete;
1500         } else {
1501             $result->incomplete = $incomplete;
1502         }
1503     }
1504     $result->toc .= '</ul>';
1506     // NEW IMS TOC
1507     if ($tocheader) {
1508         $result->toc .= '</div></div></div>';
1509         $result->toc .= '<div id="scorm_navpanel"></div>';
1510     }
1512     $url = new moodle_url('/mod/scorm/player.php?a='.$scorm->id.'&currentorg='.$currentorg.$modestr);
1513     $result->tocmenu = $OUTPUT->single_select($url, 'scoid', $tocmenus, $sco->id, null, "tocmenu");
1515     return $result;