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