MOBILE-2235 h5p: Install H5P libraries and save content
parent
b9850b08dc
commit
93259097d6
|
@ -148,6 +148,8 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.observer = this.eventsProvider.on(eventName, () => {
|
this.observer = this.eventsProvider.on(eventName, () => {
|
||||||
this.calculateState();
|
this.calculateState();
|
||||||
});
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
// An error probably means the file cannot be downloaded or we cannot check it (offline).
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -15,12 +15,14 @@
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CoreH5PComponentsModule } from './components/components.module';
|
import { CoreH5PComponentsModule } from './components/components.module';
|
||||||
import { CoreH5PProvider } from './providers/h5p';
|
import { CoreH5PProvider } from './providers/h5p';
|
||||||
|
import { CoreH5PUtilsProvider } from './providers/utils';
|
||||||
import { CoreH5PPluginFileHandler } from './providers/pluginfile-handler';
|
import { CoreH5PPluginFileHandler } from './providers/pluginfile-handler';
|
||||||
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
|
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
|
||||||
|
|
||||||
// List of providers (without handlers).
|
// List of providers (without handlers).
|
||||||
export const CORE_H5P_PROVIDERS: any[] = [
|
export const CORE_H5P_PROVIDERS: any[] = [
|
||||||
CoreH5PProvider
|
CoreH5PProvider,
|
||||||
|
CoreH5PUtilsProvider
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -30,6 +32,7 @@ export const CORE_H5P_PROVIDERS: any[] = [
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CoreH5PProvider,
|
CoreH5PProvider,
|
||||||
|
CoreH5PUtilsProvider,
|
||||||
CoreH5PPluginFileHandler
|
CoreH5PPluginFileHandler
|
||||||
],
|
],
|
||||||
exports: []
|
exports: []
|
||||||
|
|
|
@ -13,11 +13,15 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreFileProvider } from '@providers/file';
|
||||||
import { CoreLoggerProvider } from '@providers/logger';
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
|
||||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
|
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
|
||||||
|
import { CoreH5PUtilsProvider } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to provide H5P functionalities.
|
* Service to provide H5P functionalities.
|
||||||
|
@ -25,15 +29,179 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CoreH5PProvider {
|
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 ROOT_CACHE_KEY = 'mmH5P:';
|
||||||
|
|
||||||
protected logger;
|
protected logger;
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider,
|
constructor(logger: CoreLoggerProvider,
|
||||||
|
eventsProvider: CoreEventsProvider,
|
||||||
private sitesProvider: CoreSitesProvider,
|
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.
|
* @param site Site. If not defined, current site.
|
||||||
* @return Promise resolved with true if ws is available, false otherwise.
|
* @return Promise resolved with true if ws is available, false otherwise.
|
||||||
* @since 3.4
|
* @since 3.8
|
||||||
*/
|
*/
|
||||||
canGetTrustedH5PFileInSite(site?: CoreSite): boolean {
|
canGetTrustedH5PFileInSite(site?: CoreSite): boolean {
|
||||||
site = site || this.sitesProvider.getCurrentSite();
|
site = site || this.sitesProvider.getCurrentSite();
|
||||||
|
@ -62,6 +230,346 @@ export class CoreH5PProvider {
|
||||||
return site.wsAvailable('core_h5p_get_trusted_h5p_file');
|
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.
|
* Get a trusted H5P file.
|
||||||
*
|
*
|
||||||
|
@ -128,21 +636,6 @@ export class CoreH5PProvider {
|
||||||
return this.ROOT_CACHE_KEY + 'trustedH5PFile:';
|
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.
|
* Invalidates all trusted H5P file WS calls.
|
||||||
*
|
*
|
||||||
|
@ -167,6 +660,362 @@ export class CoreH5PProvider {
|
||||||
return site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url));
|
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.
|
files: CoreWSExternalFile[]; // Files.
|
||||||
warnings: CoreWSExternalWarning[]; // List of warnings.
|
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.
|
||||||
|
};
|
||||||
|
|
|
@ -108,14 +108,6 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> {
|
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> {
|
||||||
// Unzip the file.
|
return this.h5pProvider.extractH5PFile(fileUrl, file, siteId);
|
||||||
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.
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ export class CoreFileProvider {
|
||||||
static FORMATDATAURL = 1;
|
static FORMATDATAURL = 1;
|
||||||
static FORMATBINARYSTRING = 2;
|
static FORMATBINARYSTRING = 2;
|
||||||
static FORMATARRAYBUFFER = 3;
|
static FORMATARRAYBUFFER = 3;
|
||||||
|
static FORMATJSON = 4;
|
||||||
|
|
||||||
// Folders.
|
// Folders.
|
||||||
static SITESFOLDER = 'sites';
|
static SITESFOLDER = 'sites';
|
||||||
|
@ -491,6 +492,7 @@ export class CoreFileProvider {
|
||||||
* FORMATDATAURL
|
* FORMATDATAURL
|
||||||
* FORMATBINARYSTRING
|
* FORMATBINARYSTRING
|
||||||
* FORMATARRAYBUFFER
|
* FORMATARRAYBUFFER
|
||||||
|
* FORMATJSON
|
||||||
* @return Promise to be resolved when the file is read.
|
* @return Promise to be resolved when the file is read.
|
||||||
*/
|
*/
|
||||||
readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
|
readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
|
||||||
|
@ -505,6 +507,16 @@ export class CoreFileProvider {
|
||||||
return this.file.readAsBinaryString(this.basePath, path);
|
return this.file.readAsBinaryString(this.basePath, path);
|
||||||
case CoreFileProvider.FORMATARRAYBUFFER:
|
case CoreFileProvider.FORMATARRAYBUFFER:
|
||||||
return this.file.readAsArrayBuffer(this.basePath, path);
|
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:
|
default:
|
||||||
return this.file.readAsText(this.basePath, path);
|
return this.file.readAsText(this.basePath, path);
|
||||||
}
|
}
|
||||||
|
@ -519,6 +531,7 @@ export class CoreFileProvider {
|
||||||
* FORMATDATAURL
|
* FORMATDATAURL
|
||||||
* FORMATBINARYSTRING
|
* FORMATBINARYSTRING
|
||||||
* FORMATARRAYBUFFER
|
* FORMATARRAYBUFFER
|
||||||
|
* FORMATJSON
|
||||||
* @return Promise to be resolved when the file is read.
|
* @return Promise to be resolved when the file is read.
|
||||||
*/
|
*/
|
||||||
readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
|
readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise<any> {
|
||||||
|
@ -531,7 +544,18 @@ export class CoreFileProvider {
|
||||||
reader.onloadend = (evt): void => {
|
reader.onloadend = (evt): void => {
|
||||||
const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties.
|
const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties.
|
||||||
if (target.result !== undefined || target.result !== null) {
|
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);
|
resolve(target.result);
|
||||||
|
}
|
||||||
} else if (target.error !== undefined || target.error !== null) {
|
} else if (target.error !== undefined || target.error !== null) {
|
||||||
reject(target.error);
|
reject(target.error);
|
||||||
} else {
|
} 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.
|
* Move a file.
|
||||||
*
|
*
|
||||||
* @param originalPath Path to the file to move.
|
* @param originalPath Path to the file to move.
|
||||||
* @param newPath New path of the file.
|
* @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.
|
* @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(() => {
|
return this.init().then(() => {
|
||||||
// Remove basePath if it's in the paths.
|
// Remove basePath if it's in the paths.
|
||||||
originalPath = this.removeStartingSlash(originalPath.replace(this.basePath, ''));
|
originalPath = this.removeStartingSlash(originalPath.replace(this.basePath, ''));
|
||||||
newPath = this.removeStartingSlash(newPath.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) {
|
if (this.isHTMLAPI) {
|
||||||
// In Cordova API we need to calculate the longest matching path to make it work.
|
// 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.
|
// 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 {
|
} 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.
|
// The move can fail if the path has encoded characters. Try again if that's the case.
|
||||||
const decodedOriginal = decodeURI(originalPath),
|
const decodedOriginal = decodeURI(originalPath),
|
||||||
decodedNew = decodeURI(newPath);
|
decodedNew = decodeURI(newPath);
|
||||||
|
|
||||||
if (decodedOriginal != originalPath || decodedNew != 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 {
|
} else {
|
||||||
return Promise.reject(error);
|
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.
|
* Copy a file.
|
||||||
*
|
*
|
||||||
* @param from Path to the file to move.
|
* @param from Path to the file to move.
|
||||||
* @param to New path of the file.
|
* @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.
|
* @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,
|
let fromFileAndDir,
|
||||||
toFileAndDir;
|
toFileAndDir;
|
||||||
|
const copyFn = isDir ? this.file.copyDir.bind(this.file) : this.file.copyFile.bind(this.file);
|
||||||
|
|
||||||
return this.init().then(() => {
|
return this.init().then(() => {
|
||||||
// Paths cannot start with "/". Remove basePath if present.
|
// Paths cannot start with "/". Remove basePath if present.
|
||||||
|
@ -799,7 +892,7 @@ export class CoreFileProvider {
|
||||||
fromFileAndDir = this.getFileAndDirectoryFromPath(from);
|
fromFileAndDir = this.getFileAndDirectoryFromPath(from);
|
||||||
toFileAndDir = this.getFileAndDirectoryFromPath(to);
|
toFileAndDir = this.getFileAndDirectoryFromPath(to);
|
||||||
|
|
||||||
if (toFileAndDir.directory) {
|
if (toFileAndDir.directory && !destDirExists) {
|
||||||
// Create the target directory if it doesn't exist.
|
// Create the target directory if it doesn't exist.
|
||||||
return this.createDir(toFileAndDir.directory);
|
return this.createDir(toFileAndDir.directory);
|
||||||
}
|
}
|
||||||
|
@ -809,15 +902,15 @@ export class CoreFileProvider {
|
||||||
const fromDir = this.textUtils.concatenatePaths(this.basePath, fromFileAndDir.directory),
|
const fromDir = this.textUtils.concatenatePaths(this.basePath, fromFileAndDir.directory),
|
||||||
toDir = this.textUtils.concatenatePaths(this.basePath, toFileAndDir.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 {
|
} 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.
|
// The copy can fail if the path has encoded characters. Try again if that's the case.
|
||||||
const decodedFrom = decodeURI(from),
|
const decodedFrom = decodeURI(from),
|
||||||
decodedTo = decodeURI(to);
|
decodedTo = decodeURI(to);
|
||||||
|
|
||||||
if (from != decodedFrom || to != decodedTo) {
|
if (from != decodedFrom || to != decodedTo) {
|
||||||
return this.file.copyFile(this.basePath, decodedFrom, this.basePath, decodedTo);
|
return copyFn(this.basePath, decodedFrom, this.basePath, decodedTo);
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue