MOBILE-2235 h5p: Install H5P libraries and save content

main
Dani Palou 2019-11-07 17:30:37 +01:00
parent b9850b08dc
commit 93259097d6
6 changed files with 1093 additions and 39 deletions

View File

@ -148,6 +148,8 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
this.observer = this.eventsProvider.on(eventName, () => {
this.calculateState();
});
}).catch(() => {
// An error probably means the file cannot be downloaded or we cannot check it (offline).
});
return;

View File

@ -15,12 +15,14 @@
import { NgModule } from '@angular/core';
import { CoreH5PComponentsModule } from './components/components.module';
import { CoreH5PProvider } from './providers/h5p';
import { CoreH5PUtilsProvider } from './providers/utils';
import { CoreH5PPluginFileHandler } from './providers/pluginfile-handler';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
// List of providers (without handlers).
export const CORE_H5P_PROVIDERS: any[] = [
CoreH5PProvider
CoreH5PProvider,
CoreH5PUtilsProvider
];
@NgModule({
@ -30,6 +32,7 @@ export const CORE_H5P_PROVIDERS: any[] = [
],
providers: [
CoreH5PProvider,
CoreH5PUtilsProvider,
CoreH5PPluginFileHandler
],
exports: []

View File

@ -13,11 +13,15 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreFileProvider } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreH5PUtilsProvider } from './utils';
/**
* Service to provide H5P functionalities.
@ -25,15 +29,179 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
@Injectable()
export class CoreH5PProvider {
protected CONTENT_TABLE = 'h5p_content'; // H5P content.
protected LIBRARIES_TABLE = 'h5p_libraries'; // Installed libraries.
protected LIBRARY_DEPENDENCIES_TABLE = 'h5p_library_dependencies'; // Library dependencies.
protected siteSchema: CoreSiteSchema = {
name: 'CoreH5PProvider',
version: 1,
tables: [
{
name: this.CONTENT_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
autoIncrement: true
},
{
name: 'jsoncontent',
type: 'TEXT',
notNull: true
},
{
name: 'mainlibraryid',
type: 'INTEGER',
notNull: true
},
{
name: 'displayoptions',
type: 'INTEGER'
},
{
name: 'foldername',
type: 'TEXT',
notNull: true
},
{
name: 'filtered',
type: 'TEXT'
},
{
name: 'timecreated',
type: 'INTEGER',
notNull: true
},
{
name: 'timemodified',
type: 'INTEGER',
notNull: true
}
]
},
{
name: this.LIBRARIES_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
autoIncrement: true
},
{
name: 'machinename',
type: 'TEXT',
notNull: true
},
{
name: 'title',
type: 'TEXT',
notNull: true
},
{
name: 'majorversion',
type: 'INTEGER',
notNull: true
},
{
name: 'minorversion',
type: 'INTEGER',
notNull: true
},
{
name: 'patchversion',
type: 'INTEGER',
notNull: true
},
{
name: 'runnable',
type: 'INTEGER',
notNull: true
},
{
name: 'fullscreen',
type: 'INTEGER',
notNull: true
},
{
name: 'embedtypes',
type: 'TEXT',
notNull: true
},
{
name: 'preloadedjs',
type: 'TEXT'
},
{
name: 'preloadedcss',
type: 'TEXT'
},
{
name: 'droplibrarycss',
type: 'TEXT'
},
{
name: 'semantics',
type: 'TEXT'
},
{
name: 'addto',
type: 'TEXT'
}
]
},
{
name: this.LIBRARY_DEPENDENCIES_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
autoIncrement: true
},
{
name: 'libraryid',
type: 'INTEGER',
notNull: true
},
{
name: 'requiredlibraryid',
type: 'INTEGER',
notNull: true
},
{
name: 'dependencytype',
type: 'TEXT',
notNull: true
}
]
}
]
};
protected ROOT_CACHE_KEY = 'mmH5P:';
protected logger;
constructor(logger: CoreLoggerProvider,
eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider,
private textUtils: CoreTextUtilsProvider) {
private textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider,
private mimeUtils: CoreMimetypeUtilsProvider,
private h5pUtils: CoreH5PUtilsProvider) {
this.logger = logger.getInstance('CoreFilterProvider');
this.logger = logger.getInstance('CoreH5PProvider');
this.sitesProvider.registerSiteSchema(this.siteSchema);
eventsProvider.on(CoreEventsProvider.SITE_STORAGE_DELETED, (data) => {
this.deleteAllData(data.siteId).catch((error) => {
this.logger.error('Error deleting all H5P data from site.', error);
});
});
}
/**
@ -54,7 +222,7 @@ export class CoreH5PProvider {
*
* @param site Site. If not defined, current site.
* @return Promise resolved with true if ws is available, false otherwise.
* @since 3.4
* @since 3.8
*/
canGetTrustedH5PFileInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();
@ -62,6 +230,346 @@ export class CoreH5PProvider {
return site.wsAvailable('core_h5p_get_trusted_h5p_file');
}
/**
* Will clear filtered params for all the content that uses the specified libraries.
* This means that the content dependencies will have to be rebuilt and the parameters re-filtered.
*
* @param libraryIds Array of library ids.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected clearFilteredParameters(libraryIds: number[], siteId?: string): Promise<any> {
if (!libraryIds || !libraryIds.length) {
return Promise.resolve();
}
return this.sitesProvider.getSiteDb(siteId).then((db) => {
const whereAndParams = db.getInOrEqual(libraryIds);
whereAndParams[0] = 'mainlibraryid ' + whereAndParams[0];
return db.updateRecordsWhere(this.CONTENT_TABLE, { filtered: null }, whereAndParams[0], whereAndParams[1]);
});
}
/**
* Delete all the H5P data from the DB of a certain site.
*
* @param siteId Site ID.
* @return Promise resolved when done.
*/
protected deleteAllData(siteId: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return Promise.all([
db.deleteRecords(this.CONTENT_TABLE),
db.deleteRecords(this.LIBRARIES_TABLE),
db.deleteRecords(this.LIBRARY_DEPENDENCIES_TABLE)
]);
});
}
/**
* Delete content data from DB.
*
* @param id Content ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
deleteContentData(id: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.deleteRecords(this.CONTENT_TABLE, {id: id});
});
}
/**
* Delete library data from DB.
*
* @param id Library ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
deleteLibraryData(id: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.deleteRecords(this.LIBRARIES_TABLE, {id: id});
});
}
/**
* Delete all dependencies belonging to given library.
*
* @param libraryId Library ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
deleteLibraryDependencies(libraryId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.deleteRecords(this.LIBRARY_DEPENDENCIES_TABLE, {libraryid: libraryId});
});
}
/**
* Deletes a library from the file system.
*
* @param libraryData The library data.
* @param folderName Folder name. If not provided, it will be calculated.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
deleteLibraryFolder(libraryData: any, folderName?: string, siteId?: string): Promise<any> {
return this.fileProvider.removeDir(this.getLibraryFolderPath(libraryData, siteId, folderName));
}
/**
* Extract an H5P file. Some of this code was copied from the isValidPackage function in Moodle's H5PValidator.
* This function won't validate most things because it should've been done by the server already.
*
* @param fileUrl The file URL used to download the file.
* @param file The file entry of the downloaded file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
extractH5PFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Unzip the file.
const folderName = this.mimeUtils.removeExtension(file.name),
destFolder = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName);
// Make sure the dest dir doesn't exist already.
return this.fileProvider.removeDir(destFolder).catch(() => {
// Ignore errors.
}).then(() => {
return this.fileProvider.createDir(destFolder);
}).then(() => {
return this.fileProvider.unzipFile(file.toURL(), destFolder);
}).then(() => {
// Read the contents of the unzipped dir.
return this.fileProvider.getDirectoryContents(destFolder);
}).then((contents) => {
return this.processH5PFiles(destFolder, contents).then((data) => {
const content: any = {};
// Save the libraries that were processed.
return this.saveLibraries(data.librariesJsonData, siteId).then(() => {
// Now treat contents.
// Find main library version
for (const i in data.mainJsonData.preloadedDependencies) {
const dependency = data.mainJsonData.preloadedDependencies[i];
if (dependency.machineName === data.mainJsonData.mainLibrary) {
return this.getLibraryIdByData(dependency).then((id) => {
dependency.libraryId = id;
content.library = dependency;
});
}
}
}).then(() => {
// Save the content data in DB.
content.params = JSON.stringify(data.contentJsonData);
return this.saveContentData(content, folderName, siteId);
}).then(() => {
// Save the content files in their right place.
const contentPath = this.textUtils.concatenatePaths(destFolder, 'content');
return this.saveContentInFS(contentPath, folderName, siteId).catch((error) => {
// An error occurred, delete the DB data because the content data has been deleted.
return this.deleteContentData(content.id, siteId).catch(() => {
// Ignore errors.
}).then(() => {
return Promise.reject(error);
});
});
}).then(() => {
// Remove tmp folder.
return this.fileProvider.removeDir(destFolder).catch(() => {
// Ignore errors, it will be deleted eventually.
});
});
// @todo: Load content? It's done in the player construct.
});
});
}
/**
* Get a package content path.
*
* @param folderName Name of the folder of the H5P package.
* @param siteId The site ID.
* @return Folder path.
*/
getContentFolderPath(folderName: string, siteId: string): string {
return this.textUtils.concatenatePaths(this.fileProvider.getSiteFolder(siteId), 'h5p/packages/' + folderName + '/content');
}
/**
* Get library data. This code is based on the getLibraryData from Moodle's H5PValidator.
* This function won't validate most things because it should've been done by the server already.
*
* @param libDir Directory where the library files are.
* @param libPath Path to the directory where the library files are.
* @param h5pDir Path to the directory where this h5p files are.
* @return Library data.
*/
protected getLibraryData(libDir: DirectoryEntry, libPath: string, h5pDir: string): any {
const libraryJsonPath = this.textUtils.concatenatePaths(libPath, 'library.json'),
semanticsPath = this.textUtils.concatenatePaths(libPath, 'semantics.json'),
langPath = this.textUtils.concatenatePaths(libPath, 'language'),
iconPath = this.textUtils.concatenatePaths(libPath, 'icon.svg'),
promises = [];
let h5pData,
semanticsData,
langData,
hasIcon;
// Read the library json file.
promises.push(this.fileProvider.readFile(libraryJsonPath, CoreFileProvider.FORMATJSON).then((data) => {
h5pData = data;
}));
// Get library semantics if it exists.
promises.push(this.fileProvider.readFile(semanticsPath, CoreFileProvider.FORMATJSON).then((data) => {
semanticsData = data;
}).catch(() => {
// Probably doesn't exist, ignore.
}));
// Get language data if it exists.
promises.push(this.fileProvider.getDirectoryContents(langPath).then((entries) => {
const subPromises = [];
langData = {};
entries.forEach((entry) => {
const langFilePath = this.textUtils.concatenatePaths(langPath, entry.name);
subPromises.push(this.fileProvider.readFile(langFilePath, CoreFileProvider.FORMATJSON).then((data) => {
const parts = entry.name.split('.'); // The language code is in parts[0].
langData[parts[0]] = data;
}));
});
}).catch(() => {
// Probably doesn't exist, ignore.
}));
// Check if it has icon.
promises.push(this.fileProvider.getFile(iconPath).then(() => {
hasIcon = true;
}).catch(() => {
hasIcon = false;
}));
return Promise.all(promises).then(() => {
h5pData.semantics = semanticsData;
h5pData.language = langData;
h5pData.hasIcon = hasIcon;
return h5pData;
});
}
/**
* Get a library data stored in DB.
*
* @param machineName Machine name.
* @param majorVersion Major version number.
* @param minorVersion Minor version number.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the library data, rejected if not found.
*/
protected getLibrary(machineName: string, majorVersion?: string | number, minorVersion?: string | number, siteId?: string)
: Promise<CoreH5PLibraryDBData> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
const conditions: any = {
machinename: machineName
};
if (typeof majorVersion != 'undefined') {
conditions.majorversion = majorVersion;
}
if (typeof minorVersion != 'undefined') {
conditions.minorversion = minorVersion;
}
return db.getRecords(this.LIBRARIES_TABLE, conditions);
}).then((libraries) => {
if (!libraries.length) {
return Promise.reject(null);
}
return libraries[0];
});
}
/**
* Get a library data stored in DB.
*
* @param libraryData Library data.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the library data, rejected if not found.
*/
protected getLibraryByData(libraryData: any, siteId?: string): Promise<CoreH5PLibraryDBData> {
return this.getLibrary(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId);
}
/**
* Get a library ID. If not found, return null.
*
* @param machineName Machine name.
* @param majorVersion Major version number.
* @param minorVersion Minor version number.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the library ID, null if not found.
*/
protected getLibraryId(machineName: string, majorVersion?: string | number, minorVersion?: string | number, siteId?: string)
: Promise<number> {
return this.getLibrary(machineName, majorVersion, minorVersion, siteId).then((library) => {
return (library && library.id) || null;
}).catch(() => {
return null;
});
}
/**
* Get a library ID. If not found, return null.
*
* @param libraryData Library data.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the library ID, null if not found.
*/
protected getLibraryIdByData(libraryData: any, siteId?: string): Promise<number> {
return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId);
}
/**
* Get libraries folder path.
*
* @param siteId The site ID.
* @return Folder path.
*/
getLibrariesFolderPath(siteId: string): string {
return this.textUtils.concatenatePaths(this.fileProvider.getSiteFolder(siteId), 'h5p/lib');
}
/**
* Get a library's folder path.
*
* @param libraryData The library data.
* @param siteId The site ID.
* @param folderName Folder name. If not provided, it will be calculated.
* @return Folder path.
*/
getLibraryFolderPath(libraryData: any, siteId: string, folderName?: string): string {
if (!folderName) {
folderName = this.libraryToString(libraryData, true);
}
return this.textUtils.concatenatePaths(this.getLibrariesFolderPath(siteId), folderName);
}
/**
* Get a trusted H5P file.
*
@ -128,21 +636,6 @@ export class CoreH5PProvider {
return this.ROOT_CACHE_KEY + 'trustedH5PFile:';
}
/**
* Treat an H5P url before sending it to WS.
*
* @param url H5P file URL.
* @param siteUrl Site URL.
* @return Treated url.
*/
protected treatH5PUrl(url: string, siteUrl: string): string {
if (url.indexOf(this.textUtils.concatenatePaths(siteUrl, '/webservice/pluginfile.php')) === 0) {
url = url.replace('/webservice/pluginfile', '/pluginfile');
}
return url;
}
/**
* Invalidates all trusted H5P file WS calls.
*
@ -167,6 +660,362 @@ export class CoreH5PProvider {
return site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url));
});
}
/**
* Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}.
*
* @param libraryData Library data.
* @param folderName Use hyphen instead of space in returned string.
* @return String on the form {machineName} {majorVersion}.{minorVersion}.
*/
protected libraryToString(libraryData: any, folderName?: boolean): string {
return (libraryData.machineName ? libraryData.machineName : libraryData.name) + (folderName ? '-' : ' ') +
libraryData.majorVersion + '.' + libraryData.minorVersion;
}
/**
* Process libraries from an H5P library, getting the required data to save them.
* This code was copied from the isValidPackage function in Moodle's H5PValidator.
* This function won't validate most things because it should've been done by the server already.
*
* @param fileUrl The file URL used to download the file.
* @param file The file entry of the downloaded file.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected processH5PFiles(destFolder: string, entries: (DirectoryEntry | FileEntry)[])
: Promise<{librariesJsonData: any, mainJsonData: any, contentJsonData: any}> {
const promises = [],
libraries: any = {};
let contentJsonData,
mainH5PData;
// Read the h5p.json file.
const h5pJsonPath = this.textUtils.concatenatePaths(destFolder, 'h5p.json');
promises.push(this.fileProvider.readFile(h5pJsonPath, CoreFileProvider.FORMATJSON).then((data) => {
mainH5PData = data;
}));
// Read the content.json file.
const contentJsonPath = this.textUtils.concatenatePaths(destFolder, 'content/content.json');
promises.push(this.fileProvider.readFile(contentJsonPath, CoreFileProvider.FORMATJSON).then((data) => {
contentJsonData = data;
}));
// Treat libraries.
entries.forEach((entry) => {
if (entry.name[0] == '.' || entry.name[0] == '_' || entry.name == 'content' || entry.isFile) {
// Skip files, the content folder and any folder starting with a . or _.
return;
}
const libDirPath = this.textUtils.concatenatePaths(destFolder, entry.name);
promises.push(this.getLibraryData(<DirectoryEntry> entry, libDirPath, destFolder).then((libraryH5PData) => {
libraryH5PData.uploadDirectory = libDirPath;
libraries[this.libraryToString(libraryH5PData)] = libraryH5PData;
}));
});
return Promise.all(promises).then(() => {
return {
librariesJsonData: libraries,
mainJsonData: mainH5PData,
contentJsonData: contentJsonData
};
});
}
/**
* Save content data in DB and clear cache.
*
* @param content Content to save.
* @param folderName The name of the folder that contains the H5P.
* @return Promise resolved with content ID.
*/
protected saveContentData(content: any, folderName: string, siteId?: string): Promise<number> {
// Save in DB.
return this.sitesProvider.getSiteDb(siteId).then((db) => {
const data: any = {
jsoncontent: content.params,
displayoptions: content.disable,
mainlibraryid: content.library.libraryId,
timemodified: Date.now(),
filtered: null,
foldername: folderName
};
if (typeof content.id != 'undefined') {
data.id = content.id;
} else {
data.timecreated = data.timemodified;
}
return db.insertRecord(this.CONTENT_TABLE, data).then(() => {
if (!data.id) {
// New content. Get its ID.
return db.getRecord(this.CONTENT_TABLE, data).then((entry) => {
content.id = entry.id;
});
}
});
}).then(() => {
// If resetContentUserData is implemented in the future, it should be called in here.
return content.id;
});
}
/**
* Save the content in filesystem.
*
* @param contentPath Path to the current content folder (tmp).
* @param folderName Name to put to the content folder.
* @param siteId Site ID.
* @return Promise resolved when done.
*/
protected saveContentInFS(contentPath: string, folderName: string, siteId: string): Promise<any> {
const folderPath = this.getContentFolderPath(folderName, siteId);
// Delete existing content for this package.
return this.fileProvider.removeDir(folderPath).catch(() => {
// Ignore errors, maybe it doesn't exist.
}).then(() => {
// Copy the new one.
return this.fileProvider.moveDir(contentPath, folderPath);
});
}
/**
* Save libraries. This code is based on the saveLibraries function from Moodle's H5PStorage.
*
* @param librariesJsonData Data about libraries.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected saveLibraries(librariesJsonData: any, siteId?: string): Promise<any> {
const libraryIds = [];
// First of all, try to create the dir where the libraries are stored. This way we don't have to do it for each lib.
return this.fileProvider.createDir(this.getLibrariesFolderPath(siteId)).then(() => {
const promises = [];
// Go through libraries that came with this package.
for (const libString in librariesJsonData) {
const libraryData = librariesJsonData[libString];
// Find local library identifier
promises.push(this.getLibraryByData(libraryData).catch(() => {
// Not found.
}).then((dbData) => {
if (dbData) {
// Library already installed.
libraryData.libraryId = dbData.id;
if (libraryData.patchVersion <= dbData.patchversion) {
// Same or older version, no need to save.
libraryData.saveDependencies = false;
return;
}
}
libraryData.saveDependencies = true;
// Convert metadataSettings values to boolean and json_encode it before saving.
libraryData.metadataSettings = libraryData.metadataSettings ?
this.h5pUtils.boolifyAndEncodeMetadataSettings(libraryData.metadataSettings) : null;
// Save the library data in DB.
return this.saveLibraryData(libraryData, siteId).then(() => {
// Now save it in FS.
return this.saveLibraryInFS(libraryData, siteId).catch((error) => {
// An error occurred, delete the DB data because the lib FS data has been deleted.
return this.deleteLibraryData(libraryData.libraryId, siteId).catch(() => {
// Ignore errors.
}).then(() => {
return Promise.reject(error);
});
});
}).then(() => {
// @todo: Remove cached asses that use this library.
});
}));
}
return Promise.all(promises);
}).then(() => {
// Go through the libraries again to save dependencies.
const promises = [];
for (const libString in librariesJsonData) {
const libraryData = librariesJsonData[libString];
if (!libraryData.saveDependencies) {
continue;
}
libraryIds.push(libraryData.libraryId);
// Remove any old dependencies.
promises.push(this.deleteLibraryDependencies(libraryData.libraryId).then(() => {
// Insert the different new ones.
const subPromises = [];
if (typeof libraryData.preloadedDependencies != 'undefined') {
subPromises.push(this.saveLibraryDependencies(libraryData.libraryId, libraryData.preloadedDependencies,
'preloaded'));
}
if (typeof libraryData.dynamicDependencies != 'undefined') {
subPromises.push(this.saveLibraryDependencies(libraryData.libraryId, libraryData.dynamicDependencies,
'dynamic'));
}
if (typeof libraryData.editorDependencies != 'undefined') {
subPromises.push(this.saveLibraryDependencies(libraryData.libraryId, libraryData.editorDependencies,
'editor'));
}
return Promise.all(subPromises);
}));
}
return Promise.all(promises);
}).then(() => {
// Make sure dependencies, parameter filtering and export files get regenerated for content who uses these libraries.
if (libraryIds.length) {
return this.clearFilteredParameters(libraryIds, siteId);
}
});
}
/**
* Save a library in filesystem.
*
* @param libraryData Library data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected saveLibraryInFS(libraryData: any, siteId?: string): Promise<any> {
const folderPath = this.getLibraryFolderPath(libraryData, siteId);
// Delete existing library version.
return this.fileProvider.removeDir(folderPath).catch(() => {
// Ignore errors, maybe it doesn't exist.
}).then(() => {
// Copy the new one.
return this.fileProvider.moveDir(libraryData.uploadDirectory, folderPath, true);
});
}
/**
* Save library data in DB.
*
* @param libraryData Library data to save.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected saveLibraryData(libraryData: any, siteId?: string): Promise<any> {
// Some special properties needs some checking and converting before they can be saved.
const preloadedJS = this.h5pUtils.libraryParameterValuesToCsv(libraryData, 'preloadedJs', 'path'),
preloadedCSS = this.h5pUtils.libraryParameterValuesToCsv(libraryData, 'preloadedCss', 'path'),
dropLibraryCSS = this.h5pUtils.libraryParameterValuesToCsv(libraryData, 'dropLibraryCss', 'machineName');
if (typeof libraryData.semantics == 'undefined') {
libraryData.semantics = '';
}
if (typeof libraryData.fullscreen == 'undefined') {
libraryData.fullscreen = 0;
}
let embedTypes = '';
if (typeof libraryData.embedTypes != 'undefined') {
embedTypes = libraryData.embedTypes.join(', ');
}
return this.sitesProvider.getSite(siteId).then((site) => {
const db = site.getDb(),
data: any = {
title: libraryData.title,
machinename: libraryData.machineName,
majorversion: libraryData.majorVersion,
minorversion: libraryData.minorVersion,
patchversion: libraryData.patchVersion,
runnable: libraryData.runnable,
fullscreen: libraryData.fullscreen,
embedtypes: embedTypes,
preloadedjs: preloadedJS,
preloadedcss: preloadedCSS,
droplibrarycss: dropLibraryCSS,
semantics: libraryData.semantics,
addto: typeof libraryData.addTo != 'undefined' ? JSON.stringify(libraryData.addTo) : null,
};
if (libraryData.libraryId) {
data.id = libraryData.libraryId;
}
return db.insertRecord(this.LIBRARIES_TABLE, data).then(() => {
if (!data.id) {
// New library. Get its ID.
return db.getRecord(this.LIBRARIES_TABLE, data).then((entry) => {
libraryData.libraryId = entry.id;
});
} else {
// Updated libary. Remove old dependencies.
return this.deleteLibraryDependencies(data.id, site.getId());
}
});
});
}
/**
* Save what libraries a library is depending on.
*
* @param libraryId Library Id for the library we're saving dependencies for.
* @param dependencies List of dependencies as associative arrays containing machineName, majorVersion, minorVersion.
* @param dependencytype The type of dependency.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected saveLibraryDependencies(libraryId: number, dependencies: any[], dependencyType: string, siteId?: string)
: Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => {
const promises = [];
dependencies.forEach((dependency) => {
// Get the ID of the library.
promises.push(this.getLibraryIdByData(dependency, siteId).then((dependencyId) => {
// Create the relation.
const entry = {
libraryid: libraryId,
requiredlibraryid: dependencyId,
dependencytype: dependencyType
};
return db.insertRecord(this.LIBRARY_DEPENDENCIES_TABLE, entry);
}));
});
return Promise.all(promises);
});
}
/**
* Treat an H5P url before sending it to WS.
*
* @param url H5P file URL.
* @param siteUrl Site URL.
* @return Treated url.
*/
protected treatH5PUrl(url: string, siteUrl: string): string {
if (url.indexOf(this.textUtils.concatenatePaths(siteUrl, '/webservice/pluginfile.php')) === 0) {
url = url.replace('/webservice/pluginfile', '/pluginfile');
}
return url;
}
}
/**
@ -186,3 +1035,47 @@ export type CoreH5PGetTrustedH5PFileResult = {
files: CoreWSExternalFile[]; // Files.
warnings: CoreWSExternalWarning[]; // List of warnings.
};
/**
* Content data stored in DB.
*/
export type CoreH5PContentDBData = {
id: number; // The id of the content.
jsoncontent: string; // The content in json format.
mainlibraryid: number; // The library we first instantiate for this node.
displayoptions: number; // H5P Button display options.
foldername: string; // Name of the folder that contains the contents.
filtered: string; // Filtered version of json_content.
timecreated: number; // Time created.
timemodified: number; // Time modified.
};
/**
* Library data stored in DB.
*/
export type CoreH5PLibraryDBData = {
id: number; // The id of the library.
machinename: string; // The library machine name.
title: string; // The human readable name of this library.
majorversion: number; // Major version.
minorversion: number; // Minor version.
patchversion: number; // Patch version.
runnable: number; // Can this library be started by the module? I.e. not a dependency.
fullscreen: number; // Display fullscreen button.
embedtypes: string; // List of supported embed types.
preloadedjs?: string; // Comma separated list of scripts to load.
preloadedcss?: string; // Comma separated list of stylesheets to load.
droplibrarycss?: string; // List of libraries that should not have CSS included if this library is used. Comma separated list.
semantics?: string; // The semantics definition in json format.
addto?: string; // Plugin configuration data.
};
/**
* Library dependencies stored in DB.
*/
export type CoreH5PLibraryDependenciesDBData = {
id: number; // Id.
libraryid: number; // The id of an H5P library.
requiredlibraryid: number; // The dependent library to load.
dependencytype: string; // Type: preloaded, dynamic, or editor.
};

