Revert "MDL-43247 SCORM: improve pop-up handling and redirect to homepage on close."
[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('SCORM_SKIPVIEW_NEVER', '0');
26 define('SCORM_SKIPVIEW_FIRST', '1');
27 define('SCORM_SKIPVIEW_ALWAYS', '2');
29 define('SCO_ALL', 0);
30 define('SCO_DATA', 1);
31 define('SCO_ONLY', 2);
33 define('GRADESCOES', '0');
34 define('GRADEHIGHEST', '1');
35 define('GRADEAVERAGE', '2');
36 define('GRADESUM', '3');
38 define('HIGHESTATTEMPT', '0');
39 define('AVERAGEATTEMPT', '1');
40 define('FIRSTATTEMPT', '2');
41 define('LASTATTEMPT', '3');
43 define('TOCJSLINK', 1);
44 define('TOCFULLURL', 2);
46 /// Local Library of functions for module scorm
48 /**
49  * @package   mod-scorm
50  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
51  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
52  */
53 class scorm_package_file_info extends file_info_stored {
54     public function get_parent() {
55         if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
56             return $this->browser->get_file_info($this->context);
57         }
58         return parent::get_parent();
59     }
60     public function get_visible_name() {
61         if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
62             return $this->topvisiblename;
63         }
64         return parent::get_visible_name();
65     }
66 }
68 /**
69  * Returns an array of the popup options for SCORM and each options default value
70  *
71  * @return array an array of popup options as the key and their defaults as the value
72  */
73 function scorm_get_popup_options_array() {
74     $cfgscorm = get_config('scorm');
76     return array('scrollbars'=> isset($cfgscorm->scrollbars) ? $cfgscorm->scrollbars : 0,
77                  'directories'=> isset($cfgscorm->directories) ? $cfgscorm->directories : 0,
78                  'location'=> isset($cfgscorm->location) ? $cfgscorm->location : 0,
79                  'menubar'=> isset($cfgscorm->menubar) ? $cfgscorm->menubar : 0,
80                  'toolbar'=> isset($cfgscorm->toolbar) ? $cfgscorm->toolbar : 0,
81                  'status'=> isset($cfgscorm->status) ? $cfgscorm->status : 0);
82 }
84 /**
85  * Returns an array of the array of what grade options
86  *
87  * @return array an array of what grade options
88  */
89 function scorm_get_grade_method_array() {
90     return array (GRADESCOES => get_string('gradescoes', 'scorm'),
91                   GRADEHIGHEST => get_string('gradehighest', 'scorm'),
92                   GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
93                   GRADESUM => get_string('gradesum', 'scorm'));
94 }
96 /**
97  * Returns an array of the array of what grade options
98  *
99  * @return array an array of what grade options
100  */
101 function scorm_get_what_grade_array() {
102     return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
103                   AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
104                   FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
105                   LASTATTEMPT => get_string('lastattempt', 'scorm'));
108 /**
109  * Returns an array of the array of skip view options
110  *
111  * @return array an array of skip view options
112  */
113 function scorm_get_skip_view_array() {
114     return array(SCORM_SKIPVIEW_NEVER => get_string('never'),
115                  SCORM_SKIPVIEW_FIRST => get_string('firstaccess', 'scorm'),
116                  SCORM_SKIPVIEW_ALWAYS => get_string('always'));
119 /**
120  * Returns an array of the array of hide table of contents options
121  *
122  * @return array an array of hide table of contents options
123  */
124 function scorm_get_hidetoc_array() {
125      return array(SCORM_TOC_SIDE => get_string('sided', 'scorm'),
126                   SCORM_TOC_HIDDEN => get_string('hidden', 'scorm'),
127                   SCORM_TOC_POPUP => get_string('popupmenu', 'scorm'),
128                   SCORM_TOC_DISABLED => get_string('disabled', 'scorm'));
131 /**
132  * Returns an array of the array of update frequency options
133  *
134  * @return array an array of update frequency options
135  */
136 function scorm_get_updatefreq_array() {
137     return array(SCORM_UPDATE_NEVER => get_string('never'),
138                  SCORM_UPDATE_EVERYDAY => get_string('everyday', 'scorm'),
139                  SCORM_UPDATE_EVERYTIME => get_string('everytime', 'scorm'));
142 /**
143  * Returns an array of the array of popup display options
144  *
145  * @return array an array of popup display options
146  */
147 function scorm_get_popup_display_array() {
148     return array(0 => get_string('currentwindow', 'scorm'),
149                  1 => get_string('popup', 'scorm'));
152 /**
153  * Returns an array of the array of navigation buttons display options
154  *
155  * @return array an array of navigation buttons display options
156  */
157 function scorm_get_navigation_display_array() {
158     return array(SCORM_NAV_DISABLED => get_string('no'),
159                  SCORM_NAV_UNDER_CONTENT => get_string('undercontent', 'scorm'),
160                  SCORM_NAV_FLOATING => get_string('floating', 'scorm'));
163 /**
164  * Returns an array of the array of attempt options
165  *
166  * @return array an array of attempt options
167  */
168 function scorm_get_attempts_array() {
169     $attempts = array(0 => get_string('nolimit', 'scorm'),
170                       1 => get_string('attempt1', 'scorm'));
172     for ($i=2; $i<=6; $i++) {
173         $attempts[$i] = get_string('attemptsx', 'scorm', $i);
174     }
176     return $attempts;
179 /**
180  * Returns an array of the attempt status options
181  *
182  * @return array an array of attempt status options
183  */
184 function scorm_get_attemptstatus_array() {
185     return array(SCORM_DISPLAY_ATTEMPTSTATUS_NO => get_string('no'),
186                  SCORM_DISPLAY_ATTEMPTSTATUS_ALL => get_string('attemptstatusall', 'scorm'),
187                  SCORM_DISPLAY_ATTEMPTSTATUS_MY => get_string('attemptstatusmy', 'scorm'),
188                  SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY => get_string('attemptstatusentry', 'scorm'));
191 /**
192  * Extracts scrom package, sets up all variables.
193  * Called whenever scorm changes
194  * @param object $scorm instance - fields are updated and changes saved into database
195  * @param bool $full force full update if true
196  * @return void
197  */
198 function scorm_parse($scorm, $full) {
199     global $CFG, $DB;
200     $cfg_scorm = get_config('scorm');
202     if (!isset($scorm->cmid)) {
203         $cm = get_coursemodule_from_instance('scorm', $scorm->id);
204         $scorm->cmid = $cm->id;
205     }
206     $context = context_module::instance($scorm->cmid);
207     $newhash = $scorm->sha1hash;
209     if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
211         $fs = get_file_storage();
212         $packagefile = false;
213         $packagefileimsmanifest = false;
215         if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
216             if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
217                 if ($packagefile->is_external_file()) { // Get zip file so we can check it is correct.
218                     $packagefile->import_external_file_contents();
219                 }
220                 $newhash = $packagefile->get_contenthash();
221                 if (strtolower($packagefile->get_filename()) == 'imsmanifest.xml') {
222                     $packagefileimsmanifest = true;
223                 }
224             } else {
225                 $newhash = null;
226             }
227         } else {
228             if (!$cfg_scorm->allowtypelocalsync) {
229                 // sorry - localsync disabled
230                 return;
231             }
232             if ($scorm->reference !== '' and (!$full or $scorm->sha1hash !== sha1($scorm->reference))) {
233                 $fs->delete_area_files($context->id, 'mod_scorm', 'package');
234                 $file_record = array('contextid'=>$context->id, 'component'=>'mod_scorm', 'filearea'=>'package', 'itemid'=>0, 'filepath'=>'/');
235                 if ($packagefile = $fs->create_file_from_url($file_record, $scorm->reference, array('calctimeout' => true))) {
236                     $newhash = sha1($scorm->reference);
237                 } else {
238                     $newhash = null;
239                 }
240             }
241         }
243         if ($packagefile) {
244             if (!$full and $packagefile and $scorm->sha1hash === $newhash) {
245                 if (strpos($scorm->version, 'SCORM') !== false) {
246                     if ($packagefileimsmanifest || $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
247                         // No need to update.
248                         return;
249                     }
250                 } else if (strpos($scorm->version, 'AICC') !== false) {
251                     // TODO: add more sanity checks - something really exists in scorm_content area
252                     return;
253                 }
254             }
255             if (!$packagefileimsmanifest) {
256                 // Now extract files.
257                 $fs->delete_area_files($context->id, 'mod_scorm', 'content');
259                 $packer = get_file_packer('application/zip');
260                 $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/');
261             }
263         } else if (!$full) {
264             return;
265         }
266         if ($packagefileimsmanifest) {
267             require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
268             // Direct link to imsmanifest.xml file.
269             if (!scorm_parse_scorm($scorm, $packagefile)) {
270                 $scorm->version = 'ERROR';
271             }
273         } else if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
274             require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
275             // SCORM
276             if (!scorm_parse_scorm($scorm, $manifest)) {
277                 $scorm->version = 'ERROR';
278             }
279         } else {
280             require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
281             // AICC
282             if (!scorm_parse_aicc($scorm)) {
283                 $scorm->version = 'ERROR';
284             }
285             $scorm->version = 'AICC';
286         }
288     } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfg_scorm->allowtypeexternal) {
289         require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
290         // SCORM only, AICC can not be external
291         if (!scorm_parse_scorm($scorm, $scorm->reference)) {
292             $scorm->version = 'ERROR';
293         }
294         $newhash = sha1($scorm->reference);
296     } else if ($scorm->scormtype === SCORM_TYPE_AICCURL  and $cfg_scorm->allowtypeexternalaicc) {
297         require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
298         // AICC
299         if (!scorm_parse_aicc($scorm)) {
300             $scorm->version = 'ERROR';
301         }
302         $scorm->version = 'AICC';
303     } else {
304         // sorry, disabled type
305         return;
306     }
308     $scorm->revision++;
309     $scorm->sha1hash = $newhash;
310     $DB->update_record('scorm', $scorm);
314 function scorm_array_search($item, $needle, $haystacks, $strict=false) {
315     if (!empty($haystacks)) {
316         foreach ($haystacks as $key => $element) {
317             if ($strict) {
318                 if ($element->{$item} === $needle) {
319                     return $key;
320                 }
321             } else {
322                 if ($element->{$item} == $needle) {
323                     return $key;
324                 }
325             }
326         }
327     }
328     return false;
331 function scorm_repeater($what, $times) {
332     if ($times <= 0) {
333         return null;
334     }
335     $return = '';
336     for ($i=0; $i<$times; $i++) {
337         $return .= $what;
338     }
339     return $return;
342 function scorm_external_link($link) {
343     // check if a link is external
344     $result = false;
345     $link = strtolower($link);
346     if (substr($link, 0, 7) == 'http://') {
347         $result = true;
348     } else if (substr($link, 0, 8) == 'https://') {
349         $result = true;
350     } else if (substr($link, 0, 4) == 'www.') {
351         $result = true;
352     }
353     return $result;
356 /**
357  * Returns an object containing all datas relative to the given sco ID
358  *
359  * @param integer $id The sco ID
360  * @return mixed (false if sco id does not exists)
361  */
362 function scorm_get_sco($id, $what=SCO_ALL) {
363     global $DB;
365     if ($sco = $DB->get_record('scorm_scoes', array('id'=>$id))) {
366         $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
367         if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id)))) {
368             foreach ($scodatas as $scodata) {
369                 $sco->{$scodata->name} = $scodata->value;
370             }
371         } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id))))) {
372             $sco->parameters = '';
373         }
374         return $sco;
375     } else {
376         return false;
377     }
380 /**
381  * Returns an object (array) containing all the scoes data related to the given sco ID
382  *
383  * @param integer $id The sco ID
384  * @param integer $organisation an organisation ID - defaults to false if not required
385  * @return mixed (false if there are no scoes or an array)
386  */
387 function scorm_get_scoes($id, $organisation=false) {
388     global $DB;
390     $queryarray = array('scorm'=>$id);
391     if (!empty($organisation)) {
392         $queryarray['organization'] = $organisation;
393     }
394     if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'sortorder, id')) {
395         // Drop keys so that it is a simple array as expected.
396         $scoes = array_values($scoes);
397         foreach ($scoes as $sco) {
398             if ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$sco->id))) {
399                 foreach ($scodatas as $scodata) {
400                     $sco->{$scodata->name} = $scodata->value;
401                 }
402             }
403         }
404         return $scoes;
405     } else {
406         return false;
407     }
410 function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $value, $forcecompleted=false) {
411     global $DB, $CFG;
413     $id = null;
415     if ($forcecompleted) {
416         //TODO - this could be broadened to encompass SCORM 2004 in future
417         if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) {
418             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))) {
419                 $value = 'completed';
420             }
421         }
422         if ($element == 'cmi.core.score.raw') {
423             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))) {
424                 if ($tracktest->value == "incomplete") {
425                     $tracktest->value = "completed";
426                     $DB->update_record('scorm_scoes_track', $tracktest);
427                 }
428             }
429         }
430         if (($element == 'cmi.success_status') && ($value == 'passed' || $value == 'failed')) {
431             if ($DB->get_record('scorm_scoes_data', array('scoid' => $scoid, 'name' => 'objectivesetbycontent'))) {
432                 $objectiveprogressstatus = true;
433                 $objectivesatisfiedstatus = false;
434                 if ($value == 'passed') {
435                     $objectivesatisfiedstatus = true;
436                 }
438                 if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
439                                                                         'scormid' => $scormid,
440                                                                         'scoid' => $scoid,
441                                                                         'attempt' => $attempt,
442                                                                         'element' => 'objectiveprogressstatus'))) {
443                     $track->value = $objectiveprogressstatus;
444                     $track->timemodified = time();
445                     $DB->update_record('scorm_scoes_track', $track);
446                     $id = $track->id;
447                 } else {
448                     $track = new stdClass();
449                     $track->userid = $userid;
450                     $track->scormid = $scormid;
451                     $track->scoid = $scoid;
452                     $track->attempt = $attempt;
453                     $track->element = 'objectiveprogressstatus';
454                     $track->value = $objectiveprogressstatus;
455                     $track->timemodified = time();
456                     $id = $DB->insert_record('scorm_scoes_track', $track);
457                 }
458                 if ($objectivesatisfiedstatus) {
459                     if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
460                                                                             'scormid' => $scormid,
461                                                                             'scoid' => $scoid,
462                                                                             'attempt' => $attempt,
463                                                                             'element' => 'objectivesatisfiedstatus'))) {
464                         $track->value = $objectivesatisfiedstatus;
465                         $track->timemodified = time();
466                         $DB->update_record('scorm_scoes_track', $track);
467                         $id = $track->id;
468                     } else {
469                         $track = new stdClass();
470                         $track->userid = $userid;
471                         $track->scormid = $scormid;
472                         $track->scoid = $scoid;
473                         $track->attempt = $attempt;
474                         $track->element = 'objectivesatisfiedstatus';
475                         $track->value = $objectivesatisfiedstatus;
476                         $track->timemodified = time();
477                         $id = $DB->insert_record('scorm_scoes_track', $track);
478                         ob_start();
479                         $filepath = $CFG->dataroot."\\temp\\tempfile.txt";
480                         $fh = fopen($filepath, "a+");
481                         var_dump($track);
482                         $string = ob_get_clean();
483                         fwrite($fh, $string);
484                         fclose($fh);
485                     }
486                 }
487             }
488         }
490     }
492     if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
493                                                             'scormid' => $scormid,
494                                                             'scoid' => $scoid,
495                                                             'attempt' => $attempt,
496                                                             'element' => $element))) {
497         if ($element != 'x.start.time' ) { // Don't update x.start.time - keep the original value.
498             $track->value = $value;
499             $track->timemodified = time();
500             $DB->update_record('scorm_scoes_track', $track);
501             $id = $track->id;
502         }
503     } else {
504         $track = new stdClass();
505         $track->userid = $userid;
506         $track->scormid = $scormid;
507         $track->scoid = $scoid;
508         $track->attempt = $attempt;
509         $track->element = $element;
510         $track->value = $value;
511         $track->timemodified = time();
512         $id = $DB->insert_record('scorm_scoes_track', $track);
513     }
515     if (strstr($element, '.score.raw') ||
516         (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
517          && in_array($track->value, array('completed', 'passed')))) {
518         $scorm = $DB->get_record('scorm', array('id' => $scormid));
519         include_once($CFG->dirroot.'/mod/scorm/lib.php');
520         scorm_update_grades($scorm, $userid);
521     }
523     return $id;
526 /**
527  * simple quick function to return true/false if this user has tracks in this scorm
528  *
529  * @param integer $scormid The scorm ID
530  * @param integer $userid the users id
531  * @return boolean (false if there are no tracks)
532  */
533 function scorm_has_tracks($scormid, $userid) {
534     global $DB;
535     return $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scormid));
538 function scorm_get_tracks($scoid, $userid, $attempt='') {
539     // Gets all tracks of specified sco and user.
540     global $DB;
542     if (empty($attempt)) {
543         if ($scormid = $DB->get_field('scorm_scoes', 'scorm', array('id'=>$scoid))) {
544             $attempt = scorm_get_last_attempt($scormid, $userid);
545         } else {
546             $attempt = 1;
547         }
548     }
549     if ($tracks = $DB->get_records('scorm_scoes_track', array('userid'=>$userid, 'scoid'=>$scoid,
550                                                               'attempt'=>$attempt), 'element ASC')) {
551         $usertrack = scorm_format_interactions($tracks);
552         $usertrack->userid = $userid;
553         $usertrack->scoid = $scoid;
555         return $usertrack;
556     } else {
557         return false;
558     }
560 /**
561  * helper function to return a formatted list of interactions for reports.
562  *
563  * @param array $trackdata the records from scorm_scoes_track table
564  * @return object formatted list of interactions
565  */
566 function scorm_format_interactions($trackdata) {
567     $usertrack = new stdClass();
569     // Defined in order to unify scorm1.2 and scorm2004.
570     $usertrack->score_raw = '';
571     $usertrack->status = '';
572     $usertrack->total_time = '00:00:00';
573     $usertrack->session_time = '00:00:00';
574     $usertrack->timemodified = 0;
576     foreach ($trackdata as $track) {
577         $element = $track->element;
578         $usertrack->{$element} = $track->value;
579         switch ($element) {
580             case 'cmi.core.lesson_status':
581             case 'cmi.completion_status':
582                 if ($track->value == 'not attempted') {
583                     $track->value = 'notattempted';
584                 }
585                 $usertrack->status = $track->value;
586                 break;
587             case 'cmi.core.score.raw':
588             case 'cmi.score.raw':
589                 $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
590                 break;
591             case 'cmi.core.session_time':
592             case 'cmi.session_time':
593                 $usertrack->session_time = $track->value;
594                 break;
595             case 'cmi.core.total_time':
596             case 'cmi.total_time':
597                 $usertrack->total_time = $track->value;
598                 break;
599         }
600         if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
601             $usertrack->timemodified = $track->timemodified;
602         }
603     }
605     return $usertrack;
607 /* Find the start and finsh time for a a given SCO attempt
608  *
609  * @param int $scormid SCORM Id
610  * @param int $scoid SCO Id
611  * @param int $userid User Id
612  * @param int $attemt Attempt Id
613  *
614  * @return object start and finsh time EPOC secods
615  *
616  */
617 function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
618     global $DB;
620     $timedata = new stdClass();
621     $sql = !empty($scoid) ? "userid=$userid AND scormid=$scormid AND scoid=$scoid AND attempt=$attempt" : "userid=$userid AND scormid=$scormid AND attempt=$attempt";
622     $tracks = $DB->get_records_select('scorm_scoes_track', "$sql ORDER BY timemodified ASC");
623     if ($tracks) {
624         $tracks = array_values($tracks);
625     }
627     if ($tracks) {
628         $timedata->start = $tracks[0]->timemodified;
629     } else {
630         $timedata->start = false;
631     }
632     if ($tracks && $track = array_pop($tracks)) {
633         $timedata->finish = $track->timemodified;
634     } else {
635         $timedata->finish = $timedata->start;
636     }
637     return $timedata;
640 function scorm_grade_user_attempt($scorm, $userid, $attempt=1) {
641     global $DB;
642     $attemptscore = new stdClass();
643     $attemptscore->scoes = 0;
644     $attemptscore->values = 0;
645     $attemptscore->max = 0;
646     $attemptscore->sum = 0;
647     $attemptscore->lastmodify = 0;
649     if (!$scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) {
650         return null;
651     }
653     foreach ($scoes as $sco) {
654         if ($userdata=scorm_get_tracks($sco->id, $userid, $attempt)) {
655             if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
656                 $attemptscore->scoes++;
657             }
658             if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type=='sco' && isset($userdata->score_raw))) {
659                 $attemptscore->values++;
660                 $attemptscore->sum += $userdata->score_raw;
661                 $attemptscore->max = ($userdata->score_raw > $attemptscore->max)?$userdata->score_raw:$attemptscore->max;
662                 if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
663                     $attemptscore->lastmodify = $userdata->timemodified;
664                 } else {
665                     $attemptscore->lastmodify = 0;
666                 }
667             }
668         }
669     }
670     switch ($scorm->grademethod) {
671         case GRADEHIGHEST:
672             $score = (float) $attemptscore->max;
673         break;
674         case GRADEAVERAGE:
675             if ($attemptscore->values > 0) {
676                 $score = $attemptscore->sum/$attemptscore->values;
677             } else {
678                 $score = 0;
679             }
680         break;
681         case GRADESUM:
682             $score = $attemptscore->sum;
683         break;
684         case GRADESCOES:
685             $score = $attemptscore->scoes;
686         break;
687         default:
688             $score = $attemptscore->max;   // Remote Learner GRADEHIGHEST is default
689     }
691     return $score;
694 function scorm_grade_user($scorm, $userid) {
696     // ensure we dont grade user beyond $scorm->maxattempt settings
697     $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
698     if ($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt) {
699         $lastattempt = $scorm->maxattempt;
700     }
702     switch ($scorm->whatgrade) {
703         case FIRSTATTEMPT:
704             return scorm_grade_user_attempt($scorm, $userid, 1);
705         break;
706         case LASTATTEMPT:
707             return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
708         break;
709         case HIGHESTATTEMPT:
710             $maxscore = 0;
711             for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
712                 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
713                 $maxscore = $attemptscore > $maxscore ? $attemptscore: $maxscore;
714             }
715             return $maxscore;
717         break;
718         case AVERAGEATTEMPT:
719             $attemptcount = scorm_get_attempt_count($userid, $scorm, true, true);
720             if (empty($attemptcount)) {
721                 return 0;
722             } else {
723                 $attemptcount = count($attemptcount);
724             }
725             $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
726             $sumscore = 0;
727             for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
728                 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
729                 $sumscore += $attemptscore;
730             }
732             return round($sumscore / $attemptcount);
733         break;
734     }
737 function scorm_count_launchable($scormid, $organization='') {
738     global $DB;
740     $sqlorganization = '';
741     $params = array($scormid);
742     if (!empty($organization)) {
743         $sqlorganization = " AND organization=?";
744         $params[] = $organization;
745     }
746     return $DB->count_records_select('scorm_scoes', "scorm = ? $sqlorganization AND ".$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), $params);
749 /**
750  * Returns the last attempt used - if no attempts yet, returns 1 for first attempt
751  *
752  * @param int $scormid the id of the scorm.
753  * @param int $userid the id of the user.
754  *
755  * @return int The attempt number to use.
756  */
757 function scorm_get_last_attempt($scormid, $userid) {
758     global $DB;
760     /// Find the last attempt number for the given user id and scorm id
761     $sql = "SELECT MAX(attempt)
762               FROM {scorm_scoes_track}
763              WHERE userid = ? AND scormid = ?";
764     $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
765     if (empty($lastattempt)) {
766         return '1';
767     } else {
768         return $lastattempt;
769     }
772 /**
773  * Returns the last completed attempt used - if no completed attempts yet, returns 1 for first attempt
774  *
775  * @param int $scormid the id of the scorm.
776  * @param int $userid the id of the user.
777  *
778  * @return int The attempt number to use.
779  */
780 function scorm_get_last_completed_attempt($scormid, $userid) {
781     global $DB;
783     /// Find the last completed attempt number for the given user id and scorm id
784     $sql = "SELECT MAX(attempt)
785               FROM {scorm_scoes_track}
786              WHERE userid = ? AND scormid = ?
787                AND (value='completed' OR value='passed')";
788     $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
789     if (empty($lastattempt)) {
790         return '1';
791     } else {
792         return $lastattempt;
793     }
796 /**
797  * Returns the full list of attempts a user has made.
798  *
799  * @param int $scormid the id of the scorm.
800  * @param int $userid the id of the user.
801  *
802  * @return array array of attemptids
803  */
804 function scorm_get_all_attempts($scormid, $userid) {
805     global $DB;
806     $attemptids = array();
807     $sql = "SELECT DISTINCT attempt FROM {scorm_scoes_track} WHERE userid = ? AND scormid = ? ORDER BY attempt";
808     $attempts = $DB->get_records_sql($sql, array($userid, $scormid));
809     foreach ($attempts as $attempt) {
810         $attemptids[] = $attempt->attempt;
811     }
812     return $attemptids;
815 function scorm_view_display ($user, $scorm, $action, $cm) {
816     global $CFG, $DB, $PAGE, $OUTPUT, $COURSE;
818     if ($scorm->scormtype != SCORM_TYPE_LOCAL && $scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
819         scorm_parse($scorm, false);
820     }
822     $organization = optional_param('organization', '', PARAM_INT);
824     if ($scorm->displaycoursestructure == 1) {
825         echo $OUTPUT->box_start('generalbox boxaligncenter toc');
826         ?>
827         <div class="structurehead"><?php print_string('contents', 'scorm') ?></div>
828         <?php
829     }
830     if (empty($organization)) {
831         $organization = $scorm->launch;
832     }
833     if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '.
834                                          $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
835                                          $DB->sql_isempty('scorm_scoes', 'organization', false, false),
836                                          array($scorm->id), 'sortorder, id', 'id,title')) {
837         if (count($orgs) > 1) {
838             $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null);
839             $select->label = get_string('organizations', 'scorm');
840             $select->class = 'scorm-center';
841             echo $OUTPUT->render($select);
842         }
843     }
844     $orgidentifier = '';
845     if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
846         if (($sco->organization == '') && ($sco->launch == '')) {
847             $orgidentifier = $sco->identifier;
848         } else {
849             $orgidentifier = $sco->organization;
850         }
851     }
853     $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR));   // Just to be safe
854     if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
855         $scorm->version = 'scorm_12';
856     }
857     require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
859     $result = scorm_get_toc($user, $scorm, $cm->id, TOCFULLURL, $orgidentifier);
860     $incomplete = $result->incomplete;
862     // do we want the TOC to be displayed?
863     if ($scorm->displaycoursestructure == 1) {
864         echo $result->toc;
865         echo $OUTPUT->box_end();
866     }
868     // is this the first attempt ?
869     $attemptcount = scorm_get_attempt_count($user->id, $scorm);
871     // do not give the player launch FORM if the SCORM object is locked after the final attempt
872     if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) {
873         ?>
874             <div class="scorm-center">
875                <form id="scormviewform" method="post" action="<?php echo $CFG->wwwroot ?>/mod/scorm/player.php">
876         <?php
877         if ($scorm->hidebrowse == 0) {
878             print_string('mode', 'scorm');
879             echo ': <input type="radio" id="b" name="mode" value="browse" /><label for="b">'.get_string('browse', 'scorm').'</label>'."\n";
880             echo '<input type="radio" id="n" name="mode" value="normal" checked="checked" /><label for="n">'.get_string('normal', 'scorm')."</label>\n";
881         } else {
882             echo '<input type="hidden" name="mode" value="normal" />'."\n";
883         }
884         if ($scorm->forcenewattempt == 1) {
885             if ($incomplete === false) {
886                 echo '<input type="hidden" name="newattempt" value="on" />'."\n";
887             }
888         } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
889             ?>
890                       <br />
891                       <input type="checkbox" id="a" name="newattempt" />
892                       <label for="a"><?php print_string('newattempt', 'scorm') ?></label>
893             <?php
894         }
895         if (!empty($scorm->popup)) {
896             echo '<input type="hidden" name="display" value="popup" />'."\n";
897         }
898         ?>
899               <br />
900               <input type="hidden" name="scoid"/>
901               <input type="hidden" name="cm" value="<?php echo $cm->id ?>"/>
902               <input type="hidden" name="currentorg" value="<?php echo $orgidentifier ?>" />
903               <input type="submit" value="<?php print_string('enter', 'scorm') ?>" />
904               </form>
905           </div>
906         <?php
907     }
910 function scorm_simple_play($scorm, $user, $context, $cmid) {
911     global $DB;
913     $result = false;
915     if (has_capability('mod/scorm:viewreport', $context)) { //if this user can view reports, don't skipview so they can see links to reports.
916         return $result;
917     }
919     if ($scorm->scormtype != SCORM_TYPE_LOCAL && $scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
920         scorm_parse($scorm, false);
921     }
922     $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.
923         $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'sortorder, id', 'id');
925     if ($scoes) {
926         $orgidentifier = '';
927         if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) {
928             if (($sco->organization == '') && ($sco->launch == '')) {
929                 $orgidentifier = $sco->identifier;
930             } else {
931                 $orgidentifier = $sco->organization;
932             }
933         }
934         if ($scorm->skipview >= SCORM_SKIPVIEW_FIRST) {
935             $sco = current($scoes);
936             $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id,
937                                                                 'currentorg'=>$orgidentifier,
938                                                                 'scoid'=>$sco->id));
939             if ($scorm->skipview == SCORM_SKIPVIEW_ALWAYS || !scorm_has_tracks($scorm->id, $user->id)) {
940                 if (!empty($scorm->forcenewattempt)) {
941                     $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier);
942                     if ($result->incomplete === false) {
943                         $url->param('newattempt','on');
944                     }
945                 }
946                 redirect($url);
947             }
948         }
949     }
950     return $result;
953 function scorm_get_count_users($scormid, $groupingid=null) {
954     global $CFG, $DB;
956     if (!empty($groupingid)) {
957         $sql = "SELECT COUNT(DISTINCT st.userid)
958                 FROM {scorm_scoes_track} st
959                     INNER JOIN {groups_members} gm ON st.userid = gm.userid
960                     INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
961                 WHERE st.scormid = ? AND gg.groupingid = ?
962                 ";
963         $params = array($scormid, $groupingid);
964     } else {
965         $sql = "SELECT COUNT(DISTINCT st.userid)
966                 FROM {scorm_scoes_track} st
967                 WHERE st.scormid = ?
968                 ";
969         $params = array($scormid);
970     }
972     return ($DB->count_records_sql($sql, $params));
975 /**
976  * Build up the JavaScript representation of an array element
977  *
978  * @param string $sversion SCORM API version
979  * @param array $userdata User track data
980  * @param string $element_name Name of array element to get values for
981  * @param array $children list of sub elements of this array element that also need instantiating
982  * @return None
983  */
984 function scorm_reconstitute_array_element($sversion, $userdata, $element_name, $children) {
985     // reconstitute comments_from_learner and comments_from_lms
986     $current = '';
987     $current_subelement = '';
988     $current_sub = '';
989     $count = 0;
990     $count_sub = 0;
991     $scormseperator = '_';
992     if (scorm_version_check($sversion, SCORM_13)) { //scorm 1.3 elements use a . instead of an _
993         $scormseperator = '.';
994     }
995     // filter out the ones we want
996     $element_list = array();
997     foreach ($userdata as $element => $value) {
998         if (substr($element, 0, strlen($element_name)) == $element_name) {
999             $element_list[$element] = $value;
1000         }
1001     }
1003     // sort elements in .n array order
1004     uksort($element_list, "scorm_element_cmp");
1006     // generate JavaScript
1007     foreach ($element_list as $element => $value) {
1008         if (scorm_version_check($sversion, SCORM_13)) {
1009             $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
1010             preg_match('/\.(N\d+)\./', $element, $matches);
1011         } else {
1012             $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
1013             preg_match('/\_(\d+)\./', $element, $matches);
1014         }
1015         if (count($matches) > 0 && $current != $matches[1]) {
1016             if ($count_sub > 0) {
1017                 echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
1018             }
1019             $current = $matches[1];
1020             $count++;
1021             $current_subelement = '';
1022             $current_sub = '';
1023             $count_sub = 0;
1024             $end = strpos($element, $matches[1])+strlen($matches[1]);
1025             $subelement = substr($element, 0, $end);
1026             echo '    '.$subelement." = new Object();\n";
1027             // now add the children
1028             foreach ($children as $child) {
1029                 echo '    '.$subelement.".".$child." = new Object();\n";
1030                 echo '    '.$subelement.".".$child."._children = ".$child."_children;\n";
1031             }
1032         }
1034         // now - flesh out the second level elements if there are any
1035         if (scorm_version_check($sversion, SCORM_13)) {
1036             $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
1037             preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
1038         } else {
1039             $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
1040             preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
1041         }
1043         // check the sub element type
1044         if (count($matches) > 0 && $current_subelement != $matches[1]) {
1045             if ($count_sub > 0) {
1046                 echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
1047             }
1048             $current_subelement = $matches[1];
1049             $current_sub = '';
1050             $count_sub = 0;
1051             $end = strpos($element, $matches[1])+strlen($matches[1]);
1052             $subelement = substr($element, 0, $end);
1053             echo '    '.$subelement." = new Object();\n";
1054         }
1056         // now check the subelement subscript
1057         if (count($matches) > 0 && $current_sub != $matches[2]) {
1058             $current_sub = $matches[2];
1059             $count_sub++;
1060             $end = strrpos($element, $matches[2])+strlen($matches[2]);
1061             $subelement = substr($element, 0, $end);
1062             echo '    '.$subelement." = new Object();\n";
1063         }
1065         echo '    '.$element.' = \''.$value."';\n";
1066     }
1067     if ($count_sub > 0) {
1068         echo '    '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
1069     }
1070     if ($count > 0) {
1071         echo '    '.$element_name.'._count = '.$count.";\n";
1072     }
1075 /**
1076  * Build up the JavaScript representation of an array element
1077  *
1078  * @param string $a left array element
1079  * @param string $b right array element
1080  * @return comparator - 0,1,-1
1081  */
1082 function scorm_element_cmp($a, $b) {
1083     preg_match('/.*?(\d+)\./', $a, $matches);
1084     $left = intval($matches[1]);
1085     preg_match('/.?(\d+)\./', $b, $matches);
1086     $right = intval($matches[1]);
1087     if ($left < $right) {
1088         return -1; // smaller
1089     } else if ($left > $right) {
1090         return 1;  // bigger
1091     } else {
1092         // look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern
1093         if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
1094             $leftterm = intval($matches[2]);
1095             $left = intval($matches[3]);
1096             if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
1097                 $rightterm = intval($matches[2]);
1098                 $right = intval($matches[3]);
1099                 if ($leftterm < $rightterm) {
1100                     return -1; // smaller
1101                 } else if ($leftterm > $rightterm) {
1102                     return 1;  // bigger
1103                 } else {
1104                     if ($left < $right) {
1105                         return -1; // smaller
1106                     } else if ($left > $right) {
1107                         return 1;  // bigger
1108                     }
1109                 }
1110             }
1111         }
1112         // fall back for no second level matches or second level matches are equal
1113         return 0;  // equal to
1114     }
1117 /**
1118  * Generate the user attempt status string
1119  *
1120  * @param object $user Current context user
1121  * @param object $scorm a moodle scrom object - mdl_scorm
1122  * @return string - Attempt status string
1123  */
1124 function scorm_get_attempt_status($user, $scorm, $cm='') {
1125     global $DB, $PAGE, $OUTPUT;
1127     $attempts = scorm_get_attempt_count($user->id, $scorm, true);
1128     if (empty($attempts)) {
1129         $attemptcount = 0;
1130     } else {
1131         $attemptcount = count($attempts);
1132     }
1134     $result = '<p>'.get_string('noattemptsallowed', 'scorm').': ';
1135     if ($scorm->maxattempt > 0) {
1136         $result .= $scorm->maxattempt . '<br />';
1137     } else {
1138         $result .= get_string('unlimited').'<br />';
1139     }
1140     $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . '<br />';
1142     if ($scorm->maxattempt == 1) {
1143         switch ($scorm->grademethod) {
1144             case GRADEHIGHEST:
1145                 $grademethod = get_string('gradehighest', 'scorm');
1146             break;
1147             case GRADEAVERAGE:
1148                 $grademethod = get_string('gradeaverage', 'scorm');
1149             break;
1150             case GRADESUM:
1151                 $grademethod = get_string('gradesum', 'scorm');
1152             break;
1153             case GRADESCOES:
1154                 $grademethod = get_string('gradescoes', 'scorm');
1155             break;
1156         }
1157     } else {
1158         switch ($scorm->whatgrade) {
1159             case HIGHESTATTEMPT:
1160                 $grademethod = get_string('highestattempt', 'scorm');
1161             break;
1162             case AVERAGEATTEMPT:
1163                 $grademethod = get_string('averageattempt', 'scorm');
1164             break;
1165             case FIRSTATTEMPT:
1166                 $grademethod = get_string('firstattempt', 'scorm');
1167             break;
1168             case LASTATTEMPT:
1169                 $grademethod = get_string('lastattempt', 'scorm');
1170             break;
1171         }
1172     }
1174     if (!empty($attempts)) {
1175         $i = 1;
1176         foreach ($attempts as $attempt) {
1177             $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber);
1178             if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1179                 $gradereported = $gradereported/$scorm->maxgrade;
1180                 $gradereported = number_format($gradereported*100, 0) .'%';
1181             }
1182             $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .'<br />';
1183             $i++;
1184         }
1185     }
1186     $calculatedgrade = scorm_grade_user($scorm, $user->id);
1187     if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1188         $calculatedgrade = $calculatedgrade/$scorm->maxgrade;
1189         $calculatedgrade = number_format($calculatedgrade*100, 0) .'%';
1190     }
1191     $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod;
1192     if (empty($attempts)) {
1193         $result .= '<br />' . get_string('gradereported', 'scorm') . ': ' . get_string('none') . '<br />';
1194     } else {
1195         $result .= '<br />' . get_string('gradereported', 'scorm') . ': ' . $calculatedgrade . '<br />';
1196     }
1197     $result .= '</p>';
1198     if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) {
1199         $result .= '<p><font color="#cc0000">'.get_string('exceededmaxattempts', 'scorm').'</font></p>';
1200     }
1201     if (!empty($cm)) {
1202         $context = context_module::instance($cm->id);
1203         if (has_capability('mod/scorm:deleteownresponses', $context) &&
1204             $DB->record_exists('scorm_scoes_track', array('userid' => $user->id, 'scormid' => $scorm->id))) {
1205             //check to see if any data is stored for this user:
1206             $deleteurl = new moodle_url($PAGE->url, array('action'=>'delete', 'sesskey' => sesskey()));
1207             $result .= $OUTPUT->single_button($deleteurl, get_string('deleteallattempts', 'scorm'));
1208         }
1209     }
1212     return $result;
1215 /**
1216  * Get SCORM attempt count
1217  *
1218  * @param object $user Current context user
1219  * @param object $scorm a moodle scrom object - mdl_scorm
1220  * @param bool $returnobjects if true returns a object with attempts, if false returns count of attempts.
1221  * @param bool $ignoremissingcompletion - ignores attempts that haven't reported a grade/completion.
1222  * @return int - no. of attempts so far
1223  */
1224 function scorm_get_attempt_count($userid, $scorm, $returnobjects = false, $ignoremissingcompletion = false) {
1225     global $DB;
1227     // Historically attempts that don't report these elements haven't been included in the average attempts grading method
1228     // we may want to change this in future, but to avoid unexpected grade decreases we're leaving this in. MDL-43222
1229     if (scorm_version_check($scorm->version, SCORM_13)) {
1230         $element = 'cmi.score.raw';
1231     } else if ($scorm->grademethod == GRADESCOES) {
1232         $element = 'cmi.core.lesson_status';
1233     } else {
1234         $element = 'cmi.core.score.raw';
1235     }
1237     if ($returnobjects) {
1238         $params = array('userid' => $userid, 'scormid' => $scorm->id);
1239         if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested.
1240             $params['element'] = $element;
1241         }
1242         $attempts = $DB->get_records('scorm_scoes_track', $params, 'attempt', 'DISTINCT attempt AS attemptnumber');
1243         return $attempts;
1244     } else {
1245         $params = array($userid, $scorm->id);
1246         $sql = "SELECT COUNT(DISTINCT attempt)
1247                   FROM {scorm_scoes_track}
1248                  WHERE userid = ? AND scormid = ?";
1249         if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested.
1250             $sql .= ' AND element = ?';
1251             $params[] = $element;
1252         }
1254         $attemptscount = $DB->count_records_sql($sql, $params);
1255         return $attemptscount;
1256     }
1259 /**
1260  * Figure out with this is a debug situation
1261  *
1262  * @param object $scorm a moodle scrom object - mdl_scorm
1263  * @return boolean - debugging true/false
1264  */
1265 function scorm_debugging($scorm) {
1266     global $CFG, $USER;
1267     $cfg_scorm = get_config('scorm');
1269     if (!$cfg_scorm->allowapidebug) {
1270         return false;
1271     }
1272     $identifier = $USER->username.':'.$scorm->name;
1273     $test = $cfg_scorm->apidebugmask;
1274     // check the regex is only a short list of safe characters
1275     if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) {
1276         return false;
1277     }
1278     $res = false;
1279     eval('$res = preg_match(\'/^'.$test.'/\', $identifier) ? true : false;');
1280     return $res;
1283 /**
1284  * Delete Scorm tracks for selected users
1285  *
1286  * @param array $attemptids list of attempts that need to be deleted
1287  * @param stdClass $scorm instance
1288  *
1289  * @return bool true deleted all responses, false failed deleting an attempt - stopped here
1290  */
1291 function scorm_delete_responses($attemptids, $scorm) {
1292     if (!is_array($attemptids) || empty($attemptids)) {
1293         return false;
1294     }
1296     foreach ($attemptids as $num => $attemptid) {
1297         if (empty($attemptid)) {
1298             unset($attemptids[$num]);
1299         }
1300     }
1302     foreach ($attemptids as $attempt) {
1303         $keys = explode(':', $attempt);
1304         if (count($keys) == 2) {
1305             $userid = clean_param($keys[0], PARAM_INT);
1306             $attemptid = clean_param($keys[1], PARAM_INT);
1307             if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) {
1308                     return false;
1309             }
1310         } else {
1311             return false;
1312         }
1313     }
1314     return true;
1317 /**
1318  * Delete Scorm tracks for selected users
1319  *
1320  * @param int $userid ID of User
1321  * @param stdClass $scorm Scorm object
1322  * @param int $attemptid user attempt that need to be deleted
1323  *
1324  * @return bool true suceeded
1325  */
1326 function scorm_delete_attempt($userid, $scorm, $attemptid) {
1327     global $DB;
1329     $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid));
1330     $cm = get_coursemodule_from_instance('scorm', $scorm->id);
1332     // Trigger instances list viewed event.
1333     $event = \mod_scorm\event\attempt_deleted::create(array(
1334          'other' => array('attemptid' => $attemptid),
1335          'context' => context_module::instance($cm->id),
1336          'relateduserid' => $userid
1337     ));
1338     $event->add_record_snapshot('course_modules', $cm);
1339     $event->add_record_snapshot('scorm', $scorm);
1340     $event->trigger();
1342     include_once('lib.php');
1343     scorm_update_grades($scorm, $userid, true);
1344     return true;
1347 /**
1348  * Converts SCORM duration notation to human-readable format
1349  * The function works with both SCORM 1.2 and SCORM 2004 time formats
1350  * @param $duration string SCORM duration
1351  * @return string human-readable date/time
1352  */
1353 function scorm_format_duration($duration) {
1354     // fetch date/time strings
1355     $stryears = get_string('years');
1356     $strmonths = get_string('nummonths');
1357     $strdays = get_string('days');
1358     $strhours = get_string('hours');
1359     $strminutes = get_string('minutes');
1360     $strseconds = get_string('seconds');
1362     if ($duration[0] == 'P') {
1363         // if timestamp starts with 'P' - it's a SCORM 2004 format
1364         // this regexp discards empty sections, takes Month/Minute ambiguity into consideration,
1365         // and outputs filled sections, discarding leading zeroes and any format literals
1366         // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero
1367         $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#',
1368                           '#([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#' );
1369         $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ', '',
1370                           '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '');
1371     } else {
1372         // else we have SCORM 1.2 format there
1373         // first convert the timestamp to some SCORM 2004-like format for conveniency
1374         $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration);
1375         // then convert in the same way as SCORM 2004
1376         $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#' );
1377         $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '' );
1378     }
1380     $result = preg_replace($pattern, $replace, $duration);
1382     return $result;
1385 function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='normal', $attempt='', $play=false, $organizationsco=null) {
1386     global $CFG, $DB, $PAGE, $OUTPUT;
1388     $modestr = '';
1389     if ($mode != 'normal') {
1390         $modestr = '&mode='.$mode;
1391     }
1393     $result = array();
1394     $incomplete = false;
1396     if (!empty($organizationsco)) {
1397         $result[0] = $organizationsco;
1398         $result[0]->isvisible = true;
1399         $result[0]->statusicon = '';
1400         $result[0]->url = '';
1401     }
1403     if ($scoes = scorm_get_scoes($scorm->id, $currentorg)){
1404         // Retrieve user tracking data for each learning object.
1405         $usertracks = array();
1406         foreach ($scoes as $sco) {
1407             if (!empty($sco->launch)) {
1408                 if ($usertrack = scorm_get_tracks($sco->id, $user->id, $attempt)) {
1409                     if ($usertrack->status == '') {
1410                         $usertrack->status = 'notattempted';
1411                     }
1412                     $usertracks[$sco->identifier] = $usertrack;
1413                 }
1414             }
1415         }
1416         foreach ($scoes as $sco) {
1417             if (!isset($sco->isvisible)) {
1418                 $sco->isvisible = true;
1419             }
1421             if (empty($sco->title)) {
1422                 $sco->title = $sco->identifier;
1423             }
1425             if (scorm_version_check($scorm->version, SCORM_13)) {
1426                 $sco->prereq = true;
1427             } else {
1428                 $sco->prereq = empty($sco->prerequisites) || scorm_eval_prerequisites($sco->prerequisites, $usertracks);
1429             }
1431             if ($sco->isvisible) {
1432                 if (!empty($sco->launch)) {
1433                     if (empty($scoid) && ($mode != 'normal')) {
1434                         $scoid = $sco->id;
1435                     }
1437                     if (isset($usertracks[$sco->identifier])) {
1438                         $usertrack = $usertracks[$sco->identifier];
1439                         $strstatus = get_string($usertrack->status,'scorm');
1441                         if ($sco->scormtype == 'sco') {
1442                             $statusicon = '<img src="'.$OUTPUT->pix_url($usertrack->status, 'scorm').'" alt="'.$strstatus.'" title="'.$strstatus.'" />';
1443                         } else {
1444                             $statusicon = '<img src="'.$OUTPUT->pix_url('asset', 'scorm').'" alt="'.get_string('assetlaunched', 'scorm').'" title="'.get_string('assetlaunched', 'scorm').'" />';
1445                         }
1447                         if (($usertrack->status == 'notattempted') || ($usertrack->status == 'incomplete') || ($usertrack->status == 'browsed')) {
1448                             $incomplete = true;
1449                             if ($play && empty($scoid)) {
1450                                 $scoid = $sco->id;
1451                             }
1452                         }
1454                         $strsuspended = get_string('suspended', 'scorm');
1456                         $exitvar = 'cmi.core.exit';
1458                         if (scorm_version_check($scorm->version, SCORM_13)) {
1459                             $exitvar = 'cmi.exit';
1460                         }
1462                         if ($incomplete && isset($usertrack->{$exitvar}) && ($usertrack->{$exitvar} == 'suspend')) {
1463                             $statusicon = '<img src="'.$OUTPUT->pix_url('suspend', 'scorm').'" alt="'.$strstatus.' - '.$strsuspended.'" title="'.$strstatus.' - '.$strsuspended.'" />';
1464                         }
1466                     } else {
1467                         if ($play && empty($scoid)) {
1468                             $scoid = $sco->id;
1469                         }
1471                         $incomplete = true;
1473                         if ($sco->scormtype == 'sco') {
1474                             $statusicon = '<img src="'.$OUTPUT->pix_url('notattempted', 'scorm').'" alt="'.get_string('notattempted', 'scorm').'" title="'.get_string('notattempted', 'scorm').'" />';
1475                         } else {
1476                             $statusicon = '<img src="'.$OUTPUT->pix_url('asset', 'scorm').'" alt="'.get_string('asset', 'scorm').'" title="'.get_string('asset', 'scorm').'" />';
1477                         }
1478                     }
1479                 }
1480             }
1482             if (empty($statusicon)) {
1483                 $sco->statusicon = '<img src="'.$OUTPUT->pix_url('notattempted', 'scorm').'" alt="'.get_string('notattempted', 'scorm').'" title="'.get_string('notattempted', 'scorm').'" />';
1484             } else {
1485                 $sco->statusicon = $statusicon;
1486             }
1488             $sco->url = 'a='.$scorm->id.'&scoid='.$sco->id.'&currentorg='.$currentorg.$modestr.'&attempt='.$attempt;
1489             $sco->incomplete = $incomplete;
1491             if (!in_array($sco->id, array_keys($result))) {
1492                 $result[$sco->id] = $sco;
1493             }
1494         }
1495     }
1497     // Get the parent scoes!
1498     $result = scorm_get_toc_get_parent_child($result, $currentorg);
1500     // Be safe, prevent warnings from showing up while returning array
1501     if (!isset($scoid)) {
1502         $scoid = '';
1503     }
1505     return array('scoes' => $result, 'usertracks' => $usertracks, 'scoid' => $scoid);
1508 function scorm_get_toc_get_parent_child(&$result, $currentorg) {
1509     $final = array();
1510     $level = 0;
1511     // Organization is always the root, prevparent.
1512     if (!empty($currentorg)) {
1513         $prevparent = $currentorg;
1514     } else {
1515         $prevparent = '/';
1516     }
1518     foreach ($result as $sco) {
1519         if ($sco->parent == '/') {
1520             $final[$level][$sco->identifier] = $sco;
1521             $prevparent = $sco->identifier;
1522             unset($result[$sco->id]);
1523         } else {
1524             if ($sco->parent == $prevparent) {
1525                 $final[$level][$sco->identifier] = $sco;
1526                 $prevparent = $sco->identifier;
1527                 unset($result[$sco->id]);
1528             } else {
1529                 if (!empty($final[$level])) {
1530                     $found = false;
1531                     foreach ($final[$level] as $fin) {
1532                         if ($sco->parent == $fin->identifier) {
1533                             $found = true;
1534                         }
1535                     }
1537                     if ($found) {
1538                         $final[$level][$sco->identifier] = $sco;
1539                         unset($result[$sco->id]);
1540                         $found = false;
1541                     } else {
1542                         $level++;
1543                         $final[$level][$sco->identifier] = $sco;
1544                         unset($result[$sco->id]);
1545                     }
1546                 }
1547             }
1548         }
1549     }
1551     for ($i = 0; $i <= $level; $i++) {
1552         $prevparent = '';
1553         foreach ($final[$i] as $ident => $sco) {
1554             if (empty($prevparent)) {
1555                 $prevparent = $ident;
1556             }
1557             if (!isset($final[$i][$prevparent]->children)) {
1558                 $final[$i][$prevparent]->children = array();
1559             }
1560             if ($sco->parent == $prevparent) {
1561                 $final[$i][$prevparent]->children[] = $sco;
1562                 $prevparent = $ident;
1563             } else {
1564                 $parent = false;
1565                 foreach ($final[$i] as $identifier => $scoobj) {
1566                     if ($identifier == $sco->parent) {
1567                         $parent = $identifier;
1568                     }
1569                 }
1571                 if ($parent !== false) {
1572                     $final[$i][$parent]->children[] = $sco;
1573                 }
1574             }
1575         }
1576     }
1578     $results = array();
1579     for ($i = 0; $i <= $level; $i++) {
1580         $keys = array_keys($final[$i]);
1581         $results[] = $final[$i][$keys[0]];
1582     }
1584     return $results;
1587 function scorm_format_toc_for_treeview($user, $scorm, $scoes, $usertracks, $cmid, $toclink=TOCJSLINK, $currentorg='', $attempt='', $play=false, $organizationsco=null, $children=false) {
1588     global $CFG;
1590     $result = new stdClass();
1591     $result->prerequisites = true;
1592     $result->incomplete = true;
1593     $result->toc = '';
1595     if (!$children) {
1596         $attemptsmade = scorm_get_attempt_count($user->id, $scorm);
1597         $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attemptsmade;
1598     }
1600     if (!$children) {
1601         $result->toc = "<ul>\n";
1603         if (!$play && !empty($organizationsco))  {
1604             $result->toc .= "\t<li>".$organizationsco->title."</li>\n";
1605         }
1606     }
1608     $prevsco = '';
1609     if (!empty($scoes)) {
1610         foreach ($scoes as $sco) {
1611             $result->toc .= "\t<li>\n";
1612             $scoid = $sco->id;
1614             $sco->isvisible = true;
1616             if ($sco->isvisible) {
1617                 $score = '';
1619                 if (isset($usertracks[$sco->identifier])) {
1620                     $viewscore = has_capability('mod/scorm:viewscores', context_module::instance($cmid));
1621                     if (isset($usertracks[$sco->identifier]->score_raw) && $viewscore) {
1622                         if ($usertracks[$sco->identifier]->score_raw != '') {
1623                             $score = '('.get_string('score','scorm').':&nbsp;'.$usertracks[$sco->identifier]->score_raw.')';
1624                         }
1625                     }
1626                 }
1628                 if (!empty($sco->prereq)) {
1629                     if ($sco->id == $scoid) {
1630                         $result->prerequisites = true;
1631                     }
1633                     if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) {
1634                         if ($sco->scormtype == 'sco') {
1635                             $result->toc .= '<span>'.$sco->statusicon.'&nbsp;'.format_string($sco->title).'</span>';
1636                         } else {
1637                             $result->toc .= '<span>&nbsp;'.format_string($sco->title).'</span>';
1638                         }
1639                     } else if ($toclink == TOCFULLURL) {
1640                         $url = $CFG->wwwroot.'/mod/scorm/player.php?'.$sco->url;
1641                         if (!empty($sco->launch)) {
1642                             if ($sco->scormtype == 'sco') {
1643                                 $result->toc .= $sco->statusicon.'&nbsp;<a href="'.$url.'">'.format_string($sco->title).'</a>'.$score."\n";
1644                             } else {
1645                                 $result->toc .= '&nbsp;<a data-scoid="'.$sco->id.'" href="'.$url.'">'.format_string($sco->title).'</a>'.$score."\n";
1646                             }
1647                         } else {
1648                             if ($sco->scormtype == 'sco') {
1649                                 $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title).$score."\n";
1650                             } else {
1651                                 $result->toc .= '&nbsp;'.format_string($sco->title).$score."\n";
1652                             }
1653                         }
1654                     } else {
1655                         if (!empty($sco->launch)) {
1656                             if ($sco->scormtype == 'sco') {
1657                                 $result->toc .= '<a data-scoid="'.$sco->id.'" title="'.$sco->url.'">'.$sco->statusicon.'&nbsp;'.format_string($sco->title).'&nbsp;'.$score.'</a>';
1658                             } else {
1659                                 $result->toc .= '<a data-scoid="'.$sco->id.'" title="'.$sco->url.'">&nbsp;'.format_string($sco->title).'&nbsp;'.$score.'</a>';
1660                             }
1661                         } else {
1662                             if ($sco->scormtype == 'sco') {
1663                                 $result->toc .= '<span>'.$sco->statusicon.'&nbsp;'.format_string($sco->title).'</span>';
1664                             } else {
1665                                 $result->toc .= '<span>&nbsp;'.format_string($sco->title).'</span>';
1666                             }
1667                         }
1668                     }
1670                 } else {
1671                     if ($play) {
1672                         if ($sco->scormtype == 'sco') {
1673                             $result->toc .= '<span>'.$sco->statusicon.'&nbsp;'.format_string($sco->title).'</span>';
1674                         } else {
1675                             $result->toc .= '&nbsp;'.format_string($sco->title).'</span>';
1676                         }
1677                     } else {
1678                         if ($sco->scormtype == 'sco') {
1679                             $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title)."\n";
1680                         } else {
1681                             $result->toc .= '&nbsp;'.format_string($sco->title)."\n";
1682                         }
1683                     }
1684                 }
1686             } else {
1687                 $result->toc .= "\t\t&nbsp;".format_string($sco->title)."\n";
1688             }
1690             if (!empty($sco->children)) {
1691                 $result->toc .= "\n\t\t<ul>\n";
1692                 $childresult = scorm_format_toc_for_treeview($user, $scorm, $sco->children, $usertracks, $cmid, $toclink, $currentorg, $attempt, $play, $organizationsco, true);
1693                 $result->toc .= $childresult->toc;
1694                 $result->toc .= "\t\t</ul>\n";
1695                 $result->toc .= "\t</li>\n";
1696             } else {
1697                 $result->toc .= "\t</li>\n";
1698             }
1699             $prevsco = $sco;
1700         }
1701         $result->incomplete = $sco->incomplete;
1702     }
1704     if (!$children) {
1705         $result->toc .= "</ul>\n";
1706     }
1708     return $result;
1711 function scorm_format_toc_for_droplist($scorm, $scoes, $usertracks, $currentorg='', $organizationsco=null, $children=false, $level=0, $tocmenus=array()) {
1712     if (!empty($scoes)) {
1713         if (!empty($organizationsco) && !$children) {
1714             $tocmenus[$organizationsco->id] = $organizationsco->title;
1715         }
1717         $parents[$level]='/';
1718         foreach ($scoes as $sco) {
1719             if ($parents[$level] != $sco->parent) {
1720                 if ($newlevel = array_search($sco->parent, $parents)) {
1721                     $level = $newlevel;
1722                 } else {
1723                     $i = $level;
1724                     while (($i > 0) && ($parents[$level] != $sco->parent)) {
1725                         $i--;
1726                     }
1728                     if (($i == 0) && ($sco->parent != $currentorg)) {
1729                         $level++;
1730                     } else {
1731                         $level = $i;
1732                     }
1734                     $parents[$level] = $sco->parent;
1735                 }
1736             }
1738             if ($sco->prereq) {
1739                 if ($sco->scormtype == 'sco') {
1740                     $tocmenus[$sco->id] = scorm_repeater('&minus;', $level) . '&gt;' . format_string($sco->title);
1741                 }
1742             } else {
1743                 if ($sco->scormtype == 'sco') {
1744                     $tocmenus[$sco->id] = scorm_repeater('&minus;', $level) . '&gt;' . format_string($sco->title);
1745                 }
1746             }
1748             if (!empty($sco->children)) {
1749                 $tocmenus = scorm_format_toc_for_droplist($scorm, $sco->children, $usertracks, $currentorg, $organizationsco, true, $level, $tocmenus);
1750             }
1751         }
1752     }
1754     return $tocmenus;
1757 function scorm_get_toc($user, $scorm, $cmid, $toclink=TOCJSLINK, $currentorg='', $scoid='', $mode='normal', $attempt='', $play=false, $tocheader=false) {
1758     global $CFG, $DB, $OUTPUT;
1760     if (empty($attempt)) {
1761         $attempt = scorm_get_last_attempt($scorm->id, $user->id);
1762     }
1764     $result = new stdClass();
1765     $organizationsco = null;
1767     if ($tocheader) {
1768         $result->toc = "<div id=\"scorm_layout\" class=\"yui3-g-r\">\n";
1769         $result->toc .= "<div id=\"scorm_toc\" class=\"yui3-u-1-5\">\n";
1770         $result->toc .= "<div id=\"scorm_toc_title\"></div>\n";
1771         $result->toc .= "<div id=\"scorm_tree\">\n";
1772     }
1774     if (!empty($currentorg)) {
1775         $organizationsco = $DB->get_record('scorm_scoes', array('scorm'=>$scorm->id, 'identifier'=>$currentorg));
1776         if (!empty($organizationsco->title)) {
1777             if ($play) {
1778                 $result->toctitle = $organizationsco->title;
1779             }
1780         }
1781     }
1783     $scoes = scorm_get_toc_object($user, $scorm, $currentorg, $scoid, $mode, $attempt, $play, $organizationsco);
1785     $treeview = scorm_format_toc_for_treeview($user, $scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], $cmid, $toclink, $currentorg, $attempt, $play, $organizationsco, false);
1787     if ($tocheader) {
1788         $result->toc .= $treeview->toc;
1789     } else {
1790         $result->toc = $treeview->toc;
1791     }
1793     if (!empty($scoes['scoid'])) {
1794         $scoid = $scoes['scoid'];
1795     }
1797     if (empty($scoid)) {
1798         $result->sco = $scoes['scoes'][0]->children[0];
1799     } else {
1800         $result->sco = scorm_get_sco($scoid);
1801     }
1803     if ($scorm->hidetoc == SCORM_TOC_POPUP) {
1804         $tocmenu = scorm_format_toc_for_droplist($scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], $currentorg, $organizationsco);
1806         $modestr = '';
1807         if ($mode != 'normal') {
1808             $modestr = '&mode='.$mode;
1809         }
1811         $url = new moodle_url('/mod/scorm/player.php?a='.$scorm->id.'&currentorg='.$currentorg.$modestr);
1812         $result->tocmenu = $OUTPUT->single_select($url, 'scoid', $tocmenu, $result->sco->id, null, "tocmenu");
1813     }
1815     $result->prerequisites = $treeview->prerequisites;
1816     $result->incomplete = $treeview->incomplete;
1817     $result->attemptleft = $treeview->attemptleft;
1819     if ($tocheader) {
1820         $result->toc .= "</div></div>\n";
1821         $result->toc .= "<div id=\"scorm_toc_toggle\">\n";
1822         $result->toc .= "<button id=\"scorm_toc_toggle_btn\"></button></div>\n";
1823         $result->toc .= "<div id=\"scorm_content\">";
1824         $result->toc .= "<div id=\"scorm_navpanel\"></div>\n";
1825         $result->toc .= "</div></div>\n";
1826     }
1828     return $result;
1831 function scorm_get_adlnav_json ($scoes, &$adlnav = array(), $parentscoid = null) {
1832     if (is_object($scoes)) {
1833         $sco = $scoes;
1834         if (isset($sco->url)) {
1835             $adlnav[$sco->id]['identifier'] = $sco->identifier;
1836             $adlnav[$sco->id]['launch'] = $sco->launch;
1837             $adlnav[$sco->id]['title'] = $sco->title;
1838             $adlnav[$sco->id]['url'] = $sco->url;
1839             $adlnav[$sco->id]['parent'] = $sco->parent;
1840             if (isset($sco->choice)) {
1841                 $adlnav[$sco->id]['choice'] = $sco->choice;
1842             }
1843             if (isset($sco->flow)) {
1844                 $adlnav[$sco->id]['flow'] = $sco->flow;
1845             } else if (isset($parentscoid) && isset($adlnav[$parentscoid]['flow'])) {
1846                 $adlnav[$sco->id]['flow'] = $adlnav[$parentscoid]['flow'];
1847             }
1848             if (isset($sco->isvisible)) {
1849                 $adlnav[$sco->id]['isvisible'] = $sco->isvisible;
1850             }
1851             if (isset($sco->parameters)) {
1852                 $adlnav[$sco->id]['parameters'] = $sco->parameters;
1853             }
1854             if (isset($sco->hidecontinue)) {
1855                 $adlnav[$sco->id]['hidecontinue'] = $sco->hidecontinue;
1856             }
1857             if (isset($sco->hideprevious)) {
1858                 $adlnav[$sco->id]['hideprevious'] = $sco->hideprevious;
1859             }
1860             if (isset($sco->hidesuspendall)) {
1861                 $adlnav[$sco->id]['hidesuspendall'] = $sco->hidesuspendall;
1862             }
1863             if (!empty($parentscoid)) {
1864                 $adlnav[$sco->id]['parentscoid'] = $parentscoid;
1865             }
1866             if (isset($adlnav['prevscoid'])) {
1867                 $adlnav[$sco->id]['prevscoid'] = $adlnav['prevscoid'];
1868                 $adlnav[$adlnav['prevscoid']]['nextscoid'] = $sco->id;
1869                 if (isset($adlnav['prevparent']) && $adlnav['prevparent'] == $sco->parent) {
1870                     $adlnav[$sco->id]['prevsibling'] = $adlnav['prevscoid'];
1871                     $adlnav[$adlnav['prevscoid']]['nextsibling'] = $sco->id;
1872                 }
1873             }
1874             $adlnav['prevscoid'] = $sco->id;
1875             $adlnav['prevparent'] = $sco->parent;
1876         }
1877         if (isset($sco->children)) {
1878             foreach ($sco->children as $children) {
1879                 scorm_get_adlnav_json($children, $adlnav, $sco->id);
1880             }
1881         }
1882     } else {
1883         foreach ($scoes as $sco) {
1884             scorm_get_adlnav_json ($sco, $adlnav);
1885         }
1886         unset($adlnav['prevscoid']);
1887         unset($adlnav['prevparent']);
1888     }
1889     return json_encode($adlnav);
1892 /**
1893  * Check for the availability of a resource by URL.
1894  *
1895  * Check is performed using an HTTP HEAD call.
1896  *
1897  * @param $url string A valid URL
1898  * @return bool|string True if no issue is found. The error string message, otherwise
1899  */
1900 function scorm_check_url($url) {
1901     $curl = new curl;
1902     // Same options as in {@link download_file_content()}, used in {@link scorm_parse_scorm()}.
1903     $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 5));
1904     $cmsg = $curl->head($url);
1905     $info = $curl->get_info();
1906     if (empty($info['http_code']) || $info['http_code'] != 200) {
1907         return get_string('invalidurlhttpcheck', 'scorm', array('cmsg' => $cmsg));
1908     }
1910     return true;