From 9dcd84c9031c20803859e67406c761d5c844a8a8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 6 Feb 2020 15:06:10 +0100 Subject: [PATCH 1/2] MOBILE-3332 h5p: Allow disabling offline h5p --- scripts/langindex.json | 1 + .../mod/resource/components/index/index.ts | 36 ++++--- src/addon/mod/resource/providers/helper.ts | 34 ++++++- src/assets/lang/en.json | 1 + src/components/file/file.ts | 28 +++--- .../h5p/components/h5p-player/h5p-player.ts | 3 +- src/core/h5p/lang/en.json | 1 + src/core/h5p/providers/h5p.ts | 24 +++++ src/core/h5p/providers/pluginfile-handler.ts | 26 +++++- src/providers/filepool.ts | 17 ++++ src/providers/plugin-file-delegate.ts | 93 +++++++++++++++---- src/providers/utils/url.ts | 28 ++++++ 12 files changed, 244 insertions(+), 48 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 71c85af4c..c0970550a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1619,6 +1619,7 @@ "core.h5p.offlineDialogRetryButtonLabel": "h5p", "core.h5p.offlineDialogRetryMessage": "h5p", "core.h5p.offlineSuccessfulSubmit": "h5p", + "core.h5p.offlinedisabled": "local_moodlemobileapp", "core.h5p.originator": "h5p", "core.h5p.pd": "h5p", "core.h5p.pddl": "h5p", diff --git a/src/addon/mod/resource/components/index/index.ts b/src/addon/mod/resource/components/index/index.ts index 12913fe40..c05b4f15e 100644 --- a/src/addon/mod/resource/components/index/index.ts +++ b/src/addon/mod/resource/components/index/index.ts @@ -14,6 +14,7 @@ import { Component, Injector } from '@angular/core'; import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -38,10 +39,15 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource contentText: string; displayDescription = true; - constructor(injector: Injector, private resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider, - private appProvider: CoreAppProvider, private prefetchHandler: AddonModResourcePrefetchHandler, - private resourceHelper: AddonModResourceHelperProvider, private sitesProvider: CoreSitesProvider, - private utils: CoreUtilsProvider) { + constructor(injector: Injector, + protected resourceProvider: AddonModResourceProvider, + protected courseProvider: CoreCourseProvider, + protected appProvider: CoreAppProvider, + protected prefetchHandler: AddonModResourcePrefetchHandler, + protected resourceHelper: AddonModResourceHelperProvider, + protected sitesProvider: CoreSitesProvider, + protected utils: CoreUtilsProvider, + protected filepoolProvider: CoreFilepoolProvider) { super(injector); } @@ -147,15 +153,23 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource /** * Opens a file. + * + * @return Promise resolved when done. */ - open(): void { - this.prefetchHandler.isDownloadable(this.module, this.courseId).then((downloadable) => { + async open(): Promise { + let downloadable = await this.prefetchHandler.isDownloadable(this.module, this.courseId); + + if (downloadable) { + // Check if the main file is downloadle. + // This isn't done in "isDownloadable" to prevent extra WS calls in the course page. + downloadable = await this.resourceHelper.isMainFileDownloadable(this.module); + if (downloadable) { - this.resourceHelper.openModuleFile(this.module, this.courseId); - } else { - // The resource cannot be downloaded, open the activity in browser. - return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(this.module.url); + return this.resourceHelper.openModuleFile(this.module, this.courseId); } - }); + } + + // The resource cannot be downloaded, open the activity in browser. + return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(this.module.url); } } diff --git a/src/addon/mod/resource/providers/helper.ts b/src/addon/mod/resource/providers/helper.ts index 401783103..cdbf17270 100644 --- a/src/addon/mod/resource/providers/helper.ts +++ b/src/addon/mod/resource/providers/helper.ts @@ -20,6 +20,7 @@ import { AddonModResourceProvider } from './resource'; import { CoreSitesProvider } from '@providers/sites'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreFileProvider } from '@providers/file'; +import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreAppProvider } from '@providers/app'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -36,11 +37,17 @@ export class AddonModResourceHelperProvider { // Display using object tag. protected DISPLAY_EMBED = 1; - constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, - private resourceProvider: AddonModResourceProvider, private courseHelper: CoreCourseHelperProvider, - private textUtils: CoreTextUtilsProvider, private mimetypeUtils: CoreMimetypeUtilsProvider, - private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider, - private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider) { + constructor(protected courseProvider: CoreCourseProvider, + protected domUtils: CoreDomUtilsProvider, + protected resourceProvider: AddonModResourceProvider, + protected courseHelper: CoreCourseHelperProvider, + protected textUtils: CoreTextUtilsProvider, + protected mimetypeUtils: CoreMimetypeUtilsProvider, + protected fileProvider: CoreFileProvider, + protected appProvider: CoreAppProvider, + protected filepoolProvider: CoreFilepoolProvider, + protected sitesProvider: CoreSitesProvider, + protected fileHelper: CoreFileHelperProvider) { } /** @@ -136,6 +143,23 @@ export class AddonModResourceHelperProvider { return mimetype == 'text/html'; } + /** + * Check if main file of resource is downloadable. + * + * @param module Module instance. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether main file is downloadable. + */ + isMainFileDownloadable(module: any, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const mainFile = module.contents[0]; + const fileUrl = this.fileHelper.getFileUrl(mainFile); + const timemodified = this.fileHelper.getFileTimemodified(mainFile); + + return this.filepoolProvider.isFileDownloadable(siteId, fileUrl, timemodified); + } + /** * Check if the resource is a Nextcloud file. * diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 215946d1a..7d2dd4286 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1619,6 +1619,7 @@ "core.h5p.offlineDialogRetryButtonLabel": "Retry now", "core.h5p.offlineDialogRetryMessage": "Retrying in :num....", "core.h5p.offlineSuccessfulSubmit": "Successfully submitted results.", + "core.h5p.offlinedisabled": "The site doesn't allow downloading H5P packages.", "core.h5p.originator": "Originator", "core.h5p.pd": "Public Domain", "core.h5p.pddl": "Public Domain Dedication and Licence", diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 57c509107..22534fe16 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -20,6 +20,7 @@ import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreConstants } from '@core/constants'; @@ -57,16 +58,17 @@ export class CoreFileComponent implements OnInit, OnDestroy { protected timemodified: number; protected observer; - constructor(private sitesProvider: CoreSitesProvider, - private utils: CoreUtilsProvider, - private domUtils: CoreDomUtilsProvider, - private filepoolProvider: CoreFilepoolProvider, - private appProvider: CoreAppProvider, - private fileHelper: CoreFileHelperProvider, - private mimeUtils: CoreMimetypeUtilsProvider, - private eventsProvider: CoreEventsProvider, - private textUtils: CoreTextUtilsProvider, - private pluginFileDelegate: CorePluginFileDelegate) { + constructor(protected sitesProvider: CoreSitesProvider, + protected utils: CoreUtilsProvider, + protected domUtils: CoreDomUtilsProvider, + protected filepoolProvider: CoreFilepoolProvider, + protected appProvider: CoreAppProvider, + protected fileHelper: CoreFileHelperProvider, + protected mimeUtils: CoreMimetypeUtilsProvider, + protected eventsProvider: CoreEventsProvider, + protected textUtils: CoreTextUtilsProvider, + protected pluginFileDelegate: CorePluginFileDelegate, + protected urlUtils: CoreUrlUtilsProvider) { this.onDelete = new EventEmitter(); } @@ -104,6 +106,8 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.observer = this.eventsProvider.on(eventName, () => { this.calculateState(); }); + }).catch(() => { + // File not downloadable. }); } } @@ -152,14 +156,14 @@ export class CoreFileComponent implements OnInit, OnDestroy { return; } - if (!this.canDownload) { + if (!this.canDownload || !this.state || this.state == CoreConstants.NOT_DOWNLOADABLE) { // File cannot be downloaded, just open it. if (this.file.toURL) { // Local file. this.utils.openFile(this.file.toURL()); } else if (this.fileUrl) { if (this.fileUrl.indexOf('http') === 0) { - this.utils.openOnlineFile(this.fileUrl); + this.utils.openOnlineFile(this.urlUtils.unfixPluginfileURL(this.fileUrl)); } else { this.utils.openFile(this.fileUrl); } diff --git a/src/core/h5p/components/h5p-player/h5p-player.ts b/src/core/h5p/components/h5p-player/h5p-player.ts index 4804c4dbf..25153b69c 100644 --- a/src/core/h5p/components/h5p-player/h5p-player.ts +++ b/src/core/h5p/components/h5p-player/h5p-player.ts @@ -73,7 +73,8 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent'); this.site = sitesProvider.getCurrentSite(); this.siteId = this.site.getId(); - this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles(); + this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles() && + !this.h5pProvider.isOfflineDisabledInSite(); } /** diff --git a/src/core/h5p/lang/en.json b/src/core/h5p/lang/en.json index 923542f14..d661721f0 100644 --- a/src/core/h5p/lang/en.json +++ b/src/core/h5p/lang/en.json @@ -64,6 +64,7 @@ "offlineDialogRetryButtonLabel": "Retry now", "offlineDialogRetryMessage": "Retrying in :num....", "offlineSuccessfulSubmit": "Successfully submitted results.", + "offlinedisabled": "The site doesn't allow downloading H5P packages.", "originator": "Originator", "pd": "Public Domain", "pddl": "Public Domain Dedication and Licence", diff --git a/src/core/h5p/providers/h5p.ts b/src/core/h5p/providers/h5p.ts index 6750e226c..e332278d6 100644 --- a/src/core/h5p/providers/h5p.ts +++ b/src/core/h5p/providers/h5p.ts @@ -1876,6 +1876,30 @@ export class CoreH5PProvider { }); } + /** + * Check whether H5P offline is disabled. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether is disabled. + */ + async isOfflineDisabled(siteId?: string): Promise { + const site = await this.sitesProvider.getSite(siteId); + + return this.isOfflineDisabledInSite(site); + } + + /** + * Check whether H5P offline is disabled. + * + * @param site Site instance. If not defined, current site. + * @return Whether is disabled. + */ + isOfflineDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('NoDelegate_H5POffline'); + } + /** * Performs actions required when a library has been installed. * diff --git a/src/core/h5p/providers/pluginfile-handler.ts b/src/core/h5p/providers/pluginfile-handler.ts index 3a38e6381..8f0ca98d7 100644 --- a/src/core/h5p/providers/pluginfile-handler.ts +++ b/src/core/h5p/providers/pluginfile-handler.ts @@ -22,6 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreH5PProvider } from './h5p'; import { CoreWSExternalFile } from '@providers/ws'; import { FileEntry } from '@ionic-native/file'; +import { TranslateService } from '@ngx-translate/core'; /** * Handler to treat H5P files. @@ -35,7 +36,8 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler { protected textUtils: CoreTextUtilsProvider, protected utils: CoreUtilsProvider, protected fileProvider: CoreFileProvider, - protected h5pProvider: CoreH5PProvider) { } + protected h5pProvider: CoreH5PProvider, + protected translate: TranslateService) { } /** * React to a file being deleted. @@ -112,6 +114,28 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler { return this.h5pProvider.canGetTrustedH5PFileInSite(); } + /** + * Check if a file is downloadable. + * + * @param file The file data. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed. + */ + async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<{downloadable: boolean, reason?: string}> { + const offlineDisabled = await this.h5pProvider.isOfflineDisabled(siteId); + + if (offlineDisabled) { + return { + downloadable: false, + reason: this.translate.instant('core.h5p.offlinedisabled'), + }; + } else { + return { + downloadable: true, + }; + } + } + /** * Check whether the file should be treated by this handler. It is used in functions where the component isn't used. * diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 11e53f420..a4faa08f3 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -2385,6 +2385,23 @@ export class CoreFilepoolProvider { }); } + /** + * Check whether a file is downloadable. + * + * @param siteId The site ID. + * @param fileUrl File URL. + * @param timemodified The time this file was modified. + * @param filePath Filepath to download the file to. If defined, no extension will be added. + * @param revision File revision. If not defined, it will be calculated using the URL. + * @return Promise resolved with a boolean: whether a file is downloadable. + */ + async isFileDownloadable(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number) + : Promise { + const state = await this.getFileStateByUrl(siteId, fileUrl, timemodified, filePath, revision); + + return state != CoreConstants.NOT_DOWNLOADABLE; + } + /** * Check if a file is downloading. * diff --git a/src/providers/plugin-file-delegate.ts b/src/providers/plugin-file-delegate.ts index 469518698..42ee781a7 100644 --- a/src/providers/plugin-file-delegate.ts +++ b/src/providers/plugin-file-delegate.ts @@ -84,6 +84,15 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { */ getFileSize?(file: CoreWSExternalFile, siteId?: string): Promise; + /** + * Check if a file is downloadable. + * + * @param file The file data. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed. + */ + isFileDownloadable?(file: CoreWSExternalFile, siteId?: string): Promise; + /** * Check whether the file should be treated by this handler. It is used in functions where the component isn't used. * @@ -103,6 +112,21 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string): Promise; } +/** + * Data about if a file is downloadable. + */ +export type CorePluginFileDownloadableResult = { + /** + * Whether it's downloadable. + */ + downloadable: boolean; + + /** + * If not downloadable, the reason why it isn't. + */ + reason?: string; +}; + /** * Delegate to register pluginfile information handlers. */ @@ -155,16 +179,22 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the file to use. Rejected if cannot download. */ - protected getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string) + protected async getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string) : Promise { - if (handler && handler.getDownloadableFile) { - return handler.getDownloadableFile(file, siteId).then((newFile) => { - return newFile || file; - }); + const isDownloadable = await this.isFileDownloadable(file, siteId); + + if (!isDownloadable.downloadable) { + throw isDownloadable.reason; } - return Promise.resolve(file); + if (handler && handler.getDownloadableFile) { + const newFile = await handler.getDownloadableFile(file, siteId); + + return newFile || file; + } + + return file; } /** @@ -240,23 +270,32 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the size. */ - getFileSize(file: CoreWSExternalFile, siteId?: string): Promise { + async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise { + const isDownloadable = await this.isFileDownloadable(file, siteId); + + if (!isDownloadable.downloadable) { + return 0; + } + const handler = this.getHandlerForFile(file); // First of all check if file can be downloaded. - return this.getHandlerDownloadableFile(file, handler, siteId).then((file) => { - if (!file) { - return 0; - } + const downloadableFile = await this.getHandlerDownloadableFile(file, handler, siteId); + if (!downloadableFile) { + return 0; + } - if (handler && handler.getFileSize) { - return handler.getFileSize(file, siteId).catch(() => { - return file.filesize; - }); - } + if (handler && handler.getFileSize) { + try { + const size = handler.getFileSize(downloadableFile, siteId); - return Promise.resolve(file.filesize); - }); + return size; + } catch (error) { + // Ignore errors. + } + } + + return downloadableFile.filesize; } /** @@ -275,6 +314,24 @@ export class CorePluginFileDelegate extends CoreDelegate { } } + /** + * Check if a file is downloadable. + * + * @param file The file data. + * @param siteId Site ID. If not defined, current site. + * @return Promise with the data. + */ + isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise { + const handler = this.getHandlerForFile(file); + + if (handler && handler.isFileDownloadable) { + return handler.isFileDownloadable(file, siteId); + } + + // Default to true. + return Promise.resolve({downloadable: true}); + } + /** * Removes the revision number from a file URL. * diff --git a/src/providers/utils/url.ts b/src/providers/utils/url.ts index ec233b1c0..5c8c0e35a 100644 --- a/src/providers/utils/url.ts +++ b/src/providers/utils/url.ts @@ -469,4 +469,32 @@ export class CoreUrlUtilsProvider { return matches && matches[0]; } + + /** + * Modifies a pluginfile URL to use the default pluginfile script instead of the webservice one. + * + * @param url The url to be fixed. + * @param siteUrl The URL of the site the URL belongs to. + * @return Modified URL. + */ + unfixPluginfileURL(url: string, siteUrl?: string): string { + if (!url) { + return ''; + } + + url = url.replace(/&/g, '&'); + + // It site URL is supplied, check if the URL belongs to the site. + if (siteUrl && url.indexOf(this.textUtils.addEndingSlash(siteUrl)) !== 0) { + return url; + } + + // Not a pluginfile URL. Treat webservice/pluginfile case. + url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/'); + + // Make sure the URL doesn't contain the token. + url.replace(/([?&])token=[^&]*&?/, '$1'); + + return url; + } } From 3aea77ae0106f4c32b62a594221fc94ba5336bd9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 6 Feb 2020 16:01:36 +0100 Subject: [PATCH 2/2] MOBILE-3332 course: Display more data in errordownloadingsomefiles --- src/addon/mod/book/components/index/index.ts | 6 ++- src/addon/mod/imscp/components/index/index.ts | 6 ++- src/addon/mod/page/components/index/index.ts | 6 ++- .../mod/resource/components/index/index.ts | 6 ++- .../course/classes/main-resource-component.ts | 16 ++++++- .../courses/providers/course-link-handler.ts | 5 ++- src/providers/utils/text.ts | 44 ++++++++++++++++++- 7 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 40717a6fc..ee66c3b9a 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -119,6 +119,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp protected fetchContent(refresh?: boolean): Promise { const promises = []; let downloadFailed = false; + let downloadFailError; // Try to get the book data. promises.push(this.bookProvider.getBook(this.courseId, this.module.id).then((book) => { @@ -129,9 +130,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp })); // Download content. This function also loads module contents if needed. - promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch(() => { + promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch((error) => { // Mark download as failed but go on since the main files could have been downloaded. downloadFailed = true; + downloadFailError = error; if (!this.module.contents.length) { // Try to load module contents for offline usage. @@ -163,7 +165,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp return this.loadChapter(this.currentChapter).then(() => { if (downloadFailed && this.appProvider.isOnline()) { // We could load the main file but the download failed. Show error message. - this.domUtils.showErrorModal('core.errordownloadingsomefiles', true); + this.showErrorDownloadingSomeFiles(downloadFailError); } }).catch(() => { // Ignore errors, they're handled inside the loadChapter function. diff --git a/src/addon/mod/imscp/components/index/index.ts b/src/addon/mod/imscp/components/index/index.ts index c6690b324..a17aa8de4 100644 --- a/src/addon/mod/imscp/components/index/index.ts +++ b/src/addon/mod/imscp/components/index/index.ts @@ -76,6 +76,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom */ protected fetchContent(refresh?: boolean): Promise { let downloadFailed = false; + let downloadFailError; const promises = []; promises.push(this.imscpProvider.getImscp(this.courseId, this.module.id).then((imscp) => { @@ -83,9 +84,10 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom this.dataRetrieved.emit(imscp); })); - promises.push(this.imscpPrefetch.download(this.module, this.courseId).catch(() => { + promises.push(this.imscpPrefetch.download(this.module, this.courseId).catch((error) => { // Mark download as failed but go on since the main files could have been downloaded. downloadFailed = true; + downloadFailError = error; return this.courseProvider.loadModuleContents(this.module, this.courseId).catch((error) => { // Error getting module contents, fail. @@ -109,7 +111,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom }).then(() => { if (downloadFailed && this.appProvider.isOnline()) { // We could load the main file but the download failed. Show error message. - this.domUtils.showErrorModal('core.errordownloadingsomefiles', true); + this.showErrorDownloadingSomeFiles(downloadFailError); } }).finally(() => { diff --git a/src/addon/mod/page/components/index/index.ts b/src/addon/mod/page/components/index/index.ts index 9ee38717e..b8300246a 100644 --- a/src/addon/mod/page/components/index/index.ts +++ b/src/addon/mod/page/components/index/index.ts @@ -78,11 +78,13 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp */ protected fetchContent(refresh?: boolean): Promise { let downloadFailed = false; + let downloadFailError; // Download content. This function also loads module contents if needed. - return this.pagePrefetch.download(this.module, this.courseId).catch(() => { + return this.pagePrefetch.download(this.module, this.courseId).catch((error) => { // Mark download as failed but go on since the main files could have been downloaded. downloadFailed = true; + downloadFailError = error; }).then(() => { if (!this.module.contents.length) { // Try to load module contents for offline usage. @@ -132,7 +134,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp if (downloadFailed && this.appProvider.isOnline()) { // We could load the main file but the download failed. Show error message. - this.domUtils.showErrorModal('core.errordownloadingsomefiles', true); + this.showErrorDownloadingSomeFiles(downloadFailError); } })); diff --git a/src/addon/mod/resource/components/index/index.ts b/src/addon/mod/resource/components/index/index.ts index c05b4f15e..8875bae06 100644 --- a/src/addon/mod/resource/components/index/index.ts +++ b/src/addon/mod/resource/components/index/index.ts @@ -110,10 +110,12 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource if (this.resourceHelper.isDisplayedInIframe(this.module)) { let downloadFailed = false; + let downloadFailError; - return this.prefetchHandler.download(this.module, this.courseId).catch(() => { + return this.prefetchHandler.download(this.module, this.courseId).catch((error) => { // Mark download as failed but go on since the main files could have been downloaded. downloadFailed = true; + downloadFailError = error; }).then(() => { return this.resourceHelper.getIframeSrc(this.module).then((src) => { this.mode = 'iframe'; @@ -131,7 +133,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource if (downloadFailed && this.appProvider.isOnline()) { // We could load the main file but the download failed. Show error message. - this.domUtils.showErrorModal('core.errordownloadingsomefiles', true); + this.showErrorDownloadingSomeFiles(downloadFailError); } }); }); diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index 87691f7d8..50f432894 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -17,7 +17,7 @@ import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTextUtilsProvider, CoreTextErrorObject } from '@providers/utils/text'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseModuleMainComponent, CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreCourseSectionPage } from '@core/course/pages/section/section.ts'; @@ -265,6 +265,20 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId); } + /** + * Show an error occurred while downloading files. + * + * @param error The specific error. + */ + protected showErrorDownloadingSomeFiles(error: string | CoreTextErrorObject): void { + const errorMessage = this.textUtils.buildSeveralParagraphsMessage([ + this.translate.instant('core.errordownloadingsomefiles'), + error, + ]); + + this.domUtils.showErrorModal(errorMessage); + } + /** * Component being destroyed. */ diff --git a/src/core/courses/providers/course-link-handler.ts b/src/core/courses/providers/course-link-handler.ts index b94813f4c..c7fdd7df4 100644 --- a/src/core/courses/providers/course-link-handler.ts +++ b/src/core/courses/providers/course-link-handler.ts @@ -178,8 +178,9 @@ export class CoreCoursesCourseLinkHandler extends CoreContentLinksHandlerBase { error = this.translate.instant('core.courses.notenroled'); } - const body = this.translate.instant('core.twoparagraphs', - { p1: error, p2: this.translate.instant('core.confirmopeninbrowser') }); + const body = this.textUtils.buildSeveralParagraphsMessage( + [error, this.translate.instant('core.confirmopeninbrowser')]); + this.domUtils.showConfirm(body).then(() => { this.sitesProvider.getCurrentSite().openInBrowserWithAutoLogin(url); }).catch(() => { diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 0f1202161..8565b6dfc 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -18,6 +18,16 @@ import { ModalController, Platform } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreLangProvider } from '../lang'; +/** + * Different type of errors the app can treat. + */ +export type CoreTextErrorObject = { + message?: string; + error?: string; + content?: string; + body?: string; +}; + /* * "Utils" service with helper functions for text. */ @@ -122,6 +132,38 @@ export class CoreTextUtilsProvider { return result; } + /** + * Build a message with several paragraphs. + * + * @param paragraphs List of paragraphs. + * @return Built message. + */ + buildSeveralParagraphsMessage(paragraphs: (string | CoreTextErrorObject)[]): string { + // Filter invalid messages, and convert them to messages in case they're errors. + const messages: string[] = []; + + paragraphs.forEach((paragraph) => { + // If it's an error, get its message. + const message = this.getErrorMessageFromError(paragraph); + + if (paragraph) { + messages.push(message); + } + }); + + if (messages.length < 2) { + return messages[0] || ''; + } + + let builtMessage = messages[0]; + + for (let i = 1; i < messages.length; i++) { + builtMessage = this.translate.instant('core.twoparagraphs', { p1: builtMessage, p2: messages[i] }); + } + + return builtMessage; + } + /** * Convert size in bytes into human readable format * @@ -449,7 +491,7 @@ export class CoreTextUtilsProvider { * @param error Error object. * @return Error message, undefined if not found. */ - getErrorMessageFromError(error: any): string { + getErrorMessageFromError(error: string | CoreTextErrorObject): string { if (typeof error == 'string') { return error; }