@@ -39,5 +44,5 @@
-
+
diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts
index bf879ebe0..36167eb04 100644
--- a/src/addon/mod/h5pactivity/components/index/index.ts
+++ b/src/addon/mod/h5pactivity/components/index/index.ts
@@ -24,6 +24,7 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main
import { CoreH5P } from '@core/h5p/providers/h5p';
import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
import { CoreH5PHelper } from '@core/h5p/classes/helper';
+import { CoreXAPI } from '@core/xapi/providers/xapi';
import { CoreConstants } from '@core/constants';
import { CoreSite } from '@classes/site';
@@ -57,10 +58,12 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
fileUrl: string; // The fileUrl to use to play the package.
state: string; // State of the file.
siteCanDownload: boolean;
+ trackComponent: string; // Component for tracking.
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
protected site: CoreSite;
protected observer;
+ protected messageListenerFunction: (event: MessageEvent) => Promise;
constructor(injector: Injector,
@Optional() protected content: Content) {
@@ -68,6 +71,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
this.site = this.sitesProvider.getCurrentSite();
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
+
+ // Listen for messages from the iframe.
+ this.messageListenerFunction = this.onIframeMessage.bind(this);
+ window.addEventListener('message', this.messageListenerFunction);
}
/**
@@ -102,17 +109,19 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
this.description = this.h5pActivity.intro;
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
- if (this.h5pActivity.package && this.h5pActivity.package[0]) {
- // The online player should use the original file, not the trusted one.
- this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
- this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions);
- }
-
await Promise.all([
this.fetchAccessInfo(),
this.fetchDeployedFileData(),
]);
+ this.trackComponent = this.accessInfo.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : '';
+
+ if (this.h5pActivity.package && this.h5pActivity.package[0]) {
+ // The online player should use the original file, not the trusted one.
+ this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
+ this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions, this.trackComponent);
+ }
+
if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) {
// Cannot download the file or already downloaded, play the package directly.
this.play();
@@ -326,10 +335,56 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id});
}
+ /**
+ * Treat an iframe message event.
+ *
+ * @param event Event.
+ * @return Promise resolved when done.
+ */
+ protected async onIframeMessage(event: MessageEvent): Promise {
+ if (!event.data || !CoreXAPI.instance.canPostStatementInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
+ return;
+ }
+
+ try {
+ await CoreXAPI.instance.postStatement(event.data.component, JSON.stringify(event.data.statements));
+ } catch (error) {
+ CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.');
+ }
+ }
+
+ /**
+ * Check if an event is an XAPI post statement of the current activity.
+ *
+ * @param data Event data.
+ * @return Whether it's an XAPI post statement of the current activity.
+ */
+ protected isCurrentXAPIPost(data: any): boolean {
+ if (data.context != 'moodleapp' || data.action != 'xapi_post_statement' || !data.statements) {
+ return false;
+ }
+
+ // Check the event belongs to this activity.
+ const trackingUrl = data.statements[0] && data.statements[0].object && data.statements[0].object.id;
+ if (!trackingUrl) {
+ return false;
+ }
+
+ if (!this.site.containsUrl(trackingUrl)) {
+ // The event belongs to another site, weird scenario. Maybe some JS running in background.
+ return false;
+ }
+
+ const match = trackingUrl.match(/xapi\/activity\/(\d+)/);
+
+ return match && match[1] == this.h5pActivity.context;
+ }
+
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.observer && this.observer.off();
+ window.removeEventListener('message', this.messageListenerFunction);
}
}
diff --git a/src/addon/mod/h5pactivity/lang/en.json b/src/addon/mod/h5pactivity/lang/en.json
index 21bb9eecd..af3034dda 100644
--- a/src/addon/mod/h5pactivity/lang/en.json
+++ b/src/addon/mod/h5pactivity/lang/en.json
@@ -24,6 +24,7 @@
"no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
"offlinedisabledwarning": "You will need to be online to view the H5P package.",
"outcome": "Outcome",
+ "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
"result_fill-in": "Fill-in text",
"result_other": "Unkown interaction type",
"review_my_attempts": "View my attempts",
diff --git a/src/addon/mod/h5pactivity/pages/index/index.ts b/src/addon/mod/h5pactivity/pages/index/index.ts
index 7435400a6..51a21a82f 100644
--- a/src/addon/mod/h5pactivity/pages/index/index.ts
+++ b/src/addon/mod/h5pactivity/pages/index/index.ts
@@ -14,9 +14,12 @@
import { Component, ViewChild } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
+import { CoreDomUtils } from '@providers/utils/dom';
import { AddonModH5PActivityIndexComponent } from '../../components/index/index';
import { AddonModH5PActivityData } from '../../providers/h5pactivity';
+import { Translate } from '@singletons/core.singletons';
+
/**
* Page that displays an H5P activity.
*/
@@ -46,4 +49,17 @@ export class AddonModH5PActivityIndexPage {
updateData(h5p: AddonModH5PActivityData): void {
this.title = h5p.name || this.title;
}
+
+ /**
+ * Check if we can leave the page or not.
+ *
+ * @return Resolved if we can leave it, rejected if not.
+ */
+ ionViewCanLeave(): Promise {
+ if (!this.h5pComponent.playing) {
+ return;
+ }
+
+ return CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmleaveunknownchanges'));
+ }
}
diff --git a/src/addon/mod/h5pactivity/providers/h5pactivity.ts b/src/addon/mod/h5pactivity/providers/h5pactivity.ts
index dd9ff494d..ef7ccab9e 100644
--- a/src/addon/mod/h5pactivity/providers/h5pactivity.ts
+++ b/src/addon/mod/h5pactivity/providers/h5pactivity.ts
@@ -31,6 +31,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
@Injectable()
export class AddonModH5PActivityProvider {
static COMPONENT = 'mmaModH5PActivity';
+ static TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
protected ROOT_CACHE_KEY = 'mmaModH5PActivity:';
@@ -595,6 +596,7 @@ export type AddonModH5PActivityData = {
grademethod: number; // Which H5P attempt is used for grading.
contenthash?: string; // Sha1 hash of file content.
coursemodule: number; // Coursemodule.
+ context: number; // Context ID.
introfiles: CoreWSExternalFile[];
package: CoreWSExternalFile[];
deployedfile?: {
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index d78d45edb..d5080dccb 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -88,6 +88,7 @@ import { CoreFilterModule } from '@core/filter/filter.module';
import { CoreH5PModule } from '@core/h5p/h5p.module';
import { CoreSearchModule } from '@core/search/search.module';
import { CoreEditorModule } from '@core/editor/editor.module';
+import { CoreXAPIModule } from '@core/xapi/xapi.module';
// Addon modules.
import { AddonBadgesModule } from '@addon/badges/badges.module';
@@ -241,6 +242,7 @@ export const WP_PROVIDER: any = null;
CoreH5PModule,
CoreSearchModule,
CoreEditorModule,
+ CoreXAPIModule,
AddonBadgesModule,
AddonBlogModule,
AddonCalendarModule,
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 38dddb5cb..4682e88d7 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -684,6 +684,7 @@
"addon.mod_h5pactivity.no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
"addon.mod_h5pactivity.offlinedisabledwarning": "You will need to be online to view the H5P package.",
"addon.mod_h5pactivity.outcome": "Outcome",
+ "addon.mod_h5pactivity.previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
"addon.mod_h5pactivity.result_fill-in": "Fill-in text",
"addon.mod_h5pactivity.result_other": "Unkown interaction type",
"addon.mod_h5pactivity.review_my_attempts": "View my attempts",
@@ -1407,6 +1408,7 @@
"core.confirmdeletefile": "Are you sure you want to delete this file?",
"core.confirmgotabroot": "Are you sure you want to go back to {{name}}?",
"core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
+ "core.confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have some unsaved changes they will be lost.",
"core.confirmloss": "Are you sure? All changes will be lost.",
"core.confirmopeninbrowser": "Do you want to open it in a web browser?",
"core.considereddigitalminor": "You are too young to create an account on this site.",
diff --git a/src/core/h5p/assets/moodle/js/displayoptions.js b/src/core/h5p/assets/moodle/js/displayoptions.js
deleted file mode 100644
index 59088886d..000000000
--- a/src/core/h5p/assets/moodle/js/displayoptions.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// (C) Copyright 2015 Moodle Pty Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * Handle display options included in the URL and put them in the H5PIntegration object if it exists.
- */
-
-if (window.H5PIntegration && window.H5PIntegration.contents && location.search) {
- var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]];
-
- if (contentData) {
- contentData.displayOptions = contentData.displayOptions || {};
-
- var search = location.search.replace(/^\?/, ''),
- split = search.split('&');
-
- split.forEach(function(param) {
- var nameAndValue = param.split('=');
- if (nameAndValue.length == 2) {
- contentData.displayOptions[nameAndValue[0]] = nameAndValue[1] === '1' || nameAndValue[1] === 'true';
- }
- });
- }
-}
diff --git a/src/core/h5p/assets/moodle/js/embed.js b/src/core/h5p/assets/moodle/js/embed.js
index 6135db2e5..8df9b26a3 100644
--- a/src/core/h5p/assets/moodle/js/embed.js
+++ b/src/core/h5p/assets/moodle/js/embed.js
@@ -71,6 +71,21 @@ 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) {
+ window.parent.postMessage({
+ context: 'moodleapp',
+ action: 'xapi_post_statement',
+ component: component,
+ statements: statements,
+ }, '*');
+ };
}
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
@@ -150,6 +165,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/src/core/h5p/assets/moodle/js/h5p_overrides.js b/src/core/h5p/assets/moodle/js/h5p_overrides.js
new file mode 100644
index 000000000..c9350e204
--- /dev/null
+++ b/src/core/h5p/assets/moodle/js/h5p_overrides.js
@@ -0,0 +1,55 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+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;
+};
\ No newline at end of file
diff --git a/src/core/h5p/assets/moodle/js/params.js b/src/core/h5p/assets/moodle/js/params.js
new file mode 100644
index 000000000..87722aacd
--- /dev/null
+++ b/src/core/h5p/assets/moodle/js/params.js
@@ -0,0 +1,46 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * Handle params included in the URL and put them in the H5PIntegration object if it exists.
+ */
+
+if (window.H5PIntegration && window.H5PIntegration.contents && location.search) {
+ var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]];
+
+ var search = location.search.replace(/^\?/, '');
+ var split = search.split('&');
+
+ split.forEach(function(param) {
+ var nameAndValue = param.split('=');
+
+ if (nameAndValue[0] == 'displayOptions' && contentData) {
+ try {
+ contentData.displayOptions = contentData.displayOptions || {};
+
+ var displayOptions = JSON.parse(decodeURIComponent(nameAndValue[1]));
+
+ if (displayOptions && typeof displayOptions == 'object') {
+ Object.assign(contentData.displayOptions, displayOptions);
+ }
+ } catch (error) {
+ console.error('Error parsing display options', decodeURIComponent(nameAndValue[1]));
+ }
+ } else if (nameAndValue[0] == 'component') {
+ window.H5PIntegration.moodleComponent = nameAndValue[1];
+ } else if (nameAndValue[0] == 'trackingUrl' && contentData) {
+ contentData.url = nameAndValue[1];
+ }
+ });
+}
diff --git a/src/core/h5p/classes/core.ts b/src/core/h5p/classes/core.ts
index 20984c8b2..7e65b67c7 100644
--- a/src/core/h5p/classes/core.ts
+++ b/src/core/h5p/classes/core.ts
@@ -615,6 +615,8 @@ export class CoreH5PCore {
urls.push(libUrl + script);
});
+ urls.push(CoreTextUtils.instance.concatenatePaths(libUrl, 'moodle/js/h5p_overrides.js'));
+
return urls;
}
diff --git a/src/core/h5p/classes/helper.ts b/src/core/h5p/classes/helper.ts
index 1d434308c..30f3807fd 100644
--- a/src/core/h5p/classes/helper.ts
+++ b/src/core/h5p/classes/helper.ts
@@ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites';
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreUtils } from '@providers/utils/utils';
+import { CoreUser } from '@core/user/providers/user';
import { CoreH5P } from '../providers/h5p';
import { CoreH5PCore, CoreH5PDisplayOptions } from './core';
import { FileEntry } from '@ionic-native/file';
@@ -90,6 +91,13 @@ export class CoreH5PHelper {
static async getCoreSettings(siteId?: string): Promise {
const site = await CoreSites.instance.getSite(siteId);
+ let user;
+
+ try {
+ user = await CoreUser.instance.getProfile(site.getUserId(), undefined, true);
+ } catch (error) {
+ // Ignore errors.
+ }
const basePath = CoreFile.instance.getBasePathInstant();
const ajaxPaths = {
@@ -110,7 +118,7 @@ export class CoreH5PHelper {
l10n: {
H5P: CoreH5P.instance.h5pCore.getLocalization(),
},
- user: [],
+ user: {name: site.getInfo().fullname, mail: user && user.email},
hubIsEnabled: false,
reportingIsEnabled: false,
crossorigin: null,
diff --git a/src/core/h5p/classes/player.ts b/src/core/h5p/classes/player.ts
index cefa413b6..adede11e4 100644
--- a/src/core/h5p/classes/player.ts
+++ b/src/core/h5p/classes/player.ts
@@ -17,6 +17,7 @@ import { CoreSites } from '@providers/sites';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreUrlUtils } from '@providers/utils/url';
import { CoreUtils } from '@providers/utils/utils';
+import { CoreXAPI } from '@core/xapi/providers/xapi';
import { CoreH5P } from '../providers/h5p';
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core';
import { CoreH5PHelper } from './helper';
@@ -81,7 +82,7 @@ export class CoreH5PPlayer {
resizeCode: this.getResizeCode(),
title: content.slug,
displayOptions: {},
- url: this.getEmbedUrl(site.getURL(), h5pUrl),
+ url: '', // It will be filled using dynamic params if needed.
contentUrl: contentUrl,
metadata: content.metadata,
contentUserData: [
@@ -109,9 +110,9 @@ export class CoreH5PPlayer {
html += '';
- // Add our own script to handle the display options.
+ // Add our own script to handle the params.
html += '';
+ this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/params.js') + '">';
html += '';
@@ -241,20 +242,34 @@ export class CoreH5PPlayer {
*
* @param fileUrl URL of the H5P package.
* @param displayOptions Display options.
+ * @param component Component to send xAPI events to.
+ * @param contextId Context ID where the H5P is. Required for tracking.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the file URL if exists, rejected otherwise.
*/
- async getContentIndexFileUrl(fileUrl: string, displayOptions?: CoreH5PDisplayOptions, siteId?: string): Promise {
+ async getContentIndexFileUrl(fileUrl: string, displayOptions?: CoreH5PDisplayOptions, component?: string, contextId?: number,
+ siteId?: string): Promise {
+
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId);
- // Add display options to the URL.
+ // Add display options and component to the URL.
const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId);
displayOptions = this.h5pCore.fixDisplayOptions(displayOptions, data.id);
- return CoreUrlUtils.instance.addParamsToUrl(path, displayOptions, undefined, true);
+ const params = {
+ displayOptions: JSON.stringify(displayOptions),
+ component: component || '',
+ trackingUrl: undefined,
+ };
+
+ if (contextId) {
+ params.trackingUrl = await CoreXAPI.instance.getUrl(contextId, 'activity', siteId);
+ }
+
+ return CoreUrlUtils.instance.addParamsToUrl(path, params);
}
/**
diff --git a/src/core/h5p/components/h5p-iframe/h5p-iframe.ts b/src/core/h5p/components/h5p-iframe/h5p-iframe.ts
index 8d9a74ca2..3f9c4b3c6 100644
--- a/src/core/h5p/components/h5p-iframe/h5p-iframe.ts
+++ b/src/core/h5p/components/h5p-iframe/h5p-iframe.ts
@@ -38,6 +38,8 @@ export class CoreH5PIframeComponent implements OnChanges {
@Input() fileUrl?: string; // The URL of the H5P file. If not supplied, onlinePlayerUrl is required.
@Input() displayOptions?: CoreH5PDisplayOptions; // Display options.
@Input() onlinePlayerUrl?: string; // The URL of the online player to display the H5P package.
+ @Input() trackComponent?: string; // Component to send xAPI events to.
+ @Input() contextId?: number; // Context ID. Required for tracking.
@Output() onIframeUrlSet = new EventEmitter<{src: string, online: boolean}>();
@Output() onIframeLoaded = new EventEmitter();
@@ -93,7 +95,7 @@ export class CoreH5PIframeComponent implements OnChanges {
this.iframeSrc = localUrl;
} else {
this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
- this.site.getURL(), this.fileUrl, this.displayOptions);
+ this.site.getURL(), this.fileUrl, this.displayOptions, this.trackComponent);
// Never allow downloading in the app. This will only work if the user is allowed to change the params.
const src = this.onlinePlayerUrl.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1',
@@ -121,7 +123,8 @@ export class CoreH5PIframeComponent implements OnChanges {
*/
protected async getLocalUrl(): Promise {
try {
- const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions, this.siteId);
+ const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions,
+ this.trackComponent, this.contextId, this.siteId);
return url;
} catch (error) {
@@ -135,7 +138,7 @@ export class CoreH5PIframeComponent implements OnChanges {
// File treated. Try to get the index file URL again.
const url = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.fileUrl, this.displayOptions,
- this.siteId);
+ this.trackComponent, this.contextId, this.siteId);
return url;
} catch (error) {
diff --git a/src/core/h5p/providers/h5p.ts b/src/core/h5p/providers/h5p.ts
index dc62ec2dd..cdccc72be 100644
--- a/src/core/h5p/providers/h5p.ts
+++ b/src/core/h5p/providers/h5p.ts
@@ -391,7 +391,7 @@ export class CoreH5PProvider {
* @param siteId Site ID (empty for current site).
* @return Promise resolved when the data is invalidated.
*/
- async invalidateAvailableInContexts(url: string, siteId?: string): Promise {
+ async invalidateGetTrustedH5PFile(url: string, siteId?: string): Promise {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url));
diff --git a/src/core/xapi/providers/xapi.ts b/src/core/xapi/providers/xapi.ts
new file mode 100644
index 000000000..e9cccc985
--- /dev/null
+++ b/src/core/xapi/providers/xapi.ts
@@ -0,0 +1,91 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreSites } from '@providers/sites';
+import { CoreTextUtils } from '@providers/utils/text';
+import { CoreSite } from '@classes/site';
+
+import { makeSingleton } from '@singletons/core.singletons';
+
+/**
+ * Service to provide XAPI functionalities.
+ */
+@Injectable()
+export class CoreXAPIProvider {
+
+ protected ROOT_CACHE_KEY = 'CoreXAPI:';
+
+ /**
+ * Returns whether or not WS to post XAPI statement is available.
+ *
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved with true if ws is available, false otherwise.
+ * @since 3.9
+ */
+ async canPostStatement(siteId?: string): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ return this.canPostStatementInSite(site);
+ }
+
+ /**
+ * Returns whether or not WS to post XAPI statement is available in a certain site.
+ *
+ * @param site Site. If not defined, current site.
+ * @return Promise resolved with true if ws is available, false otherwise.
+ * @since 3.9
+ */
+ canPostStatementInSite(site?: CoreSite): boolean {
+ site = site || CoreSites.instance.getCurrentSite();
+
+ return site.wsAvailable('core_xapi_statement_post');
+ }
+
+ /**
+ * Get URL for XAPI events.
+ *
+ * @param contextId Context ID.
+ * @param type Type (e.g. 'activity').
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when done.
+ */
+ async getUrl(contextId: number, type: string, siteId?: string): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ return CoreTextUtils.instance.concatenatePaths(site.getURL(), `xapi/${type}/${contextId}`);
+ }
+
+ /**
+ * Post an statement.
+ *
+ * @param component Component.
+ * @param json JSON string to send.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when done.
+ */
+ async postStatement(component: string, json: string, siteId?: string): Promise {
+
+ const site = await CoreSites.instance.getSite(siteId);
+
+ const data = {
+ component: component,
+ requestjson: json,
+ };
+
+ return site.write('core_xapi_statement_post', data);
+ }
+}
+
+export class CoreXAPI extends makeSingleton(CoreXAPIProvider) {}
diff --git a/src/core/xapi/xapi.module.ts b/src/core/xapi/xapi.module.ts
new file mode 100644
index 000000000..0d3a9ee2f
--- /dev/null
+++ b/src/core/xapi/xapi.module.ts
@@ -0,0 +1,31 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CoreXAPIProvider } from './providers/xapi';
+
+// List of providers (without handlers).
+export const CORE_XAPI_PROVIDERS: any[] = [
+ CoreXAPIProvider,
+];
+
+@NgModule({
+ declarations: [],
+ imports: [],
+ providers: [
+ CoreXAPIProvider,
+ ],
+ exports: []
+})
+export class CoreXAPIModule { }
diff --git a/src/lang/en.json b/src/lang/en.json
index 929f5f8aa..d925a1bea 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -43,6 +43,7 @@
"confirmdeletefile": "Are you sure you want to delete this file?",
"confirmgotabroot": "Are you sure you want to go back to {{name}}?",
"confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
+ "confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have some unsaved changes they will be lost.",
"confirmloss": "Are you sure? All changes will be lost.",
"confirmopeninbrowser": "Do you want to open it in a web browser?",
"considereddigitalminor": "You are too young to create an account on this site.",