diff --git a/src/addon/mod/url/components/index/addon-mod-url-index.html b/src/addon/mod/url/components/index/addon-mod-url-index.html index 7f4f8dd28..422095419 100644 --- a/src/addon/mod/url/components/index/addon-mod-url-index.html +++ b/src/addon/mod/url/components/index/addon-mod-url-index.html @@ -10,16 +10,31 @@ - + - -

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

-

{{ url }}

-
-
- - - {{ 'addon.mod_url.accessurl' | translate }} - +
+
+ + + + + + + + +

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

+

{{ url }}

+
+
+ + + {{ 'addon.mod_url.accessurl' | translate }} + +
+
diff --git a/src/addon/mod/url/components/index/index.scss b/src/addon/mod/url/components/index/index.scss new file mode 100644 index 000000000..a9d107124 --- /dev/null +++ b/src/addon/mod/url/components/index/index.scss @@ -0,0 +1,6 @@ +ion-app.app-root addon-mod-url-index { + + .addon-mod_url-embedded-url { + height: 100%; + } +} diff --git a/src/addon/mod/url/components/index/index.ts b/src/addon/mod/url/components/index/index.ts index d1602299f..589de7cbf 100644 --- a/src/addon/mod/url/components/index/index.ts +++ b/src/addon/mod/url/components/index/index.ts @@ -13,10 +13,13 @@ // limitations under the License. import { Component, Injector } from '@angular/core'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModUrlProvider } from '../../providers/url'; import { AddonModUrlHelperProvider } from '../../providers/helper'; +import { CoreConstants } from '@core/constants'; /** * Component that displays a url. @@ -30,9 +33,19 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo canGetUrl: boolean; url: string; + name: string; + shouldEmbed = false; + shouldIframe = false; + isImage = false; + isAudio = false; + isVideo = false; + isOther = false; + mimetype: string; + displayDescription = true; constructor(injector: Injector, private urlProvider: AddonModUrlProvider, private courseProvider: CoreCourseProvider, - private urlHelper: AddonModUrlHelperProvider) { + private urlHelper: AddonModUrlHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider, + private sitesProvider: CoreSitesProvider) { super(injector); } @@ -65,6 +78,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo protected fetchContent(refresh?: boolean): Promise { let canGetUrl = this.canGetUrl, mod, + url, promise; // Fetch the module data. @@ -79,10 +93,18 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo // Fallback in case is not prefetched or not available. return this.courseProvider.getModule(this.module.id, this.courseId, undefined, false, false, undefined, 'url'); - }).then((url) => { + }).then((urlData) => { + url = urlData; + + this.name = url.name || this.module.name; this.description = url.intro || url.description; this.dataRetrieved.emit(url); + if (canGetUrl && url.displayoptions) { + const unserialized = this.textUtils.unserialize(url.displayoptions); + this.displayDescription = typeof unserialized.printintro == 'undefined' || !!unserialized.printintro; + } + if (!canGetUrl) { mod = url; @@ -101,9 +123,50 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo }).then(() => { // Always use the URL from the module because it already includes the parameters. this.url = mod.contents && mod.contents[0] && mod.contents[0].fileurl ? mod.contents[0].fileurl : undefined; + + if (canGetUrl) { + return this.calculateDisplayOptions(url); + } }); } + /** + * Calculate the display options to determine how the URL should be rendered. + * + * @param {any} url Object with the URL data. + * @return {Promise} Promise resolved when done. + */ + protected calculateDisplayOptions(url: any): Promise { + const displayType = this.urlProvider.getFinalDisplayType(url); + + this.shouldEmbed = displayType == CoreConstants.RESOURCELIB_DISPLAY_EMBED; + this.shouldIframe = displayType == CoreConstants.RESOURCELIB_DISPLAY_FRAME; + + if (this.shouldEmbed) { + const extension = this.mimeUtils.guessExtensionFromUrl(url.externalurl); + + this.mimetype = this.mimeUtils.getMimeType(extension); + this.isImage = this.mimeUtils.isExtensionInGroup(extension, ['web_image']); + this.isAudio = this.mimeUtils.isExtensionInGroup(extension, ['web_audio']); + this.isVideo = this.mimeUtils.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 = this.sitesProvider.getCurrentSite(); + + if (currentSite && currentSite.containsUrl(this.url)) { + // Format the URL to add auto-login. + return currentSite.getAutoLoginUrl(this.url, false).then((url) => { + this.url = url; + }); + } + } + + return Promise.resolve(); + } + /** * Opens a file. */ diff --git a/src/addon/mod/url/providers/module-handler.ts b/src/addon/mod/url/providers/module-handler.ts index 57480c0d5..e182343fb 100644 --- a/src/addon/mod/url/providers/module-handler.ts +++ b/src/addon/mod/url/providers/module-handler.ts @@ -14,11 +14,14 @@ import { Injectable } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { AddonModUrlIndexComponent } from '../components/index/index'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonModUrlProvider } from './url'; import { AddonModUrlHelperProvider } from './helper'; +import { CoreConstants } from '@core/constants'; /** * Handler to support url modules. @@ -29,7 +32,8 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler { modName = 'url'; constructor(private courseProvider: CoreCourseProvider, private urlProvider: AddonModUrlProvider, - private urlHelper: AddonModUrlHelperProvider) { } + private urlHelper: AddonModUrlHelperProvider, private domUtils: CoreDomUtilsProvider, + private contentLinksHelper: CoreContentLinksHelperProvider) { } /** * Check if the handler is enabled on a site level. @@ -49,33 +53,65 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler { * @return {CoreCourseModuleHandlerData} Data to render the module. */ getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + // tslint:disable: no-this-assignment + const handler = this; const handlerData = { icon: this.courseProvider.getModuleIconSrc(this.modName), title: module.name, class: 'addon-mod_url-handler', showDownloadButton: false, action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { - navCtrl.push('AddonModUrlIndexPage', {module: module, courseId: courseId}, options); + const modal = handler.domUtils.showModalLoading(); + + // First of all, check if the URL can be handled by the app. If so, always open it directly. + handler.contentLinksHelper.canHandleLink(module.contents[0].fileurl, courseId).then((canHandle) => { + if (canHandle) { + // URL handled by the app, open it directly. + return true; + } + + // Not handled by the app, check the display type. + if (handler.urlProvider.isGetUrlWSAvailable()) { + return handler.urlProvider.getUrl(courseId, module.id).catch(() => { + // Ignore errors. + }).then((url) => { + const displayType = handler.urlProvider.getFinalDisplayType(url); + + return displayType == CoreConstants.RESOURCELIB_DISPLAY_OPEN || + displayType == CoreConstants.RESOURCELIB_DISPLAY_POPUP; + }); + } else { + return false; + } + + }).then((shouldOpen) => { + if (shouldOpen) { + handler.openUrl(module, courseId); + } else { + navCtrl.push('AddonModUrlIndexPage', {module: module, courseId: courseId}, options); + } + }).finally(() => { + modal.dismiss(); + }); }, buttons: [ { - hidden: !(module.contents && module.contents[0] && module.contents[0].fileurl), + hidden: true, // Hide it until we calculate if it should be displayed or not. icon: 'link', label: 'core.openinbrowser', action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => { - this.hideLinkButton(module, courseId).then((hide) => { - if (!hide) { - this.urlProvider.logView(module.instance).then(() => { - this.courseProvider.checkModuleCompletion(courseId, module.completionstatus); - }); - this.urlHelper.open(module.contents[0].fileurl); - } - }); + handler.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 = this.urlProvider.guessIcon(module.contents[0].fileurl) || + this.courseProvider.getModuleIconSrc(this.modName); + } }); return handlerData; @@ -106,4 +142,17 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler { getMainComponent(course: any, module: any): any { return AddonModUrlIndexComponent; } + + /** + * Open the URL. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + */ + protected openUrl(module: any, courseId: number): void { + this.urlProvider.logView(module.instance).then(() => { + this.courseProvider.checkModuleCompletion(courseId, module.completionstatus); + }); + this.urlHelper.open(module.contents[0].fileurl); + } } diff --git a/src/addon/mod/url/providers/url.ts b/src/addon/mod/url/providers/url.ts index 19750f8c7..c1642b6ca 100644 --- a/src/addon/mod/url/providers/url.ts +++ b/src/addon/mod/url/providers/url.ts @@ -15,8 +15,10 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreConstants } from '@core/constants'; /** * Service that provides some features for urls. @@ -29,10 +31,62 @@ export class AddonModUrlProvider { protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, - private utils: CoreUtilsProvider) { + private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider) { this.logger = logger.getInstance('AddonModUrlProvider'); } + /** + * Get the final display type for a certain URL. Based on Moodle's url_get_final_display_type. + * + * @param {any} url URL data. + * @return {number} Final display type. + */ + getFinalDisplayType(url: any): number { + if (!url) { + return -1; + } + + const extension = this.mimeUtils.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 = this.sitesProvider.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 = this.mimeUtils.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 (download.indexOf(mimetype) != -1) { + return CoreConstants.RESOURCELIB_DISPLAY_DOWNLOAD; + } + + if (this.mimeUtils.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. * @@ -88,6 +142,33 @@ export class AddonModUrlProvider { return this.getUrlDataByKey(courseId, 'coursemodule', cmId, siteId); } + /** + * Guess the icon for a certain URL. Based on Moodle's url_guess_icon. + * + * @param {string} url URL to check. + * @return {string} Icon, empty if it should use the default icon. + */ + guessIcon(url: string): string { + url = url || ''; + + const matches = url.match(/\//g), + extension = this.mimeUtils.getFileExtension(url); + + if (!matches || matches.length < 3 || url.substr(-1) === '/' || extension == 'php') { + // Use default icon. + return ''; + } + + const icon = this.mimeUtils.getFileIcon(url); + + // We do not want to return those icon types, the module icon is more appropriate. + if (icon === this.mimeUtils.getFileIconForType('unknown') || icon === this.mimeUtils.getFileIconForType('html')) { + return ''; + } + + return icon; + } + /** * Invalidate the prefetched content. * diff --git a/src/core/constants.ts b/src/core/constants.ts index ac25f07b6..f93bb41d5 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -47,4 +47,13 @@ export class CoreConstants { static NOT_DOWNLOADED = 'notdownloaded'; static OUTDATED = 'outdated'; static NOT_DOWNLOADABLE = 'notdownloadable'; + + // Constants from Moodle's resourcelib. + static RESOURCELIB_DISPLAY_AUTO = 0; // Try the best way. + static RESOURCELIB_DISPLAY_EMBED = 1; // Display using object tag. + static RESOURCELIB_DISPLAY_FRAME = 2; // Display inside frame. + static RESOURCELIB_DISPLAY_NEW = 3; // Display normal link in new window. + static RESOURCELIB_DISPLAY_DOWNLOAD = 4; // Force download of file instead of display. + static RESOURCELIB_DISPLAY_OPEN = 5; // Open directly. + static RESOURCELIB_DISPLAY_POPUP = 6; // Open in "emulated" pop-up without navigation. } diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index 8cd2660d8..ad3d9b6f5 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -47,6 +47,22 @@ export class CoreContentLinksHelperProvider { eventsProvider.on(CoreEventsProvider.APP_LAUNCHED_URL, this.handleCustomUrl.bind(this)); } + /** + * Check whether a link can be handled by the app. + * + * @param {string} url URL to handle. + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @param {string} [username] Username to use to filter sites. + * @return {Promise} Promise resolved with a boolean: whether the URL can be handled. + */ + canHandleLink(url: string, courseId?: number, username?: string): Promise { + return this.contentLinksDelegate.getActionsFor(url, undefined, username).then((actions) => { + return !!this.getFirstValidAction(actions); + }).catch(() => { + return false; + }); + } + /** * Get the first valid action in a list of actions. * diff --git a/src/providers/utils/mimetype.ts b/src/providers/utils/mimetype.ts index a1bce44ce..9df797d94 100644 --- a/src/providers/utils/mimetype.ts +++ b/src/providers/utils/mimetype.ts @@ -186,7 +186,7 @@ export class CoreMimetypeUtilsProvider { } } - return 'assets/img/files/' + icon + '-64.png'; + return this.getFileIconForType(icon); } /** @@ -198,6 +198,16 @@ export class CoreMimetypeUtilsProvider { return 'assets/img/files/folder-64.png'; } + /** + * Given a type (audio, video, html, ...), return its file icon path. + * + * @param {string} type The type to get the icon. + * @return {string} The icon path. + */ + getFileIconForType(type: string): string { + return 'assets/img/files/' + type + '-64.png'; + } + /** * Guess the extension of a file from its URL. * This is very weak and unreliable.