diff --git a/src/addons/mod/book/book.module.ts b/src/addons/mod/book/book.module.ts index bde5f4860..d5b2b5f3a 100644 --- a/src/addons/mod/book/book.module.ts +++ b/src/addons/mod/book/book.module.ts @@ -26,7 +26,6 @@ import { AddonModBookListLinkHandler } from './services/handlers/list-link'; import { AddonModBookPrefetchHandler } from './services/handlers/prefetch'; import { AddonModBookTagAreaHandler } from './services/handlers/tag-area'; - const routes: Routes = [ { path: AddonModBookModuleHandlerService.PAGE_NAME, diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 208df1220..c467b8a8d 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -22,6 +22,7 @@ import { AddonModLabelModule } from './label/label.module'; import { AddonModLessonModule } from './lesson/lesson.module'; import { AddonModPageModule } from './page/page.module'; import { AddonModQuizModule } from './quiz/quiz.module'; +import { AddonModUrlModule } from './url/url.module'; @NgModule({ declarations: [], @@ -32,6 +33,7 @@ import { AddonModQuizModule } from './quiz/quiz.module'; AddonModLessonModule, AddonModPageModule, AddonModQuizModule, + AddonModUrlModule, AddonModLabelModule, AddonModFolderModule, ], diff --git a/src/addons/mod/url/components/components.module.ts b/src/addons/mod/url/components/components.module.ts new file mode 100644 index 000000000..b28ac2555 --- /dev/null +++ b/src/addons/mod/url/components/components.module.ts @@ -0,0 +1,34 @@ +// (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 { AddonModUrlIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModUrlIndexComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + ], + exports: [ + AddonModUrlIndexComponent, + ], +}) +export class AddonModUrlComponentsModule {} diff --git a/src/addons/mod/url/components/index/addon-mod-url-index.html b/src/addons/mod/url/components/index/addon-mod-url-index.html new file mode 100644 index 000000000..11392ce43 --- /dev/null +++ b/src/addons/mod/url/components/index/addon-mod-url-index.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +

{{ 'addon.mod_url.pointingtourl' | translate }}

+

{{ url }}

+
+
+ + + + + {{ 'addon.mod_url.accessurl' | translate }} + + + +
+
diff --git a/src/addons/mod/url/components/index/index.scss b/src/addons/mod/url/components/index/index.scss new file mode 100644 index 000000000..44c2f04e0 --- /dev/null +++ b/src/addons/mod/url/components/index/index.scss @@ -0,0 +1,3 @@ +.addon-mod_url-embedded-url { + height: 100%; +} diff --git a/src/addons/mod/url/components/index/index.ts b/src/addons/mod/url/components/index/index.ts new file mode 100644 index 000000000..75b80f768 --- /dev/null +++ b/src/addons/mod/url/components/index/index.ts @@ -0,0 +1,185 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Component, OnInit, Optional } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreSites } from '@services/sites'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreTextUtils } from '@services/utils/text'; +import { AddonModUrl, AddonModUrlDisplayOptions, AddonModUrlProvider, AddonModUrlUrl } from '../../services/url'; +import { AddonModUrlHelper } from '../../services/url-helper'; + +/** + * Component that displays a url. + */ +@Component({ + selector: 'addon-mod-url-index', + templateUrl: 'addon-mod-url-index.html', + styleUrls: ['index.scss'], +}) +export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { + + component = AddonModUrlProvider.COMPONENT; + + canGetUrl = false; + url?: string; + name?: string; + shouldEmbed = false; + shouldIframe = false; + isImage = false; + isAudio = false; + isVideo = false; + isOther = false; + mimetype?: string; + displayDescription = true; + + constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { + super('AddonModUrlIndexComponent', courseContentsPage); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + this.canGetUrl = AddonModUrl.isGetUrlWSAvailable(); + + await this.loadContent(); + + if ((this.shouldIframe || + (this.shouldEmbed && this.isOther)) || + (!this.shouldIframe && (!this.shouldEmbed || !this.isOther))) { + this.logView(); + } + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + await AddonModUrl.invalidateContent(this.module!.id, this.courseId!); + } + + /** + * Download url contents. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async fetchContent(refresh = false): Promise { + try { + if (!this.canGetUrl) { + throw null; + } + // Fetch the module data. + const url = await AddonModUrl.getUrl(this.courseId!, this.module!.id); + + this.name = url.name; + this.description = url.intro; + this.dataRetrieved.emit(url); + + if (url.displayoptions) { + const unserialized = CoreTextUtils.unserialize(url.displayoptions); + this.displayDescription = typeof unserialized.printintro == 'undefined' || !!unserialized.printintro; + } + + // Try to load module contents, it's needed to get the URL with parameters. + await CoreCourse.loadModuleContents(this.module!, this.courseId, undefined, false, refresh, undefined, 'url'); + + // Always use the URL from the module because it already includes the parameters. + this.url = this.module!.contents[0] && this.module!.contents[0].fileurl ? this.module!.contents[0].fileurl : undefined; + + await this.calculateDisplayOptions(url); + + } catch { + // Fallback in case is not prefetched or not available. + const mod = + await CoreCourse.getModule(this.module!.id, this.courseId, undefined, false, false, undefined, 'url'); + + this.name = mod.name; + this.description = mod.description; + this.dataRetrieved.emit(mod); + + if (!mod.contents.length) { + // If the data was cached maybe we don't have contents. Reject. + throw new CoreError('No contents found in module.'); + } + + this.url = mod.contents && mod.contents[0] && mod.contents[0].fileurl ? mod.contents[0].fileurl : undefined; + } + } + + /** + * Calculate the display options to determine how the URL should be rendered. + * + * @param url Object with the URL data. + * @return Promise resolved when done. + */ + protected async calculateDisplayOptions(url: AddonModUrlUrl): Promise { + const displayType = AddonModUrl.getFinalDisplayType(url); + + this.shouldEmbed = displayType == CoreConstants.RESOURCELIB_DISPLAY_EMBED; + this.shouldIframe = displayType == CoreConstants.RESOURCELIB_DISPLAY_FRAME; + + if (this.shouldEmbed) { + const extension = CoreMimetypeUtils.guessExtensionFromUrl(url.externalurl); + + this.mimetype = CoreMimetypeUtils.getMimeType(extension); + this.isImage = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_image']); + this.isAudio = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_audio']); + this.isVideo = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_video']); + this.isOther = !this.isImage && !this.isAudio && !this.isVideo; + } + + if (this.shouldIframe || (this.shouldEmbed && !this.isImage && !this.isAudio && !this.isVideo)) { + // Will be displayed in an iframe. Check if we need to auto-login. + const currentSite = CoreSites.getCurrentSite(); + + if (currentSite?.containsUrl(this.url)) { + // Format the URL to add auto-login. + this.url = await currentSite.getAutoLoginUrl(this.url!, false); + } + } + } + + /** + * Log view into the site and checks module completion. + * + * @return Promise resolved when done. + */ + protected async logView(): Promise { + try { + await AddonModUrl.logView(this.module!.instance!, this.module!.name); + CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata); + } catch { + // Ignore errors. + } + } + + /** + * Opens a file. + */ + go(): void { + this.logView(); + AddonModUrlHelper.open(this.url!); + } + +} diff --git a/src/addons/mod/url/lang.json b/src/addons/mod/url/lang.json new file mode 100644 index 000000000..18eff8be5 --- /dev/null +++ b/src/addons/mod/url/lang.json @@ -0,0 +1,5 @@ +{ + "accessurl": "Access the URL", + "modulenameplural": "URLs", + "pointingtourl": "URL that the resource points to." +} \ No newline at end of file diff --git a/src/addons/mod/url/pages/index/index.html b/src/addons/mod/url/pages/index/index.html new file mode 100644 index 000000000..6a1cda2f0 --- /dev/null +++ b/src/addons/mod/url/pages/index/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/url/pages/index/index.page.ts b/src/addons/mod/url/pages/index/index.page.ts new file mode 100644 index 000000000..639eb9b08 --- /dev/null +++ b/src/addons/mod/url/pages/index/index.page.ts @@ -0,0 +1,30 @@ +// (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 { AddonModUrlIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a url. + */ +@Component({ + selector: 'page-addon-mod-url-index', + templateUrl: 'index.html', +}) +export class AddonModUrlIndexPage extends CoreCourseModuleMainActivityPage { + + @ViewChild(AddonModUrlIndexComponent) activityComponent?: AddonModUrlIndexComponent; + +} diff --git a/src/addons/mod/url/services/handlers/index-link.ts b/src/addons/mod/url/services/handlers/index-link.ts new file mode 100644 index 000000000..2f3ced076 --- /dev/null +++ b/src/addons/mod/url/services/handlers/index-link.ts @@ -0,0 +1,34 @@ +// (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 url. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModUrlIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModUrlLinkHandler'; + useModNameToGetModule = true; + + constructor() { + super('AddonModUrl', 'url', 'u'); + } + +} + +export const AddonModUrlIndexLinkHandler = makeSingleton(AddonModUrlIndexLinkHandlerService); diff --git a/src/addons/mod/url/services/handlers/list-link.ts b/src/addons/mod/url/services/handlers/list-link.ts new file mode 100644 index 000000000..37a88442e --- /dev/null +++ b/src/addons/mod/url/services/handlers/list-link.ts @@ -0,0 +1,32 @@ +// (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 URL list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModUrlListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModUrlListLinkHandler'; + + constructor() { + super('AddonModUrl', 'url'); + } + +} +export const AddonModUrlListLinkHandler = makeSingleton(AddonModUrlListLinkHandlerService); diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts new file mode 100644 index 000000000..464d876e9 --- /dev/null +++ b/src/addons/mod/url/services/handlers/module.ts @@ -0,0 +1,189 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonModUrlIndexComponent } from '../../components/index/index'; +import { AddonModUrl } from '../url'; +import { AddonModUrlHelper } from '../url-helper'; + +/** + * Handler to support url modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModUrlModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_url'; + + name = 'AddonModUrl'; + modName = 'url'; + + supportedFeatures = { + [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE, + [CoreConstants.FEATURE_GROUPS]: false, + [CoreConstants.FEATURE_GROUPINGS]: false, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseAnyModuleData, courseId: number): CoreCourseModuleHandlerData { + + /** + * Open the URL. + * + * @param module The module object. + * @param courseId The course ID. + */ + const openUrl = async (module: CoreCourseModule, courseId: number): Promise => { + try { + if (module.instance) { + await AddonModUrl.logView(module.instance, module.name); + CoreCourse.checkModuleCompletion(courseId, module.completiondata); + } + } catch { + // Ignore errors. + } + + AddonModUrlHelper.open(module.contents[0].fileurl); + }; + + const handlerData: CoreCourseModuleHandlerData = { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_url-handler', + showDownloadButton: false, + async action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise { + const modal = await CoreDomUtils.showModalLoading(); + + // First of all, make sure module contents are loaded. + try { + await CoreCourse.loadModuleContents( + module, + courseId, + undefined, + false, + false, + undefined, + this.modName, + ); + + // Check if the URL can be handled by the app. If so, always open it directly. + const canHandle = + await CoreContentLinksHelper.canHandleLink(module.contents[0].fileurl, courseId, undefined, true); + + let shouldOpen = false; + if (canHandle) { + // URL handled by the app, open it directly. + shouldOpen = true; + } else if (AddonModUrl.isGetUrlWSAvailable()) { + // Not handled by the app, check the display type. + const url = await CoreUtils.ignoreErrors(AddonModUrl.getUrl(courseId, module.id)); + const displayType = AddonModUrl.getFinalDisplayType(url); + + shouldOpen = displayType == CoreConstants.RESOURCELIB_DISPLAY_OPEN || + displayType == CoreConstants.RESOURCELIB_DISPLAY_POPUP; + } else { + shouldOpen = false; + } + + if (shouldOpen) { + openUrl(module, courseId); + } else { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModUrlModuleHandlerService.PAGE_NAME + routeParams, options); + } + } finally { + modal.dismiss(); + } + }, + buttons: [{ + hidden: true, // Hide it until we calculate if it should be displayed or not. + icon: 'link', + label: 'core.openmodinbrowser', + action: (event: Event, module: CoreCourseModule, courseId: number): void => { + openUrl(module, courseId); + }, + }], + }; + + this.hideLinkButton(module, courseId).then((hideButton) => { + handlerData.buttons![0]!.hidden = hideButton; + + if (module.contents && module.contents[0]) { + // Calculate the icon to use. + handlerData.icon = AddonModUrl.guessIcon(module.contents[0].fileurl) || + CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined); + } + + return; + }).catch(() => { + // Ignore errors. + }); + + return handlerData; + } + + /** + * Returns if contents are loaded to show link button. + * + * @param module The module object. + * @param courseId The course ID. + * @return Resolved when done. + */ + protected async hideLinkButton(module: CoreCourseAnyModuleData, courseId: number): Promise { + try { + await CoreCourse.loadModuleContents(module, courseId, undefined, false, false, undefined, this.modName); + + return !(module.contents && module.contents[0] && module.contents[0].fileurl); + } catch { + // Module contents could not be loaded, most probably device is offline. + return true; + } + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise | undefined> { + return AddonModUrlIndexComponent; + } + +} +export const AddonModUrlModuleHandler = makeSingleton(AddonModUrlModuleHandlerService); diff --git a/src/addons/mod/url/services/handlers/prefetch.ts b/src/addons/mod/url/services/handlers/prefetch.ts new file mode 100644 index 000000000..d702def86 --- /dev/null +++ b/src/addons/mod/url/services/handlers/prefetch.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 { Injectable } from '@angular/core'; +import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; +import { AddonModUrlProvider } from '../url'; + +/** + * Handler to prefetch URLs. URLs cannot be prefetched, but the handler will be used to invalidate some data on course PTR. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModUrlPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase { + + name = 'AddonModUrl'; + modName = 'url'; + component = AddonModUrlProvider.COMPONENT; + + /** + * @inheritdoc + */ + async download(): Promise { + return; + } + + /** + * @inheritdoc + */ + invalidateModule(module: CoreCourseAnyModuleData): Promise { + return CoreCourse.invalidateModule(module.id, undefined, this.modName); + } + + /** + * @inheritdoc + */ + async isDownloadable(): Promise { + return false; // URLs aren't downloadable. + } + + /** + * @inheritdoc + */ + async prefetch(): Promise { + return; + } + +} +export const AddonModUrlPrefetchHandler = makeSingleton(AddonModUrlPrefetchHandlerService); diff --git a/src/addons/mod/url/services/url-helper.ts b/src/addons/mod/url/services/url-helper.ts new file mode 100644 index 000000000..076569e31 --- /dev/null +++ b/src/addons/mod/url/services/url-helper.ts @@ -0,0 +1,47 @@ +// (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 { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; + +/** + * Service that provides helper functions for urls. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModUrlHelperProvider { + + /** + * Opens a URL. + * + * @param url The URL to go to. + */ + async open(url: string): Promise { + const modal = await CoreDomUtils.showModalLoading(); + + try { + const treated = await CoreContentLinksHelper.handleLink(url, undefined, true, true); + + if (!treated) { + await CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url); + } + } finally { + modal.dismiss(); + } + } + +} +export const AddonModUrlHelper = makeSingleton(AddonModUrlHelperProvider); diff --git a/src/addons/mod/url/services/url.ts b/src/addons/mod/url/services/url.ts new file mode 100644 index 000000000..7890d9966 --- /dev/null +++ b/src/addons/mod/url/services/url.ts @@ -0,0 +1,297 @@ +// (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, CoreSitesCommonWSOptions } from '@services/sites'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { CoreConstants } from '@/core/constants'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreError } from '@classes/errors/error'; + +const ROOT_CACHE_KEY = 'mmaModUrl:'; + +/** + * Service that provides some features for urls. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModUrlProvider { + + static readonly COMPONENT = 'mmaModUrl'; + + /** + * Get the final display type for a certain URL. Based on Moodle's url_get_final_display_type. + * + * @param url URL data. + * @return Final display type. + */ + getFinalDisplayType(url?: AddonModUrlUrl): number { + if (!url) { + return -1; + } + + const extension = CoreMimetypeUtils.guessExtensionFromUrl(url.externalurl); + + // PDFs can be embedded in web, but not in the Mobile app. + if (url.display == CoreConstants.RESOURCELIB_DISPLAY_EMBED && extension == 'pdf') { + return CoreConstants.RESOURCELIB_DISPLAY_DOWNLOAD; + } + + if (url.display != CoreConstants.RESOURCELIB_DISPLAY_AUTO) { + return url.display; + } + + // Detect links to local moodle pages. + const currentSite = CoreSites.getCurrentSite(); + if (currentSite && currentSite.containsUrl(url.externalurl)) { + if (url.externalurl.indexOf('file.php') == -1 && url.externalurl.indexOf('.php') != -1) { + // Most probably our moodle page with navigation. + return CoreConstants.RESOURCELIB_DISPLAY_OPEN; + } + } + + const download = ['application/zip', 'application/x-tar', 'application/g-zip', 'application/pdf', 'text/html']; + let mimetype = CoreMimetypeUtils.getMimeType(extension); + + if (url.externalurl.indexOf('.php') != -1 || url.externalurl.substr(-1) === '/' || + (url.externalurl.indexOf('//') != -1 && url.externalurl.match(/\//g)?.length == 2)) { + // Seems to be a web, use HTML mimetype. + mimetype = 'text/html'; + } + + if (mimetype && download.indexOf(mimetype) != -1) { + return CoreConstants.RESOURCELIB_DISPLAY_DOWNLOAD; + } + + if (extension && CoreMimetypeUtils.canBeEmbedded(extension)) { + return CoreConstants.RESOURCELIB_DISPLAY_EMBED; + } + + // Let the browser deal with it somehow. + return CoreConstants.RESOURCELIB_DISPLAY_OPEN; + } + + /** + * Get cache key for url data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getUrlCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'url:' + courseId; + } + + /** + * Get a url data. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param options Other options. + * @return Promise resolved when the url is retrieved. + */ + protected async getUrlDataByKey( + courseId: number, + key: string, + value: number, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModUrlGetUrlsByCoursesWSParams = { + courseids: [courseId], + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUrlCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModUrlProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), + }; + + const response = await site.read('mod_url_get_urls_by_courses', params, preSets); + + const currentUrl = response.urls.find((url) => url[key] == value); + if (currentUrl) { + return currentUrl; + } + + throw new CoreError('Url not found'); + } + + /** + * Get a url by course module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the url is retrieved. + */ + getUrl(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getUrlDataByKey(courseId, 'coursemodule', cmId, options); + } + + /** + * Guess the icon for a certain URL. Based on Moodle's url_guess_icon. + * + * @param url URL to check. + * @return Icon, empty if it should use the default icon. + */ + guessIcon(url: string): string { + url = url || ''; + + const matches = url.match(/\//g); + const extension = CoreMimetypeUtils.getFileExtension(url); + + if (!matches || matches.length < 3 || url.substr(-1) === '/' || extension == 'php') { + // Use default icon. + return ''; + } + + const icon = CoreMimetypeUtils.getFileIcon(url); + + // We do not want to return those icon types, the module icon is more appropriate. + if (icon === CoreMimetypeUtils.getFileIconForType('unknown') || + icon === CoreMimetypeUtils.getFileIconForType('html')) { + return ''; + } + + return icon; + } + + /** + * Invalidate the prefetched content. + * + * @param moduleId The module ID. + * @param courseId Course ID of the module. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const promises: Promise[] = []; + + promises.push(this.invalidateUrlData(courseId, siteId)); + promises.push(CoreCourse.invalidateModule(moduleId, siteId, 'url')); + + return CoreUtils.allPromises(promises); + } + + /** + * Invalidates url data. + * + * @param courseid Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateUrlData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getUrlCacheKey(courseId)); + } + + /** + * Returns whether or not getUrl WS available or not. + * + * @return If WS is abalaible. + * @since 3.3 + */ + isGetUrlWSAvailable(): boolean { + return CoreSites.wsAvailableInCurrentSite('mod_url_get_urls_by_courses'); + } + + /** + * Report the url as being viewed. + * + * @param id Module ID. + * @param name Name of the assign. + * @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: AddonModUrlViewUrlWSParams = { + urlid: id, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_url_view_url', + params, + AddonModUrlProvider.COMPONENT, + id, + name, + 'url', + {}, + siteId, + ); + } + +} +export const AddonModUrl = makeSingleton(AddonModUrlProvider); + +/** + * Params of mod_url_get_urls_by_courses WS. + */ +type AddonModUrlGetUrlsByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Params of mod_url_view_url WS. + */ +type AddonModUrlViewUrlWSParams = { + urlid: number; // Url instance id. +}; + + +/** + * URL returnd by mod_url_get_urls_by_courses. + */ +export type AddonModUrlUrl = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // URL name. + intro: string; // Summary. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + externalurl: string; // External URL. + display: number; // How to display the url. + displayoptions: string; // Display options (width, height). + parameters: string; // Parameters to append to the URL. + timemodified: number; // Last time the url was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +/** + * Result of WS mod_url_get_urls_by_courses. + */ +export type AddonModUrlGetUrlsByCoursesResult = { + urls: AddonModUrlUrl[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Url Display options as object. + */ +export type AddonModUrlDisplayOptions = { + printintro?: boolean; +}; diff --git a/src/addons/mod/url/url-lazy.module.ts b/src/addons/mod/url/url-lazy.module.ts new file mode 100644 index 000000000..5f159faf8 --- /dev/null +++ b/src/addons/mod/url/url-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 { AddonModUrlComponentsModule } from './components/components.module'; +import { AddonModUrlIndexPage } from './pages/index/index.page'; + +const routes: Routes = [ + { + path: ':courseId/:cmId', + component: AddonModUrlIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonModUrlComponentsModule, + ], + declarations: [ + AddonModUrlIndexPage, + ], +}) +export class AddonModUrlLazyModule {} diff --git a/src/addons/mod/url/url.module.ts b/src/addons/mod/url/url.module.ts new file mode 100644 index 000000000..072753931 --- /dev/null +++ b/src/addons/mod/url/url.module.ts @@ -0,0 +1,53 @@ +// (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 } 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 { AddonModUrlComponentsModule } from './components/components.module'; +import { AddonModUrlIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModUrlListLinkHandler } from './services/handlers/list-link'; +import { AddonModUrlModuleHandler, AddonModUrlModuleHandlerService } from './services/handlers/module'; +import { AddonModUrlPrefetchHandler } from './services/handlers/prefetch'; + +const routes: Routes = [ + { + path: AddonModUrlModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./url-lazy.module').then(m => m.AddonModUrlLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModUrlComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModUrlModuleHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModUrlIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModUrlListLinkHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModUrlPrefetchHandler.instance); + }, + }, + ], +}) +export class AddonModUrlModule {} diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index daec4b9e6..0fe269dd8 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -1404,7 +1404,7 @@ export class CoreSite { * @param url URL to check. * @return Whether the URL belongs to this site. */ - containsUrl(url: string): boolean { + containsUrl(url?: string): boolean { if (!url) { return false; } diff --git a/src/core/services/utils/mimetype.ts b/src/core/services/utils/mimetype.ts index 7c57a147d..3985facdb 100644 --- a/src/core/services/utils/mimetype.ts +++ b/src/core/services/utils/mimetype.ts @@ -542,7 +542,11 @@ export class CoreMimetypeUtilsProvider { * @param groups List of groups to check. * @return Whether the extension belongs to any of the groups. */ - isExtensionInGroup(extension: string, groups: string[]): boolean { + isExtensionInGroup(extension: string | undefined, groups: string[]): boolean { + if (!extension) { + return false; + } + extension = this.cleanExtension(extension); if (groups?.length && this.extToMime[extension]?.groups) {