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