MOBILE-3332 h5p: Allow disabling offline h5p

main
Dani Palou 2020-02-06 15:06:10 +01:00
parent 67da1cdeba
commit 9dcd84c903
12 changed files with 244 additions and 48 deletions

View File

@ -1619,6 +1619,7 @@
"core.h5p.offlineDialogRetryButtonLabel": "h5p", "core.h5p.offlineDialogRetryButtonLabel": "h5p",
"core.h5p.offlineDialogRetryMessage": "h5p", "core.h5p.offlineDialogRetryMessage": "h5p",
"core.h5p.offlineSuccessfulSubmit": "h5p", "core.h5p.offlineSuccessfulSubmit": "h5p",
"core.h5p.offlinedisabled": "local_moodlemobileapp",
"core.h5p.originator": "h5p", "core.h5p.originator": "h5p",
"core.h5p.pd": "h5p", "core.h5p.pd": "h5p",
"core.h5p.pddl": "h5p", "core.h5p.pddl": "h5p",

View File

@ -14,6 +14,7 @@
import { Component, Injector } from '@angular/core'; import { Component, Injector } from '@angular/core';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseProvider } from '@core/course/providers/course';
@ -38,10 +39,15 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
contentText: string; contentText: string;
displayDescription = true; displayDescription = true;
constructor(injector: Injector, private resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider, constructor(injector: Injector,
private appProvider: CoreAppProvider, private prefetchHandler: AddonModResourcePrefetchHandler, protected resourceProvider: AddonModResourceProvider,
private resourceHelper: AddonModResourceHelperProvider, private sitesProvider: CoreSitesProvider, protected courseProvider: CoreCourseProvider,
private utils: CoreUtilsProvider) { protected appProvider: CoreAppProvider,
protected prefetchHandler: AddonModResourcePrefetchHandler,
protected resourceHelper: AddonModResourceHelperProvider,
protected sitesProvider: CoreSitesProvider,
protected utils: CoreUtilsProvider,
protected filepoolProvider: CoreFilepoolProvider) {
super(injector); super(injector);
} }
@ -147,15 +153,23 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
/** /**
* Opens a file. * Opens a file.
*
* @return Promise resolved when done.
*/ */
open(): void { async open(): Promise<void> {
this.prefetchHandler.isDownloadable(this.module, this.courseId).then((downloadable) => { let downloadable = await this.prefetchHandler.isDownloadable(this.module, this.courseId);
if (downloadable) { if (downloadable) {
this.resourceHelper.openModuleFile(this.module, this.courseId); // Check if the main file is downloadle.
} else { // This isn't done in "isDownloadable" to prevent extra WS calls in the course page.
downloadable = await this.resourceHelper.isMainFileDownloadable(this.module);
if (downloadable) {
return this.resourceHelper.openModuleFile(this.module, this.courseId);
}
}
// The resource cannot be downloaded, open the activity in browser. // The resource cannot be downloaded, open the activity in browser.
return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(this.module.url); return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(this.module.url);
} }
});
}
} }

View File