View File

@ -108,14 +108,6 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
* @return Promise resolved when done.
*/
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> {
// Unzip the file.
const destFolder = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER,
'h5p/' + this.mimeUtils.removeExtension(file.name));
return this.fileProvider.createDir(destFolder).then(() => {
return this.fileProvider.unzipFile(file.toURL(), destFolder);
}).then(() => {
// @todo: Deploy the package.
});
return this.h5pProvider.extractH5PFile(fileUrl, file, siteId);
}
}

View File

@ -0,0 +1,71 @@
// (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';
/**
* Utils service with helper functions for H5P.
*/
@Injectable()
export class CoreH5PUtilsProvider {
constructor() {
// Nothing to do.
}
/**
* The metadataSettings field in libraryJson uses 1 for true and 0 for false.
* Here we are converting these to booleans, and also doing JSON encoding.
*
* @param metadataSettings Settings.
* @return Stringified settings.
*/
boolifyAndEncodeMetadataSettings(metadataSettings: any): string {
// Convert metadataSettings values to boolean.
if (typeof metadataSettings.disable != 'undefined') {
metadataSettings.disable = metadataSettings.disable === 1;
}
if (typeof metadataSettings.disableExtraTitleField != 'undefined') {
metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1;
}
return JSON.stringify(metadataSettings);
}
/**
* Convert list of library parameter values to csv.
*
* @param libraryData Library data as found in library.json files.
* @param key Key that should be found in libraryData.
* @param searchParam The library parameter (Default: 'path').
* @return Library parameter values separated by ', '
*/
libraryParameterValuesToCsv(libraryData: any, key: string, searchParam: string = 'path'): string {
if (typeof libraryData[key] != 'undefined') {
const parameterValues = [];
libraryData[key].forEach((file) => {
for (const index in file) {
if (index === searchParam) {
parameterValues.push(file[index]);
}
}
});
return parameterValues.join(',');
}
return '';
}
}

