diff --git a/h5p/classes/player.php b/h5p/classes/player.php index 9a86b449676..3f664fa8714 100644 --- a/h5p/classes/player.php +++ b/h5p/classes/player.php @@ -27,6 +27,7 @@ namespace core_h5p; defined('MOODLE_INTERNAL') || die(); use core_h5p\local\library\autoloader; +use core_xapi\local\statement\item_activity; /** * H5P player class, for displaying any local H5P content. @@ -67,6 +68,11 @@ class player { */ private $content; + /** + * @var string optional component name to send xAPI statements. + */ + private $component; + /** * @var string Type of embed object, div or iframe. */ @@ -98,8 +104,9 @@ class player { * @param string $url Local URL of the H5P file to display. * @param stdClass $config Configuration for H5P buttons. * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions + * @param string $component optional moodle component to sent xAPI tracking */ - public function __construct(string $url, \stdClass $config, bool $preventredirect = true) { + public function __construct(string $url, \stdClass $config, bool $preventredirect = true, string $component = '') { if (empty($url)) { throw new \moodle_exception('h5pinvalidurl', 'core_h5p'); } @@ -110,6 +117,8 @@ class player { $this->messages = new \stdClass(); + $this->component = $component; + // Create \core_h5p\core instance. $this->core = $this->factory->get_core(); @@ -129,14 +138,17 @@ class player { * @param string $url Local URL of the H5P file to display. * @param stdClass $config Configuration for H5P buttons. * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions + * @param string $component optional moodle component to sent xAPI tracking * * @return string The embedable code to display a H5P file. */ - public static function display(string $url, \stdClass $config, bool $preventredirect = true): string { + public static function display(string $url, \stdClass $config, bool $preventredirect = true, + string $component = ''): string { global $OUTPUT; $params = [ 'url' => $url, 'preventredirect' => $preventredirect, + 'component' => $component, ]; $optparams = ['frame', 'export', 'embed', 'copyright']; @@ -193,6 +205,7 @@ class player { $contenturl = \moodle_url::make_pluginfile_url($systemcontext->id, \core_h5p\file_storage::COMPONENT, \core_h5p\file_storage::CONTENT_FILEAREA, $this->h5pid, null, null); $exporturl = $this->get_export_settings($displayoptions[ core::DISPLAY_OPTION_DOWNLOAD ]); + $xapiobject = item_activity::create_from_id($this->context->id); $contentsettings = [ 'library' => core::libraryToString($this->content['library']), 'fullScreen' => $this->content['library']['fullscreen'], @@ -202,7 +215,7 @@ class player { 'resizeCode' => self::get_resize_code(), 'title' => $this->content['slug'], 'displayOptions' => $displayoptions, - 'url' => self::get_embed_url($this->url->out())->out(), + 'url' => $xapiobject->get_data()->id, 'contentUrl' => $contenturl->out(), 'metadata' => $this->content['metadata'], 'contentUserData' => [0 => ['state' => '{}']] @@ -698,7 +711,7 @@ class player { * @return array The settings. */ private function get_core_settings(): array { - global $CFG; + global $CFG, $USER; $basepath = $CFG->wwwroot . '/'; $systemcontext = \context_system::instance(); @@ -717,7 +730,7 @@ class player { 'saveFreq' => false, 'siteUrl' => $CFG->wwwroot, 'l10n' => array('H5P' => $this->core->getLocalization()), - 'user' => [], + 'user' => ['name' => $USER->username, 'mail' => $USER->email], 'hubIsEnabled' => false, 'reportingIsEnabled' => false, 'crossorigin' => null, @@ -725,6 +738,7 @@ class player { 'pluginCacheBuster' => $this->get_cache_buster(), 'libraryUrl' => autoloader::get_h5p_core_library_url('js'), 'moodleLibraryPaths' => $this->core->get_dependency_roots($this->h5pid), + 'moodleComponent' => $this->component, ); return $settings; diff --git a/h5p/embed.php b/h5p/embed.php index d29ffc1cfd8..360143649b4 100644 --- a/h5p/embed.php +++ b/h5p/embed.php @@ -36,9 +36,11 @@ $config->copyright = optional_param('copyright', 0, PARAM_INT); $preventredirect = optional_param('preventredirect', true, PARAM_BOOL); +$component = optional_param('component', '', PARAM_COMPONENT); + $PAGE->set_url(new \moodle_url('/h5p/embed.php', array('url' => $url))); try { - $h5pplayer = new \core_h5p\player($url, $config, $preventredirect); + $h5pplayer = new \core_h5p\player($url, $config, $preventredirect, $component); $messages = $h5pplayer->get_messages(); } catch (\Exception $e) { @@ -92,4 +94,4 @@ if (empty($messages->error) && empty($messages->exception)) { echo $OUTPUT->render_from_template('core_h5p/h5perror', $messages); } -echo $OUTPUT->footer(); \ No newline at end of file +echo $OUTPUT->footer(); diff --git a/h5p/js/embed.js b/h5p/js/embed.js index 6135db2e534..87cb99663c9 100644 --- a/h5p/js/embed.js +++ b/h5p/js/embed.js @@ -71,6 +71,27 @@ H5PEmbedCommunicator = (function() { // Parent origin can be anything. window.parent.postMessage(data, '*'); }; + + /** + * Send a xAPI statement to LMS. + * + * @param {string} component + * @param {Object} statements + */ + self.post = function(component, statements) { + require(['core/ajax'], function(ajax) { + var data = { + component: component, + requestjson: JSON.stringify(statements) + }; + ajax.call([ + { + methodname: 'core_xapi_statement_post', + args: data + } + ]); + }); + }; } return (window.postMessage && window.addEventListener ? new Communicator() : undefined); @@ -150,6 +171,38 @@ document.onreadystatechange = function() { }, 0); }); + // Get emitted xAPI data. + H5P.externalDispatcher.on('xAPI', function(event) { + var moodlecomponent = H5P.getMoodleComponent(); + if (moodlecomponent == undefined) { + return; + } + // Skip malformed events. + var hasStatement = event && event.data && event.data.statement; + if (!hasStatement) { + return; + } + + var statement = event.data.statement; + var validVerb = statement.verb && statement.verb.id; + if (!validVerb) { + return; + } + + var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered' + || statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed'; + + var isChild = statement.context && statement.context.contextActivities && + statement.context.contextActivities.parent && + statement.context.contextActivities.parent[0] && + statement.context.contextActivities.parent[0].id; + + if (isCompleted && !isChild) { + var statements = H5P.getXAPIStatements(this.contentId, statement); + H5PEmbedCommunicator.post(moodlecomponent, statements); + } + }); + // Trigger initial resize for instance. H5P.trigger(instance, 'resize'); }; diff --git a/h5p/js/h5p_overrides.js b/h5p/js/h5p_overrides.js index 10679e0c61d..eea05b484a5 100644 --- a/h5p/js/h5p_overrides.js +++ b/h5p/js/h5p_overrides.js @@ -7,3 +7,43 @@ H5P.getLibraryPath = function (library) { } return H5P._getLibraryPath(library); }; +H5P.findInstanceFromId = function (contentId) { + if (!contentId) { + return H5P.instances[0]; + } + if (H5P.instances !== undefined) { + for (var i = 0; i < H5P.instances.length; i++) { + if (H5P.instances[i].contentId === contentId) { + return H5P.instances[i]; + } + } + } + return undefined; +}; +H5P.getXAPIStatements = function (contentId, statement) { + var statements = []; + var instance = H5P.findInstanceFromId(contentId); + if (!instance){ + return statements; + } + if (instance.getXAPIData == undefined) { + var xAPIData = { + statement: statement + }; + } else { + var xAPIData = instance.getXAPIData(); + } + if (xAPIData.statement != undefined) { + statements.push(xAPIData.statement); + } + if (xAPIData.children != undefined) { + statements = statements.concat(xAPIData.children.map(a => a.statement)); + } + return statements; +}; +H5P.getMoodleComponent = function () { + if (H5PIntegration.moodleComponent) { + return H5PIntegration.moodleComponent; + } + return undefined; +};