diff --git a/src/addons/mod/lti/components/components.module.ts b/src/addons/mod/lti/components/components.module.ts new file mode 100644 index 000000000..27ae3a4f2 --- /dev/null +++ b/src/addons/mod/lti/components/components.module.ts @@ -0,0 +1,33 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; +import { AddonModLtiIndexComponent } from './index'; + +@NgModule({ + declarations: [ + AddonModLtiIndexComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + ], + exports: [ + AddonModLtiIndexComponent, + ], +}) +export class AddonModLtiComponentsModule {} diff --git a/src/addons/mod/lti/components/index/addon-mod-lti-index.html b/src/addons/mod/lti/components/index/addon-mod-lti-index.html new file mode 100644 index 000000000..3302fe4ef --- /dev/null +++ b/src/addons/mod/lti/components/index/addon-mod-lti-index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + +
+ + + {{ 'addon.mod_lti.launchactivity' | translate }} + +
+
diff --git a/src/addons/mod/lti/components/index/index.ts b/src/addons/mod/lti/components/index/index.ts new file mode 100644 index 000000000..f18244c01 --- /dev/null +++ b/src/addons/mod/lti/components/index/index.ts @@ -0,0 +1,90 @@ +// (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 { Component, Optional, OnInit } from '@angular/core'; +import { IonContent } from '@ionic/angular'; + +import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { AddonModLti, AddonModLtiLti, AddonModLtiProvider } from '../../services/lti'; +import { AddonModLtiHelper } from '../../services/lti-helper'; + +/** + * Component that displays an LTI entry page. + */ +@Component({ + selector: 'addon-mod-lti-index', + templateUrl: 'addon-mod-lti-index.html', +}) +export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { + + component = AddonModLtiProvider.COMPONENT; + moduleName = 'lti'; + + lti?: AddonModLtiLti; // The LTI object. + + protected fetchContentDefaultError = 'addon.mod_lti.errorgetlti'; + + constructor( + protected content?: IonContent, + @Optional() courseContentsPage?: CoreCourseContentsPage, + ) { + super('AddonModLtiIndexComponent', content, courseContentsPage); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + this.loadContent(); + } + + /** + * @inheritdoc + */ + protected async fetchContent(refresh: boolean = false): Promise { + try { + this.lti = await AddonModLti.getLti(this.courseId, this.module.id); + + this.description = this.lti.intro; + this.dataRetrieved.emit(this.lti); + } finally { + this.fillContextMenu(refresh); + } + } + + /** + * @inheritdoc + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(AddonModLti.invalidateLti(this.courseId)); + if (this.lti) { + promises.push(AddonModLti.invalidateLtiLaunchData(this.lti.id)); + } + + await Promise.all(promises); + } + + /** + * Launch the LTI. + */ + launch(): void { + AddonModLtiHelper.getDataAndLaunch(this.courseId, this.module, this.lti); + } + +} diff --git a/src/addons/mod/lti/lang.json b/src/addons/mod/lti/lang.json new file mode 100644 index 000000000..7a70ea4e7 --- /dev/null +++ b/src/addons/mod/lti/lang.json @@ -0,0 +1,6 @@ +{ + "errorgetlti": "Error getting module data.", + "errorinvalidlaunchurl": "The launch URL is not valid.", + "launchactivity": "Launch the activity", + "modulenameplural": "External tools" +} \ No newline at end of file diff --git a/src/addons/mod/lti/lti-lazy.module.ts b/src/addons/mod/lti/lti-lazy.module.ts new file mode 100644 index 000000000..163e70a80 --- /dev/null +++ b/src/addons/mod/lti/lti-lazy.module.ts @@ -0,0 +1,38 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AddonModLtiComponentsModule } from './components/components.module'; +import { AddonModLtiIndexPage } from './pages/index/index.page'; + +const routes: Routes = [ + { + path: ':courseId/:cmId', + component: AddonModLtiIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonModLtiComponentsModule, + ], + declarations: [ + AddonModLtiIndexPage, + ], +}) +export class AddonModLtiLazyModule {} diff --git a/src/addons/mod/lti/lti.module.ts b/src/addons/mod/lti/lti.module.ts new file mode 100644 index 000000000..6c6b08390 --- /dev/null +++ b/src/addons/mod/lti/lti.module.ts @@ -0,0 +1,60 @@ +// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { AddonModLtiComponentsModule } from './components/components.module'; +import { AddonModLtiIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModLtiListLinkHandler } from './services/handlers/list-link'; +import { AddonModLtiModuleHandler, AddonModLtiModuleHandlerService } from './services/handlers/module'; +import { AddonModLtiPrefetchHandler } from './services/handlers/prefetch'; +import { AddonModLtiProvider } from './services/lti'; +import { AddonModLtiHelperProvider } from './services/lti-helper'; + +export const ADDON_MOD_LTI_SERVICES: Type[] = [ + AddonModLtiProvider, + AddonModLtiHelperProvider, +]; + +const routes: Routes = [ + { + path: AddonModLtiModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./lti-lazy.module').then(m => m.AddonModLtiLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModLtiComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModLtiModuleHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModLtiIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModLtiListLinkHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModLtiPrefetchHandler.instance); + }, + }, + ], +}) +export class AddonModLtiModule {} diff --git a/src/addons/mod/lti/pages/index/index.html b/src/addons/mod/lti/pages/index/index.html new file mode 100644 index 000000000..784478fbf --- /dev/null +++ b/src/addons/mod/lti/pages/index/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/lti/pages/index/index.page.ts b/src/addons/mod/lti/pages/index/index.page.ts new file mode 100644 index 000000000..a19c7515b --- /dev/null +++ b/src/addons/mod/lti/pages/index/index.page.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 { Component, ViewChild } from '@angular/core'; + +import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; +import { AddonModLtiIndexComponent } from '../../components/index/index'; + +/** + * Page that displays an LTI. + */ +@Component({ + selector: 'page-addon-mod-lti-index', + templateUrl: 'index.html', +}) +export class AddonModLtiIndexPage extends CoreCourseModuleMainActivityPage { + + @ViewChild(AddonModLtiIndexComponent) activityComponent?: AddonModLtiIndexComponent; + +} diff --git a/src/addons/mod/lti/services/handlers/index-link.ts b/src/addons/mod/lti/services/handlers/index-link.ts new file mode 100644 index 000000000..3b2318049 --- /dev/null +++ b/src/addons/mod/lti/services/handlers/index-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to LTI. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLtiIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModLtiIndexLinkHandlerService'; + + constructor() { + super('AddonModLti', 'lti', 'l'); + } + +} + +export const AddonModLtiIndexLinkHandler = makeSingleton(AddonModLtiIndexLinkHandlerService); diff --git a/src/addons/mod/lti/services/handlers/list-link.ts b/src/addons/mod/lti/services/handlers/list-link.ts new file mode 100644 index 000000000..17d7bb815 --- /dev/null +++ b/src/addons/mod/lti/services/handlers/list-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to LTI list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLtiListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModLtiListLinkHandler'; + + constructor() { + super('AddonModLti', 'lti'); + } + +} + +export const AddonModLtiListLinkHandler = makeSingleton(AddonModLtiListLinkHandlerService); diff --git a/src/addons/mod/lti/services/handlers/module.ts b/src/addons/mod/lti/services/handlers/module.ts new file mode 100644 index 000000000..c09f5e5f0 --- /dev/null +++ b/src/addons/mod/lti/services/handlers/module.ts @@ -0,0 +1,145 @@ +// (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, Type } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +import { CoreConstants } from '@/core/constants'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreApp } from '@services/app'; +import { CoreFilepool } from '@services/filepool'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonModLtiHelper } from '../lti-helper'; +import { AddonModLti, AddonModLtiProvider } from '../lti'; +import { AddonModLtiIndexComponent } from '../../components/index'; + +/** + * Handler to support LTI modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLtiModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_lti'; + + name = 'AddonModLti'; + modName = 'lti'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: false, + [CoreConstants.FEATURE_GROUPINGS]: false, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + constructor(protected sanitizer: DomSanitizer) {} + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData( + module: CoreCourseAnyModuleData, + courseId: number, + ): CoreCourseModuleHandlerData { + + const data: CoreCourseModuleHandlerData = { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_lti-handler', + action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModLtiModuleHandlerService.PAGE_NAME + routeParams, options); + }, + buttons: [{ + icon: 'link', + label: 'addon.mod_lti.launchactivity', + action: (event: Event, module: CoreCourseModule, courseId: number): void => { + // Launch the LTI. + AddonModLtiHelper.getDataAndLaunch(courseId, module); + }, + }], + }; + + // Handle custom icons. + CoreUtils.ignoreErrors(this.loadCustomIcon(module, courseId, data)); + + return data; + } + + /** + * Load the custom icon. + * + * @param module Module. + * @param courseId Course ID. + * @param data Handler data. + * @return Promise resolved when done. + */ + protected async loadCustomIcon( + module: CoreCourseAnyModuleData, + courseId: number, + handlerData: CoreCourseModuleHandlerData, + ): Promise { + const lti = await AddonModLti.getLti(courseId, module.id); + + const icon = lti.secureicon || lti.icon; + if (!icon) { + return; + } + + const siteId = CoreSites.getCurrentSiteId(); + + try { + await CoreFilepool.downloadUrl(siteId, icon, false, AddonModLtiProvider.COMPONENT, module.id); + + // Get the internal URL. + const url = await CoreFilepool.getSrcByUrl(siteId, icon, AddonModLtiProvider.COMPONENT, module.id); + + handlerData.icon = this.sanitizer.bypassSecurityTrustUrl(url); + } catch { + // Error downloading. If we're online we'll set the online url. + if (CoreApp.isOnline()) { + handlerData.icon = this.sanitizer.bypassSecurityTrustUrl(icon); + } + } + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise | undefined> { + return AddonModLtiIndexComponent; + } + +} + +export const AddonModLtiModuleHandler = makeSingleton(AddonModLtiModuleHandlerService); diff --git a/src/addons/mod/lti/services/handlers/prefetch.ts b/src/addons/mod/lti/services/handlers/prefetch.ts new file mode 100644 index 000000000..b8682bb6a --- /dev/null +++ b/src/addons/mod/lti/services/handlers/prefetch.ts @@ -0,0 +1,62 @@ +// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourseAnyModuleData } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; +import { AddonModLti, AddonModLtiProvider } from '../lti'; + +/** + * Handler to prefetch LTIs. LTIs cannot be prefetched, but the handler will be used to invalidate some data on course PTR. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLtiPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModLti'; + modName = 'lti'; + component = AddonModLtiProvider.COMPONENT; + + /** + * @inheritdoc + */ + async download(): Promise { + return; + } + + /** + * @inheritdoc + */ + invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + return AddonModLti.invalidateLti(courseId); + } + + /** + * @inheritdoc + */ + async isDownloadable(): Promise { + return false; // LTIs aren't downloadable. + } + + /** + * @inheritdoc + */ + async prefetch(): Promise { + return; + } + +} + +export const AddonModLtiPrefetchHandler = makeSingleton(AddonModLtiPrefetchHandlerService); diff --git a/src/addons/mod/lti/services/lti-helper.ts b/src/addons/mod/lti/services/lti-helper.ts new file mode 100644 index 000000000..3f11b180f --- /dev/null +++ b/src/addons/mod/lti/services/lti-helper.ts @@ -0,0 +1,125 @@ +// (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 { CoreCourse } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton, Platform } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonModLti, AddonModLtiLti } from './lti'; + +/** + * Service that provides some helper functions for LTI. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModLtiHelperProvider { + + protected pendingCheckCompletion: {[moduleId: string]: {courseId: number; module: CoreCourseModule}} = {}; + + constructor() { + Platform.resume.subscribe(() => { + // User went back to the app, check pending completions. + for (const moduleId in this.pendingCheckCompletion) { + const data = this.pendingCheckCompletion[moduleId]; + + CoreCourse.checkModuleCompletion(data.courseId, data.module.completiondata); + } + }); + + // Clear pending completion on logout. + CoreEvents.on(CoreEvents.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: CoreCourseModule, lti?: AddonModLtiLti, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const modal = await CoreDomUtils.showModalLoading(); + + try { + const openInBrowser = await AddonModLti.isOpenInAppBrowserDisabled(siteId); + + if (openInBrowser) { + const site = await CoreSites.getSite(siteId); + + // The view event is triggered by the browser, mark the module as pending to check completion. + this.pendingCheckCompletion[module.id] = { + courseId, + module, + }; + + return site.openInBrowserWithAutoLogin(module.url!); + } + + // Open in app. + if (!lti) { + lti = await AddonModLti.getLti(courseId, module.id); + } + + const launchData = await AddonModLti.getLtiLaunchData(lti.id); + + // "View" LTI without blocking the UI. + this.logViewAndCheckCompletion(courseId, module, lti.id, lti.name, siteId); + + // Launch LTI. + return AddonModLti.launch(launchData.endpoint, launchData.parameters); + } catch (error) { + CoreDomUtils.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: CoreCourseModule, + ltiId: number, + name?: string, + siteId?: string, + ): Promise { + try { + await AddonModLti.logView(ltiId, name, siteId); + + CoreCourse.checkModuleCompletion(courseId, module.completiondata); + } catch (error) { + // Ignore errors. + } + } + +} + +export const AddonModLtiHelper = makeSingleton(AddonModLtiHelperProvider); diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts new file mode 100644 index 000000000..de9409d71 --- /dev/null +++ b/src/addons/mod/lti/services/lti.ts @@ -0,0 +1,344 @@ +// (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 { CoreApp } from '@services/app'; +import { CoreFile } from '@services/file'; +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 { + 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 { + if (!CoreFile.isAvailable()) { + return url; + } + + // Generate a form with the params. + let text = `
\n`; + params.forEach((p) => { + if (p.name == 'ext_submit') { + text += ' \n'; + }); + text += '
\n'; + + // Add an in-line script to automatically submit the form. + text += ' \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 { + 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('mod_lti_get_ltis_by_courses', params, preSets); + + const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId); + if (currentLti) { + return currentLti; + } + + throw new CoreError('Activity not found.'); + } + + /** + * 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 { + 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('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 { + 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 { + const site = await CoreSites.getSite(siteId); + + await site.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 { + const site = await CoreSites.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 || 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 { + if (!CoreUrlUtils.isHttpURL(url)) { + throw Translate.instant('addon.mod_lti.errorinvalidlaunchurl'); + } + + // Generate launcher and open it. + const launcherUrl = await this.generateLauncher(url, params); + + if (CoreApp.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 { + const params: AddonModLtiViewLtiWSParams = { + ltiid: id, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_lti_view_lti', + params, + AddonModLtiProvider.COMPONENT, + id, + name, + 'lti', + {}, + siteId, + ); + } + +} + +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[]; // @since 3.2. + 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. +}; diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 5688284c5..bf3163e78 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -25,6 +25,7 @@ import { AddonModPageModule } from './page/page.module'; import { AddonModQuizModule } from './quiz/quiz.module'; import { AddonModResourceModule } from './resource/resource.module'; import { AddonModUrlModule } from './url/url.module'; +import { AddonModLtiModule } from './lti/lti.module'; @NgModule({ declarations: [], @@ -40,6 +41,7 @@ import { AddonModUrlModule } from './url/url.module'; AddonModResourceModule, AddonModFolderModule, AddonModImscpModule, + AddonModLtiModule, ], providers: [], exports: [], diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 2eeeff320..44b1ac0c2 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -132,7 +132,7 @@ import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module'; // @todo import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module'; import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module'; import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module'; -// @todo import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module'; +import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module'; import { ADDON_MOD_PAGE_SERVICES } from '@addons/mod/page/page.module'; import { ADDON_MOD_QUIZ_SERVICES } from '@addons/mod/quiz/quiz.module'; import { ADDON_MOD_RESOURCE_SERVICES } from '@addons/mod/resource/resource.module'; @@ -297,7 +297,7 @@ export class CoreCompileProvider { // @todo ...ADDON_MOD_H5P_ACTIVITY_SERVICES, ...ADDON_MOD_IMSCP_SERVICES, ...ADDON_MOD_LESSON_SERVICES, - // @todo ...ADDON_MOD_LTI_SERVICES, + ...ADDON_MOD_LTI_SERVICES, ...ADDON_MOD_PAGE_SERVICES, ...ADDON_MOD_QUIZ_SERVICES, ...ADDON_MOD_RESOURCE_SERVICES, diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index 653f24f03..9be217cb1 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -32,6 +32,7 @@ import { CoreViewerModule } from './viewer/viewer.module'; import { CoreSearchModule } from './search/search.module'; import { CoreCommentsModule } from './comments/comments.module'; import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; +import { CoreRatingModule } from './rating/rating.module'; @NgModule({ imports: [ @@ -53,6 +54,7 @@ import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; CoreViewerModule, CoreCommentsModule, CoreSitePluginsModule, + CoreRatingModule, ], }) export class CoreFeaturesModule {}