First step to SCORM2004, new communication subsystem
[moodle.git] / mod / scorm / lib.php
1 <?php  // $Id$
3 /// Library of functions and constants for module scorm
4 /// (replace scorm with the name of your module and delete this line)
6 define('VALUESCOES', '0');
7 define('VALUEHIGHEST', '1');
8 define('VALUEAVERAGE', '2');
9 define('VALUESUM', '3');
10 $SCORM_GRADE_METHOD = array (VALUESCOES => get_string("gradescoes", "scorm"), 
11                              VALUEHIGHEST => get_string("gradehighest", "scorm"),
12                              VALUEAVERAGE => get_string("gradeaverage", "scorm"),
13                              VALUESUM => get_string("gradesum", "scorm"));
14                              
15 $SCORM_WINDOW_OPTIONS = array('resizable', 'scrollbars', 'status', 'height', 'width');
17 if (!isset($CFG->scorm_popup)) {
18     set_config('scorm_popup', '');
19 }
20 if (!isset($CFG->scorm_validate)) {
21     $scorm_validate = 'none';
22     //I've commented this out for Moodle 1.4, as I've seen errors in 
23     //SCORM packages even though the actual package worked fine. -- Martin Dougiamas
24     if (extension_loaded('domxml') && version_compare(phpversion(),'5.0.0','<')) {
25         $scorm_validate = 'domxml';
26     }
27     if (version_compare(phpversion(),'5.0.0','>=')) {
28         $scorm_validate = 'php5';
29     }
30     set_config('scorm_validate', $scorm_validate);
31 }
33 foreach ($SCORM_WINDOW_OPTIONS as $popupoption) {
34     $popupoption = "scorm_popup$popupoption";
35     if (!isset($CFG->$popupoption)) {
36         if ($popupoption == 'scorm_popupheight') {
37             set_config($popupoption, 450);
38         } else if ($popupoption == 'scorm_popupwidth') {
39             set_config($popupoption, 620);
40         } else {
41             set_config($popupoption, 'checked');
42         }
43     }  
44 }
46 if (!isset($CFG->scorm_framesize)) {
47     set_config('scorm_framesize', 140);
48 }
50 function scorm_add_instance($scorm) {
51 /// Given an object containing all the necessary data, 
52 /// (defined by the form in mod.html) this function 
53 /// will create a new instance and return the id number 
54 /// of the new instance.
56     $scorm->timemodified = time();
58     # May have to add extra stuff in here #
59     global $CFG,$SCORM_WINDOW_OPTIONS;
60     
61     $scorm->popup = '';
63     $optionlist = array();
64     foreach ($SCORM_WINDOW_OPTIONS as $option) {
65         if (isset($scorm->$option)) {
66             $optionlist[] = $option.'='.$scorm->$option;
67         }
68     }
69     $scorm->popup = implode(',', $optionlist);
70     
72     if ($scorm->popup != '') {
73         $scorm->popup .= ',location=0,menubar=0,toolbar=0';
74         $scorm->auto = '0';
75     }
76     $id = insert_record('scorm', $scorm);
77     
78     //
79     // Parse scorm manifest
80     //
81     if ($scorm->launch == 0) {
82         $basedir = $CFG->dataroot."/".$scorm->course;
83         $scormdir = "/moddata/scorm";
84         $scorm->launch = scorm_parse($basedir,$scormdir.$scorm->datadir."/imsmanifest.xml",$id);
85         set_field("scorm","launch",$scorm->launch,"id",$id);
86     }
87     
88     return $id;
89 }
92 function scorm_update_instance($scorm) {
93 /// Given an object containing all the necessary data, 
94 /// (defined by the form in mod.html) this function 
95 /// will update an existing instance with new data.
96     
97     $scorm->timemodified = time();
98     $scorm->id = $scorm->instance;
100     # May have to add extra stuff in here #
101     global $CFG,$SCORM_WINDOW_OPTIONS;
102     
103     $scorm->popup = '';
104     
105     $optionlist = array();
106     foreach ($SCORM_WINDOW_OPTIONS as $option) {
107         if (isset($scorm->$option)) {
108             $optionlist[] = $option.'='.$scorm->$option;
109         }
110     }
111     $scorm->popup = implode(',', $optionlist);
113     if ($scorm->popup != '') {
114         $scorm->popup .= ',location=0,menubar=0,toolbar=0';
115         $scorm->auto = '0';
116     }
117     
118     //
119     // Check if scorm manifest needs to be reparsed
120     //
121     if ($scorm->launch == 0) {
122         $basedir = $CFG->dataroot."/".$scorm->course;
123         $scormdir = "/moddata/scorm";
124         $scorm->launch = scorm_parse($basedir,$scormdir.$scorm->datadir."/imsmanifest.xml",$scorm->id);
125     }
126     
127     return update_record('scorm', $scorm);
131 function scorm_delete_instance($id) {
132 /// Given an ID of an instance of this module, 
133 /// this function will permanently delete the instance 
134 /// and any data that depends on it.  
135     
136     require('../config.php');
138     if (! $scorm = get_record('scorm', 'id', $id)) {
139         return false;
140     }
142     $result = true;
144     # Delete any dependent files #
145     scorm_delete_files($CFG->dataroot.'/'.$scorm->course.'/moddata/scorm'.$scorm->datadir);
147     # Delete any dependent records here #
148     if (! delete_records('scorm_sco_users', 'scormid', $scorm->id)) {
149         $result = false;
150     }
151     if (! delete_records('scorm_scoes', 'scorm', $scorm->id)) {
152         $result = false;
153     }
154     if (! delete_records('scorm', 'id', $scorm->id)) {
155         $result = false;
156     }
157     
159     return $result;
162 function scorm_user_outline($course, $user, $mod, $scorm) {
163 /// Return a small object with summary information about what a 
164 /// user has done with a given particular instance of this module
165 /// Used for user activity reports.
166 /// $return->time = the time they did it
167 /// $return->info = a short text description
169     return $return;
172 function scorm_user_complete($course, $user, $mod, $scorm) {
173 /// Print a detailed representation of what a  user has done with 
174 /// a given particular instance of this module, for user activity reports.
176     return true;
179 function scorm_print_recent_activity(&$logs, $isteacher=false) {
180 /// Given a list of logs, assumed to be those since the last login 
181 /// this function prints a short list of changes related to this module
182 /// If isteacher is true then perhaps additional information is printed.
183 /// This function is called from course/lib.php: print_recent_activity()
185     global $CFG, $COURSE_TEACHER_COLOR;
187     $content = NULL;
189     return $content;  // True if anything was printed, otherwise false
192 function scorm_cron () {
193 /// Function to be run periodically according to the moodle cron
194 /// This function searches for things that need to be done, such 
195 /// as sending out mail, toggling flags etc ... 
197     global $CFG;
199     return true;
202 function scorm_grades($scormid) {
203 /// Must return an array of grades for a given instance of this module, 
204 /// indexed by user.  It also returns a maximum allowed grade.
206     global $CFG;
207     
208     if (!$scorm = get_record("scorm", "id", $scormid)) {
209         return NULL;
210     }
211     
212     if ($scorm->grademethod == VALUESCOES) {
213         if (!$return->maxgrade = count_records_select('scorm_scoes',"scorm='$scormid' AND launch<>''")) {
214             return NULL;
215         }
216     
217         $return->grades = NULL;
218         if ($sco_users=get_records_select('scorm_sco_users', "scormid='$scormid' GROUP BY userid")) {
219             foreach ($sco_users as $sco_user) {
220                 $user_data=get_records_select('scorm_sco_users',"scormid='$scormid' AND userid='$sco_user->userid'");
221                 $scores->completed=0;
222                 $scores->browsed=0;
223                 $scores->incomplete=0;
224                 $scores->failed=0;
225                 $scores->notattempted=0;
226                 $result='';
227                 $data = current($user_data);
228                 foreach ($user_data as $data) {
229                     if ($data->cmi_core_lesson_status=='passed')
230                         $scores->completed++;
231                     else
232                         $scores->{scorm_remove_spaces($data->cmi_core_lesson_status)}++;
233                 }
234                 if ($scores->completed)
235                     $result.="<img src=\"$CFG->wwwroot/mod/scorm/pix/completed.gif\" alt=\"".get_string('completed','scorm')."\" title=\"".get_string('completed','scorm')."\" /> $scores->completed ";
236                 if ($scores->incomplete)
237                     $result.="<img src=\"$CFG->wwwroot/mod/scorm/pix/incomplete.gif\" alt=\"".get_string('incomplete','scorm')."\" title=\"".get_string('incomplete','scorm')."\" /> $scores->incomplete ";
238                 if ($scores->failed)
239                     $result.="<img src=\"$CFG->wwwroot/mod/scorm/pix/failed.gif\" alt=\"".get_string('failed','scorm')."\" title=\"".get_string('failed','scorm')."\" /> $scores->failed ";
240                 if ($scores->browsed)
241                     $result.="<img src=\"$CFG->wwwroot/mod/scorm/pix/browsed.gif\" alt=\"".get_string('browsed','scorm')."\" title=\"".get_string('browsed','scorm')."\" /> $scores->browsed ";
242                 if ($scores->notattempted)
243                     $result.="<img src=\"$CFG->wwwroot/mod/scorm/pix/notattempted.gif\" alt=\"".get_string('notattempted','scorm')."\" title=\"".get_string('notattempted','scorm')."\" /> $scores->notattempted ";
244             
245                 $return->grades[$sco_user->userid]=$result;
246             }
247         
248         }
249     } else {
250         $grades = get_records_select("scorm_sco_users", "scormid=$scormid AND cmi_core_score_raw>0","","id,userid,cmi_core_score_raw");
251         //$grades = get_records_menu("scorm_sco_users", "scormid",$scormid,"","userid,cmi_core_score_raw");
252         $valutations = array();
253         foreach ($grades as $grade) {
254             if (!isset($valutations[$grade->userid])) {
255                 if ($scorm->grademethod == VALUEAVERAGE) {
256                     $values = array();
257                     $values[$grade->userid]->grade = 0;
258                     $values[$grade->userid]->values = 0;
259                 }
260                 $valutations[$grade->userid] = 0;
261             }
262             switch ($scorm->grademethod) {
263                 case VALUEHIGHEST:
264                     if ($grade->cmi_core_score_raw > $valutations[$grade->userid]) {
265                         $valutations[$grade->userid] = $grade->cmi_core_score_raw;
266                     }
267                 break;
268                 case VALUEAVERAGE:
269                     $values[$grade->userid]->grade += $grade->cmi_core_score_raw;
270                     $values[$grade->userid]->values++;
271                 break;
272                 case VALUESUM:
273                     $valutations[$grade->userid] += $grade->cmi_core_score_raw;
274                 break;
275             }
276         }
277         if ($scorm->grademethod == VALUEAVERAGE) {
278             foreach($values as $userid => $value) {
279                 $valutations[$userid] = $value->grade/$value->values;
280             }
281         }
282         //print_r($grades);
283         $return->grades = $valutations;
284         $return->maxgrade = $scorm->maxgrade;
285     }
286     return $return;
290 //////////////////////////////////////////////////////////////////////////////////////
291 /// Any other scorm functions go here.  Each of them must have a name that 
292 /// starts with scorm_
295 function scorm_randstring($len = '8')
297         $rstring = NULL;
298         $lchar = '';
299         for($i=0; $i<$len; $i++) {
300                 $char = chr(rand(48,122));
301                 while (!ereg('[a-zA-Z0-9]', $char)){
302                         if($char == $lchar) continue;
303                         $char = chr(rand(48,90));
304                 }
305                 $rstring .= $char;
306                 $lchar = $char;
307         }
308         return $rstring;
311  
312 function scorm_datadir($strPath, $existingdir='', $prefix = 'SCORM')
314     global $CFG;
316     if (($existingdir!='') && (is_dir($strPath.$existingdir)))
317         return $strPath.$existingdir;
318         
319     if (is_dir($strPath)) {
320         do {
321             $datadir='/'.$prefix.scorm_randstring();
322         } while (file_exists($strPath.$datadir));
323         mkdir($strPath.$datadir, $CFG->directorypermissions);
324         @chmod($strPath.$datadir, $CFG->directorypermissions);  // Just in case mkdir didn't do it
325         return $strPath.$datadir;
326     } else {
327         return false;
328     }
329
331 if ($CFG->scorm_validate == 'domxml') {
332     require_once('validatordomxml.php');
335 function scorm_validate($manifest)
337     global $CFG;
338     
339     global $item_idref_array;
340     global $idres_array;
341     global $def_org_array;
342     global $id_org_array;
343     
344     if (is_file ($manifest)) {
345         if (file_exists($manifest)) {
346             if ($CFG->scorm_validate == 'domxml') {
347                 $manifest_string = file_get_contents($manifest);
349                 /* Elimino i caratteri speciali di spaziatura e ritorno a capo dal file xml */
351                 $spec = array('\n', '\r', '\t', '\0', '\x0B');
352                 $content = str_replace($spec, '', $manifest_string);
354                 if ($xmldoc = domxml_open_mem($content)) {
355                     $root = $xmldoc->document_element();
356                     if (!testRoot($root)) {
357                         return 'syntax';
358                     }
359                     if (testNode($root)) {      
360                         // Nel corpo di questo if si controllano le corrispondenze fra gli attributi
361                         // Nello Standard SCORM ad ogni attributo idRef di <item> deve corrispondere
362                         // un attributo ID di <resource>
363                         // Gli array degli attributi sono stati dichiarati globali in validator.php
364                         // pertanto possono essere utilizzati direttamente all'interno di main.php
366                         foreach($item_idref_array as $elem_it) {  
367                             if (array_search($elem_it, $idres_array) === false) {
368                                 return 'mismatch';
369                             }
370                         }
371   
372                         foreach($def_org_array as $elem_def) {  
373                             if (array_search($elem_it, $id_org_array) === false) {
374                                 return 'mismatch';
375                             }
376                         }
377                    
378                     } else {
379                         return 'badmanifest';
380                     }
381                 }
382                 return 'regular';
383             } else {
384                 return 'found';
385             }
386         }
387     } else {
388         return 'nomanifest';
389     }
392 function scorm_delete_files($directory) {
393     if (is_dir($directory)) {
394         $files=scorm_scandir($directory);
395         //print_r($files);
396         foreach($files as $file) {
397             if (($file != '.') && ($file != '..')) {
398                 if (!is_dir($directory.'/'.$file)) {
399                     //chmod($directory.'/'.$file,0777);
400                     unlink($directory.'/'.$file);
401                 } else {
402                     scorm_delete_files($directory.'/'.$file);
403                 }
404             }
405         }
406         rmdir($directory);
407     }
410 function scorm_scandir($directory) {
411     if (version_compare(phpversion(),'5.0.0','>=')) {
412         return scandir($directory);
413     } else {
414         $files = null;
415         if ($dh = opendir($directory)) {
416             while (($file = readdir($dh)) !== false) {
417                 $files[] = $file;
418             }
419             closedir($dh);
420         }
421         return $files;
422     }
425 function scorm_startElement($parser, $name, $attrs) {
427     global $scoes,$i,$resources,$parent,$level,$organization,$manifest,$defaultorg;
429     if ($name == 'ITEM') {
430         $i++;
431         $scoes[$i]['manifest'] = $manifest;
432         $scoes[$i]['organization'] = $organization;
433         $scoes[$i]['identifier'] = $attrs['IDENTIFIER'];
434         if (empty($attrs['IDENTIFIERREF']))
435             $attrs['IDENTIFIERREF'] = '';
436         $scoes[$i]['identifierref'] = $attrs['IDENTIFIERREF'];
437         if (empty($attrs['ISVISIBLE']))
438             $attrs['ISVISIBLE'] = '';
439         $scoes[$i]['isvisible'] = $attrs['ISVISIBLE'];
440         $scoes[$i]['parent'] = $parent[$level];
441         $level++;
442         $parent[$level] = $attrs['IDENTIFIER'];
443     }
444     if ($name == 'RESOURCE') {
445         if (!isset($attrs['HREF'])) {
446             $attrs['HREF'] = '';
447         }
448         $resources[$attrs['IDENTIFIER']]['href']=$attrs['HREF'];
449         if (!isset($attrs['ADLCP:SCORMTYPE'])) {
450             $attrs['ADLCP:SCORMTYPE'] = '';
451         }
452         $resources[$attrs['IDENTIFIER']]['type']=$attrs['ADLCP:SCORMTYPE'];
453     }
454     if ($name == 'ORGANIZATION') {
455         $i++;
456         $scoes[$i]['manifest'] = $manifest;
457         $scoes[$i]['organization'] = '';
458         $scoes[$i]['identifier'] = $attrs['IDENTIFIER'];
459         $scoes[$i]['identifierref'] = '';
460         $scoes[$i]['isvisible'] = '';
461         $scoes[$i]['parent'] = $parent[$level];
462         $level++;
463         $parent[$level] = $attrs['IDENTIFIER'];
464         $organization = $attrs['IDENTIFIER'];
465     }
466     if ($name == 'MANIFEST') {
467         $manifest = $attrs['IDENTIFIER'];
468     }
469     if ($name == 'ORGANIZATIONS') {
470         if (!isset($attrs['DEFAULT'])) {
471             $attrs['DEFAULT'] = '';
472         }
473         $defaultorg = $attrs['DEFAULT'];
474     }
477 function scorm_endElement($parser, $name) {
478     global $scoes,$i,$level,$datacontent,$navigation;
479     if ($name == 'ITEM') {
480         $level--;
481     }
482     //if ($name == 'TITLE' && $level>0) {
483     if ($name == 'TITLE') {
484         $scoes[$i]['title'] = $datacontent;
485     }
486     if ($name == 'ADLCP:HIDERTSUI') {
487         $scoes[$i][$datacontent] = 1;
488     }
489     if ($name == 'ADLCP:DATAFROMLMS') {
490         $scoes[$i]['datafromlms'] = $datacontent;
491     }
492     if ($name == 'ORGANIZATION') {
493         $organization = '';
494         $level--;
495     }
496     if ($name == 'MANIFEST') {
497         $manifest = '';
498     }
501 function scorm_characterData($parser, $data) {
502     global $datacontent;
503     $datacontent = utf8_decode($data);
506 function scorm_parse($basedir,$file,$scorm_id) {
507     global $scoes,$i,$resources,$parent,$level,$defaultorg;
508     $datacontent = '';
509     $scoes[][] = '';
510     $resources[] = '';
511     $organization = '';
512     $defaultorg = '';
513     $i = 0;
514     $level = 0;
515     $parent[$level] = '/';
517     $xml_parser = xml_parser_create('UTF-8');
518     // use case-folding so we are sure to find the tag in $map_array
519     xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);
520     xml_set_element_handler($xml_parser, 'scorm_startElement', 'scorm_endElement');
521     xml_set_character_data_handler($xml_parser, 'scorm_characterData');
522     if (!($fp = fopen($basedir.$file, 'r'))) {
523        die('could not open XML input');
524     }
526     while ($data = fread($fp, 4096)) {
527         if (!xml_parse($xml_parser, $data, feof($fp))) {
528             die(sprintf('XML error: %s at line %d',
529                     xml_error_string(xml_get_error_code($xml_parser)),
530                     xml_get_current_line_number($xml_parser)));
531         }
532     }
533     xml_parser_free($xml_parser);
534     $launch = 0;
536     $sco->scorm = $scorm_id;
537     delete_records('scorm_scoes','scorm',$scorm_id);
538     delete_records('scorm_sco_users','scormid',$scorm_id);
539     
540     if (isset($scoes[1])) {
541         for ($j=1; $j<=$i; $j++) {
542             $sco->identifier = $scoes[$j]['identifier'];
543             $sco->parent = $scoes[$j]['parent'];
544             $sco->title = $scoes[$j]['title'];
545             $sco->organization = $scoes[$j]['organization'];
546             if (!isset($scoes[$j]['datafromlms'])) {
547                 $scoes[$j]['datafromlms'] = '';
548             } 
549             $sco->datafromlms = $scoes[$j]['datafromlms'];
550         
551             if (!isset($resources[($scoes[$j]['identifierref'])]['href'])) {
552                 $resources[($scoes[$j]['identifierref'])]['href'] = '';
553             }
554             $sco->launch = $resources[($scoes[$j]['identifierref'])]['href'];
555         
556             if (!isset($resources[($scoes[$j]['identifierref'])]['type'])) {
557                 $resources[($scoes[$j]['identifierref'])]['type'] = '';
558             }
559             $sco->type = $resources[($scoes[$j]['identifierref'])]['type'];
560         
561             if (!isset($scoes[$j]['previous'])) {
562                 $scoes[$j]['previous'] = 0;
563             }
564             $sco->previous = $scoes[$j]['previous'];
565         
566             if (!isset($scoes[$j]['continue'])) {
567                 $scoes[$j]['continue'] = 0;
568             }
569             $sco->next = $scoes[$j]['continue'];
570         
571             if (scorm_remove_spaces($scoes[$j]['isvisible']) != 'false') {
572                 $id = insert_record('scorm_scoes',$sco);
573             }
574             //if (($launch==0) && (isset($sco->launch)) && ($defaultorg==$sco->organization)) {
575             if (($launch==0) && ($defaultorg==$sco->identifier)) {
576                 $launch = $id;
577             }
578         }
579     } else {
580         foreach ($resources as $label => $resource) {
581             if (!empty($resource['href'])) {
582                 $sco->identifier = $label;
583                 $sco->title = $label;
584                 $sco->parent = '/';
585                 $sco->launch = $resource['href'];
586                 $sco->type = $resource['type'];
587                 $id = insert_record('scorm_scoes',$sco);
588                 
589                 if ($launch == 0) {
590                     $launch = $id;
591                 }
592             }
593         }
594     }
595     return $launch;
598 function scorm_get_scoes_records($sco_user) {
599 /// Gets all info required to display the table of scorm results
600 /// for report.php
601     global $CFG;
603     return get_records_sql("SELECT su.*, u.firstname, u.lastname, u.picture 
604                             FROM {$CFG->prefix}scorm_sco_users su, 
605                                  {$CFG->prefix}user u
606                             WHERE su.scormid = '$sco_user->scormid'
607                               AND su.userid = u.id
608                               AND su.userid = '$sco_user->userid'
609                               ORDER BY scoid");
612 function scorm_remove_spaces($sourcestr) {
613 // Remove blank space from a string
614     $newstr='';
615     for( $i=0; $i<strlen($sourcestr); $i++) {
616         if ($sourcestr[$i]!=' ')
617             $newstr .=$sourcestr[$i];
618     }
619     return $newstr;
622 function scorm_string_round($stringa) {
623 // Crop a string to $len character and set an anchor title to the full string
624     $len=11;
625     if ( strlen($stringa)>$len ) {
626     return "<a name=\"none\" title=\"$stringa\">".substr($stringa,0,$len-4).'...'.substr($stringa,strlen($stringa)-1,1).'</a>';
627     } else
628     return $stringa;
631 function scorm_external_link($link) {
632 // check if a link is external
633     $result = false;
634     $link = strtolower($link);
635     if (substr($link,0,7) == 'http://')
636         $result = true;
637     else if (substr($link,0,8) == 'https://')
638         $result = true;
639     else if (substr($link,0,4) == 'www.')
640         $result = true;
641     /*else if (substr($link,0,7) == 'rstp://')
642         $result = true;
643     else if (substr($link,0,6) == 'rtp://')
644         $result = true;
645     else if (substr($link,0,6) == 'ftp://')
646         $result = true;
647     else if (substr($link,0,9) == 'gopher://')
648         $result = true; */
649     return $result;
650 }    
651 ?>