Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
509 changes: 227 additions & 282 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
"cordova-plugin-file-transfer": "1.7.1",
"cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff",
"cordova-plugin-globalization": "1.11.0",
"cordova-plugin-inappbrowser": "3.2.0",
"cordova-plugin-inappbrowser": "git+https://github.com/apache/cordova-plugin-inappbrowser.git#d2b512ed048ce9345e61901b29ba7dd4271a73bf",
"cordova-plugin-ionic-keyboard": "2.1.3",
"cordova-plugin-ionic-webview": "4.1.3",
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
Expand Down
15 changes: 2 additions & 13 deletions src/addon/mod/lti/components/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Component, Optional, Injector } from '@angular/core';
import { Content } from 'ionic-angular';
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
import { AddonModLtiProvider, AddonModLtiLti } from '../../providers/lti';
import { AddonModLtiHelper } from '../../providers/helper';

/**
* Component that displays an LTI entry page.
Expand Down Expand Up @@ -92,18 +93,6 @@ export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityCompo
* Launch the LTI.
*/
launch(): void {
this.ltiProvider.getLtiLaunchData(this.lti.id).then((launchData) => {
// "View" LTI.
this.ltiProvider.logView(this.lti.id, this.lti.name).then(() => {
this.checkCompletion();
}).catch((error) => {
// Ignore errors.
});

// Launch LTI.
return this.ltiProvider.launch(launchData.endpoint, launchData.parameters);
}).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'core.error', true);
});
AddonModLtiHelper.instance.getDataAndLaunch(this.courseId, this.module, this.lti);
}
}
7 changes: 5 additions & 2 deletions src/addon/mod/lti/lti.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
import { AddonModLtiComponentsModule } from './components/components.module';
import { AddonModLtiModuleHandler } from './providers/module-handler';
import { AddonModLtiProvider } from './providers/lti';
import { AddonModLtiHelperProvider } from './providers/helper';
import { AddonModLtiLinkHandler } from './providers/link-handler';
import { AddonModLtiListLinkHandler } from './providers/list-link-handler';
import { AddonModLtiPrefetchHandler } from './providers/prefetch-handler';
Expand All @@ -25,7 +26,8 @@ import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-

// List of providers (without handlers).
export const ADDON_MOD_LTI_PROVIDERS: any[] = [
AddonModLtiProvider
AddonModLtiProvider,
AddonModLtiHelperProvider,
];