@ -20,6 +20,7 @@ import { AddonModResourceProvider } from './resource';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreFileProvider } from '@providers/file'; import { CoreFileProvider } from '@providers/file';
import { CoreFileHelperProvider } from '@providers/file-helper';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -36,11 +37,17 @@ export class AddonModResourceHelperProvider {
// Display using object tag. // Display using object tag.
protected DISPLAY_EMBED = 1; protected DISPLAY_EMBED = 1;
constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, constructor(protected courseProvider: CoreCourseProvider,
private resourceProvider: AddonModResourceProvider, private courseHelper: CoreCourseHelperProvider, protected domUtils: CoreDomUtilsProvider,
private textUtils: CoreTextUtilsProvider, private mimetypeUtils: CoreMimetypeUtilsProvider, protected resourceProvider: AddonModResourceProvider,
private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider, protected courseHelper: CoreCourseHelperProvider,
private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider) { 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'; 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<boolean> {
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. * Check if the resource is a Nextcloud file.
* *

View File

@ -1619,6 +1619,7 @@
"core.h5p.offlineDialogRetryButtonLabel": "Retry now", "core.h5p.offlineDialogRetryButtonLabel": "Retry now",
"core.h5p.offlineDialogRetryMessage": "Retrying in :num....", "core.h5p.offlineDialogRetryMessage": "Retrying in :num....",
"core.h5p.offlineSuccessfulSubmit": "Successfully submitted results.", "core.h5p.offlineSuccessfulSubmit": "Successfully submitted results.",
"core.h5p.offlinedisabled": "The site doesn't allow downloading H5P packages.",
"core.h5p.originator": "Originator", "core.h5p.originator": "Originator",
"core.h5p.pd": "Public Domain", "core.h5p.pd": "Public Domain",
"core.h5p.pddl": "Public Domain Dedication and Licence", "core.h5p.pddl": "Public Domain Dedication and Licence",

View File

@ -20,6 +20,7 @@ import { CoreFileHelperProvider } from '@providers/file-helper';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
@ -57,16 +58,17 @@ export class CoreFileComponent implements OnInit, OnDestroy {
protected timemodified: number; protected timemodified: number;
protected observer; protected observer;
constructor(private sitesProvider: CoreSitesProvider, constructor(protected sitesProvider: CoreSitesProvider,
private utils: CoreUtilsProvider, protected utils: CoreUtilsProvider,
private domUtils: CoreDomUtilsProvider, protected domUtils: CoreDomUtilsProvider,
private filepoolProvider: CoreFilepoolProvider, protected filepoolProvider: CoreFilepoolProvider,
private appProvider: CoreAppProvider, protected appProvider: CoreAppProvider,
private fileHelper: CoreFileHelperProvider, protected fileHelper: CoreFileHelperProvider,
private mimeUtils: CoreMimetypeUtilsProvider, protected mimeUtils: CoreMimetypeUtilsProvider,
private eventsProvider: CoreEventsProvider, protected eventsProvider: CoreEventsProvider,
private textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
private pluginFileDelegate: CorePluginFileDelegate) { protected pluginFileDelegate: CorePluginFileDelegate,
protected urlUtils: CoreUrlUtilsProvider) {
this.onDelete = new EventEmitter(); this.onDelete = new EventEmitter();
} }
@ -104,6 +106,8 @@ export class CoreFileComponent implements OnInit, OnDestroy {
this.observer = this.eventsProvider.on(eventName, () => { this.observer = this.eventsProvider.on(eventName, () => {
this.calculateState(); this.calculateState();
}); });
}).catch(() => {
// File not downloadable.
}); });
} }
} }
@ -152,14 +156,14 @@ export class CoreFileComponent implements OnInit, OnDestroy {
return; return;
} }
if (!this.canDownload) { if (!this.canDownload || !this.state || this.state == CoreConstants.NOT_DOWNLOADABLE) {
// File cannot be downloaded, just open it. // File cannot be downloaded, just open it.
if (this.file.toURL) { if (this.file.toURL) {
// Local file. // Local file.
this.utils.openFile(this.file.toURL()); this.utils.openFile(this.file.toURL());
} else if (this.fileUrl) { } else if (this.fileUrl) {
if (this.fileUrl.indexOf('http') === 0) { if (this.fileUrl.indexOf('http') === 0) {
this.utils.openOnlineFile(this.fileUrl); this.utils.openOnlineFile(this.urlUtils.unfixPluginfileURL(this.fileUrl));
} else { } else {
this.utils.openFile(this.fileUrl); this.utils.openFile(this.fileUrl);
} }

View File

@ -73,7 +73,8 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent'); this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent');
this.site = sitesProvider.getCurrentSite(); this.site = sitesProvider.getCurrentSite();
this.siteId = this.site.getId(); this.siteId = this.site.getId();
this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles(); this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles() &&
!this.h5pProvider.isOfflineDisabledInSite();
} }
/** /**

View File

@ -64,6 +64,7 @@
"offlineDialogRetryButtonLabel": "Retry now", "offlineDialogRetryButtonLabel": "Retry now",
"offlineDialogRetryMessage": "Retrying in :num....", "offlineDialogRetryMessage": "Retrying in :num....",
"offlineSuccessfulSubmit": "Successfully submitted results.", "offlineSuccessfulSubmit": "Successfully submitted results.",
"offlinedisabled": "The site doesn't allow downloading H5P packages.",
"originator": "Originator", "originator": "Originator",
"pd": "Public Domain", "pd": "Public Domain",
"pddl": "Public Domain Dedication and Licence", "pddl": "Public Domain Dedication and Licence",

View File

@ -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<boolean> {
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. * Performs actions required when a library has been installed.
* *

View File

@ -22,6 +22,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreH5PProvider } from './h5p'; import { CoreH5PProvider } from './h5p';
import { CoreWSExternalFile } from '@providers/ws'; import { CoreWSExternalFile } from '@providers/ws';
import { FileEntry } from '@ionic-native/file'; import { FileEntry } from '@ionic-native/file';
import { TranslateService } from '@ngx-translate/core';
/** /**
* Handler to treat H5P files. * Handler to treat H5P files.
@ -35,7 +36,8 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
protected textUtils: CoreTextUtilsProvider, protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider, protected utils: CoreUtilsProvider,
protected fileProvider: CoreFileProvider, protected fileProvider: CoreFileProvider,
protected h5pProvider: CoreH5PProvider) { } protected h5pProvider: CoreH5PProvider,
protected translate: TranslateService) { }
/** /**
* React to a file being deleted. * React to a file being deleted.
@ -112,6 +114,28 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
return this.h5pProvider.canGetTrustedH5PFileInSite(); 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. * Check whether the file should be treated by this handler. It is used in functions where the component isn't used.
* *

View File

@ -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<boolean> {
const state = await this.getFileStateByUrl(siteId, fileUrl, timemodified, filePath, revision);
return state != CoreConstants.NOT_DOWNLOADABLE;
}
/** /**
* Check if a file is downloading. * Check if a file is downloading.
* *

View File

@ -84,6 +84,15 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
*/ */
getFileSize?(file: CoreWSExternalFile, siteId?: string): Promise<number>; getFileSize?(file: CoreWSExternalFile, siteId?: string): Promise<number>;
/**
* 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<CorePluginFileDownloadableResult>;
/** /**
* Check whether the file should be treated by this handler. It is used in functions where the component isn't used. * 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<any>; treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string): Promise<any>;
} }
/**
* 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. * Delegate to register pluginfile information handlers.
*/ */
@ -155,16 +179,22 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the file to use. Rejected if cannot download. * @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<CoreWSExternalFile> { : Promise<CoreWSExternalFile> {
if (handler && handler.getDownloadableFile) { const isDownloadable = await this.isFileDownloadable(file, siteId);
return handler.getDownloadableFile(file, siteId).then((newFile) => {
return newFile || file; 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. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the size. * @return Promise resolved with the size.
*/ */
getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> { async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
const isDownloadable = await this.isFileDownloadable(file, siteId);
if (!isDownloadable.downloadable) {
return 0;
}
const handler = this.getHandlerForFile(file); const handler = this.getHandlerForFile(file);
// First of all check if file can be downloaded. // First of all check if file can be downloaded.
return this.getHandlerDownloadableFile(file, handler, siteId).then((file) => { const downloadableFile = await this.getHandlerDownloadableFile(file, handler, siteId);
if (!file) { if (!downloadableFile) {
return 0; return 0;
} }
if (handler && handler.getFileSize) { if (handler && handler.getFileSize) {
return handler.getFileSize(file, siteId).catch(() => { try {
return file.filesize; const size = handler.getFileSize(downloadableFile, siteId);
});
return size;
} catch (error) {
// Ignore errors.
}
} }
return Promise.resolve(file.filesize); 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<CorePluginFileDownloadableResult> {
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. * Removes the revision number from a file URL.
* *

View File

@ -469,4 +469,32 @@ export class CoreUrlUtilsProvider {
return matches && matches[0]; 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(/&amp;/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;
}
} }