PageRenderTime 52ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/mod/scorm/locallib.php

https://bitbucket.org/moodle/moodle
PHP | 2505 lines | 1849 code | 248 blank | 408 comment | 564 complexity | a7be4ad0ea071443075fc55fd7f0eb0b MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Library of internal classes and functions for module SCORM
  18. *
  19. * @package mod_scorm
  20. * @copyright 1999 onwards Roberto Pinna
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. require_once("$CFG->dirroot/mod/scorm/lib.php");
  24. require_once("$CFG->libdir/filelib.php");
  25. // Constants and settings for module scorm.
  26. define('SCORM_UPDATE_NEVER', '0');
  27. define('SCORM_UPDATE_EVERYDAY', '2');
  28. define('SCORM_UPDATE_EVERYTIME', '3');
  29. define('SCORM_SKIPVIEW_NEVER', '0');
  30. define('SCORM_SKIPVIEW_FIRST', '1');
  31. define('SCORM_SKIPVIEW_ALWAYS', '2');
  32. define('SCO_ALL', 0);
  33. define('SCO_DATA', 1);
  34. define('SCO_ONLY', 2);
  35. define('GRADESCOES', '0');
  36. define('GRADEHIGHEST', '1');
  37. define('GRADEAVERAGE', '2');
  38. define('GRADESUM', '3');
  39. define('HIGHESTATTEMPT', '0');
  40. define('AVERAGEATTEMPT', '1');
  41. define('FIRSTATTEMPT', '2');
  42. define('LASTATTEMPT', '3');
  43. define('TOCJSLINK', 1);
  44. define('TOCFULLURL', 2);
  45. define('SCORM_FORCEATTEMPT_NO', 0);
  46. define('SCORM_FORCEATTEMPT_ONCOMPLETE', 1);
  47. define('SCORM_FORCEATTEMPT_ALWAYS', 2);
  48. // Local Library of functions for module scorm.
  49. /**
  50. * @package mod_scorm
  51. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  52. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  53. */
  54. class scorm_package_file_info extends file_info_stored {
  55. public function get_parent() {
  56. if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
  57. return $this->browser->get_file_info($this->context);
  58. }
  59. return parent::get_parent();
  60. }
  61. public function get_visible_name() {
  62. if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
  63. return $this->topvisiblename;
  64. }
  65. return parent::get_visible_name();
  66. }
  67. }
  68. /**
  69. * Returns an array of the popup options for SCORM and each options default value
  70. *
  71. * @return array an array of popup options as the key and their defaults as the value
  72. */
  73. function scorm_get_popup_options_array() {
  74. $cfgscorm = get_config('scorm');
  75. return array('scrollbars' => isset($cfgscorm->scrollbars) ? $cfgscorm->scrollbars : 0,
  76. 'directories' => isset($cfgscorm->directories) ? $cfgscorm->directories : 0,
  77. 'location' => isset($cfgscorm->location) ? $cfgscorm->location : 0,
  78. 'menubar' => isset($cfgscorm->menubar) ? $cfgscorm->menubar : 0,
  79. 'toolbar' => isset($cfgscorm->toolbar) ? $cfgscorm->toolbar : 0,
  80. 'status' => isset($cfgscorm->status) ? $cfgscorm->status : 0);
  81. }
  82. /**
  83. * Returns an array of the array of what grade options
  84. *
  85. * @return array an array of what grade options
  86. */
  87. function scorm_get_grade_method_array() {
  88. return array (GRADESCOES => get_string('gradescoes', 'scorm'),
  89. GRADEHIGHEST => get_string('gradehighest', 'scorm'),
  90. GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
  91. GRADESUM => get_string('gradesum', 'scorm'));
  92. }
  93. /**
  94. * Returns an array of the array of what grade options
  95. *
  96. * @return array an array of what grade options
  97. */
  98. function scorm_get_what_grade_array() {
  99. return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
  100. AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
  101. FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
  102. LASTATTEMPT => get_string('lastattempt', 'scorm'));
  103. }
  104. /**
  105. * Returns an array of the array of skip view options
  106. *
  107. * @return array an array of skip view options
  108. */
  109. function scorm_get_skip_view_array() {
  110. return array(SCORM_SKIPVIEW_NEVER => get_string('never'),
  111. SCORM_SKIPVIEW_FIRST => get_string('firstaccess', 'scorm'),
  112. SCORM_SKIPVIEW_ALWAYS => get_string('always'));
  113. }
  114. /**
  115. * Returns an array of the array of hide table of contents options
  116. *
  117. * @return array an array of hide table of contents options
  118. */
  119. function scorm_get_hidetoc_array() {
  120. return array(SCORM_TOC_SIDE => get_string('sided', 'scorm'),
  121. SCORM_TOC_HIDDEN => get_string('hidden', 'scorm'),
  122. SCORM_TOC_POPUP => get_string('popupmenu', 'scorm'),
  123. SCORM_TOC_DISABLED => get_string('disabled', 'scorm'));
  124. }
  125. /**
  126. * Returns an array of the array of update frequency options
  127. *
  128. * @return array an array of update frequency options
  129. */
  130. function scorm_get_updatefreq_array() {
  131. return array(SCORM_UPDATE_NEVER => get_string('never'),
  132. SCORM_UPDATE_EVERYDAY => get_string('everyday', 'scorm'),
  133. SCORM_UPDATE_EVERYTIME => get_string('everytime', 'scorm'));
  134. }
  135. /**
  136. * Returns an array of the array of popup display options
  137. *
  138. * @return array an array of popup display options
  139. */
  140. function scorm_get_popup_display_array() {
  141. return array(0 => get_string('currentwindow', 'scorm'),
  142. 1 => get_string('popup', 'scorm'));
  143. }
  144. /**
  145. * Returns an array of the array of navigation buttons display options
  146. *
  147. * @return array an array of navigation buttons display options
  148. */
  149. function scorm_get_navigation_display_array() {
  150. return array(SCORM_NAV_DISABLED => get_string('no'),
  151. SCORM_NAV_UNDER_CONTENT => get_string('undercontent', 'scorm'),
  152. SCORM_NAV_FLOATING => get_string('floating', 'scorm'));
  153. }
  154. /**
  155. * Returns an array of the array of attempt options
  156. *
  157. * @return array an array of attempt options
  158. */
  159. function scorm_get_attempts_array() {
  160. $attempts = array(0 => get_string('nolimit', 'scorm'),
  161. 1 => get_string('attempt1', 'scorm'));
  162. for ($i = 2; $i <= 6; $i++) {
  163. $attempts[$i] = get_string('attemptsx', 'scorm', $i);
  164. }
  165. return $attempts;
  166. }
  167. /**
  168. * Returns an array of the attempt status options
  169. *
  170. * @return array an array of attempt status options
  171. */
  172. function scorm_get_attemptstatus_array() {
  173. return array(SCORM_DISPLAY_ATTEMPTSTATUS_NO => get_string('no'),
  174. SCORM_DISPLAY_ATTEMPTSTATUS_ALL => get_string('attemptstatusall', 'scorm'),
  175. SCORM_DISPLAY_ATTEMPTSTATUS_MY => get_string('attemptstatusmy', 'scorm'),
  176. SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY => get_string('attemptstatusentry', 'scorm'));
  177. }
  178. /**
  179. * Returns an array of the force attempt options
  180. *
  181. * @return array an array of attempt options
  182. */
  183. function scorm_get_forceattempt_array() {
  184. return array(SCORM_FORCEATTEMPT_NO => get_string('no'),
  185. SCORM_FORCEATTEMPT_ONCOMPLETE => get_string('forceattemptoncomplete', 'scorm'),
  186. SCORM_FORCEATTEMPT_ALWAYS => get_string('forceattemptalways', 'scorm'));
  187. }
  188. /**
  189. * Extracts scrom package, sets up all variables.
  190. * Called whenever scorm changes
  191. * @param object $scorm instance - fields are updated and changes saved into database
  192. * @param bool $full force full update if true
  193. * @return void
  194. */
  195. function scorm_parse($scorm, $full) {
  196. global $CFG, $DB;
  197. $cfgscorm = get_config('scorm');
  198. if (!isset($scorm->cmid)) {
  199. $cm = get_coursemodule_from_instance('scorm', $scorm->id);
  200. $scorm->cmid = $cm->id;
  201. }
  202. $context = context_module::instance($scorm->cmid);
  203. $newhash = $scorm->sha1hash;
  204. if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
  205. $fs = get_file_storage();
  206. $packagefile = false;
  207. $packagefileimsmanifest = false;
  208. if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
  209. if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
  210. if ($packagefile->is_external_file()) { // Get zip file so we can check it is correct.
  211. $packagefile->import_external_file_contents();
  212. }
  213. $newhash = $packagefile->get_contenthash();
  214. if (strtolower($packagefile->get_filename()) == 'imsmanifest.xml') {
  215. $packagefileimsmanifest = true;
  216. }
  217. } else {
  218. $newhash = null;
  219. }
  220. } else {
  221. if (!$cfgscorm->allowtypelocalsync) {
  222. // Sorry - localsync disabled.
  223. return;
  224. }
  225. if ($scorm->reference !== '') {
  226. $fs->delete_area_files($context->id, 'mod_scorm', 'package');
  227. $filerecord = array('contextid' => $context->id, 'component' => 'mod_scorm', 'filearea' => 'package',
  228. 'itemid' => 0, 'filepath' => '/');
  229. if ($packagefile = $fs->create_file_from_url($filerecord, $scorm->reference, array('calctimeout' => true), true)) {
  230. $newhash = $packagefile->get_contenthash();
  231. } else {
  232. $newhash = null;
  233. }
  234. }
  235. }
  236. if ($packagefile) {
  237. if (!$full and $packagefile and $scorm->sha1hash === $newhash) {
  238. if (strpos($scorm->version, 'SCORM') !== false) {
  239. if ($packagefileimsmanifest || $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
  240. // No need to update.
  241. return;
  242. }
  243. } else if (strpos($scorm->version, 'AICC') !== false) {
  244. // TODO: add more sanity checks - something really exists in scorm_content area.
  245. return;
  246. }
  247. }
  248. if (!$packagefileimsmanifest) {
  249. // Now extract files.
  250. $fs->delete_area_files($context->id, 'mod_scorm', 'content');
  251. $packer = get_file_packer('application/zip');
  252. $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/');
  253. }
  254. } else if (!$full) {
  255. return;
  256. }
  257. if ($packagefileimsmanifest) {
  258. require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
  259. // Direct link to imsmanifest.xml file.
  260. if (!scorm_parse_scorm($scorm, $packagefile)) {
  261. $scorm->version = 'ERROR';
  262. }
  263. } else if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
  264. require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
  265. // SCORM.
  266. if (!scorm_parse_scorm($scorm, $manifest)) {
  267. $scorm->version = 'ERROR';
  268. }
  269. } else {
  270. require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
  271. // AICC.
  272. $result = scorm_parse_aicc($scorm);
  273. if (!$result) {
  274. $scorm->version = 'ERROR';
  275. } else {
  276. $scorm->version = 'AICC';
  277. }
  278. }
  279. } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfgscorm->allowtypeexternal) {
  280. require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
  281. // SCORM only, AICC can not be external.
  282. if (!scorm_parse_scorm($scorm, $scorm->reference)) {
  283. $scorm->version = 'ERROR';
  284. }
  285. $newhash = sha1($scorm->reference);
  286. } else if ($scorm->scormtype === SCORM_TYPE_AICCURL and $cfgscorm->allowtypeexternalaicc) {
  287. require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
  288. // AICC.
  289. $result = scorm_parse_aicc($scorm);
  290. if (!$result) {
  291. $scorm->version = 'ERROR';
  292. } else {
  293. $scorm->version = 'AICC';
  294. }
  295. } else {
  296. // Sorry, disabled type.
  297. return;
  298. }
  299. $scorm->revision++;
  300. $scorm->sha1hash = $newhash;
  301. $DB->update_record('scorm', $scorm);
  302. }
  303. function 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;
  313. }
  314. }
  315. }
  316. }
  317. return false;
  318. }
  319. function scorm_repeater($what, $times) {
  320. if ($times <= 0) {
  321. return null;
  322. }
  323. $return = '';
  324. for ($i = 0; $i < $times; $i++) {
  325. $return .= $what;
  326. }
  327. return $return;
  328. }
  329. function scorm_external_link($link) {
  330. // Check if a link is external.
  331. $result = false;
  332. $link = strtolower($link);
  333. if (substr($link, 0, 7) == 'http://') {
  334. $result = true;
  335. } else if (substr($link, 0, 8) == 'https://') {
  336. $result = true;
  337. } else if (substr($link, 0, 4) == 'www.') {
  338. $result = true;
  339. }
  340. return $result;
  341. }
  342. /**
  343. * Returns an object containing all datas relative to the given sco ID
  344. *
  345. * @param integer $id The sco ID
  346. * @return mixed (false if sco id does not exists)
  347. */
  348. function scorm_get_sco($id, $what=SCO_ALL) {
  349. global $DB;
  350. if ($sco = $DB->get_record('scorm_scoes', array('id' => $id))) {
  351. $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
  352. if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id)))) {
  353. foreach ($scodatas as $scodata) {
  354. $sco->{$scodata->name} = $scodata->value;
  355. }
  356. } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $id))))) {
  357. $sco->parameters = '';
  358. }
  359. return $sco;
  360. } else {
  361. return false;
  362. }
  363. }
  364. /**
  365. * Returns an object (array) containing all the scoes data related to the given sco ID
  366. *
  367. * @param integer $id The sco ID
  368. * @param integer $organisation an organisation ID - defaults to false if not required
  369. * @return mixed (false if there are no scoes or an array)
  370. */
  371. function scorm_get_scoes($id, $organisation=false) {
  372. global $DB;
  373. $queryarray = array('scorm' => $id);
  374. if (!empty($organisation)) {
  375. $queryarray['organization'] = $organisation;
  376. }
  377. if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'sortorder, id')) {
  378. // Drop keys so that it is a simple array as expected.
  379. $scoes = array_values($scoes);
  380. foreach ($scoes as $sco) {
  381. if ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid' => $sco->id))) {
  382. foreach ($scodatas as $scodata) {
  383. $sco->{$scodata->name} = $scodata->value;
  384. }
  385. }
  386. }
  387. return $scoes;
  388. } else {
  389. return false;
  390. }
  391. }
  392. function scorm_insert_track($userid, $scormid, $scoid, $attempt, $element, $value, $forcecompleted=false, $trackdata = null) {
  393. global $DB, $CFG;
  394. $id = null;
  395. if ($forcecompleted) {
  396. // TODO - this could be broadened to encompass SCORM 2004 in future.
  397. if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) {
  398. if ($track = $DB->get_record_select('scorm_scoes_track',
  399. 'userid=? AND scormid=? AND scoid=? AND attempt=? '.
  400. 'AND element=\'cmi.core.score.raw\'',
  401. array($userid, $scormid, $scoid, $attempt))) {
  402. $value = 'completed';
  403. }
  404. }
  405. if ($element == 'cmi.core.score.raw') {
  406. if ($tracktest = $DB->get_record_select('scorm_scoes_track',
  407. 'userid=? AND scormid=? AND scoid=? AND attempt=? '.
  408. 'AND element=\'cmi.core.lesson_status\'',
  409. array($userid, $scormid, $scoid, $attempt))) {
  410. if ($tracktest->value == "incomplete") {
  411. $tracktest->value = "completed";
  412. $DB->update_record('scorm_scoes_track', $tracktest);
  413. }
  414. }
  415. }
  416. if (($element == 'cmi.success_status') && ($value == 'passed' || $value == 'failed')) {
  417. if ($DB->get_record('scorm_scoes_data', array('scoid' => $scoid, 'name' => 'objectivesetbycontent'))) {
  418. $objectiveprogressstatus = true;
  419. $objectivesatisfiedstatus = false;
  420. if ($value == 'passed') {
  421. $objectivesatisfiedstatus = true;
  422. }
  423. if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
  424. 'scormid' => $scormid,
  425. 'scoid' => $scoid,
  426. 'attempt' => $attempt,
  427. 'element' => 'objectiveprogressstatus'))) {
  428. $track->value = $objectiveprogressstatus;
  429. $track->timemodified = time();
  430. $DB->update_record('scorm_scoes_track', $track);
  431. $id = $track->id;
  432. } else {
  433. $track = new stdClass();
  434. $track->userid = $userid;
  435. $track->scormid = $scormid;
  436. $track->scoid = $scoid;
  437. $track->attempt = $attempt;
  438. $track->element = 'objectiveprogressstatus';
  439. $track->value = $objectiveprogressstatus;
  440. $track->timemodified = time();
  441. $id = $DB->insert_record('scorm_scoes_track', $track);
  442. }
  443. if ($objectivesatisfiedstatus) {
  444. if ($track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
  445. 'scormid' => $scormid,
  446. 'scoid' => $scoid,
  447. 'attempt' => $attempt,
  448. 'element' => 'objectivesatisfiedstatus'))) {
  449. $track->value = $objectivesatisfiedstatus;
  450. $track->timemodified = time();
  451. $DB->update_record('scorm_scoes_track', $track);
  452. $id = $track->id;
  453. } else {
  454. $track = new stdClass();
  455. $track->userid = $userid;
  456. $track->scormid = $scormid;
  457. $track->scoid = $scoid;
  458. $track->attempt = $attempt;
  459. $track->element = 'objectivesatisfiedstatus';
  460. $track->value = $objectivesatisfiedstatus;
  461. $track->timemodified = time();
  462. $id = $DB->insert_record('scorm_scoes_track', $track);
  463. }
  464. }
  465. }
  466. }
  467. }
  468. $track = null;
  469. if ($trackdata !== null) {
  470. if (isset($trackdata[$element])) {
  471. $track = $trackdata[$element];
  472. }
  473. } else {
  474. $track = $DB->get_record('scorm_scoes_track', array('userid' => $userid,
  475. 'scormid' => $scormid,
  476. 'scoid' => $scoid,
  477. 'attempt' => $attempt,
  478. 'element' => $element));
  479. }
  480. if ($track) {
  481. if ($element != 'x.start.time' ) { // Don't update x.start.time - keep the original value.
  482. if ($track->value != $value) {
  483. $track->value = $value;
  484. $track->timemodified = time();
  485. $DB->update_record('scorm_scoes_track', $track);
  486. }
  487. $id = $track->id;
  488. }
  489. } else {
  490. $track = new stdClass();
  491. $track->userid = $userid;
  492. $track->scormid = $scormid;
  493. $track->scoid = $scoid;
  494. $track->attempt = $attempt;
  495. $track->element = $element;
  496. $track->value = $value;
  497. $track->timemodified = time();
  498. $id = $DB->insert_record('scorm_scoes_track', $track);
  499. $track->id = $id;
  500. }
  501. // Trigger updating grades based on a given set of SCORM CMI elements.
  502. $scorm = false;
  503. if (in_array($element, array('cmi.core.score.raw', 'cmi.score.raw')) ||
  504. (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
  505. && in_array($track->value, array('completed', 'passed')))) {
  506. $scorm = $DB->get_record('scorm', array('id' => $scormid));
  507. include_once($CFG->dirroot.'/mod/scorm/lib.php');
  508. scorm_update_grades($scorm, $userid);
  509. }
  510. // Trigger CMI element events.
  511. if (in_array($element, array('cmi.core.score.raw', 'cmi.score.raw')) ||
  512. (in_array($element, array('cmi.completion_status', 'cmi.core.lesson_status', 'cmi.success_status'))
  513. && in_array($track->value, array('completed', 'failed', 'passed')))) {
  514. if (!$scorm) {
  515. $scorm = $DB->get_record('scorm', array('id' => $scormid));
  516. }
  517. $cm = get_coursemodule_from_instance('scorm', $scormid);
  518. $data = array(
  519. 'other' => array('attemptid' => $attempt, 'cmielement' => $element, 'cmivalue' => $track->value),
  520. 'objectid' => $scorm->id,
  521. 'context' => context_module::instance($cm->id),
  522. 'relateduserid' => $userid
  523. );
  524. if (in_array($element, array('cmi.core.score.raw', 'cmi.score.raw'))) {
  525. // Create score submitted event.
  526. $event = \mod_scorm\event\scoreraw_submitted::create($data);
  527. } else {
  528. // Create status submitted event.
  529. $event = \mod_scorm\event\status_submitted::create($data);
  530. }
  531. // Fix the missing track keys when the SCORM track record already exists, see $trackdata in datamodel.php.
  532. // There, for performances reasons, columns are limited to: element, id, value, timemodified.
  533. // Missing fields are: userid, scormid, scoid, attempt.
  534. $track->userid = $userid;
  535. $track->scormid = $scormid;
  536. $track->scoid = $scoid;
  537. $track->attempt = $attempt;
  538. // Trigger submitted event.
  539. $event->add_record_snapshot('scorm_scoes_track', $track);
  540. $event->add_record_snapshot('course_modules', $cm);
  541. $event->add_record_snapshot('scorm', $scorm);
  542. $event->trigger();
  543. }
  544. return $id;
  545. }
  546. /**
  547. * simple quick function to return true/false if this user has tracks in this scorm
  548. *
  549. * @param integer $scormid The scorm ID
  550. * @param integer $userid the users id
  551. * @return boolean (false if there are no tracks)
  552. */
  553. function scorm_has_tracks($scormid, $userid) {
  554. global $DB;
  555. return $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scormid));
  556. }
  557. function scorm_get_tracks($scoid, $userid, $attempt='') {
  558. // Gets all tracks of specified sco and user.
  559. global $DB;
  560. if (empty($attempt)) {
  561. if ($scormid = $DB->get_field('scorm_scoes', 'scorm', array('id' => $scoid))) {
  562. $attempt = scorm_get_last_attempt($scormid, $userid);
  563. } else {
  564. $attempt = 1;
  565. }
  566. }
  567. if ($tracks = $DB->get_records('scorm_scoes_track', array('userid' => $userid, 'scoid' => $scoid,
  568. 'attempt' => $attempt), 'element ASC')) {
  569. $usertrack = scorm_format_interactions($tracks);
  570. $usertrack->userid = $userid;
  571. $usertrack->scoid = $scoid;
  572. return $usertrack;
  573. } else {
  574. return false;
  575. }
  576. }
  577. /**
  578. * helper function to return a formatted list of interactions for reports.
  579. *
  580. * @param array $trackdata the records from scorm_scoes_track table
  581. * @return object formatted list of interactions
  582. */
  583. function scorm_format_interactions($trackdata) {
  584. $usertrack = new stdClass();
  585. // Defined in order to unify scorm1.2 and scorm2004.
  586. $usertrack->score_raw = '';
  587. $usertrack->status = '';
  588. $usertrack->total_time = '00:00:00';
  589. $usertrack->session_time = '00:00:00';
  590. $usertrack->timemodified = 0;
  591. foreach ($trackdata as $track) {
  592. $element = $track->element;
  593. $usertrack->{$element} = $track->value;
  594. switch ($element) {
  595. case 'cmi.core.lesson_status':
  596. case 'cmi.completion_status':
  597. if ($track->value == 'not attempted') {
  598. $track->value = 'notattempted';
  599. }
  600. $usertrack->status = $track->value;
  601. break;
  602. case 'cmi.core.score.raw':
  603. case 'cmi.score.raw':
  604. $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
  605. break;
  606. case 'cmi.core.session_time':
  607. case 'cmi.session_time':
  608. $usertrack->session_time = $track->value;
  609. break;
  610. case 'cmi.core.total_time':
  611. case 'cmi.total_time':
  612. $usertrack->total_time = $track->value;
  613. break;
  614. }
  615. if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
  616. $usertrack->timemodified = $track->timemodified;
  617. }
  618. }
  619. return $usertrack;
  620. }
  621. /* Find the start and finsh time for a a given SCO attempt
  622. *
  623. * @param int $scormid SCORM Id
  624. * @param int $scoid SCO Id
  625. * @param int $userid User Id
  626. * @param int $attemt Attempt Id
  627. *
  628. * @return object start and finsh time EPOC secods
  629. *
  630. */
  631. function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
  632. global $DB;
  633. $timedata = new stdClass();
  634. $params = array('userid' => $userid, 'scormid' => $scormid, 'attempt' => $attempt);
  635. if (!empty($scoid)) {
  636. $params['scoid'] = $scoid;
  637. }
  638. $tracks = $DB->get_records('scorm_scoes_track', $params, "timemodified ASC");
  639. if ($tracks) {
  640. $tracks = array_values($tracks);
  641. }
  642. if ($tracks) {
  643. $timedata->start = $tracks[0]->timemodified;
  644. } else {
  645. $timedata->start = false;
  646. }
  647. if ($tracks && $track = array_pop($tracks)) {
  648. $timedata->finish = $track->timemodified;
  649. } else {
  650. $timedata->finish = $timedata->start;
  651. }
  652. return $timedata;
  653. }
  654. function scorm_grade_user_attempt($scorm, $userid, $attempt=1) {
  655. global $DB;
  656. $attemptscore = new stdClass();
  657. $attemptscore->scoes = 0;
  658. $attemptscore->values = 0;
  659. $attemptscore->max = 0;
  660. $attemptscore->sum = 0;
  661. $attemptscore->lastmodify = 0;
  662. if (!$scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) {
  663. return null;
  664. }
  665. foreach ($scoes as $sco) {
  666. if ($userdata = scorm_get_tracks($sco->id, $userid, $attempt)) {
  667. if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
  668. $attemptscore->scoes++;
  669. }
  670. if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type == 'sco' && isset($userdata->score_raw))) {
  671. $attemptscore->values++;
  672. $attemptscore->sum += $userdata->score_raw;
  673. $attemptscore->max = ($userdata->score_raw > $attemptscore->max) ? $userdata->score_raw : $attemptscore->max;
  674. if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
  675. $attemptscore->lastmodify = $userdata->timemodified;
  676. } else {
  677. $attemptscore->lastmodify = 0;
  678. }
  679. }
  680. }
  681. }
  682. switch ($scorm->grademethod) {
  683. case GRADEHIGHEST:
  684. $score = (float) $attemptscore->max;
  685. break;
  686. case GRADEAVERAGE:
  687. if ($attemptscore->values > 0) {
  688. $score = $attemptscore->sum / $attemptscore->values;
  689. } else {
  690. $score = 0;
  691. }
  692. break;
  693. case GRADESUM:
  694. $score = $attemptscore->sum;
  695. break;
  696. case GRADESCOES:
  697. $score = $attemptscore->scoes;
  698. break;
  699. default:
  700. $score = $attemptscore->max; // Remote Learner GRADEHIGHEST is default.
  701. }
  702. return $score;
  703. }
  704. function scorm_grade_user($scorm, $userid) {
  705. // Ensure we dont grade user beyond $scorm->maxattempt settings.
  706. $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
  707. if ($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt) {
  708. $lastattempt = $scorm->maxattempt;
  709. }
  710. switch ($scorm->whatgrade) {
  711. case FIRSTATTEMPT:
  712. return scorm_grade_user_attempt($scorm, $userid, scorm_get_first_attempt($scorm->id, $userid));
  713. break;
  714. case LASTATTEMPT:
  715. return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
  716. break;
  717. case HIGHESTATTEMPT:
  718. $maxscore = 0;
  719. for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
  720. $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
  721. $maxscore = $attemptscore > $maxscore ? $attemptscore : $maxscore;
  722. }
  723. return $maxscore;
  724. break;
  725. case AVERAGEATTEMPT:
  726. $attemptcount = scorm_get_attempt_count($userid, $scorm, true, true);
  727. if (empty($attemptcount)) {
  728. return 0;
  729. } else {
  730. $attemptcount = count($attemptcount);
  731. }
  732. $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
  733. $sumscore = 0;
  734. for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
  735. $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
  736. $sumscore += $attemptscore;
  737. }
  738. return round($sumscore / $attemptcount);
  739. break;
  740. }
  741. }
  742. function scorm_count_launchable($scormid, $organization='') {
  743. global $DB;
  744. $sqlorganization = '';
  745. $params = array($scormid);
  746. if (!empty($organization)) {
  747. $sqlorganization = " AND organization=?";
  748. $params[] = $organization;
  749. }
  750. return $DB->count_records_select('scorm_scoes', "scorm = ? $sqlorganization AND ".
  751. $DB->sql_isnotempty('scorm_scoes', 'launch', false, true),
  752. $params);
  753. }
  754. /**
  755. * Returns the last attempt used - if no attempts yet, returns 1 for first attempt
  756. *
  757. * @param int $scormid the id of the scorm.
  758. * @param int $userid the id of the user.
  759. *
  760. * @return int The attempt number to use.
  761. */
  762. function scorm_get_last_attempt($scormid, $userid) {
  763. global $DB;
  764. // Find the last attempt number for the given user id and scorm id.
  765. $sql = "SELECT MAX(attempt)
  766. FROM {scorm_scoes_track}
  767. WHERE userid = ? AND scormid = ?";
  768. $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
  769. if (empty($lastattempt)) {
  770. return '1';
  771. } else {
  772. return $lastattempt;
  773. }
  774. }
  775. /**
  776. * Returns the first attempt used - if no attempts yet, returns 1 for first attempt.
  777. *
  778. * @param int $scormid the id of the scorm.
  779. * @param int $userid the id of the user.
  780. *
  781. * @return int The first attempt number.
  782. */
  783. function scorm_get_first_attempt($scormid, $userid) {
  784. global $DB;
  785. // Find the first attempt number for the given user id and scorm id.
  786. $sql = "SELECT MIN(attempt)
  787. FROM {scorm_scoes_track}
  788. WHERE userid = ? AND scormid = ?";
  789. $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
  790. if (empty($lastattempt)) {
  791. return '1';
  792. } else {
  793. return $lastattempt;
  794. }
  795. }
  796. /**
  797. * Returns the last completed attempt used - if no completed attempts yet, returns 1 for first attempt
  798. *
  799. * @param int $scormid the id of the scorm.
  800. * @param int $userid the id of the user.
  801. *
  802. * @return int The attempt number to use.
  803. */
  804. function scorm_get_last_completed_attempt($scormid, $userid) {
  805. global $DB;
  806. // Find the last completed attempt number for the given user id and scorm id.
  807. $sql = "SELECT MAX(attempt)
  808. FROM {scorm_scoes_track}
  809. WHERE userid = ? AND scormid = ?
  810. AND (".$DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?')." OR ".
  811. $DB->sql_compare_text('value')." = ".$DB->sql_compare_text('?').")";
  812. $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid, 'completed', 'passed'));
  813. if (empty($lastattempt)) {
  814. return '1';
  815. } else {
  816. return $lastattempt;
  817. }
  818. }
  819. /**
  820. * Returns the full list of attempts a user has made.
  821. *
  822. * @param int $scormid the id of the scorm.
  823. * @param int $userid the id of the user.
  824. *
  825. * @return array array of attemptids
  826. */
  827. function scorm_get_all_attempts($scormid, $userid) {
  828. global $DB;
  829. $attemptids = array();
  830. $sql = "SELECT DISTINCT attempt FROM {scorm_scoes_track} WHERE userid = ? AND scormid = ? ORDER BY attempt";
  831. $attempts = $DB->get_records_sql($sql, array($userid, $scormid));
  832. foreach ($attempts as $attempt) {
  833. $attemptids[] = $attempt->attempt;
  834. }
  835. return $attemptids;
  836. }
  837. /**
  838. * Displays the entry form and toc if required.
  839. *
  840. * @param stdClass $user user object
  841. * @param stdClass $scorm scorm object
  842. * @param string $action base URL for the organizations select box
  843. * @param stdClass $cm course module object
  844. */
  845. function scorm_print_launch ($user, $scorm, $action, $cm) {
  846. global $CFG, $DB, $OUTPUT;
  847. if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
  848. scorm_parse($scorm, false);
  849. }
  850. $organization = optional_param('organization', '', PARAM_INT);
  851. if ($scorm->displaycoursestructure == 1) {
  852. echo $OUTPUT->box_start('generalbox boxaligncenter toc container', 'toc');
  853. echo html_writer::div(get_string('contents', 'scorm'), 'structurehead');
  854. }
  855. if (empty($organization)) {
  856. $organization = $scorm->launch;
  857. }
  858. if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '.
  859. $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
  860. $DB->sql_isempty('scorm_scoes', 'organization', false, false),
  861. array($scorm->id), 'sortorder, id', 'id,title')) {
  862. if (count($orgs) > 1) {
  863. $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null);
  864. $select->label = get_string('organizations', 'scorm');
  865. $select->class = 'scorm-center';
  866. echo $OUTPUT->render($select);
  867. }
  868. }
  869. $orgidentifier = '';
  870. if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
  871. if (($sco->organization == '') && ($sco->launch == '')) {
  872. $orgidentifier = $sco->identifier;
  873. } else {
  874. $orgidentifier = $sco->organization;
  875. }
  876. }
  877. $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR)); // Just to be safe.
  878. if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
  879. $scorm->version = 'scorm_12';
  880. }
  881. require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
  882. $result = scorm_get_toc($user, $scorm, $cm->id, TOCFULLURL, $orgidentifier);
  883. $incomplete = $result->incomplete;
  884. // Get latest incomplete sco to launch first if force new attempt isn't set to always.
  885. if (!empty($result->sco->id) && $scorm->forcenewattempt != SCORM_FORCEATTEMPT_ALWAYS) {
  886. $launchsco = $result->sco->id;
  887. } else {
  888. // Use launch defined by SCORM package.
  889. $launchsco = $scorm->launch;
  890. }
  891. // Do we want the TOC to be displayed?
  892. if ($scorm->displaycoursestructure == 1) {
  893. echo $result->toc;
  894. echo $OUTPUT->box_end();
  895. }
  896. // Is this the first attempt ?
  897. $attemptcount = scorm_get_attempt_count($user->id, $scorm);
  898. // Do not give the player launch FORM if the SCORM object is locked after the final attempt.
  899. if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) {
  900. echo html_writer::start_div('scorm-center');
  901. echo html_writer::start_tag('form', array('id' => 'scormviewform',
  902. 'method' => 'post',
  903. 'action' => $CFG->wwwroot.'/mod/scorm/player.php',
  904. 'class' => 'container'));
  905. if ($scorm->hidebrowse == 0) {
  906. print_string('mode', 'scorm');
  907. echo ': '.html_writer::empty_tag('input', array('type' => 'radio', 'id' => 'b', 'name' => 'mode',
  908. 'value' => 'browse', 'class' => 'mr-1')).
  909. html_writer::label(get_string('browse', 'scorm'), 'b');
  910. echo html_writer::empty_tag('input', array('type' => 'radio',
  911. 'id' => 'n', 'name' => 'mode',
  912. 'value' => 'normal', 'checked' => 'checked',
  913. 'class' => 'mx-1')).
  914. html_writer::label(get_string('normal', 'scorm'), 'n');
  915. } else {
  916. echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'mode', 'value' => 'normal'));
  917. }
  918. if (!empty($scorm->forcenewattempt)) {
  919. if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS ||
  920. ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE && $incomplete === false)) {
  921. echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'newattempt', 'value' => 'on'));
  922. }
  923. } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
  924. echo html_writer::empty_tag('br');
  925. echo html_writer::checkbox('newattempt', 'on', false, '', array('id' => 'a'));
  926. echo html_writer::label(get_string('newattempt', 'scorm'), 'a');
  927. }
  928. if (!empty($scorm->popup)) {
  929. echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'display', 'value' => 'popup'));
  930. }
  931. echo html_writer::empty_tag('br');
  932. echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scoid', 'value' => $launchsco));
  933. echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cm', 'value' => $cm->id));
  934. echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentorg', 'value' => $orgidentifier));
  935. echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('enter', 'scorm'),
  936. 'class' => 'btn btn-primary'));
  937. echo html_writer::end_tag('form');
  938. echo html_writer::end_div();
  939. }
  940. }
  941. function scorm_simple_play($scorm, $user, $context, $cmid) {
  942. global $DB;
  943. $result = false;
  944. if (has_capability('mod/scorm:viewreport', $context)) {
  945. // If this user can view reports, don't skipview so they can see links to reports.
  946. return $result;
  947. }
  948. if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
  949. scorm_parse($scorm, false);
  950. }
  951. $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.
  952. $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'sortorder, id', 'id');
  953. if ($scoes) {
  954. $orgidentifier = '';
  955. if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) {
  956. if (($sco->organization == '') && ($sco->launch == '')) {
  957. $orgidentifier = $sco->identifier;
  958. } else {
  959. $orgidentifier = $sco->organization;
  960. }
  961. }
  962. if ($scorm->skipview >= SCORM_SKIPVIEW_FIRST) {
  963. $sco = current($scoes);
  964. $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier);
  965. $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id, 'currentorg' => $orgidentifier));
  966. // Set last incomplete sco to launch first if forcenewattempt not set to always.
  967. if (!empty($result->sco->id) && $scorm->forcenewattempt != SCORM_FORCEATTEMPT_ALWAYS) {
  968. $url->param('scoid', $result->sco->id);
  969. } else {
  970. $url->param('scoid', $sco->id);
  971. }
  972. if ($scorm->skipview == SCORM_SKIPVIEW_ALWAYS || !scorm_has_tracks($scorm->id, $user->id)) {
  973. if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS ||
  974. ($result->incomplete === false && $scorm->forcenewattempt == SCORM_FORCEATTEMPT_ONCOMPLETE)) {
  975. $url->param('newattempt', 'on');
  976. }
  977. redirect($url);
  978. }
  979. }
  980. }
  981. return $result;
  982. }
  983. function scorm_get_count_users($scormid, $groupingid=null) {
  984. global $CFG, $DB;
  985. if (!empty($groupingid)) {
  986. $sql = "SELECT COUNT(DISTINCT st.userid)
  987. FROM {scorm_scoes_track} st
  988. INNER JOIN {groups_members} gm ON st.userid = gm.userid
  989. INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
  990. WHERE st.scormid = ? AND gg.groupingid = ?
  991. ";
  992. $params = array($scormid, $groupingid);
  993. } else {
  994. $sql = "SELECT COUNT(DISTINCT st.userid)
  995. FROM {scorm_scoes_track} st
  996. WHERE st.scormid = ?
  997. ";
  998. $params = array($scormid);
  999. }
  1000. return ($DB->count_records_sql($sql, $params));
  1001. }
  1002. /**
  1003. * Build up the JavaScript representation of an array element
  1004. *
  1005. * @param string $sversion SCORM API version
  1006. * @param array $userdata User track data
  1007. * @param string $elementname Name of array element to get values for
  1008. * @param array $children list of sub elements of this array element that also need instantiating
  1009. * @return Javascript array elements
  1010. */
  1011. function scorm_reconstitute_array_element($sversion, $userdata, $elementname, $children) {
  1012. // Reconstitute comments_from_learner and comments_from_lms.
  1013. $current = '';
  1014. $currentsubelement = '';
  1015. $currentsub = '';
  1016. $count = 0;
  1017. $countsub = 0;
  1018. $scormseperator = '_';
  1019. $return = '';
  1020. if (scorm_version_check($sversion, SCORM_13)) { // Scorm 1.3 elements use a . instead of an _ .
  1021. $scormseperator = '.';
  1022. }
  1023. // Filter out the ones we want.
  1024. $elementlist = array();
  1025. foreach ($userdata as $element => $value) {
  1026. if (substr($element, 0, strlen($elementname)) == $elementname) {
  1027. $elementlist[$element] = $value;
  1028. }
  1029. }
  1030. // Sort elements in .n array order.
  1031. uksort($elementlist, "scorm_element_cmp");
  1032. // Generate JavaScript.
  1033. foreach ($elementlist as $element => $value) {
  1034. if (scorm_version_check($sversion, SCORM_13)) {
  1035. $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
  1036. preg_match('/\.(N\d+)\./', $element, $matches);
  1037. } else {
  1038. $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
  1039. preg_match('/\_(\d+)\./', $element, $matches);
  1040. }
  1041. if (count($matches) > 0 && $current != $matches[1]) {
  1042. if ($countsub > 0) {
  1043. $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
  1044. }
  1045. $current = $matches[1];
  1046. $count++;
  1047. $currentsubelement = '';
  1048. $currentsub = '';
  1049. $countsub = 0;
  1050. $end = strpos($element, $matches[1]) + strlen($matches[1]);
  1051. $subelement = substr($element, 0, $end);
  1052. $return .= ' '.$subelement." = new Object();\n";
  1053. // Now add the children.
  1054. foreach ($children as $child) {
  1055. $return .= ' '.$subelement.".".$child." = new Object();\n";
  1056. $return .= ' '.$subelement.".".$child."._children = ".$child."_children;\n";
  1057. }
  1058. }
  1059. // Now - flesh out the second level elements if there are any.
  1060. if (scorm_version_check($sversion, SCORM_13)) {
  1061. $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
  1062. preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
  1063. } else {
  1064. $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
  1065. preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
  1066. }
  1067. // Check the sub element type.
  1068. if (count($matches) > 0 && $currentsubelement != $matches[1]) {
  1069. if ($countsub > 0) {
  1070. $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
  1071. }
  1072. $currentsubelement = $matches[1];
  1073. $currentsub = '';
  1074. $countsub = 0;
  1075. $end = strpos($element, $matches[1]) + strlen($matches[1]);
  1076. $subelement = substr($element, 0, $end);
  1077. $return .= ' '.$subelement." = new Object();\n";
  1078. }
  1079. // Now check the subelement subscript.
  1080. if (count($matches) > 0 && $currentsub != $matches[2]) {
  1081. $currentsub = $matches[2];
  1082. $countsub++;
  1083. $end = strrpos($element, $matches[2]) + strlen($matches[2]);
  1084. $subelement = substr($element, 0, $end);
  1085. $return .= ' '.$subelement." = new Object();\n";
  1086. }
  1087. $return .= ' '.$element.' = '.json_encode($value).";\n";
  1088. }
  1089. if ($countsub > 0) {
  1090. $return .= ' '.$elementname.$scormseperator.$current.'.'.$currentsubelement.'._count = '.$countsub.";\n";
  1091. }
  1092. if ($count > 0) {
  1093. $return .= ' '.$elementname.'._count = '.$count.";\n";
  1094. }
  1095. return $return;
  1096. }
  1097. /**
  1098. * Build up the JavaScript representation of an array element
  1099. *
  1100. * @param string $a left array element
  1101. * @param string $b right array element
  1102. * @return comparator - 0,1,-1
  1103. */
  1104. function scorm_element_cmp($a, $b) {
  1105. preg_match('/.*?(\d+)\./', $a, $matches);
  1106. $left = intval($matches[1]);
  1107. preg_match('/.?(\d+)\./', $b, $matches);
  1108. $right = intval($matches[1]);
  1109. if ($left < $right) {
  1110. return -1; // Smaller.
  1111. } else if ($left > $right) {
  1112. return 1; // Bigger.
  1113. } else {
  1114. // Look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern.
  1115. if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
  1116. $leftterm = intval($matches[2]);
  1117. $left = intval($matches[3]);
  1118. if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
  1119. $rightterm = intval($matches[2]);
  1120. $right = intval($matches[3]);
  1121. if ($leftterm < $rightterm) {
  1122. return -1; // Smaller.
  1123. } else if ($leftterm > $rightterm) {
  1124. return 1; // Bigger.
  1125. } else {
  1126. if ($left < $right) {
  1127. return -1; // Smaller.
  1128. } else if ($left > $right) {
  1129. return 1; // Bigger.
  1130. }
  1131. }
  1132. }
  1133. }
  1134. // Fall back for no second level matches or second level matches are equal.
  1135. return 0; // Equal to.
  1136. }
  1137. }
  1138. /**
  1139. * Generate the user attempt status string
  1140. *
  1141. * @param object $user Current context user
  1142. * @param object $scorm a moodle scrom object - mdl_scorm
  1143. * @return string - Attempt status string
  1144. */
  1145. function scorm_get_attempt_status($user, $scorm, $cm='') {
  1146. global $DB, $PAGE, $OUTPUT;
  1147. $attempts = scorm_get_attempt_count($user->id, $scorm, true);
  1148. if (empty($attempts)) {
  1149. $attemptcount = 0;
  1150. } else {
  1151. $attemptcount = count($attempts);
  1152. }
  1153. $result = html_writer::start_tag('p').get_string('noattemptsallowed', 'scorm').': ';
  1154. if ($scorm->maxattempt > 0) {
  1155. $result .= $scorm->maxattempt . html_writer::empty_tag('br');
  1156. } else {
  1157. $result .= get_string('unlimited').html_writer::empty_tag('br');
  1158. }
  1159. $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . html_writer::empty_tag('br');
  1160. if ($scorm->maxattempt == 1) {
  1161. switch ($scorm->grademethod) {
  1162. case GRADEHIGHEST:
  1163. $grademethod = get_string('gradehighest', 'scorm');
  1164. break;
  1165. case GRADEAVERAGE:
  1166. $grademethod = get_string('gradeaverage', 'scorm');
  1167. break;
  1168. case GRADESUM:
  1169. $grademethod = get_string('gradesum', 'scorm');
  1170. break;
  1171. case GRADESCOES:
  1172. $grademethod = get_string('gradescoes', 'scorm');
  1173. break;
  1174. }
  1175. } else {
  1176. switch ($scorm->whatgrade) {
  1177. case HIGHESTATTEMPT:
  1178. $grademethod = get_string('highestattempt', 'scorm');
  1179. break;
  1180. case AVERAGEATTEMPT:
  1181. $grademethod = get_string('…

Large files files are truncated, but you can click here to view the full file