diff --git a/lib/db/services.php b/lib/db/services.php index 21af3df3874..3642c0742ae 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1246,6 +1246,7 @@ $services = array( 'mod_scorm_get_scorm_sco_tracks', 'mod_scorm_get_scorm_attempt_count', 'mod_scorm_get_scorms_by_courses', + 'mod_scorm_launch_sco', 'mod_survey_get_surveys_by_courses', 'mod_survey_view_survey', 'mod_survey_get_questions', diff --git a/mod/scorm/classes/external.php b/mod/scorm/classes/external.php index 990c15f7fa5..ab8e166643f 100644 --- a/mod/scorm/classes/external.php +++ b/mod/scorm/classes/external.php @@ -829,4 +829,77 @@ class mod_scorm_external extends external_api { ) ); } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 3.1 + */ + public static function launch_sco_parameters() { + return new external_function_parameters( + array( + 'scormid' => new external_value(PARAM_INT, 'SCORM instance id'), + 'scoid' => new external_value(PARAM_INT, 'SCO id (empty for launching the first SCO)', VALUE_DEFAULT, 0) + ) + ); + } + + /** + * Trigger the course module viewed event. + * + * @param int $scormid the SCORM instance id + * @param int $scoid the SCO id + * @return array of warnings and status result + * @since Moodle 3.1 + * @throws moodle_exception + */ + public static function launch_sco($scormid, $scoid = 0) { + global $DB; + + $params = self::validate_parameters(self::launch_sco_parameters(), + array( + 'scormid' => $scormid, + 'scoid' => $scoid + )); + $warnings = array(); + + // Request and permission validation. + $scorm = $DB->get_record('scorm', array('id' => $params['scormid']), '*', MUST_EXIST); + list($course, $cm) = get_course_and_cm_from_instance($scorm, 'scorm'); + + $context = context_module::instance($cm->id); + self::validate_context($context); + + // If the SCORM is not open this function will throw exceptions. + scorm_require_available($scorm); + + if (!empty($params['scoid']) and !($sco = scorm_get_sco($params['scoid'], SCO_ONLY))) { + throw new moodle_exception('cannotfindsco', 'scorm'); + } + + list($sco, $scolaunchurl) = scorm_get_sco_and_launch_url($scorm, $params['scoid'], $context); + // Trigger the SCO launched event. + scorm_launch_sco($scorm, $sco, $cm, $context, $scolaunchurl); + + $result = array(); + $result['status'] = true; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 3.1 + */ + public static function launch_sco_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'warnings' => new external_warnings() + ) + ); + } } diff --git a/mod/scorm/db/services.php b/mod/scorm/db/services.php index 4042a0c2c97..ac85f701112 100644 --- a/mod/scorm/db/services.php +++ b/mod/scorm/db/services.php @@ -84,5 +84,13 @@ $functions = array( no courses are provided then all the scorm instances the user has access to will be returned.', 'type' => 'read', 'capabilities' => '' - ) + ), + + 'mod_scorm_launch_sco' => array( + 'classname' => 'mod_scorm_external', + 'methodname' => 'launch_sco', + 'description' => 'Trigger the SCO launched event.', + 'type' => 'write', + 'capabilities' => '' + ), ); diff --git a/mod/scorm/loadSCO.php b/mod/scorm/loadSCO.php index 3641f47cc66..07cc0b28db1 100644 --- a/mod/scorm/loadSCO.php +++ b/mod/scorm/loadSCO.php @@ -65,104 +65,24 @@ scorm_require_available($scorm); $context = context_module::instance($cm->id); -if (!empty($scoid)) { - // Direct SCO request. - if ($sco = scorm_get_sco($scoid)) { - if ($sco->launch == '') { - // Search for the next launchable sco. - if ($scoes = $DB->get_records_select( - 'scorm_scoes', - 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).' AND id > ?', - array($scorm->id, $sco->id), - 'sortorder, id')) { - $sco = current($scoes); - } - } - } -} - -// If no sco was found get the first of SCORM package. -if (!isset($sco)) { - $scoes = $DB->get_records_select( - 'scorm_scoes', - 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), - array($scorm->id), - 'sortorder, id' - ); - $sco = current($scoes); -} +// Forge SCO URL. +list($sco, $scolaunchurl) = scorm_get_sco_and_launch_url($scorm, $scoid, $context); if ($sco->scormtype == 'asset') { $attempt = scorm_get_last_attempt($scorm->id, $USER->id); $element = (scorm_version_check($scorm->version, SCORM_13)) ? 'cmi.completion_status' : 'cmi.core.lesson_status'; $value = 'completed'; - $result = scorm_insert_track($USER->id, $scorm->id, $sco->id, $attempt, $element, $value); + scorm_insert_track($USER->id, $scorm->id, $sco->id, $attempt, $element, $value); } -// Forge SCO URL. -$connector = ''; -$version = substr($scorm->version, 0, 4); -if ((isset($sco->parameters) && (!empty($sco->parameters))) || ($version == 'AICC')) { - if (stripos($sco->launch, '?') !== false) { - $connector = '&'; - } else { - $connector = '?'; - } - if ((isset($sco->parameters) && (!empty($sco->parameters))) && ($sco->parameters[0] == '?')) { - $sco->parameters = substr($sco->parameters, 1); - } -} - -if ($version == 'AICC') { - require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php"); - $aiccsid = scorm_aicc_get_hacp_session($scorm->id); - if (empty($aiccsid)) { - $aiccsid = sesskey(); - } - $scoparams = ''; - if (isset($sco->parameters) && (!empty($sco->parameters))) { - $scoparams = '&'. $sco->parameters; - } - $launcher = $sco->launch.$connector.'aicc_sid='.$aiccsid.'&aicc_url='.$CFG->wwwroot.'/mod/scorm/aicc.php'.$scoparams; -} else { - if (isset($sco->parameters) && (!empty($sco->parameters))) { - $launcher = $sco->launch.$connector.$sco->parameters; - } else { - $launcher = $sco->launch; - } -} - -if (scorm_external_link($sco->launch)) { - // TODO: does this happen? - $result = $launcher; -} else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) { - // Remote learning activity. - $result = dirname($scorm->reference).'/'.$launcher; -} else if ($scorm->scormtype === SCORM_TYPE_LOCAL && strtolower($scorm->reference) == 'imsmanifest.xml') { - // This SCORM content sits in a repository that allows relative links. - $result = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher"; -} else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) { - // Note: do not convert this to use get_file_url() or moodle_url() - // SCORM does not work without slasharguments and moodle_url() encodes querystring vars. - $result = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/content/$scorm->revision/$launcher"; -} - -// Trigger a Sco launched event. -$event = \mod_scorm\event\sco_launched::create(array( - 'objectid' => $sco->id, - 'context' => $context, - 'other' => array('instanceid' => $scorm->id, 'loadedcontent' => $result) -)); -$event->add_record_snapshot('course_modules', $cm); -$event->add_record_snapshot('scorm', $scorm); -$event->add_record_snapshot('scorm_scoes', $sco); -$event->trigger(); +// Trigger the SCO launched event. +scorm_launch_sco($scorm, $sco, $cm, $context, $scolaunchurl); header('Content-Type: text/html; charset=UTF-8'); if ($sco->scormtype == 'asset') { // HTTP 302 Found => Moved Temporarily. - header('Location: ' . $result); + header('Location: ' . $scolaunchurl); // Provide a short feedback in case of slow network connection. echo html_writer::start_tag('html'); echo html_writer::tag('body', html_writer::tag('p', get_string('activitypleasewait', 'scorm'))); @@ -216,7 +136,7 @@ echo html_writer::tag('title', 'LoadSCO'); function doredirect() { if (myGetAPIHandle() != null) { - location = ""; + location = ""; } else { document.body.innerHTML = "
" + @@ -231,7 +151,7 @@ echo html_writer::tag('title', 'LoadSCO'); } else { clearInterval(timer); document.body.innerHTML = "
"; - location = ""; + location = ""; } }, 1000); } @@ -239,7 +159,7 @@ echo html_writer::tag('title', 'LoadSCO'); //]]> launch == '') { + // Search for the next launchable sco. + if ($scoes = $DB->get_records_select( + 'scorm_scoes', + 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true).' AND id > ?', + array($scorm->id, $sco->id), + 'sortorder, id')) { + $sco = current($scoes); + } + } + } + } + + // If no sco was found get the first of SCORM package. + if (!isset($sco)) { + $scoes = $DB->get_records_select( + 'scorm_scoes', + 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), + array($scorm->id), + 'sortorder, id' + ); + $sco = current($scoes); + } + + $connector = ''; + $version = substr($scorm->version, 0, 4); + if ((isset($sco->parameters) && (!empty($sco->parameters))) || ($version == 'AICC')) { + if (stripos($sco->launch, '?') !== false) { + $connector = '&'; + } else { + $connector = '?'; + } + if ((isset($sco->parameters) && (!empty($sco->parameters))) && ($sco->parameters[0] == '?')) { + $sco->parameters = substr($sco->parameters, 1); + } + } + + if ($version == 'AICC') { + require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php"); + $aiccsid = scorm_aicc_get_hacp_session($scorm->id); + if (empty($aiccsid)) { + $aiccsid = sesskey(); + } + $scoparams = ''; + if (isset($sco->parameters) && (!empty($sco->parameters))) { + $scoparams = '&'. $sco->parameters; + } + $launcher = $sco->launch.$connector.'aicc_sid='.$aiccsid.'&aicc_url='.$CFG->wwwroot.'/mod/scorm/aicc.php'.$scoparams; + } else { + if (isset($sco->parameters) && (!empty($sco->parameters))) { + $launcher = $sco->launch.$connector.$sco->parameters; + } else { + $launcher = $sco->launch; + } + } + + if (scorm_external_link($sco->launch)) { + // TODO: does this happen? + $scolaunchurl = $launcher; + } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) { + // Remote learning activity. + $scolaunchurl = dirname($scorm->reference).'/'.$launcher; + } else if ($scorm->scormtype === SCORM_TYPE_LOCAL && strtolower($scorm->reference) == 'imsmanifest.xml') { + // This SCORM content sits in a repository that allows relative links. + $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher"; + } else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) { + // Note: do not convert this to use get_file_url() or moodle_url() + // SCORM does not work without slasharguments and moodle_url() encodes querystring vars. + $scolaunchurl = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/content/$scorm->revision/$launcher"; + } + return array($sco, $scolaunchurl); +} + +/** + * Trigger the scorm_launched event. + * + * @param stdClass $scorm scorm object + * @param stdClass $sco sco object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @param string $scourl SCO URL + * @since Moodle 3.1 + */ +function scorm_launch_sco($scorm, $sco, $cm, $context, $scourl) { + + $event = \mod_scorm\event\sco_launched::create(array( + 'objectid' => $sco->id, + 'context' => $context, + 'other' => array('instanceid' => $scorm->id, 'loadedcontent' => $scourl) + )); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('scorm', $scorm); + $event->add_record_snapshot('scorm_scoes', $sco); + $event->trigger(); +} diff --git a/mod/scorm/tests/externallib_test.php b/mod/scorm/tests/externallib_test.php index 39dc0fdca54..63a97d042fa 100644 --- a/mod/scorm/tests/externallib_test.php +++ b/mod/scorm/tests/externallib_test.php @@ -795,4 +795,66 @@ class mod_scorm_external_testcase extends externallib_advanced_testcase { $result = external_api::clean_returnvalue($returndescription, $result); $this->assertEquals($expectedscorms, $result['scorms']); } + + /** + * Test launch_sco + */ + public function test_launch_sco() { + global $DB; + + // Test invalid instance id. + try { + mod_scorm_external::launch_sco(0); + $this->fail('Exception expected due to invalid mod_scorm instance id.'); + } catch (moodle_exception $e) { + $this->assertEquals('invalidrecord', $e->errorcode); + } + + // Test not-enrolled user. + $user = self::getDataGenerator()->create_user(); + $this->setUser($user); + try { + mod_scorm_external::launch_sco($this->scorm->id); + $this->fail('Exception expected due to not enrolled user.'); + } catch (moodle_exception $e) { + $this->assertEquals('requireloginerror', $e->errorcode); + } + + // Test user with full capabilities. + $this->setUser($this->student); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + + $scoes = scorm_get_scoes($this->scorm->id); + foreach ($scoes as $sco) { + // Find launchable SCO. + if ($sco->launch != '') { + break; + } + } + + $result = mod_scorm_external::launch_sco($this->scorm->id, $sco->id); + $result = external_api::clean_returnvalue(mod_scorm_external::launch_sco_returns(), $result); + + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = array_shift($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_scorm\event\sco_launched', $event); + $this->assertEquals($this->context, $event->get_context()); + $moodleurl = new \moodle_url('/mod/scorm/player.php', array('id' => $this->cm->id, 'scoid' => $sco->id)); + $this->assertEquals($moodleurl, $event->get_url()); + $this->assertEventContextNotUsed($event); + $this->assertNotEmpty($event->get_name()); + + // Invalid SCO. + try { + mod_scorm_external::launch_sco($this->scorm->id, 1); + $this->fail('Exception expected due to not enrolled user.'); + } catch (moodle_exception $e) { + $this->assertEquals('cannotfindsco', $e->errorcode); + } + } } diff --git a/mod/scorm/version.php b/mod/scorm/version.php index 22ee5e62e70..517576053ef 100644 --- a/mod/scorm/version.php +++ b/mod/scorm/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015112700; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2015112701; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2015111000; // Requires this Moodle version. $plugin->component = 'mod_scorm'; // Full name of the plugin (used for diagnostics). $plugin->cron = 300; diff --git a/version.php b/version.php index 1017b141dec..b657d95e2ba 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2015122300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2015122300.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.