From 9dcd84c9031c20803859e67406c761d5c844a8a8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 6 Feb 2020 15:06:10 +0100 Subject: [PATCH] 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; + } }