2022-06-08 13:17:14 +02:00

391 lines
13 KiB
TypeScript

// (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 { CoreError } from '@classes/errors/error';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreFile } from '@services/file';
import { CorePlatform } from '@services/platform';
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
const ROOT_CACHE_KEY = 'mmaModLti:';
const LAUNCHER_FILE_NAME = 'lti_launcher.html';
/**
* Service that provides some features for LTI.
*/
@Injectable({ providedIn: 'root' })
export class AddonModLtiProvider {
static readonly COMPONENT = 'mmaModLti';
/**
* Delete launcher.
*
* @return Promise resolved when the launcher file is deleted.
*/
deleteLauncher(): Promise<void> {
return CoreFile.removeFile(LAUNCHER_FILE_NAME);
}
/**
* Generates a launcher file.
*
* @param url Launch URL.
* @param params Launch params.
* @return Promise resolved with the file URL.
*/
async generateLauncher(url: string, params: AddonModLtiParam[]): Promise<string> {
if (!CoreFile.isAvailable()) {
return url;
}
// 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="' + CoreTextUtils.escapeHTML(p.name) + '"';
}
text += ' value="' + CoreTextUtils.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' +
' document.ltiLaunchForm.submit(); \n' +
' }; \n' +
'</script> \n';
const entry = await CoreFile.writeFile(LAUNCHER_FILE_NAME, text);
return entry.toURL();
}
/**
* Get a LTI.
*
* @param courseId Course ID.
* @param cmId Course module ID.
* @param options Other options.
* @return Promise resolved when the LTI is retrieved.
*/
async getLti(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModLtiLti> {
const params: AddonModLtiGetLtisByCoursesWSParams = {
courseids: [courseId],
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getLtiCacheKey(courseId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModLtiProvider.COMPONENT,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
};
const site = await CoreSites.getSite(options.siteId);
const response = await site.read<AddonModLtiGetLtisByCoursesWSResponse>('mod_lti_get_ltis_by_courses', params, preSets);
const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId);
if (currentLti) {
return currentLti;
}
throw new CoreError(Translate.instant('core.course.modulenotfound'));
}
/**
* Get cache key for LTI data WS calls.
*
* @param courseId Course ID.
* @return Cache key.
*/
protected getLtiCacheKey(courseId: number): string {
return ROOT_CACHE_KEY + 'lti:' + courseId;
}
/**
* Get a LTI launch data.
*
* @param id LTI id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the launch data is retrieved.
*/
async getLtiLaunchData(id: number, siteId?: string): Promise<AddonModLtiGetToolLaunchDataWSResponse> {
const params: AddonModLtiGetToolLaunchDataWSParams = {
toolid: id,
};
// Try to avoid using cache since the "nonce" parameter is set to a timestamp.
const preSets: CoreSiteWSPreSets = {
getFromCache: false,
saveToCache: true,
emergencyCache: true,
cacheKey: this.getLtiLaunchDataCacheKey(id),
};
const site = await CoreSites.getSite(siteId);
return site.read<AddonModLtiGetToolLaunchDataWSResponse>('mod_lti_get_tool_launch_data', params, preSets);
}
/**
* Get cache key for LTI launch data WS calls.
*
* @param id LTI id.
* @return Cache key.
*/
protected getLtiLaunchDataCacheKey(id: number): string {
return `${ROOT_CACHE_KEY}launch:${id}`;
}
/**
* Invalidates LTI data.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateLti(courseId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getLtiCacheKey(courseId));
}
/**
* Invalidates options.
*
* @param id LTI id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateLtiLaunchData(id: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
await site.invalidateWsCacheForKey(this.getLtiLaunchDataCacheKey(id));
}
/**
* Check if open LTI in browser via site with auto-login is disabled.
* This setting was added in 3.11.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether it's disabled.
*/
async isLaunchViaSiteDisabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
return this.isLaunchViaSiteDisabledInSite(site);
}
/**
* Check if open LTI in browser via site with auto-login is disabled.
* This setting was added in 3.11.
*
* @param site Site. If not defined, current site.
* @return Whether it's disabled.
*/
isLaunchViaSiteDisabledInSite(site?: CoreSite): boolean {
site = site || CoreSites.getCurrentSite();
return !!site?.isFeatureDisabled('CoreCourseModuleDelegate_AddonModLti:launchViaSite');
}
/**
* Check if open in InAppBrowser is disabled.
* This setting was removed in Moodle 3.11 because the default behaviour of the app changed.
*
* @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 CoreSites.getSite(siteId);
return this.isOpenInAppBrowserDisabledInSite(site);
}
/**
* Check if open in InAppBrowser is disabled.
* This setting was removed in Moodle 3.11 because the default behaviour of the app changed.
*
* @param site Site. If not defined, current site.
* @return Whether it's disabled.
*/
isOpenInAppBrowserDisabledInSite(site?: CoreSite): boolean {
site = site || CoreSites.getCurrentSite();
return !!site?.isFeatureDisabled('CoreCourseModuleDelegate_AddonModLti:openInAppBrowser');
}
/**
* Launch LTI.
*
* @param url Launch URL.
* @param params Launch params.
* @return Promise resolved when the WS call is successful.
*/
async launch(url: string, params: AddonModLtiParam[]): Promise<void> {
if (!CoreUrlUtils.isHttpURL(url)) {
throw Translate.instant('addon.mod_lti.errorinvalidlaunchurl');
}
// Generate launcher and open it.
const launcherUrl = await this.generateLauncher(url, params);
if (CorePlatform.isMobile()) {
CoreUtils.openInApp(launcherUrl);
} else {
// In desktop open in browser, we found some cases where inapp caused JS issues.
CoreUtils.openInBrowser(launcherUrl);
}
}
/**
* Report the LTI as being viewed.
*
* @param id LTI id.
* @param name Name of the lti.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
logView(id: number, name?: string, siteId?: string): Promise<void> {
const params: AddonModLtiViewLtiWSParams = {
ltiid: id,
};
return CoreCourseLogHelper.logSingle(
'mod_lti_view_lti',
params,
AddonModLtiProvider.COMPONENT,
id,
name,
'lti',
{},
siteId,
);
}
/**
* Check whether the LTI should be launched in browser via the site with auto-login.
*
* @param siteId Site ID.
* @return Promise resolved with boolean.
*/
async shouldLaunchInBrowser(siteId?: string): Promise<boolean> {
const site = await CoreSites.getSite(siteId);
if (site.isVersionGreaterEqualThan('3.11')) {
// In 3.11+, launch in browser by default unless it's disabled.
return !this.isLaunchViaSiteDisabledInSite(site);
} else {
// In old sites the default behaviour is to launch in InAppBrowser.
return this.isOpenInAppBrowserDisabledInSite(site);
}
}
}
export const AddonModLti = makeSingleton(AddonModLtiProvider);
/**
* Params of mod_lti_get_ltis_by_courses WS.
*/
export type AddonModLtiGetLtisByCoursesWSParams = {
courseids?: number[]; // Array of course ids.
};
/**
* Data returned by mod_lti_get_ltis_by_courses WS.
*/
export type AddonModLtiGetLtisByCoursesWSResponse = {
ltis: AddonModLtiLti[];
warnings?: CoreWSExternalWarning[];
};
/**
* LTI returned by mod_lti_get_ltis_by_courses.
*/
export type AddonModLtiLti = {
id: number; // External tool id.
coursemodule: number; // Course module id.
course: number; // Course id.
name: string; // LTI name.
intro?: string; // The LTI intro.
introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
introfiles?: CoreWSExternalFile[];
timecreated?: number; // Time of creation.
timemodified?: number; // Time of last modification.
typeid?: number; // Type id.
toolurl?: string; // Tool url.
securetoolurl?: string; // Secure tool url.
instructorchoicesendname?: string; // Instructor choice send name.
instructorchoicesendemailaddr?: number; // Instructor choice send mail address.
instructorchoiceallowroster?: number; // Instructor choice allow roster.
instructorchoiceallowsetting?: number; // Instructor choice allow setting.
instructorcustomparameters?: string; // Instructor custom parameters.
instructorchoiceacceptgrades?: number; // Instructor choice accept grades.
grade?: number; // Enable grades.
launchcontainer?: number; // Launch container mode.
resourcekey?: string; // Resource key.
password?: string; // Shared secret.
debuglaunch?: number; // Debug launch.
showtitlelaunch?: number; // Show title launch.
showdescriptionlaunch?: number; // Show description launch.
servicesalt?: string; // Service salt.
icon?: string; // Alternative icon URL.
secureicon?: string; // Secure icon URL.
section?: number; // Course section id.
visible?: number; // Visible.
groupmode?: number; // Group mode.
groupingid?: number; // Group id.
};
/**
* Params of mod_lti_get_tool_launch_data WS.
*/
export type AddonModLtiGetToolLaunchDataWSParams = {
toolid: number; // External tool instance id.
};
/**
* Data returned by mod_lti_get_tool_launch_data WS.
*/
export type AddonModLtiGetToolLaunchDataWSResponse = {
endpoint: string; // Endpoint URL.
parameters: AddonModLtiParam[];
warnings?: CoreWSExternalWarning[];
};
/**
* Param to send to the LTI.
*/
export type AddonModLtiParam = {
name: string; // Parameter name.
value: string; // Parameter value.
};
/**
* Params of mod_lti_view_lti WS.
*/
export type AddonModLtiViewLtiWSParams = {
ltiid: number; // Lti instance id.
};