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/>.
16
17/**
18 * Library of internal classes and functions for module SCORM
19 *
20 * @package    mod_scorm
21 * @copyright  1999 onwards Roberto Pinna
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25require_once("$CFG->dirroot/mod/scorm/lib.php");
26require_once("$CFG->libdir/filelib.php");
27
28// Constants and settings for module scorm.
29define('SCORM_UPDATE_NEVER', '0');
30define('SCORM_UPDATE_EVERYDAY', '2');
31define('SCORM_UPDATE_EVERYTIME', '3');
32
33define('SCORM_SKIPVIEW_NEVER', '0');
34define('SCORM_SKIPVIEW_FIRST', '1');
35define('SCORM_SKIPVIEW_ALWAYS', '2');
36
37define('SCO_ALL', 0);
38define('SCO_DATA', 1);
39define('SCO_ONLY', 2);
40
41define('GRADESCOES', '0');
42define('GRADEHIGHEST', '1');
43define('GRADEAVERAGE', '2');
44define('GRADESUM', '3');
45
46define('HIGHESTATTEMPT', '0');
47define('AVERAGEATTEMPT', '1');
48define('FIRSTATTEMPT', '2');
49define('LASTATTEMPT', '3');
50
51define('TOCJSLINK', 1);
52define('TOCFULLURL', 2);
53
54define('SCORM_FORCEATTEMPT_NO', 0);
55define('SCORM_FORCEATTEMPT_ONCOMPLETE', 1);
56define('SCORM_FORCEATTEMPT_ALWAYS', 2);
57
58// Local Library of functions for module scorm.
59
60/**
61 * @package   mod_scorm
62 * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
63 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
64 */
65class scorm_package_file_info extends file_info_stored {
66    public function get_parent() {
67        if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
68            return $this->browser->get_file_info($this->context);
69        }
70        return parent::get_parent();
71    }
72    public function get_visible_name() {
73        if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
74            return $this->topvisiblename;
75        }
76        return parent::get_visible_name();
77    }
78}
79
80/**
81 * Returns an array of the popup options for SCORM and each options default value
82 *
83 * @return array an array of popup options as the key and their defaults as the value
84 */
85function scorm_get_popup_options_array() {
86    $cfgscorm = get_config('scorm');
87
88    return array('scrollbars' => isset($cfgscorm->scrollbars) ? $cfgscorm->scrollbars : 0,
89                 'directories' => isset($cfgscorm->directories) ? $cfgscorm->directories : 0,
90                 'location' => isset($cfgscorm->location) ? $cfgscorm->location : 0,
91                 'menubar' => isset($cfgscorm->menubar) ? $cfgscorm->menubar : 0,
92                 'toolbar' => isset($cfgscorm->toolbar) ? $cfgscorm->toolbar : 0,
93                 'status' => isset($cfgscorm->status) ? $cfgscorm->status : 0);
94}
95
96/**
97 * Returns an array of the array of what grade options
98 *
99 * @return array an array of what grade options
100 */
101function scorm_get_grade_method_array() {
102    return array (GRADESCOES => get_string('gradescoes', 'scorm'),
103                  GRADEHIGHEST => get_string('gradehighest', 'scorm'),
104                  GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
105                  GRADESUM => get_string('gradesum', 'scorm'));
106}
107
108/**
109 * Returns an array of the array of what grade options
110 *
111 * @return array an array of what grade options
112 */
113function scorm_get_what_grade_array() {
114    return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
115                  AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
116                  FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
117                  LASTATTEMPT => get_string('lastattempt', 'scorm'));
118}
119
120/**
121 * Returns an array of the array of skip view options
122 *
123 * @return array an array of skip view options
124 */
125function scorm_get_skip_view_array() {
126    return array(SCORM_SKIPVIEW_NEVER => get_string('never'),
127                 SCORM_SKIPVIEW_FIRST => get_string('firstaccess', 'scorm'),
128                 SCORM_SKIPVIEW_ALWAYS => get_string('always'));
129}
130
131/**
132 * Returns an array of the array of hide table of contents options
133 *
134 * @return array an array of hide table of contents options
135 */
136function scorm_get_hidetoc_array() {
137     return array(SCORM_TOC_SIDE => get_string('sided', 'scorm'),
138                  SCORM_TOC_HIDDEN => get_string('hidden', 'scorm'),
139                  SCORM_TOC_POPUP => get_string('popupmenu', 'scorm'),
140                  SCORM_TOC_DISABLED => get_string('disabled', 'scorm'));
141}
142
143/**
144 * Returns an array of the array of update frequency options
145 *
146 * @return array an array of update frequency options
147 */
148function scorm_get_updatefreq_array() {
149    return array(SCORM_UPDATE_NEVER => get_string('never'),
150                 SCORM_UPDATE_EVERYDAY => get_string('everyday', 'scorm'),
151                 SCORM_UPDATE_EVERYTIME => get_string('everytime', 'scorm'));
152}
153
154/**
155 * Returns an array of the array of popup display options
156 *
157 * @return array an array of popup display options
158 */
159function scorm_get_popup_display_array() {
160    return array(0 => get_string('currentwindow', 'scorm'),
161                 1 => get_string('popup', 'scorm'));
162}
163
164/**
165 * Returns an array of the array of navigation buttons display options
166 *
167 * @return array an array of navigation buttons display options
168 */
169function scorm_get_navigation_display_array() {
170    return array(SCORM_NAV_DISABLED => get_string('no'),
171                 SCORM_NAV_UNDER_CONTENT => get_string('undercontent', 'scorm'),
172                 SCORM_NAV_FLOATING => get_string('floating', 'scorm'));
173}
174
175/**
176 * Returns an array of the array of attempt options
177 *
178 * @return array an array of attempt options
179 */
180function scorm_get_attempts_array() {
181    $attempts = array(0 => get_string('nolimit', 'scorm'),
182                      1 => get_string('attempt1', 'scorm'));
183
184    for ($i = 2; $i <= 6; $i++) {
185        $attempts[$i] = get_string('attemptsx', 'scorm', $i);
186    }
187
188    return $attempts;
189}
190
191/**
192 * Returns an array of the attempt status options
193 *
194 * @return array an array of attempt status options
195 */
196function scorm_get_attemptstatus_array() {
197    return array(SCORM_DISPLAY_ATTEMPTSTATUS_NO => get_string('no'),
198                 SCORM_DISPLAY_ATTEMPTSTATUS_ALL => get_string('attemptstatusall', 'scorm'),
199                 SCORM_DISPLAY_ATTEMPTSTATUS_MY => get_string('attemptstatusmy', 'scorm'),
200                 SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY => get_string('attemptstatusentry', 'scorm'));
201}
202
203/**
204 * Returns an array of the force attempt options
205 *
206 * @return array an array of attempt options
207 */
208function scorm_get_forceattempt_array() {
209    return array(SCORM_FORCEATTEMPT_NO => get_string('no'),
210                 SCORM_FORCEATTEMPT_ONCOMPLETE => get_string('forceattemptoncomplete', 'scorm'),
211                 SCORM_FORCEATTEMPT_ALWAYS => get_string('forceattemptalways', 'scorm'));
212}
213
214/**
215 * Extracts scrom package, sets up all variables.
216 * Called whenever scorm changes
217 * @param object $scorm instance - fields are updated and changes saved into database
218 * @param bool $full force full update if true
219 * @return void
220 */
221function scorm_parse($scorm, $full) {
222    global $CFG, $DB;
223    $cfgscorm = get_config('scorm');
224
225    if (!isset($scorm->cmid)) {
226        $cm = get_coursemodule_from_instance('scorm', $scorm->id);
227        $scorm->cmid = $cm->id;
228    }
229    $context = context_module::instance($scorm->cmid);
230    $newhash = $scorm->sha1hash;
231
232    if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
233
234        $fs = get_file_storage();
235        $packagefile = false;
236        $packagefileimsmanifest = false;
237
238        if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
239            if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
240                if ($packagefile->is_external_file()) { // Get zip file so we can check it is correct.
241                    $packagefile->import_external_file_contents();
242                }
243                $newhash = $packagefile->get_contenthash();
244                if (strtolower($packagefile->get_filename()) == 'imsmanifest.xml') {
245                    $packagefileimsmanifest = true;
246                }
247            } else {
248                $newhash = null;
249            }
250        } else {
251            if (!$cfgscorm->allowtypelocalsync) {
252                // Sorry - localsync disabled.
253                return;
254            }
255            if ($scorm->reference !== '') {
256                $fs->delete_area_files($context->id, 'mod_scorm', 'package');
257                $filerecord = array('contextid' => $context->id, 'component' => 'mod_scorm', 'filearea' => 'package',
258                                    'itemid' => 0, 'filepath' => '/');
259                if ($packagefile = $fs->create_file_from_url($filerecord, $scorm->reference, array('calctimeout' => true), true)) {
260                    $newhash = $packagefile->get_contenthash();
261                } else {
262                    $newhash = null;
263                }
264            }
265        }
266
267        if ($packagefile) {
268            if (!$full and $packagefile and $scorm->sha1hash === $newhash) {
269                if (strpos($scorm->version, 'SCORM') !== false) {
270                    if ($packagefileimsmanifest || $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
271                        // No need to update.
272                        return;
273                    }
274                } else if (strpos($scorm->version, 'AICC') !== false) {
275                    // TODO: add more sanity checks - something really exists in scorm_content area.
276                    return;
277                }
278            }
279            if (!$packagefileimsmanifest) {
280                // Now extract files.
281                $fs->delete_area_files($context->id, 'mod_scorm', 'content');
282
283                $packer = get_file_packer('application/zip');
284                $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/');
285            }
286
287        } else if (!$full) {
288            return;
289        }
290        if ($packagefileimsmanifest) {
291            require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
292            // Direct link to imsmanifest.xml file.
293            if (!scorm_parse_scorm($scorm, $packagefile)) {
294                $scorm->version = 'ERROR';
295            }
296
297        } else if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
298            require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
299            // SCORM.
300            if (!scorm_parse_scorm($scorm, $manifest)) {
301                $scorm->version = 'ERROR';
302            }
303        } else {
304            require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
305            // AICC.
306            $result = scorm_parse_aicc($scorm);
307            if (!$result) {
308                $scorm->version = 'ERROR';
309            } else {
310                $scorm->version = 'AICC';
311            }
312        }
313
314    } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfgscorm->allowtypeexternal) {
315        require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
316        // SCORM only, AICC can not be external.
317        if (!scorm_parse_scorm($scorm, $scorm->reference)) {
318            $scorm->version = 'ERROR';
319        }
320        $newhash = sha1($scorm->reference);
321
322    } else if ($scorm->scormtype === SCORM_TYPE_AICCURL  and $cfgscorm->allowtypeexternalaicc) {
323        require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
324        // AICC.
325        $result = scorm_parse_aicc($scorm);
326        if (!$result) {
327            $scorm->version = 'ERROR';
328        } else {
329            $scorm->version = 'AICC';
330        }
331
332    } else {
333        // Sorry, disabled type.
334        return;
335    }
336
337    $scorm->revision++;
338    $scorm->sha1hash = $newhash;
339    $DB->update_record('scorm', $scorm);
340}
341
342
343function scorm_array_search($item, $needle, $haystacks, $strict=false) {
344    if (!empty($haystacks)) {
345        foreach ($haystacks as $key => $element) {
346            if ($strict) {
347                if ($element->{$item} === $needle) {
348                    return $key;
349                }
350            } else {
351                if ($element->{$item} == $needle) {
352                    return $key;
353                }
354            }
355        }
356    }
357    return false;
358}
359
360function scorm_repeater($what, $times) {
361    if ($times <= 0) {
362        return null;
363    }
364    $return = '';
365    for ($i = 0; $i < $times; $i++) {
366        $return .= $what;
367    }
368    return $return;
369}
370
371function scorm_external_link($link) {
372    // Check if a link is external.
373    $result = false;
374    $link = strtolower($link);
375    if (substr($link, 0, 7) == 'http://') {
376        $result = true;
377    } else if (substr($link, 0, 8) == 'https://') {
378        $result = true;
379    } else if (substr($link, 0, 4) == 'www.') {
380        $result = true;
381    }
382    return $result;
383}
384
385/**
386 * Returns an object containing all datas relative to the given sco ID
387 *
388 * @param integer $id The sco ID
389 * @return mixed (false if sco id does not exists)
390 */
391function scorm_get_sco($id, $what=SCO_ALL) {
392    global $DB;
393
394    if ($sco = $DB->get_record('scorm_scoes', array('id' => $id))) {
395        $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
396        if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id)))) {
397            foreach ($scodatas as $scodata) {
398                $sco->{$scodata->name} = $scodata->value;
399            }
400        } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id))))) {
401            $sco->parameters = '';
402        }
403        return $sco;
404    } else {
405        return false;
406    }
407}
408
409/**
410 * Returns an object (array) containing all the scoes data related to the given sco ID
411 *
412 * @param integer $id The sco ID
413 * @param integer $organisation an organisation ID - defaults to false if not required
414 * @return mixed (false if there are no scoes or an array)
415 */
416function scorm_get_scoes($id, $organisation=false) {
417    global $DB;
418
419    $queryarray = array('scorm' => $id);
420    if (!empty($organisation)) {
421        $queryarray['organization'] = $organisation;
422    }
423    if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'sortorder, id')) {
424        // Drop keys so that it is a simple array as expected.
425        $scoes = array_values($scoes);
426        foreach ($scoes as $sco) {
427            if ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $sco->id))) {
428                foreach ($scodatas as $scodata) {
429                    $sco->{$scodata->name} = $scodata->value;
430                }
431            }
432        }
433        return $scoes;
434    } else {
435        return false;
436    }
437}
438
439function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $value, $forcecompleted=false, $trackdata = null) {
440    global $DB, $CFG;
441
442    $id = null;
443
444    if ($forcecompleted) {
445        // TODO - this could be broadened to encompass SCORM 2004 in future.
446        if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) {
447            if ($track = $DB->get_record_select('scorm_scoes_track',
448                                                'userid=? AND scormid=? AND scoid=? AND attempt=? '.
449                                                'AND element=\'cmi.core.score.raw\'',
450                                                array($userid, $scormid, $scoid, $attempt))) {
451                $value = 'completed';
452            }
453        }
454        if ($element == 'cmi.core.score.raw') {
455            if ($tracktest = $DB->get_record_select('scorm_scoes_track',
456                                                    'userid=? AND scormid=? AND scoid=? AND attempt=? '.
457                                                    'AND element=\'cmi.core.lesson_status\'',
458                                                    array($userid, $scormid, $scoid, $attempt))) {
459                if ($tracktest->value == "incomplete") {
460                    $tracktest->value = "completed";
461                    $DB->update_record('scorm_scoes_track', $tracktest);
462                }
463            }
464        }
465        if (($element == 'cmi.success_status') && ($value == 'passed' || $value == 'failed')) {
466            if ($DB->get_record('scorm_scoes_data', array('scoid' => $scoid, 'name' => 'objectivesetbycontent'))) {
467                $objectiveprogressstatus = true;
468                $objectivesatisfiedstatus = false;
469                if ($value == 'passed') {
470                    $objectivesatisfiedstatus = true;
471                }
472
473                if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
474                                                                        'scormid' => $scormid,
475                                                                        'scoid' => $scoid,
476                                                                        'attempt' => $attempt,
477                                                                        'element' => 'objectiveprogressstatus'))) {
478                    $track->value = $objectiveprogressstatus;
479                    $track->timemodified = time();
480                    $DB->update_record('scorm_scoes_track', $track);
481                    $id = $track->id;
482                } else {
483                    $track = new stdClass();
484                    $track->userid = $userid;
485                    $track->scormid = $scormid;
486                    $track->scoid = $scoid;
487                    $track->attempt = $attempt;
488                    $track->element = 'objectiveprogressstatus';
489                    $track->value = $objectiveprogressstatus;
490                    $track->timemodified = time();
491                    $id = $DB->insert_record('scorm_scoes_track', $track);
492                }
493                if ($objectivesatisfiedstatus) {
494                    if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
495                                                                            'scormid' => $scormid,
496                                                                            'scoid' => $scoid,
497                                                                            'attempt' => $attempt,
498                                                                            'element' => 'objectivesatisfiedstatus'))) {
499                        $track->value = $objectivesatisfiedstatus;
500                        $track->timemodified = time();
501                        $DB->update_record('scorm_scoes_track', $track);
502                        $id = $track->id;
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 = 'objectivesatisfiedstatus';
510                        $track->value = $objectivesatisfiedstatus;
511                        $track->timemodified = time();
512                        $id = $DB->insert_record('scorm_scoes_track', $track);
513                    }
514                }
515            }
516        }
517
518    }
519
520    $track = null;
521    if ($trackdata !== null) {
522        if (isset($trackdata[$element])) {
523            $track = $trackdata[$element];
524        }
525    } else {
526        $track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
527                                                            'scormid' => $scormid,
528                                                            'scoid' => $scoid,
529                                                            'attempt' => $attempt,
530                                                            'element' => $element));
531    }
532    if ($track) {
533        if ($element != 'x.start.time' ) { // Don't update x.start.time - keep the original value.
534            if ($track->value != $value) {
535                $track->value = $value;
536                $track->timemodified = time();
537                $DB->update_record('scorm_scoes_track', $track);
538            }
539            $id = $track->id;
540        }
541    } else {
542        $track = new stdClass();
543        $track->userid = $userid;
544        $track->scormid = $scormid;
545        $track->scoid = $scoid;
546        $track->attempt = $attempt;
547        $track->element = $element;
548        $track->value = $value;
549        $track->timemodified = time();
550        $id = $DB->insert_record('scorm_scoes_track', $track);
551        $track->id = $id;
552    }
553
554    // Trigger updating grades based on a given set of SCORM CMI elements.
555    $scorm = false;
556    if (in_array($element, array('cmi.core.score.raw', 'cmi.score.raw')) ||
557        (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
558         && in_array($track->value, array('completed', 'passed')))) {
559        $scorm = $DB->get_record('scorm', array('id' => $scormid));
560        include_once($CFG->dirroot.'/mod/scorm/lib.php');
561        scorm_update_grades($scorm, $userid);
562    }
563
564    // Trigger CMI element events.
565    if (in_array($element, array('cmi.core.score.raw', 'cmi.score.raw')) ||
566        (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
567        && in_array($track->value, array('completed', 'failed', 'passed')))) {
568        if (!$scorm) {
569            $scorm = $DB->get_record('scorm', array('id' => $scormid));
570        }
571        $cm = get_coursemodule_from_instance('scorm', $scormid);
572        $data = array(
573            'other' => array('attemptid' => $attempt, 'cmielement' => $element, 'cmivalue' => $track->value),
574            'objectid' => $scorm->id,
575            'context' => context_module::instance($cm->id),
576            'relateduserid' => $userid
577        );
578        if (in_array($element, array('cmi.core.score.raw', 'cmi.score.raw'))) {
579            // Create score submitted event.
580            $event = \mod_scorm\event\scoreraw_submitted::create($data);
581        } else {
582            // Create status submitted event.
583            $event = \mod_scorm\event\status_submitted::create($data);
584        }
585        // Fix the missing track keys when the SCORM track record already exists, see $trackdata in datamodel.php.
586        // There, for performances reasons, columns are limited to: element, id, value, timemodified.
587        // Missing fields are: userid, scormid, scoid, attempt.
588        $track->userid = $userid;
589        $track->scormid = $scormid;
590        $track->scoid = $scoid;
591        $track->attempt = $attempt;
592        // Trigger submitted event.
593        $event->add_record_snapshot('scorm_scoes_track', $track);
594        $event->add_record_snapshot('course_modules', $cm);
595        $event->add_record_snapshot('scorm', $scorm);
596        $event->trigger();
597    }
598
599    return $id;
600}
601
602/**
603 * simple quick function to return true/false if this user has tracks in this scorm
604 *
605 * @param integer $scormid The scorm ID
606 * @param integer $userid the users id
607 * @return boolean (false if there are no tracks)
608 */
609function scorm_has_tracks($scormid, $userid) {
610    global $DB;
611    return $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scormid));
612}
613
614function scorm_get_tracks($scoid, $userid, $attempt='') {
615    // Gets all tracks of specified sco and user.
616    global $DB;
617
618    if (empty($attempt)) {
619        if ($scormid = $DB->get_field('scorm_scoes', 'scorm', array('id' => $scoid))) {
620            $attempt = scorm_get_last_attempt($scormid, $userid);
621        } else {
622            $attempt = 1;
623        }
624    }
625    if ($tracks = $DB->get_records('scorm_scoes_track', array('userid' => $userid, 'scoid' => $scoid,
626                                                              'attempt' => $attempt), 'element ASC')) {
627        $usertrack = scorm_format_interactions($tracks);
628        $usertrack->userid = $userid;
629        $usertrack->scoid = $scoid;
630
631        return $usertrack;
632    } else {
633        return false;
634    }
635}
636/**
637 * helper function to return a formatted list of interactions for reports.
638 *
639 * @param array $trackdata the records from scorm_scoes_track table
640 * @return object formatted list of interactions
641 */
642function scorm_format_interactions($trackdata) {
643    $usertrack = new stdClass();
644
645    // Defined in order to unify scorm1.2 and scorm2004.
646    $usertrack->score_raw = '';
647    $usertrack->status = '';
648    $usertrack->total_time = '00:00:00';
649    $usertrack->session_time = '00:00:00';
650    $usertrack->timemodified = 0;
651
652    foreach ($trackdata as $track) {
653        $element = $track->element;
654        $usertrack->{$element} = $track->value;
655        switch ($element) {
656            case 'cmi.core.lesson_status':
657            case 'cmi.completion_status':
658                if ($track->value == 'not attempted') {
659                    $track->value = 'notattempted';
660                }
661                $usertrack->status = $track->value;
662                break;
663            case 'cmi.core.score.raw':
664            case 'cmi.score.raw':
665                $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
666                break;
667            case 'cmi.core.session_time':
668            case 'cmi.session_time':
669                $usertrack->session_time = $track->value;
670                break;
671            case 'cmi.core.total_time':
672            case 'cmi.total_time':
673                $usertrack->total_time = $track->value;
674                break;
675        }
676        if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
677            $usertrack->timemodified = $track->timemodified;
678        }
679    }
680
681    return $usertrack;
682}
683/* Find the start and finsh time for a a given SCO attempt
684 *
685 * @param int $scormid SCORM Id
686 * @param int $scoid SCO Id
687 * @param int $userid User Id
688 * @param int $attemt Attempt Id
689 *
690 * @return object start and finsh time EPOC secods
691 *
692 */
693function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
694    global $DB;
695
696    $timedata = new stdClass();
697    $params = array('userid' => $userid, 'scormid' => $scormid, 'attempt' => $attempt);
698    if (!empty($scoid)) {
699        $params['scoid'] = $scoid;
700    }
701    $tracks = $DB->get_records('scorm_scoes_track', $params, "timemodified ASC");
702    if ($tracks) {
703        $tracks = array_values($tracks);
704    }
705
706    if ($tracks) {
707        $timedata->start = $tracks[0]->timemodified;
708    } else {
709        $timedata->start = false;
710    }
711    if ($tracks && $track = array_pop($tracks)) {
712        $timedata->finish = $track->timemodified;
713    } else {
714        $timedata->finish = $timedata->start;
715    }
716    return $timedata;
717}
718
719function scorm_grade_user_attempt($scorm, $userid, $attempt=1) {
720    global $DB;
721    $attemptscore = new stdClass();
722    $attemptscore->scoes = 0;
723    $attemptscore->values = 0;
724    $attemptscore->max = 0;
725    $attemptscore->sum = 0;
726    $attemptscore->lastmodify = 0;
727
728    if (!$scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) {
729        return null;
730    }
731
732    foreach ($scoes as $sco) {
733        if ($userdata = scorm_get_tracks($sco->id, $userid, $attempt)) {
734            if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
735                $attemptscore->scoes++;
736            }
737            if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type == 'sco' && isset($userdata->score_raw))) {
738                $attemptscore->values++;
739                $attemptscore->sum += $userdata->score_raw;
740                $attemptscore->max = ($userdata->score_raw > $attemptscore->max) ? $userdata->score_raw : $attemptscore->max;
741                if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
742                    $attemptscore->lastmodify = $userdata->timemodified;
743                } else {
744                    $attemptscore->lastmodify = 0;
745                }
746            }
747        }
748    }
749    switch ($scorm->grademethod) {
750        case GRADEHIGHEST:
751            $score = (float) $attemptscore->max;
752        break;
753        case GRADEAVERAGE:
754            if ($attemptscore->values > 0) {
755                $score = $attemptscore->sum / $attemptscore->values;
756            } else {
757                $score = 0;
758            }
759        break;
760        case GRADESUM:
761            $score = $attemptscore->sum;
762        break;
763        case GRADESCOES:
764            $score = $attemptscore->scoes;
765        break;
766        default:
767            $score = $attemptscore->max;   // Remote Learner GRADEHIGHEST is default.
768    }
769
770    return $score;
771}
772
773function scorm_grade_user($scorm, $userid) {
774
775    // Ensure we dont grade user beyond $scorm->maxattempt settings.
776    $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
777    if ($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt) {
778        $lastattempt = $scorm->maxattempt;
779    }
780
781    switch ($scorm->whatgrade) {
782        case FIRSTATTEMPT:
783            return scorm_grade_user_attempt($scorm, $userid, scorm_get_first_attempt($scorm->id, $userid));
784        break;
785        case LASTATTEMPT:
786            return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
787        break;
788        case HIGHESTATTEMPT:
789            $maxscore = 0;
790            for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
791                $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
792                $maxscore = $attemptscore > $maxscore ? $attemptscore : $maxscore;
793            }
794            return $maxscore;
795
796        break;
797        case AVERAGEATTEMPT:
798            $attemptcount = scorm_get_attempt_count($userid, $scorm, true, true);
799            if (empty($attemptcount)) {
800                return 0;
801            } else {
802                $attemptcount = count($attemptcount);
803            }
804            $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
805            $sumscore = 0;
806            for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
807                $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
808                $sumscore += $attemptscore;
809            }
810
811            return round($sumscore / $attemptcount);
812        break;
813    }
814}
815
816function scorm_count_launchable($scormid, $organization='') {
817    global $DB;
818
819    $sqlorganization = '';
820    $params = array($scormid);
821    if (!empty($organization)) {
822        $sqlorganization = " AND organization=?";
823        $params[] = $organization;
824    }
825    return $DB->count_records_select('scorm_scoes', "scorm = ? $sqlorganization AND ".
826                                        $DB->sql_isnotempty('scorm_scoes', 'launch', false, true),
827                                        $params);
828}
829
830/**
831 * Returns the last attempt used - if no attempts yet, returns 1 for first attempt
832 *
833 * @param int $scormid the id of the scorm.
834 * @param int $userid the id of the user.
835 *
836 * @return int The attempt number to use.
837 */
838function scorm_get_last_attempt($scormid, $userid) {
839    global $DB;
840
841    // Find the last attempt number for the given user id and scorm id.
842    $sql = "SELECT MAX(attempt)
843              FROM {scorm_scoes_track}
844             WHERE userid = ? AND scormid = ?";
845    $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
846    if (empty($lastattempt)) {
847        return '1';
848    } else {
849        return $lastattempt;
850    }
851}
852
853/**
854 * Returns the first attempt used - if no attempts yet, returns 1 for first attempt.
855 *
856 * @param int $scormid the id of the scorm.
857 * @param int $userid the id of the user.
858 *
859 * @return int The first attempt number.
860 */
861function scorm_get_first_attempt($scormid, $userid) {
862    global $DB;
863
864    // Find the first attempt number for the given user id and scorm id.
865    $sql = "SELECT MIN(attempt)
866              FROM {scorm_scoes_track}
867             WHERE userid = ? AND scormid = ?";
868
869    $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
870    if (empty($lastattempt)) {
871        return '1';
872    } else {
873        return $lastattempt;
874    }
875}
876
877/**
878 * Returns the last completed attempt used - if no completed attempts yet, returns 1 for first attempt
879 *
880 * @param int $scormid the id of the scorm.
881 * @param int $userid the id of the user.
882 *
883 * @return int The attempt number to use.
884 */
885function scorm_get_last_completed_attempt($scormid, $userid) {
886    global $DB;
887
888    // Find the last completed attempt number for the given user id and scorm id.
889    $sql = "SELECT MAX(attempt)
890              FROM {scorm_scoes_track}
891             WHERE userid = ? AND scormid = ?
892               AND (".$DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?')." OR ".
893                      $DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?').")";
894    $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid, 'completed', 'passed'));
895    if (empty($lastattempt)) {
896        return '1';
897    } else {
898        return $lastattempt;
899    }
900}
901
902/**
903 * Returns the full list of attempts a user has made.
904 *
905 * @param int $scormid the id of the scorm.
906 * @param int $userid the id of the user.
907 *
908 * @return array array of attemptids
909 */
910function scorm_get_all_attempts($scormid, $userid) {
911    global $DB;
912    $attemptids = array();
913    $sql = "SELECT DISTINCT attempt FROM {scorm_scoes_track} WHERE userid = ? AND scormid = ? ORDER BY attempt";
914    $attempts = $DB->get_records_sql($sql, array($userid, $scormid));
915    foreach ($attempts as $attempt) {
916        $attemptids[] = $attempt->attempt;
917    }
918    return $attemptids;
919}
920
921/**
922 * Displays the entry form and toc if required.
923 *
924 * @param  stdClass $user   user object
925 * @param  stdClass $scorm  scorm object
926 * @param  string   $action base URL for the organizations select box
927 * @param  stdClass $cm     course module object
928 */
929function scorm_print_launch ($user, $scorm, $action, $cm) {
930    global $CFG, $DB, $OUTPUT;
931
932    if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
933        scorm_parse($scorm, false);
934    }
935
936    $organization = optional_param('organization', '', PARAM_INT);
937
938    if ($scorm->displaycoursestructure == 1) {
939        echo $OUTPUT->box_start('generalbox boxaligncenter toc container', 'toc');
940        echo html_writer::div(get_string('contents', 'scorm'), 'structurehead');
941    }
942    if (empty($organization)) {
943        $organization = $scorm->launch;
944    }
945    if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '.
946                                         $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
947                                         $DB->sql_isempty('scorm_scoes', 'organization', false, false),
948                                         array($scorm->id), 'sortorder, id', 'id,title')) {
949        if (count($orgs) > 1) {
950            $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null);
951            $select->label = get_string('organizations', 'scorm');
952            $select->class = 'scorm-center';
953            echo $OUTPUT->render($select);
954        }
955    }
956    $orgidentifier = '';
957    if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
958        if (($sco->organization == '') && ($sco->launch == '')) {
959            $orgidentifier = $sco->identifier;
960        } else {
961            $orgidentifier = $sco->organization;
962        }
963    }
964
965    $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR));   // Just to be safe.
966    if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
967        $scorm->version = 'scorm_12';
968    }
969    require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
970
971    $result = scorm_get_toc($user, $scorm, $cm->id, TOCFULLURL, $orgidentifier);
972    $incomplete = $result->incomplete;
973    // Get latest incomplete sco to launch first if force new attempt isn't set to always.
974    if (!empty($result->sco->id) && $scorm->forcenewattempt != SCORM_FORCEATTEMPT_ALWAYS) {
975        $launchsco = $result->sco->id;
976    } else {
977        // Use launch defined by SCORM package.
978        $launchsco = $scorm->launch;
979    }
980
981    // Do we want the TOC to be displayed?
982    if ($scorm->displaycoursestructure == 1) {
983        echo $result->toc;
984        echo $OUTPUT->box_end();
985    }
986
987    // Is this the first attempt ?
988    $attemptcount = scorm_get_attempt_count($user->id, $scorm);
989
990    // Do not give the player launch FORM if the SCORM object is locked after the final attempt.
991    if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) {
992            echo html_writer::start_div('scorm-center');
993            echo html_writer::start_tag('form', array('id' => 'scormviewform',
994                                                        'method' => 'post',
995                                                        'action' => $CFG->wwwroot.'/mod/scorm/player.php',
996                                                        'class' => 'container'));
997        if ($scorm->hidebrowse == 0) {
998            print_string('mode', 'scorm');
999            echo ': '.html_writer::empty_tag('input', array('type' => 'radio', 'id' => 'b', 'name' => 'mode',
1000                    'value' => 'browse', 'class' => 'mr-1')).
1001                        html_writer::label(get_string('browse', 'scorm'), 'b');
1002            echo html_writer::empty_tag('input', array('type' => 'radio',
1003                                                        'id' => 'n', 'name' => 'mode',
1004                                                        'value' => 'normal', 'checked' => 'checked',
1005                                                        'class' => 'mx-1')).
1006                    html_writer::label(get_string('normal', 'scorm'), 'n');
1007
1008        } else {
1009            echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'mode', 'value' => 'normal'));
1010        }
1011        if (!empty($scorm->forcenewattempt)) {
1012            if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS ||
1013                    ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE && $incomplete === false)) {
1014                echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'newattempt', 'value' => 'on'));
1015            }
1016        } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
1017                echo html_writer::empty_tag('br');
1018                echo html_writer::checkbox('newattempt', 'on', false, '', array('id' => 'a'));
1019                echo html_writer::label(get_string('newattempt', 'scorm'), 'a');
1020        }
1021        if (!empty($scorm->popup)) {
1022            echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'display', 'value' => 'popup'));
1023        }
1024
1025        echo html_writer::empty_tag('br');
1026        echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scoid', 'value' => $launchsco));
1027        echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cm', 'value' => $cm->id));
1028        echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentorg', 'value' => $orgidentifier));
1029        echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('enter', 'scorm'),
1030                'class' => 'btn btn-primary'));
1031        echo html_writer::end_tag('form');
1032        echo html_writer::end_div();
1033    }
1034}
1035
1036function scorm_simple_play($scorm, $user, $context, $cmid) {
1037    global $DB;
1038
1039    $result = false;
1040
1041    if (has_capability('mod/scorm:viewreport', $context)) {
1042        // If this user can view reports, don't skipview so they can see links to reports.
1043        return $result;
1044    }
1045
1046    if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
1047        scorm_parse($scorm, false);
1048    }
1049    $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.
1050        $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'sortorder, id', 'id');
1051
1052    if ($scoes) {
1053        $orgidentifier = '';
1054        if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) {
1055            if (($sco->organization == '') && ($sco->launch == '')) {
1056                $orgidentifier = $sco->identifier;
1057            } else {
1058                $orgidentifier = $sco->organization;
1059            }
1060        }
1061        if ($scorm->skipview >= SCORM_SKIPVIEW_FIRST) {
1062            $sco = current($scoes);
1063            $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier);
1064            $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id, 'currentorg' => $orgidentifier));
1065
1066            // Set last incomplete sco to launch first if forcenewattempt not set to always.
1067            if (!empty($result->sco->id) && $scorm->forcenewattempt != SCORM_FORCEATTEMPT_ALWAYS) {
1068                $url->param('scoid', $result->sco->id);
1069            } else {
1070                $url->param('scoid', $sco->id);
1071            }
1072
1073            if ($scorm->skipview == SCORM_SKIPVIEW_ALWAYS || !scorm_has_tracks($scorm->id, $user->id)) {
1074                if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS ||
1075                   ($result->incomplete === false && $scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE)) {
1076
1077                    $url->param('newattempt', 'on');
1078                }
1079                redirect($url);
1080            }
1081        }
1082    }
1083    return $result;
1084}
1085
1086function scorm_get_count_users($scormid, $groupingid=null) {
1087    global $CFG, $DB;
1088
1089    if (!empty($groupingid)) {
1090        $sql = "SELECT COUNT(DISTINCT st.userid)
1091                FROM {scorm_scoes_track} st
1092                    INNER JOIN {groups_members} gm ON st.userid = gm.userid
1093                    INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
1094                WHERE st.scormid = ? AND gg.groupingid = ?
1095                ";
1096        $params = array($scormid, $groupingid);
1097    } else {
1098        $sql = "SELECT COUNT(DISTINCT st.userid)
1099                FROM {scorm_scoes_track} st
1100                WHERE st.scormid = ?
1101                ";
1102        $params = array($scormid);
1103    }
1104
1105    return ($DB->count_records_sql($sql, $params));
1106}
1107
1108/**
1109 * Build up the JavaScript representation of an array element
1110 *
1111 * @param string $sversion SCORM API version
1112 * @param array $userdata User track data
1113 * @param string $elementname Name of array element to get values for
1114 * @param array $children list of sub elements of this array element that also need instantiating
1115 * @return Javascript array elements
1116 */
1117function scorm_reconstitute_array_element($sversion, $userdata, $elementname, $children) {
1118    // Reconstitute comments_from_learner and comments_from_lms.
1119    $current = '';
1120    $currentsubelement = '';
1121    $currentsub = '';
1122    $count = 0;
1123    $countsub = 0;
1124    $scormseperator = '_';
1125    $return = '';
1126    if (scorm_version_check($sversion, SCORM_13)) { // Scorm 1.3 elements use a . instead of an _ .
1127        $scormseperator = '.';
1128    }
1129    // Filter out the ones we want.
1130    $elementlist = array();
1131    foreach ($userdata as $element => $value) {
1132        if (substr($element, 0, strlen($elementname)) == $elementname) {
1133            $elementlist[$element] = $value;
1134        }
1135    }
1136
1137    // Sort elements in .n array order.
1138    uksort($elementlist, "scorm_element_cmp");
1139
1140    // Generate JavaScript.
1141    foreach ($elementlist as $element => $value) {
1142        if (scorm_version_check($sversion, SCORM_13)) {
1143            $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
1144            preg_match('/\.(N\d+)\./', $element, $matches);
1145        } else {
1146            $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
1147            preg_match('/\_(\d+)\./', $element, $matches);
1148        }
1149        if (count($matches) > 0 && $current != $matches[1]) {
1150            if ($countsub > 0) {
1151                $return .= '    '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
1152            }
1153            $current = $matches[1];
1154            $count++;
1155            $currentsubelement = '';
1156            $currentsub = '';
1157            $countsub = 0;
1158            $end = strpos($element, $matches[1]) + strlen($matches[1]);
1159            $subelement = substr($element, 0, $end);
1160            $return .= '    '.$subelement." = new Object();\n";
1161            // Now add the children.
1162            foreach ($children as $child) {
1163                $return .= '    '.$subelement.".".$child." = new Object();\n";
1164                $return .= '    '.$subelement.".".$child."._children = ".$child."_children;\n";
1165            }
1166        }
1167
1168        // Now - flesh out the second level elements if there are any.
1169        if (scorm_version_check($sversion, SCORM_13)) {
1170            $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
1171            preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
1172        } else {
1173            $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
1174            preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
1175        }
1176
1177        // Check the sub element type.
1178        if (count($matches) > 0 && $currentsubelement != $matches[1]) {
1179            if ($countsub > 0) {
1180                $return .= '    '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
1181            }
1182            $currentsubelement = $matches[1];
1183            $currentsub = '';
1184            $countsub = 0;
1185            $end = strpos($element, $matches[1]) + strlen($matches[1]);
1186            $subelement = substr($element, 0, $end);
1187            $return .= '    '.$subelement." = new Object();\n";
1188        }
1189
1190        // Now check the subelement subscript.
1191        if (count($matches) > 0 && $currentsub != $matches[2]) {
1192            $currentsub = $matches[2];
1193            $countsub++;
1194            $end = strrpos($element, $matches[2]) + strlen($matches[2]);
1195            $subelement = substr($element, 0, $end);
1196            $return .= '    '.$subelement." = new Object();\n";
1197        }
1198
1199        $return .= '    '.$element.' = '.json_encode($value).";\n";
1200    }
1201    if ($countsub > 0) {
1202        $return .= '    '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
1203    }
1204    if ($count > 0) {
1205        $return .= '    '.$elementname.'._count = '.$count.";\n";
1206    }
1207    return $return;
1208}
1209
1210/**
1211 * Build up the JavaScript representation of an array element
1212 *
1213 * @param string $a left array element
1214 * @param string $b right array element
1215 * @return comparator - 0,1,-1
1216 */
1217function scorm_element_cmp($a, $b) {
1218    preg_match('/.*?(\d+)\./', $a, $matches);
1219    $left = intval($matches[1]);
1220    preg_match('/.?(\d+)\./', $b, $matches);
1221    $right = intval($matches[1]);
1222    if ($left < $right) {
1223        return -1; // Smaller.
1224    } else if ($left > $right) {
1225        return 1;  // Bigger.
1226    } else {
1227        // Look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern.
1228        if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
1229            $leftterm = intval($matches[2]);
1230            $left = intval($matches[3]);
1231            if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
1232                $rightterm = intval($matches[2]);
1233                $right = intval($matches[3]);
1234                if ($leftterm < $rightterm) {
1235                    return -1; // Smaller.
1236                } else if ($leftterm > $rightterm) {
1237                    return 1;  // Bigger.
1238                } else {
1239                    if ($left < $right) {
1240                        return -1; // Smaller.
1241                    } else if ($left > $right) {
1242                        return 1;  // Bigger.
1243                    }
1244                }
1245            }
1246        }
1247        // Fall back for no second level matches or second level matches are equal.
1248        return 0;  // Equal to.
1249    }
1250}
1251
1252/**
1253 * Generate the user attempt status string
1254 *
1255 * @param object $user Current context user
1256 * @param object $scorm a moodle scrom object - mdl_scorm
1257 * @return string - Attempt status string
1258 */
1259function scorm_get_attempt_status($user, $scorm, $cm='') {
1260    global $DB, $PAGE, $OUTPUT;
1261
1262    $attempts = scorm_get_attempt_count($user->id, $scorm, true);
1263    if (empty($attempts)) {
1264        $attemptcount = 0;
1265    } else {
1266        $attemptcount = count($attempts);
1267    }
1268
1269    $result = html_writer::start_tag('p').get_string('noattemptsallowed', 'scorm').': ';
1270    if ($scorm->maxattempt > 0) {
1271        $result .= $scorm->maxattempt . html_writer::empty_tag('br');
1272    } else {
1273        $result .= get_string('unlimited').html_writer::empty_tag('br');
1274    }
1275    $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . html_writer::empty_tag('br');
1276
1277    if ($scorm->maxattempt == 1) {
1278        switch ($scorm->grademethod) {
1279            case GRADEHIGHEST:
1280                $grademethod = get_string('gradehighest', 'scorm');
1281            break;
1282            case GRADEAVERAGE:
1283                $grademethod = get_string('gradeaverage', 'scorm');
1284            break;
1285            case GRADESUM:
1286                $grademethod = get_string('gradesum', 'scorm');
1287            break;
1288            case GRADESCOES:
1289                $grademethod = get_string('gradescoes', 'scorm');
1290            break;
1291        }
1292    } else {
1293        switch ($scorm->whatgrade) {
1294            case HIGHESTATTEMPT:
1295                $grademethod = get_string('highestattempt', 'scorm');
1296            break;
1297            case AVERAGEATTEMPT:
1298                $grademethod = get_string('averageattempt', 'scorm');
1299            break;
1300            case FIRSTATTEMPT:
1301                $grademethod = get_string('firstattempt', 'scorm');
1302            break;
1303            case LASTATTEMPT:
1304                $grademethod = get_string('lastattempt', 'scorm');
1305            break;
1306        }
1307    }
1308
1309    if (!empty($attempts)) {
1310        $i = 1;
1311        foreach ($attempts as $attempt) {
1312            $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber);
1313            if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1314                $gradereported = $gradereported / $scorm->maxgrade;
1315                $gradereported = number_format($gradereported * 100, 0) .'%';
1316            }
1317            $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .html_writer::empty_tag('br');
1318            $i++;
1319        }
1320    }
1321    $calculatedgrade = scorm_grade_user($scorm, $user->id);
1322    if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1323        $calculatedgrade = $calculatedgrade / $scorm->maxgrade;
1324        $calculatedgrade = number_format($calculatedgrade * 100, 0) .'%';
1325    }
1326    $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod;
1327    if (empty($attempts)) {
1328        $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm').
1329                    ': '.get_string('none').html_writer::empty_tag('br');
1330    } else {
1331        $result .= html_writer::empty_tag('br').get_string('gradereported', 'scorm').
1332                    ': '.$calculatedgrade.html_writer::empty_tag('br');
1333    }
1334    $result .= html_writer::end_tag('p');
1335    if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) {
1336        $result .= html_writer::tag('p', get_string('exceededmaxattempts', 'scorm'), array('class' => 'exceededmaxattempts'));
1337    }
1338    if (!empty($cm)) {
1339        $context = context_module::instance($cm->id);
1340        if (has_capability('mod/scorm:deleteownresponses', $context) &&
1341            $DB->record_exists('scorm_scoes_track', array('userid' => $user->id, 'scormid' => $scorm->id))) {
1342            // Check to see if any data is stored for this user.
1343            $deleteurl = new moodle_url($PAGE->url, array('action' => 'delete', 'sesskey' => sesskey()));
1344            $result .= $OUTPUT->single_button($deleteurl, get_string('deleteallattempts', 'scorm'));
1345        }
1346    }
1347
1348    return $result;
1349}
1350
1351/**
1352 * Get SCORM attempt count
1353 *
1354 * @param object $user Current context user
1355 * @param object $scorm a moodle scrom object - mdl_scorm
1356 * @param bool $returnobjects if true returns a object with attempts, if false returns count of attempts.
1357 * @param bool $ignoremissingcompletion - ignores attempts that haven't reported a grade/completion.
1358 * @return int - no. of attempts so far
1359 */
1360function scorm_get_attempt_count($userid, $scorm, $returnobjects = false, $ignoremissingcompletion = false) {
1361    global $DB;
1362
1363    // Historically attempts that don't report these elements haven't been included in the average attempts grading method
1364    // we may want to change this in future, but to avoid unexpected grade decreases we're leaving this in. MDL-43222 .
1365    if (scorm_version_check($scorm->version, SCORM_13)) {
1366        $element = 'cmi.score.raw';
1367    } else if ($scorm->grademethod == GRADESCOES) {
1368        $element = 'cmi.core.lesson_status';
1369    } else {
1370        $element = 'cmi.core.score.raw';
1371    }
1372
1373    if ($returnobjects) {
1374        $params = array('userid' => $userid, 'scormid' => $scorm->id);
1375        if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested.
1376            $params['element'] = $element;
1377        }
1378        $attempts = $DB->get_records('scorm_scoes_track', $params, 'attempt', 'DISTINCT attempt AS attemptnumber');
1379        return $attempts;
1380    } else {
1381        $params = array($userid, $scorm->id);
1382        $sql = "SELECT COUNT(DISTINCT attempt)
1383                  FROM {scorm_scoes_track}
1384                 WHERE userid = ? AND scormid = ?";
1385        if ($ignoremissingcompletion) { // Exclude attempts that don't have the completion element requested.
1386            $sql .= ' AND element = ?';
1387            $params[] = $element;
1388        }
1389
1390        $attemptscount = $DB->count_records_sql($sql, $params);
1391        return $attemptscount;
1392    }
1393}
1394
1395/**
1396 * Figure out with this is a debug situation
1397 *
1398 * @param object $scorm a moodle scrom object - mdl_scorm
1399 * @return boolean - debugging true/false
1400 */
1401function scorm_debugging($scorm) {
1402    global $USER;
1403    $cfgscorm = get_config('scorm');
1404
1405    if (!$cfgscorm->allowapidebug) {
1406        return false;
1407    }
1408    $identifier = $USER->username.':'.$scorm->name;
1409    $test = $cfgscorm->apidebugmask;
1410    // Check the regex is only a short list of safe characters.
1411    if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) {
1412        return false;
1413    }
1414
1415    if (preg_match('/^'.$test.'/', $identifier)) {
1416        return true;
1417    }
1418    return false;
1419}
1420
1421/**
1422 * Delete Scorm tracks for selected users
1423 *
1424 * @param array $attemptids list of attempts that need to be deleted
1425 * @param stdClass $scorm instance
1426 *
1427 * @return bool true deleted all responses, false failed deleting an attempt - stopped here
1428 */
1429function scorm_delete_responses($attemptids, $scorm) {
1430    if (!is_array($attemptids) || empty($attemptids)) {
1431        return false;
1432    }
1433
1434    foreach ($attemptids as $num => $attemptid) {
1435        if (empty($attemptid)) {
1436            unset($attemptids[$num]);
1437        }
1438    }
1439
1440    foreach ($attemptids as $attempt) {
1441        $keys = explode(':', $attempt);
1442        if (count($keys) == 2) {
1443            $userid = clean_param($keys[0], PARAM_INT);
1444            $attemptid = clean_param($keys[1], PARAM_INT);
1445            if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) {
1446                    return false;
1447            }
1448        } else {
1449            return false;
1450        }
1451    }
1452    return true;
1453}
1454
1455/**
1456 * Delete Scorm tracks for selected users
1457 *
1458 * @param int $userid ID of User
1459 * @param stdClass $scorm Scorm object
1460 * @param int $attemptid user attempt that need to be deleted
1461 *
1462 * @return bool true suceeded
1463 */
1464function scorm_delete_attempt($userid, $scorm, $attemptid) {
1465    global $DB;
1466
1467    $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid));
1468    $cm = get_coursemodule_from_instance('scorm', $scorm->id);
1469
1470    // Trigger instances list viewed event.
1471    $event = \mod_scorm\event\attempt_deleted::create(array(
1472         'other' => array('attemptid' => $attemptid),
1473         'context' => context_module::instance($cm->id),
1474         'relateduserid' => $userid
1475    ));
1476    $event->add_record_snapshot('course_modules', $cm);
1477    $event->add_record_snapshot('scorm', $scorm);
1478    $event->trigger();
1479
1480    include_once('lib.php');
1481    scorm_update_grades($scorm, $userid, true);
1482    return true;
1483}
1484
1485/**
1486 * Converts SCORM duration notation to human-readable format
1487 * The function works with both SCORM 1.2 and SCORM 2004 time formats
1488 * @param $duration string SCORM duration
1489 * @return string human-readable date/time
1490 */
1491function scorm_format_duration($duration) {
1492    // Fetch date/time strings.
1493    $stryears = get_string('years');
1494    $strmonths = get_string('nummonths');
1495    $strdays = get_string('days');
1496    $strhours = get_string('hours');
1497    $strminutes = get_string('minutes');
1498    $strseconds = get_string('seconds');
1499
1500    if ($duration[0] == 'P') {
1501        // If timestamp starts with 'P' - it's a SCORM 2004 format
1502        // this regexp discards empty sections, takes Month/Minute ambiguity into consideration,
1503        // and outputs filled sections, discarding leading zeroes and any format literals
1504        // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero.
1505        $pattern = array( '#([A-Z])0+Y#', '#([A-Z])0+M#', '#([A-Z])0+D#', '#P(|\d+Y)0*(\d+)M#',
1506                            '#0*(\d+)Y#', '#0*(\d+)D#', '#P#', '#([A-Z])0+H#', '#([A-Z])[0.]+S#',
1507                            '#\.0+S#', '#T(|\d+H)0*(\d+)M#', '#0*(\d+)H#', '#0+\.(\d+)S#',
1508                            '#0*([\d.]+)S#', '#T#' );
1509        $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ',
1510                            '', '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ',
1511                            '0.$1 '.$strseconds, '$1 '.$strseconds, '');
1512    } else {
1513        // Else we have SCORM 1.2 format there
1514        // first convert the timestamp to some SCORM 2004-like format for conveniency.
1515        $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration);
1516        // Then convert in the same way as SCORM 2004.
1517        $pattern = array( '#T0+H#', '#([A-Z])0+M#', '#([A-Z])[0.]+S#', '#\.0+S#', '#0*(\d+)H#',
1518                            '#0*(\d+)M#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' );
1519        $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ',
1520                            '0.$1 '.$strseconds, '$1 '.$strseconds, '' );
1521    }
1522
1523    $result = preg_replace($pattern, $replace, $duration);
1524
1525    return $result;
1526}
1527
1528function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='normal', $attempt='',
1529                                $play=false, $organizationsco=null) {
1530    global $CFG, $DB, $PAGE, $OUTPUT;
1531
1532    // Always pass the mode even if empty as that is what is done elsewhere and the urls have to match.
1533    $modestr = '&mode=';
1534    if ($mode != 'normal') {
1535        $modestr = '&mode='.$mode;
1536    }
1537
1538    $result = array();
1539    $incomplete = false;
1540
1541    if (!empty($organizationsco)) {
1542        $result[0] = $organizationsco;
1543        $result[0]->isvisible = 'true';
1544        $result[0]->statusicon = '';
1545        $result[0]->url = '';
1546    }
1547
1548    if ($scoes = scorm_get_scoes($scorm->id, $currentorg)) {
1549        // Retrieve user tracking data for each learning object.
1550        $usertracks = array();
1551        foreach ($scoes as $sco) {
1552            if (!empty($sco->launch)) {
1553                if ($usertrack = scorm_get_tracks($sco->id, $user->id, $attempt)) {
1554                    if ($usertrack->status == '') {
1555                        $usertrack->status = 'notattempted';
1556                    }
1557                    $usertracks[$sco->identifier] = $usertrack;
1558                }
1559            }
1560        }
1561        foreach ($scoes as $sco) {
1562            if (!isset($sco->isvisible)) {
1563                $sco->isvisible = 'true';
1564            }
1565
1566            if (empty($sco->title)) {
1567                $sco->title = $sco->identifier;
1568            }
1569
1570            if (scorm_version_check($scorm->version, SCORM_13)) {
1571                $sco->prereq = true;
1572            } else {
1573                $sco->prereq = empty($sco->prerequisites) || scorm_eval_prerequisites($sco->prerequisites, $usertracks);
1574            }
1575
1576            if ($sco->isvisible === 'true') {
1577                if (!empty($sco->launch)) {
1578                    // Set first sco to launch if in browse/review mode.
1579                    if (empty($scoid) && ($mode != 'normal')) {
1580                        $scoid = $sco->id;
1581                    }
1582
1583                    if (isset($usertracks[$sco->identifier])) {
1584                        $usertrack = $usertracks[$sco->identifier];
1585
1586                        // Check we have a valid status string identifier.
1587                        if ($statusstringexists = get_string_manager()->string_exists($usertrack->status, 'scorm')) {
1588                            $strstatus = get_string($usertrack->status, 'scorm');
1589                        } else {
1590                            $strstatus = get_string('invalidstatus', 'scorm');
1591                        }
1592
1593                        if ($sco->scormtype == 'sco') {
1594                            // Assume if we didn't get a valid status string, we don't have an icon either.
1595                            $statusicon = $OUTPUT->pix_icon($statusstringexists ? $usertrack->status : 'incomplete',
1596                                $strstatus, 'scorm');
1597                        } else {
1598                            $statusicon = $OUTPUT->pix_icon('asset', get_string('assetlaunched', 'scorm'), 'scorm');
1599                        }
1600
1601                        if (($usertrack->status == 'notattempted') ||
1602                                ($usertrack->status == 'incomplete') ||
1603                                ($usertrack->status == 'browsed')) {
1604                            $incomplete = true;
1605                            if (empty($scoid)) {
1606                                $scoid = $sco->id;
1607                            }
1608                        }
1609
1610                        $strsuspended = get_string('suspended', 'scorm');
1611
1612                        $exitvar = 'cmi.core.exit';
1613
1614                        if (scorm_version_check($scorm->version, SCORM_13)) {
1615                            $exitvar = 'cmi.exit';
1616                        }
1617
1618                        if ($incomplete && isset($usertrack->{$exitvar}) && ($usertrack->{$exitvar} == 'suspend')) {
1619                            $statusicon = $OUTPUT->pix_icon('suspend', $strstatus.' - '.$strsuspended, 'scorm');
1620                        }
1621
1622                    } else {
1623                        if (empty($scoid)) {
1624                            $scoid = $sco->id;
1625                        }
1626
1627                        $incomplete = true;
1628
1629                        if ($sco->scormtype == 'sco') {
1630                            $statusicon = $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm');
1631                        } else {
1632                            $statusicon = $OUTPUT->pix_icon('asset', get_string('asset', 'scorm'), 'scorm');
1633                        }
1634                    }
1635                }
1636            }
1637
1638            if (empty($statusicon)) {
1639                $sco->statusicon = $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm');
1640            } else {
1641                $sco->statusicon = $statusicon;
1642            }
1643
1644            $sco->url = 'a='.$scorm->id.'&scoid='.$sco->id.'&currentorg='.$currentorg.$modestr.'&attempt='.$attempt;
1645            $sco->incomplete = $incomplete;
1646
1647            if (!in_array($sco->id, array_keys($result))) {
1648                $result[$sco->id] = $sco;
1649            }
1650        }
1651    }
1652
1653    // Get the parent scoes!
1654    $result = scorm_get_toc_get_parent_child($result, $currentorg);
1655
1656    // Be safe, prevent warnings from showing up while returning array.
1657    if (!isset($scoid)) {
1658        $scoid = '';
1659    }
1660
1661    return array('scoes' => $result, 'usertracks' => $usertracks, 'scoid' => $scoid);
1662}
1663
1664function scorm_get_toc_get_parent_child(&$result, $currentorg) {
1665    $final = array();
1666    $level = 0;
1667    // Organization is always the root, prevparent.
1668    if (!empty($currentorg)) {
1669        $prevparent = $currentorg;
1670    } else {
1671        $prevparent = '/';
1672    }
1673
1674    foreach ($result as $sco) {
1675        if ($sco->parent == '/') {
1676            $final[$level][$sco->identifier] = $sco;
1677            $prevparent = $sco->identifier;
1678            unset($result[$sco->id]);
1679        } else {
1680            if ($sco->parent == $prevparent) {
1681                $final[$level][$sco->identifier] = $sco;
1682                $prevparent = $sco->identifier;
1683                unset($result[$sco->id]);
1684            } else {
1685                if (!empty($final[$level])) {
1686                    $found = false;
1687                    foreach ($final[$level] as $fin) {
1688                        if ($sco->parent == $fin->identifier) {
1689                            $found = true;
1690                        }
1691                    }
1692
1693                    if ($found) {
1694                        $final[$level][$sco->identifier] = $sco;
1695                        unset($result[$sco->id]);
1696                        $found = false;
1697                    } else {
1698                        $level++;
1699                        $final[$level][$sco->identifier] = $sco;
1700                        unset($result[$sco->id]);
1701                    }
1702                }
1703            }
1704        }
1705    }
1706
1707    for ($i = 0; $i <= $level; $i++) {
1708        $prevparent = '';
1709        foreach ($final[$i] as $ident => $sco) {
1710            if (empty($prevparent)) {
1711                $prevparent = $ident;
1712            }
1713            if (!isset($final[$i][$prevparent]->children)) {
1714                $final[$i][$prevparent]->children = array();
1715            }
1716            if ($sco->parent == $prevparent) {
1717                $final[$i][$prevparent]->children[] = $sco;
1718                $prevparent = $ident;
1719            } else {
1720                $parent = false;
1721                foreach ($final[$i] as $identifier => $scoobj) {
1722                    if ($identifier == $sco->parent) {
1723                        $parent = $identifier;
1724                    }
1725                }
1726
1727                if ($parent !== false) {
1728                    $final[$i][$parent]->children[] = $sco;
1729                }
1730            }
1731        }
1732    }
1733
1734    $results = array();
1735    for ($i = 0; $i <= $level; $i++) {
1736        $keys = array_keys($final[$i]);
1737        $results[] = $final[$i][$keys[0]];
1738    }
1739
1740    return $results;
1741}
1742
1743function scorm_format_toc_for_treeview($user, $scorm, $scoes, $usertracks, $cmid, $toclink=TOCJSLINK, $currentorg='',
1744                                        $attempt='', $play=false, $organizationsco=null, $children=false) {
1745    global $CFG;
1746
1747    $result = new stdClass();
1748    $result->prerequisites = true;
1749    $result->incomplete = true;
1750    $result->toc = '';
1751
1752    if (!$children) {
1753        $attemptsmade = scorm_get_attempt_count($user->id, $scorm);
1754        $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attemptsmade;
1755    }
1756
1757    if (!$children) {
1758        $result->toc = html_writer::start_tag('ul');
1759
1760        if (!$play && !empty($organizationsco)) {
1761            $result->toc .= html_writer::start_tag('li').$organizationsco->title.html_writer::end_tag('li');
1762        }
1763    }
1764
1765    $prevsco = '';
1766    if (!empty($scoes)) {
1767        foreach ($scoes as $sco) {
1768
1769            if ($sco->isvisible === 'false') {
1770                continue;
1771            }
1772
1773            $result->toc .= html_writer::start_tag('li');
1774            $scoid = $sco->id;
1775
1776            $score = '';
1777
1778            if (isset($usertracks[$sco->identifier])) {
1779                $viewscore = has_capability('mod/scorm:viewscores', context_module::instance($cmid));
1780                if (isset($usertracks[$sco->identifier]->score_raw) && $viewscore) {
1781                    if ($usertracks[$sco->identifier]->score_raw != '') {
1782                        $score = '('.get_string('score', 'scorm').':&nbsp;'.$usertracks[$sco->identifier]->score_raw.')';
1783                    }
1784                }
1785            }
1786
1787            if (!empty($sco->prereq)) {
1788                if ($sco->id == $scoid) {
1789                    $result->prerequisites = true;
1790                }
1791
1792                if (!empty($prevsco) && scorm_version_check($scorm->version, SCORM_13) && !empty($prevsco->hidecontinue)) {
1793                    if ($sco->scormtype == 'sco') {
1794                        $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
1795                    } else {
1796                        $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
1797                    }
1798                } else if ($toclink == TOCFULLURL) {
1799                    $url = $CFG->wwwroot.'/mod/scorm/player.php?'.$sco->url;
1800                    if (!empty($sco->launch)) {
1801                        if ($sco->scormtype == 'sco') {
1802                            $result->toc .= $sco->statusicon.'&nbsp;';
1803                            $result->toc .= html_writer::link($url, format_string($sco->title)).$score;
1804                        } else {
1805                            $result->toc .= '&nbsp;'.html_writer::link($url, format_string($sco->title),
1806                                                                        array('data-scoid' => $sco->id)).$score;
1807                        }
1808                    } else {
1809                        if ($sco->scormtype == 'sco') {
1810                            $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title).$score;
1811                        } else {
1812                            $result->toc .= '&nbsp;'.format_string($sco->title).$score;
1813                        }
1814                    }
1815                } else {
1816                    if (!empty($sco->launch)) {
1817                        if ($sco->scormtype == 'sco') {
1818                            $result->toc .= html_writer::tag('a', $sco->statusicon.'&nbsp;'.
1819                                                                format_string($sco->title).'&nbsp;'.$score,
1820                                                                array('data-scoid' => $sco->id, 'title' => $sco->url));
1821                        } else {
1822                            $result->toc .= html_writer::tag('a', '&nbsp;'.format_string($sco->title).'&nbsp;'.$score,
1823                                                                array('data-scoid' => $sco->id, 'title' => $sco->url));
1824                        }
1825                    } else {
1826                        if ($sco->scormtype == 'sco') {
1827                            $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
1828                        } else {
1829                            $result->toc .= html_writer::span('&nbsp;'.format_string($sco->title));
1830                        }
1831                    }
1832                }
1833
1834            } else {
1835                if ($play) {
1836                    if ($sco->scormtype == 'sco') {
1837                        $result->toc .= html_writer::span($sco->statusicon.'&nbsp;'.format_string($sco->title));
1838                    } else {
1839                        $result->toc .= '&nbsp;'.format_string($sco->title).html_writer::end_span();
1840                    }
1841                } else {
1842                    if ($sco->scormtype == 'sco') {
1843                        $result->toc .= $sco->statusicon.'&nbsp;'.format_string($sco->title);
1844                    } else {
1845                        $result->toc .= '&nbsp;'.format_string($sco->title);
1846                    }
1847                }
1848            }
1849
1850            if (!empty($sco->children)) {
1851                $result->toc .= html_writer::start_tag('ul');
1852                $childresult = scorm_format_toc_for_treeview($user, $scorm, $sco->children, $usertracks, $cmid,
1853                                                                $toclink, $currentorg, $attempt, $play, $organizationsco, true);
1854
1855                // Is any of the children incomplete?
1856                $sco->incomplete = $childresult->incomplete;
1857                $result->toc .= $childresult->toc;
1858                $result->toc .= html_writer::end_tag('ul');
1859                $result->toc .= html_writer::end_tag('li');
1860            } else {
1861                $result->toc .= html_writer::end_tag('li');
1862            }
1863            $prevsco = $sco;
1864        }
1865        $result->incomplete = $sco->incomplete;
1866    }
1867
1868    if (!$children) {
1869        $result->toc .= html_writer::end_tag('ul');
1870    }
1871
1872    return $result;
1873}
1874
1875function scorm_format_toc_for_droplist($scorm, $scoes, $usertracks, $currentorg='', $organizationsco=null,
1876                                        $children=false, $level=0, $tocmenus=array()) {
1877    if (!empty($scoes)) {
1878        if (!empty($organizationsco) && !$children) {
1879            $tocmenus[$organizationsco->id] = $organizationsco->title;
1880        }
1881
1882        $parents[$level] = '/';
1883        foreach ($scoes as $sco) {
1884            if ($parents[$level] != $sco->parent) {
1885                if ($newlevel = array_search($sco->parent, $parents)) {
1886                    $level = $newlevel;
1887                } else {
1888                    $i = $level;
1889                    while (($i > 0) && ($parents[$level] != $sco->parent)) {
1890                        $i--;
1891                    }
1892
1893                    if (($i == 0) && ($sco->parent != $currentorg)) {
1894                        $level++;
1895                    } else {
1896                        $level = $i;
1897                    }
1898
1899                    $parents[$level] = $sco->parent;
1900                }
1901            }
1902
1903            if ($sco->scormtype == 'sco') {
1904                $tocmenus[$sco->id] = scorm_repeater('&minus;', $level) . '&gt;' . format_string($sco->title);
1905            }
1906
1907            if (!empty($sco->children)) {
1908                $tocmenus = scorm_format_toc_for_droplist($scorm, $sco->children, $usertracks, $currentorg,
1909                                                            $organizationsco, true, $level, $tocmenus);
1910            }
1911        }
1912    }
1913
1914    return $tocmenus;
1915}
1916
1917function scorm_get_toc($user, $scorm, $cmid, $toclink=TOCJSLINK, $currentorg='', $scoid='', $mode='normal',
1918                        $attempt='', $play=false, $tocheader=false) {
1919    global $CFG, $DB, $OUTPUT;
1920
1921    if (empty($attempt)) {
1922        $attempt = scorm_get_last_attempt($scorm->id, $user->id);
1923    }
1924
1925    $result = new stdClass();
1926    $organizationsco = null;
1927
1928    if ($tocheader) {
1929        $result->toc = html_writer::start_div('yui3-g-r', array('id' => 'scorm_layout'));
1930        $result->toc .= html_writer::start_div('yui3-u-1-5 loading', array('id' => 'scorm_toc'));
1931        $result->toc .= html_writer::div('', '', array('id' => 'scorm_toc_title'));
1932        $result->toc .= html_writer::start_div('', array('id' => 'scorm_tree'));
1933    }
1934
1935    if (!empty($currentorg)) {
1936        $organizationsco = $DB->get_record('scorm_scoes', array('scorm' => $scorm->id, 'identifier' => $currentorg));
1937        if (!empty($organizationsco->title)) {
1938            if ($play) {
1939                $result->toctitle = $organizationsco->title;
1940            }
1941        }
1942    }
1943
1944    $scoes = scorm_get_toc_object($user, $scorm, $currentorg, $scoid, $mode, $attempt, $play, $organizationsco);
1945
1946    $treeview = scorm_format_toc_for_treeview($user, $scorm, $scoes['scoes'][0]->children, $scoes['usertracks'], $cmid,
1947                                                $toclink, $currentorg, $attempt, $play, $organizationsco, false);
1948
1949    if ($tocheader) {
1950        $result->toc .= $treeview->toc;
1951    } else {
1952        $result->toc = $treeview->toc;
1953    }
1954
1955    if (!empty($scoes['scoid'])) {
1956        $scoid = $scoes['scoid'];
1957    }
1958
1959    if (empty($scoid)) {
1960        // If this is a normal package with an org sco and child scos get the first child.
1961        if (!empty($scoes['scoes'][0]->children)) {
1962            $result->sco = $scoes['scoes'][0]->children[0];
1963        } else { // This package only has one sco - it may be a simple external AICC package.
1964            $result->sco = $scoes['scoes'][0];
1965        }
1966
1967    } else {
1968        $result->sco = scorm_get_sco($scoid);
1969    }
1970
1971    if ($scorm->hidetoc == SCORM_TOC_POPUP) {
1972        $tocmenu = scorm_format_toc_for_droplist($scorm, $scoes['scoes'][0]->children, $scoes['usertracks'],
1973                                                    $currentorg, $organizationsco);
1974
1975        $modestr = '';
1976        if ($mode != 'normal') {
1977            $modestr = '&mode='.$mode;
1978        }
1979
1980        $url = new moodle_url('/mod/scorm/player.php?a='.$scorm->id.'&currentorg='.$currentorg.$modestr);
1981        $result->tocmenu = $OUTPUT->single_select($url, 'scoid', $tocmenu, $result->sco->id, null, "tocmenu");
1982    }
1983
1984    $result->prerequisites = $treeview->prerequisites;
1985    $result->incomplete = $treeview->incomplete;
1986    $result->attemptleft = $treeview->attemptleft;
1987
1988    if ($tocheader) {
1989        $result->toc .= html_writer::end_div().html_writer::end_div();
1990        $result->toc .= html_writer::start_div('loading', array('id' => 'scorm_toc_toggle'));
1991        $result->toc .= html_writer::tag('button', '', array('id' => 'scorm_toc_toggle_btn')).html_writer::end_div();
1992        $result->toc .= html_writer::start_div('', array('id' => 'scorm_content'));
1993        $result->toc .= html_writer::div('', '', array('id' => 'scorm_navpanel'));
1994        $result->toc .= html_writer::end_div().html_writer::end_div();
1995    }
1996
1997    return $result;
1998}
1999
2000function scorm_get_adlnav_json ($scoes, &$adlnav = array(), $parentscoid = null) {
2001    if (is_object($scoes)) {
2002        $sco = $scoes;
2003        if (isset($sco->url)) {
2004            $adlnav[$sco->id]['identifier'] = $sco->identifier;
2005            $adlnav[$sco->id]['launch'] = $sco->launch;
2006            $adlnav[$sco->id]['title'] = $sco->title;
2007            $adlnav[$sco->id]['url'] = $sco->url;
2008            $adlnav[$sco->id]['parent'] = $sco->parent;
2009            if (isset($sco->choice)) {
2010                $adlnav[$sco->id]['choice'] = $sco->choice;
2011            }
2012            if (isset($sco->flow)) {
2013                $adlnav[$sco->id]['flow'] = $sco->flow;
2014            } else if (isset($parentscoid) && isset($adlnav[$parentscoid]['flow'])) {
2015                $adlnav[$sco->id]['flow'] = $adlnav[$parentscoid]['flow'];
2016            }
2017            if (isset($sco->isvisible)) {
2018                $adlnav[$sco->id]['isvisible'] = $sco->isvisible;
2019            }
2020            if (isset($sco->parameters)) {
2021                $adlnav[$sco->id]['parameters'] = $sco->parameters;
2022            }
2023            if (isset($sco->hidecontinue)) {
2024                $adlnav[$sco->id]['hidecontinue'] = $sco->hidecontinue;
2025            }
2026            if (isset($sco->hideprevious)) {
2027                $adlnav[$sco->id]['hideprevious'] = $sco->hideprevious;
2028            }
2029            if (isset($sco->hidesuspendall)) {
2030                $adlnav[$sco->id]['hidesuspendall'] = $sco->hidesuspendall;
2031            }
2032            if (!empty($parentscoid)) {
2033                $adlnav[$sco->id]['parentscoid'] = $parentscoid;
2034            }
2035            if (isset($adlnav['prevscoid'])) {
2036                $adlnav[$sco->id]['prevscoid'] = $adlnav['prevscoid'];
2037                $adlnav[$adlnav['prevscoid']]['nextscoid'] = $sco->id;
2038                if (isset($adlnav['prevparent']) && $adlnav['prevparent'] == $sco->parent) {
2039                    $adlnav[$sco->id]['prevsibling'] = $adlnav['prevscoid'];
2040                    $adlnav[$adlnav['prevscoid']]['nextsibling'] = $sco->id;
2041                }
2042            }
2043            $adlnav['prevscoid'] = $sco->id;
2044            $adlnav['prevparent'] = $sco->parent;
2045        }
2046        if (isset($sco->children)) {
2047            foreach ($sco->children as $children) {
2048                scorm_get_adlnav_json($children, $adlnav, $sco->id);
2049            }
2050        }
2051    } else {
2052        foreach ($scoes as $sco) {
2053            scorm_get_adlnav_json ($sco, $adlnav);
2054        }
2055        unset($adlnav['prevscoid']);
2056        unset($adlnav['prevparent']);
2057    }
2058    return json_encode($adlnav);
2059}
2060
2061/**
2062 * Check for the availability of a resource by URL.
2063 *
2064 * Check is performed using an HTTP HEAD call.
2065 *
2066 * @param $url string A valid URL
2067 * @return bool|string True if no issue is found. The error string message, otherwise
2068 */
2069function scorm_check_url($url) {
2070    $curl = new curl;
2071    // Same options as in {@link download_file_content()}, used in {@link scorm_parse_scorm()}.
2072    $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => true, 'CURLOPT_MAXREDIRS' => 5));
2073    $cmsg = $curl->head($url);
2074    $info = $curl->get_info();
2075    if (empty($info['http_code']) || $info['http_code'] != 200) {
2076        return get_string('invalidurlhttpcheck', 'scorm', array('cmsg' => $cmsg));
2077    }
2078
2079    return true;
2080}
2081
2082/**
2083 * Check for a parameter in userdata and return it if it's set
2084 * or return the value from $ifempty if its empty
2085 *
2086 * @param stdClass $userdata Contains user's data
2087 * @param string $param parameter that should be checked
2088 * @param string $ifempty value to be replaced with if $param is not set
2089 * @return string value from $userdata->$param if its not empty, or $ifempty
2090 */
2091function scorm_isset($userdata, $param, $ifempty = '') {
2092    if (isset($userdata->$param)) {
2093        return $userdata->$param;
2094    } else {
2095        return $ifempty;
2096    }
2097}
2098
2099/**
2100 * Check if the current sco is launchable
2101 * If not, find the next launchable sco
2102 *
2103 * @param stdClass $scorm Scorm object
2104 * @param integer $scoid id of scorm_scoes record.
2105 * @return integer scoid of correct sco to launch or empty if one cannot be found, which will trigger first sco.
2106 */
2107function scorm_check_launchable_sco($scorm, $scoid) {
2108    global $DB;
2109    if ($sco = scorm_get_sco($scoid, SCO_ONLY)) {
2110        if ($sco->launch == '') {
2111            // This scoid might be a top level org that can't be launched, find the first launchable sco after this sco.
2112            $scoes = $DB->get_records_select('scorm_scoes',
2113                                             'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).
2114                                             ' AND id > ?', array($scorm->id, $sco->id), 'sortorder, id', 'id', 0, 1);
2115            if (!empty($scoes)) {
2116                $sco = reset($scoes); // Get first item from the list.
2117                return $sco->id;
2118            }
2119        } else {
2120            return $sco->id;
2121        }
2122    }
2123    // Returning 0 will cause default behaviour which will find the first launchable sco in the package.
2124    return 0;
2125}
2126
2127/**
2128 * Check if a SCORM is available for the current user.
2129 *
2130 * @param  stdClass  $scorm            SCORM record
2131 * @param  boolean $checkviewreportcap Check the scorm:viewreport cap
2132 * @param  stdClass  $context          Module context, required if $checkviewreportcap is set to true
2133 * @param  int  $userid                User id override
2134 * @return array                       status (available or not and possible warnings)
2135 * @since  Moodle 3.0
2136 */
2137function scorm_get_availability_status($scorm, $checkviewreportcap = false, $context = null, $userid = null) {
2138    $open = true;
2139    $closed = false;
2140    $warnings = array();
2141
2142    $timenow = time();
2143    if (!empty($scorm->timeopen) and $scorm->timeopen > $timenow) {
2144        $open = false;
2145    }
2146    if (!empty($scorm->timeclose) and $timenow > $scorm->timeclose) {
2147        $closed = true;
2148    }
2149
2150    if (!$open or $closed) {
2151        if ($checkviewreportcap and !empty($context) and has_capability('mod/scorm:viewreport', $context, $userid)) {
2152            return array(true, $warnings);
2153        }
2154
2155        if (!$open) {
2156            $warnings['notopenyet'] = userdate($scorm->timeopen);
2157        }
2158        if ($closed) {
2159            $warnings['expired'] = userdate($scorm->timeclose);
2160        }
2161        return array(false, $warnings);
2162    }
2163
2164    // Scorm is available.
2165    return array(true, $warnings);
2166}
2167
2168/**
2169 * Requires a SCORM package to be available for the current user.
2170 *
2171 * @param  stdClass  $scorm            SCORM record
2172 * @param  boolean $checkviewreportcap Check the scorm:viewreport cap
2173 * @param  stdClass  $context          Module context, required if $checkviewreportcap is set to true
2174 * @throws moodle_exception
2175 * @since  Moodle 3.0
2176 */
2177function scorm_require_available($scorm, $checkviewreportcap = false, $context = null) {
2178
2179    list($available, $warnings) = scorm_get_availability_status($scorm, $checkviewreportcap, $context);
2180
2181    if (!$available) {
2182        $reason = current(array_keys($warnings));
2183        throw new moodle_exception($reason, 'scorm', '', $warnings[$reason]);
2184    }
2185
2186}
2187
2188/**
2189 * Return a SCO object and the SCO launch URL
2190 *
2191 * @param  stdClass $scorm SCORM object
2192 * @param  int $scoid The SCO id in database
2193 * @param  stdClass $context context object
2194 * @return array the SCO object and URL
2195 * @since  Moodle 3.1
2196 */
2197function scorm_get_sco_and_launch_url($scorm, $scoid, $context) {
2198    global $CFG, $DB;
2199
2200    if (!empty($scoid)) {
2201        // Direct SCO request.
2202        if ($sco = scorm_get_sco($scoid)) {
2203            if ($sco->launch == '') {
2204                // Search for the next launchable sco.
2205                if ($scoes = $DB->get_records_select(
2206                        'scorm_scoes',
2207                        'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).' AND id > ?',
2208                        array($scorm->id, $sco->id),
2209                        'sortorder, id')) {
2210                    $sco = current($scoes);
2211                }
2212            }
2213        }
2214    }
2215
2216    // If no sco was found get the first of SCORM package.
2217    if (!isset($sco)) {
2218        $scoes = $DB->get_records_select(
2219            'scorm_scoes',
2220            'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true),
2221            array($scorm->id),
2222            'sortorder, id'
2223        );
2224        $sco = current($scoes);
2225    }
2226
2227    $connector = '';
2228    $version = substr($scorm->version, 0, 4);
2229    if ((isset($sco->parameters) && (!empty($sco->parameters))) || ($version == 'AICC')) {
2230        if (stripos($sco->launch, '?') !== false) {
2231            $connector = '&';
2232        } else {
2233            $connector = '?';
2234        }
2235        if ((isset($sco->parameters) && (!empty($sco->parameters))) && ($sco->parameters[0] == '?')) {
2236            $sco->parameters = substr($sco->parameters, 1);
2237        }
2238    }
2239
2240    if ($version == 'AICC') {
2241        require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
2242        $aiccsid = scorm_aicc_get_hacp_session($scorm->id);
2243        if (empty($aiccsid)) {
2244            $aiccsid = sesskey();
2245        }
2246        $scoparams = '';
2247        if (isset($sco->parameters) && (!empty($sco->parameters))) {
2248            $scoparams = '&'. $sco->parameters;
2249        }
2250        $launcher = $sco->launch.$connector.'aicc_sid='.$aiccsid.'&aicc_url='.$CFG->wwwroot.'/mod/scorm/aicc.php'.$scoparams;
2251    } else {
2252        if (isset($sco->parameters) && (!empty($sco->parameters))) {
2253            $launcher = $sco->launch.$connector.$sco->parameters;
2254        } else {
2255            $launcher = $sco->launch;
2256        }
2257    }
2258
2259    if (scorm_external_link($sco->launch)) {
2260        // TODO: does this happen?
2261        $scolaunchurl = $launcher;
2262    } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) {
2263        // Remote learning activity.
2264        $scolaunchurl = dirname($scorm->reference).'/'.$launcher;
2265    } else if ($scorm->scormtype === SCORM_TYPE_LOCAL && strtolower($scorm->reference) == 'imsmanifest.xml') {
2266        // This SCORM content sits in a repository that allows relative links.
2267        $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher";
2268    } else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
2269        // Note: do not convert this to use moodle_url().
2270        // SCORM does not work without slasharguments and moodle_url() encodes querystring vars.
2271        $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/content/$scorm->revision/$launcher";
2272    }
2273    return array($sco, $scolaunchurl);
2274}
2275
2276/**
2277 * Trigger the scorm_launched event.
2278 *
2279 * @param  stdClass $scorm   scorm object
2280 * @param  stdClass $sco     sco object
2281 * @param  stdClass $cm      course module object
2282 * @param  stdClass $context context object
2283 * @param  string $scourl    SCO URL
2284 * @since Moodle 3.1
2285 */
2286function scorm_launch_sco($scorm, $sco, $cm, $context, $scourl) {
2287
2288    $event = \mod_scorm\event\sco_launched::create(array(
2289        'objectid' => $sco->id,
2290        'context' => $context,
2291        'other' => array('instanceid' => $scorm->id, 'loadedcontent' => $scourl)
2292    ));
2293    $event->add_record_snapshot('course_modules', $cm);
2294    $event->add_record_snapshot('scorm', $scorm);
2295    $event->add_record_snapshot('scorm_scoes', $sco);
2296    $event->trigger();
2297}
2298
2299/**
2300 * This is really a little language parser for AICC_SCRIPT
2301 * evaluates the expression and returns a boolean answer
2302 * see 2.3.2.5.1. Sequencing/Navigation Today  - from the SCORM 1.2 spec (CAM).
2303 * Also used by AICC packages.
2304 *
2305 * @param string $prerequisites the aicc_script prerequisites expression
2306 * @param array  $usertracks the tracked user data of each SCO visited
2307 * @return boolean
2308 */
2309function scorm_eval_prerequisites($prerequisites, $usertracks) {
2310
2311    // This is really a little language parser - AICC_SCRIPT is the reference
2312    // see 2.3.2.5.1. Sequencing/Navigation Today  - from the SCORM 1.2 spec.
2313    $element = '';
2314    $stack = array();
2315    $statuses = array(
2316        'passed' => 'passed',
2317        'completed' => 'completed',
2318        'failed' => 'failed',
2319        'incomplete' => 'incomplete',
2320        'browsed' => 'browsed',
2321        'not attempted' => 'notattempted',
2322        'p' => 'passed',
2323        'c' => 'completed',
2324        'f' => 'failed',
2325        'i' => 'incomplete',
2326        'b' => 'browsed',
2327        'n' => 'notattempted'
2328    );
2329    $i = 0;
2330
2331    // Expand the amp entities.
2332    $prerequisites = preg_replace('/&amp;/', '&', $prerequisites);
2333    // Find all my parsable tokens.
2334    $prerequisites = preg_replace('/(&|\||\(|\)|\~)/', '\t$1\t', $prerequisites);
2335    // Expand operators.
2336    $prerequisites = preg_replace('/&/', '&&', $prerequisites);
2337    $prerequisites = preg_replace('/\|/', '||', $prerequisites);
2338    // Now - grab all the tokens.
2339    $elements = explode('\t', trim($prerequisites));
2340
2341    // Process each token to build an expression to be evaluated.
2342    $stack = array();
2343    foreach ($elements as $element) {
2344        $element = trim($element);
2345        if (empty($element)) {
2346            continue;
2347        }
2348        if (!preg_match('/^(&&|\|\||\(|\))$/', $element)) {
2349            // Create each individual expression.
2350            // Search for ~ = <> X*{} .
2351
2352            // Sets like 3*{S34, S36, S37, S39}.
2353            if (preg_match('/^(\d+)\*\{(.+)\}$/', $element, $matches)) {
2354                $repeat = $matches[1];
2355                $set = explode(',', $matches[2]);
2356                $count = 0;
2357                foreach ($set as $setelement) {
2358                    if (isset($usertracks[$setelement]) &&
2359                        ($usertracks[$setelement]->status == 'completed' || $usertracks[$setelement]->status == 'passed')) {
2360                        $count++;
2361                    }
2362                }
2363                if ($count >= $repeat) {
2364                    $element = 'true';
2365                } else {
2366                    $element = 'false';
2367                }
2368            } else if ($element == '~') {
2369                // Not maps ~.
2370                $element = '!';
2371            } else if (preg_match('/^(.+)(\=|\<\>)(.+)$/', $element, $matches)) {
2372                // Other symbols = | <> .
2373                $element = trim($matches[1]);
2374                if (isset($usertracks[$element])) {
2375                    $value = trim(preg_replace('/(\'|\")/', '', $matches[3]));
2376                    if (isset($statuses[$value])) {
2377                        $value = $statuses[$value];
2378                    }
2379
2380                    $elementprerequisitematch = (strcmp($usertracks[$element]->status, $value) == 0);
2381                    if ($matches[2] == '<>') {
2382                        $element = $elementprerequisitematch ? 'false' : 'true';
2383                    } else {
2384                        $element = $elementprerequisitematch ? 'true' : 'false';
2385                    }
2386                } else {
2387                    $element = 'false';
2388                }
2389            } else {
2390                // Everything else must be an element defined like S45 ...
2391                if (isset($usertracks[$element]) &&
2392                    ($usertracks[$element]->status == 'completed' || $usertracks[$element]->status == 'passed')) {
2393                    $element = 'true';
2394                } else {
2395                    $element = 'false';
2396                }
2397            }
2398
2399        }
2400        $stack[] = ' '.$element.' ';
2401    }
2402    return eval('return '.implode($stack).';');
2403}
2404
2405/**
2406 * Update the calendar entries for this scorm activity.
2407 *
2408 * @param stdClass $scorm the row from the database table scorm.
2409 * @param int $cmid The coursemodule id
2410 * @return bool
2411 */
2412function scorm_update_calendar(stdClass $scorm, $cmid) {
2413    global $DB, $CFG;
2414
2415    require_once($CFG->dirroot.'/calendar/lib.php');
2416
2417    // Scorm start calendar events.
2418    $event = new stdClass();
2419    $event->eventtype = SCORM_EVENT_TYPE_OPEN;
2420    // The SCORM_EVENT_TYPE_OPEN event should only be an action event if no close time is specified.
2421    $event->type = empty($scorm->timeclose) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD;
2422    if ($event->id = $DB->get_field('event', 'id',
2423        array('modulename' => 'scorm', 'instance' => $scorm->id, 'eventtype' => $event->eventtype))) {
2424        if ((!empty($scorm->timeopen)) && ($scorm->timeopen > 0)) {
2425            // Calendar event exists so update it.
2426            $event->name = get_string('calendarstart', 'scorm', $scorm->name);
2427            $event->description = format_module_intro('scorm', $scorm, $cmid, false);
2428            $event->format = FORMAT_HTML;
2429            $event->timestart = $scorm->timeopen;
2430            $event->timesort = $scorm->timeopen;
2431            $event->visible = instance_is_visible('scorm', $scorm);
2432            $event->timeduration = 0;
2433
2434            $calendarevent = calendar_event::load($event->id);
2435            $calendarevent->update($event, false);
2436        } else {
2437            // Calendar event is on longer needed.
2438            $calendarevent = calendar_event::load($event->id);
2439            $calendarevent->delete();
2440        }
2441    } else {
2442        // Event doesn't exist so create one.
2443        if ((!empty($scorm->timeopen)) && ($scorm->timeopen > 0)) {
2444            $event->name = get_string('calendarstart', 'scorm', $scorm->name);
2445            $event->description = format_module_intro('scorm', $scorm, $cmid, false);
2446            $event->format = FORMAT_HTML;
2447            $event->courseid = $scorm->course;
2448            $event->groupid = 0;
2449            $event->userid = 0;
2450            $event->modulename = 'scorm';
2451            $event->instance = $scorm->id;
2452            $event->timestart = $scorm->timeopen;
2453            $event->timesort = $scorm->timeopen;
2454            $event->visible = instance_is_visible('scorm', $scorm);
2455            $event->timeduration = 0;
2456
2457            calendar_event::create($event, false);
2458        }
2459    }
2460
2461    // Scorm end calendar events.
2462    $event = new stdClass();
2463    $event->type = CALENDAR_EVENT_TYPE_ACTION;
2464    $event->eventtype = SCORM_EVENT_TYPE_CLOSE;
2465    if ($event->id = $DB->get_field('event', 'id',
2466        array('modulename' => 'scorm', 'instance' => $scorm->id, 'eventtype' => $event->eventtype))) {
2467        if ((!empty($scorm->timeclose)) && ($scorm->timeclose > 0)) {
2468            // Calendar event exists so update it.
2469            $event->name = get_string('calendarend', 'scorm', $scorm->name);
2470            $event->description = format_module_intro('scorm', $scorm, $cmid, false);
2471            $event->format = FORMAT_HTML;
2472            $event->timestart = $scorm->timeclose;
2473            $event->timesort = $scorm->timeclose;
2474            $event->visible = instance_is_visible('scorm', $scorm);
2475            $event->timeduration = 0;
2476
2477            $calendarevent = calendar_event::load($event->id);
2478            $calendarevent->update($event, false);
2479        } else {
2480            // Calendar event is on longer needed.
2481            $calendarevent = calendar_event::load($event->id);
2482            $calendarevent->delete();
2483        }
2484    } else {
2485        // Event doesn't exist so create one.
2486        if ((!empty($scorm->timeclose)) && ($scorm->timeclose > 0)) {
2487            $event->name = get_string('calendarend', 'scorm', $scorm->name);
2488            $event->description = format_module_intro('scorm', $scorm, $cmid, false);
2489            $event->format = FORMAT_HTML;
2490            $event->courseid = $scorm->course;
2491            $event->groupid = 0;
2492            $event->userid = 0;
2493            $event->modulename = 'scorm';
2494            $event->instance = $scorm->id;
2495            $event->timestart = $scorm->timeclose;
2496            $event->timesort = $scorm->timeclose;
2497            $event->visible = instance_is_visible('scorm', $scorm);
2498            $event->timeduration = 0;
2499
2500            calendar_event::create($event, false);
2501        }
2502    }
2503
2504    return true;
2505}
2506