diff --git a/scripts/langindex.json b/scripts/langindex.json index c6dc699fe..9df725629 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -684,6 +684,7 @@ "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", "addon.mod_h5pactivity.outcome": "h5pactivity", + "addon.mod_h5pactivity.previewmode": "h5pactivity", "addon.mod_h5pactivity.result_fill-in": "h5pactivity", "addon.mod_h5pactivity.result_other": "h5pactivity", "addon.mod_h5pactivity.review_my_attempts": "h5pactivity", diff --git a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html index f2acd6230..da77d63c7 100644 --- a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html +++ b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -21,6 +21,11 @@ {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }} + + + {{ 'addon.mod_h5pactivity.previewmode' | translate }} + +

{{ stateMessage | translate }}

@@ -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.",