View File

@ -52,6 +52,7 @@ export class CoreFileProvider {
static FORMATDATAURL = 1;
static FORMATBINARYSTRING = 2;
static FORMATARRAYBUFFER = 3;
static FORMATJSON = 4;
// Folders.
static SITESFOLDER = 'sites';
@ -491,6 +492,7 @@ export class CoreFileProvider {
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read.
*/
readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
@ -505,6 +507,16 @@ export class CoreFileProvider {
return this.file.readAsBinaryString(this.basePath, path);
case CoreFileProvider.FORMATARRAYBUFFER:
return this.file.readAsArrayBuffer(this.basePath, path);
case CoreFileProvider.FORMATJSON:
return this.file.readAsText(this.basePath, path).then((text) => {
const parsed = this.textUtils.parseJSON(text, null);
if (parsed == null && text != null) {
return Promise.reject('Error parsing JSON file: ' + path);
}
return parsed;
});
default:
return this.file.readAsText(this.basePath, path);
}
@ -519,6 +531,7 @@ export class CoreFileProvider {
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read.
*/
readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
@ -531,7 +544,18 @@ export class CoreFileProvider {
reader.onloadend = (evt): void => {
const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties.
if (target.result !== undefined || target.result !== null) {
if (format == CoreFileProvider.FORMATJSON) {
// Convert to object.
const parsed = this.textUtils.parseJSON(target.result, null);
if (parsed == null) {
reject('Error parsing JSON file.');
}
resolve(parsed);
} else {
resolve(target.result);
}
} else if (target.error !== undefined || target.error !== null) {
reject(target.error);
} else {
@ -728,19 +752,58 @@ export class CoreFileProvider {
}
}
/**
* Move a dir.
*
* @param originalPath Path to the dir to move.
* @param newPath New path of the dir.
* @param destDirExists Set it to true if you know the directory where to put the dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> {
return this.moveFileOrDir(originalPath, newPath, true);
}
/**
* Move a file.
*
* @param originalPath Path to the file to move.
* @param newPath New path of the file.
* @param destDirExists Set it to true if you know the directory where to put the file exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
moveFile(originalPath: string, newPath: string): Promise<any> {
moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> {
return this.moveFileOrDir(originalPath, newPath, false);
}
/**
* Move a file/dir.
*
* @param originalPath Path to the file/dir to move.
* @param newPath New path of the file/dir.
* @param isDir Whether it's a dir or a file.
* @param destDirExists Set it to true if you know the directory where to put the file/dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
protected moveFileOrDir(originalPath: string, newPath: string, isDir?: boolean, destDirExists?: boolean): Promise<any> {
const moveFn = isDir ? this.file.moveDir.bind(this.file) : this.file.moveFile.bind(this.file);
return this.init().then(() => {
// Remove basePath if it's in the paths.
originalPath = this.removeStartingSlash(originalPath.replace(this.basePath, ''));
newPath = this.removeStartingSlash(newPath.replace(this.basePath, ''));
const newPathFileAndDir = this.getFileAndDirectoryFromPath(newPath);
if (newPathFileAndDir.directory && !destDirExists) {
// Create the target directory if it doesn't exist.
return this.createDir(newPathFileAndDir.directory);
}
}).then(() => {
if (this.isHTMLAPI) {
// In Cordova API we need to calculate the longest matching path to make it work.
// The function this.file.moveFile('a/', 'b/c.ext', 'a/', 'b/d.ext') doesn't work.
@ -763,15 +826,15 @@ export class CoreFileProvider {
}
}
return this.file.moveFile(commonPath, originalPath, commonPath, newPath);
return moveFn(commonPath, originalPath, commonPath, newPath);
} else {
return this.file.moveFile(this.basePath, originalPath, this.basePath, newPath).catch((error) => {
return moveFn(this.basePath, originalPath, this.basePath, newPath).catch((error) => {
// The move can fail if the path has encoded characters. Try again if that's the case.
const decodedOriginal = decodeURI(originalPath),
decodedNew = decodeURI(newPath);
if (decodedOriginal != originalPath || decodedNew != newPath) {
return this.file.moveFile(this.basePath, decodedOriginal, this.basePath, decodedNew);
return moveFn(this.basePath, decodedOriginal, this.basePath, decodedNew);
} else {
return Promise.reject(error);
}
@ -780,16 +843,46 @@ export class CoreFileProvider {
});
}
/**
* Copy a directory.
*
* @param from Path to the directory to move.
* @param to New path of the directory.
* @param destDirExists Set it to true if you know the directory where to put the dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
copyDir(from: string, to: string, destDirExists?: boolean): Promise<any> {
return this.copyFileOrDir(from, to, true, destDirExists);
}
/**
* Copy a file.
*
* @param from Path to the file to move.
* @param to New path of the file.
* @param destDirExists Set it to true if you know the directory where to put the file exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
copyFile(from: string, to: string): Promise<any> {
copyFile(from: string, to: string, destDirExists?: boolean): Promise<any> {
return this.copyFileOrDir(from, to, false, destDirExists);
}
/**
* Copy a file or a directory.
*
* @param from Path to the file/dir to move.
* @param to New path of the file/dir.
* @param isDir Whether it's a dir or a file.
* @param destDirExists Set it to true if you know the directory where to put the file/dir exists. If false, the function will
* try to create it (slower).
* @return Promise resolved when the entry is copied.
*/
protected copyFileOrDir(from: string, to: string, isDir?: boolean, destDirExists?: boolean): Promise<any> {
let fromFileAndDir,
toFileAndDir;
const copyFn = isDir ? this.file.copyDir.bind(this.file) : this.file.copyFile.bind(this.file);
return this.init().then(() => {
// Paths cannot start with "/". Remove basePath if present.
@ -799,7 +892,7 @@ export class CoreFileProvider {
fromFileAndDir = this.getFileAndDirectoryFromPath(from);
toFileAndDir = this.getFileAndDirectoryFromPath(to);
if (toFileAndDir.directory) {
if (toFileAndDir.directory && !destDirExists) {
// Create the target directory if it doesn't exist.
return this.createDir(toFileAndDir.directory);
}
@ -809,15 +902,15 @@ export class CoreFileProvider {
const fromDir = this.textUtils.concatenatePaths(this.basePath, fromFileAndDir.directory),
toDir = this.textUtils.concatenatePaths(this.basePath, toFileAndDir.directory);
return this.file.copyFile(fromDir, fromFileAndDir.name, toDir, toFileAndDir.name);
return copyFn(fromDir, fromFileAndDir.name, toDir, toFileAndDir.name);
} else {
return this.file.copyFile(this.basePath, from, this.basePath, to).catch((error) => {
return copyFn(this.basePath, from, this.basePath, to).catch((error) => {
// The copy can fail if the path has encoded characters. Try again if that's the case.
const decodedFrom = decodeURI(from),
decodedTo = decodeURI(to);
if (from != decodedFrom || to != decodedTo) {
return this.file.copyFile(this.basePath, decodedFrom, this.basePath, decodedTo);
return copyFn(this.basePath, decodedFrom, this.basePath, decodedTo);
} else {
return Promise.reject(error);
}