@NgModule({
Expand All @@ -36,10 +38,11 @@ export const ADDON_MOD_LTI_PROVIDERS: any[] = [
],
providers: [
AddonModLtiProvider,
AddonModLtiHelperProvider,
AddonModLtiModuleHandler,
AddonModLtiLinkHandler,
AddonModLtiListLinkHandler,
AddonModLtiPrefetchHandler
AddonModLtiPrefetchHandler,
]
})
export class AddonModLtiModule {
Expand Down
119 changes: 119 additions & 0 deletions src/addon/mod/lti/providers/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// (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 { Platform } from 'ionic-angular';
import { CoreEvents, CoreEventsProvider } from '@providers/events';
import { CoreSites } from '@providers/sites';
import { CoreDomUtils } from '@providers/utils/dom';
import { CoreCourse } from '@core/course/providers/course';
import { AddonModLti, AddonModLtiLti } from './lti';

import { makeSingleton } from '@singletons/core.singletons';

/**
* Service that provides some helper functions for LTI.
*/
@Injectable()
export class AddonModLtiHelperProvider {

protected pendingCheckCompletion: {[moduleId: string]: {courseId: number, module: any}} = {};

constructor(platform: Platform) {

platform.resume.subscribe(() => {
// User went back to the app, check pending completions.
for (const moduleId in this.pendingCheckCompletion) {
const data = this.pendingCheckCompletion[moduleId];

CoreCourse.instance.checkModuleCompletion(data.courseId, data.module.completiondata);
}
});

// Clear pending completion on logout.
CoreEvents.instance.on(CoreEventsProvider.LOGOUT, () => {
this.pendingCheckCompletion = {};
});
}

/**
* Get needed data and launch the LTI.
*
* @param courseId Course ID.
* @param module Module.
* @param lti LTI instance. If not provided it will be obtained.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async getDataAndLaunch(courseId: number, module: any, lti?: AddonModLtiLti, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();

const modal = CoreDomUtils.instance.showModalLoading();

try {
const openInBrowser = await AddonModLti.instance.isOpenInAppBrowserDisabled(siteId);

if (openInBrowser) {
const site = await CoreSites.instance.getSite(siteId);

// The view event is triggered by the browser, mark the module as pending to check completion.
this.pendingCheckCompletion[module.id] = {
courseId,
module,
};

await site.openInBrowserWithAutoLogin(module.url);
} else {
// Open in app.
if (!lti) {
lti = await AddonModLti.instance.getLti(courseId, module.id);
}

const launchData = await AddonModLti.instance.getLtiLaunchData(lti.id);

// "View" LTI without blocking the UI.
this.logViewAndCheckCompletion(courseId, module, lti.id, lti.name, siteId);

// Launch LTI.
return AddonModLti.instance.launch(launchData.endpoint, launchData.parameters);
}
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_lti.errorgetlti', true);
} finally {
modal.dismiss();
}
}

/**
* Report the LTI as being viewed and check completion.
*
* @param courseId Course ID.
* @param module Module.
* @param ltiId LTI id.
* @param name Name of the lti.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async logViewAndCheckCompletion(courseId: number, module: any, ltiId: number, name?: string, siteId?: string): Promise<void> {
try {
await AddonModLti.instance.logView(ltiId, name);

CoreCourse.instance.checkModuleCompletion(courseId, module.completiondata);
} catch (error) {
// Ignore errors.
}
}
}

export class AddonModLtiHelper extends makeSingleton(AddonModLtiHelperProvider) {}
125 changes: 51 additions & 74 deletions src/addon/mod/lti/providers/lti.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { Injectable, NgZone } from '@angular/core';
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreFileProvider } from '@providers/file';
Expand All @@ -24,6 +24,8 @@ import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreSite } from '@classes/site';
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';

import { makeSingleton } from '@singletons/core.singletons';

/**
* Service that provides some features for LTI.
*/
Expand All @@ -41,8 +43,7 @@ export class AddonModLtiProvider {
private utils: CoreUtilsProvider,
private translate: TranslateService,
private appProvider: CoreAppProvider,
private logHelper: CoreCourseLogHelperProvider,
protected zone: NgZone) {}
private logHelper: CoreCourseLogHelperProvider) {}

/**
* Delete launcher.
Expand All @@ -65,10 +66,23 @@ export class AddonModLtiProvider {
return url;
}

// Generate an empty page with the JS code.
const text = '<script type="text/javascript"> \n' +
// Generate a form with the params.
let text = '<form action="' + url + '" name="ltiLaunchForm" ' +
'method="post" encType="application/x-www-form-urlencoded">\n';
params.forEach((p) => {
if (p.name == 'ext_submit') {
text += ' <input type="submit"';
} else {
text += ' <input type="hidden" name="' + this.textUtils.escapeHTML(p.name) + '"';
}
text += ' value="' + this.textUtils.escapeHTML(p.value) + '"/>\n';
});
text += '</form>\n';

// Add an in-line script to automatically submit the form.
text += '<script type="text/javascript"> \n' +
' window.onload = function() { \n' +
this.getLaunchJSCode(url, params) +
' document.ltiLaunchForm.submit(); \n' +
' }; \n' +
'</script> \n';

Expand All @@ -81,42 +95,6 @@ export class AddonModLtiProvider {
}
}

/**
* Get the Javascript code to launch the LTI tool.
*
* @param url Launch URL.
* @param params Launch params.
* @return Javascript code.
*/
getLaunchJSCode(url: string, params: AddonModLtiParam[]): string {
// Create the form.
let jsCode = 'var form = document.createElement("form");\n' +
'form.method = "post";\n' +
'form.setAttribute("encType", "application/x-www-form-urlencoded");\n' +
`form.setAttribute("action", "${url}");\n`;

// Create the inputs based on the params.
params.forEach((p) => {
jsCode += 'var input = document.createElement("input");\n';

if (p.name == 'ext_submit') {
jsCode += 'input.type = "submit";\n';
} else {
jsCode += 'input.type = "hidden";\n' +
'input.name = "' + this.textUtils.escapeHTML(p.name) + '";\n';
}

jsCode += 'input.value = "' + this.textUtils.escapeHTML(p.value) + '";\n' +
'form.appendChild(input);\n';
});

// Add the form to the document and submit it.
jsCode += 'document.body.appendChild(form);\n' +
'form.submit();\n';

return jsCode;
}

/**
* Get a LTI.
*
Expand Down Expand Up @@ -217,6 +195,30 @@ export class AddonModLtiProvider {
return this.sitesProvider.getCurrentSite().invalidateWsCacheForKey(this.getLtiLaunchDataCacheKey(id));
}

/**
* Check if open in InAppBrowser is disabled.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether it's disabled.
*/
async isOpenInAppBrowserDisabled(siteId?: string): Promise<boolean> {
const site = await this.sitesProvider.getSite(siteId);

return this.isOpenInAppBrowserDisabledInSite(site);
}

/**
* Check if open in InAppBrowser is disabled.
*
* @param site Site. If not defined, current site.
* @return Whether it's disabled.
*/
isOpenInAppBrowserDisabledInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();

return site.isFeatureDisabled('CoreCourseModuleDelegate_AddonModLti:openInAppBrowser');
}

/**
* Launch LTI.
*
Expand All @@ -229,40 +231,13 @@ export class AddonModLtiProvider {
throw this.translate.instant('addon.mod_lti.errorinvalidlaunchurl');
}

if (this.appProvider.isMobile()) {
// Open it in InAppBrowser. Use JS code because IAB has a bug in iOS when opening local files.
const jsCode = this.getLaunchJSCode(url, params);

const iabInstance = this.utils.openInApp('about:blank');

// Execute the JS code when the page is loaded.
let codeExecuted = false;
const executeCode = (): void => {
if (codeExecuted) {
return;
}

codeExecuted = true;
loadStopSubscription && loadStopSubscription.unsubscribe();
// Generate launcher and open it.
const launcherUrl = await this.generateLauncher(url, params);

// Execute the callback in the Angular zone, so change detection doesn't stop working.
this.zone.run(() => {
iabInstance.executeScript({code: jsCode});
});
};

const loadStopSubscription = iabInstance.on('loadstop').subscribe((event) => {
executeCode();
});

// If loadstop hasn't triggered after 1 second, execute the code anyway.
setTimeout(() => {
executeCode();
}, 1000);
if (this.appProvider.isMobile()) {
this.utils.openInApp(launcherUrl);
} else {
// Generate launched and open it in system browser, we found some cases where inapp caused JS issues.
const launcherUrl = await this.generateLauncher(url, params);

// In desktop open in browser, we found some cases where inapp caused JS issues.
this.utils.openInBrowser(launcherUrl);
}
}
Expand All @@ -284,6 +259,8 @@ export class AddonModLtiProvider {
}
}

export class AddonModLti extends makeSingleton(AddonModLtiProvider) {}

/**
* LTI returned by mod_lti_get_ltis_by_courses.
*/
Expand Down
Loading