MOBILE-3666 h5p: Implement services and classes

main
Dani Palou 2020-12-16 14:28:59 +01:00
parent c83ff34ae0
commit f1ac735abf
16 changed files with 5712 additions and 16 deletions

View File

@ -1087,7 +1087,7 @@ export class SQLiteDB {
}
export type SQLiteDBRecordValues = {
[key in string ]: SQLiteDBRecordValue | undefined;
[key in string ]: SQLiteDBRecordValue | undefined | null;
};
export type SQLiteDBQueryParams = {

View File

@ -18,6 +18,7 @@ import { CoreCourseModule } from './course/course.module';
import { CoreCoursesModule } from './courses/courses.module';
import { CoreEmulatorModule } from './emulator/emulator.module';
import { CoreFileUploaderModule } from './fileuploader/fileuploader.module';
import { CoreH5PModule } from './h5p/h5p.module';
import { CoreLoginModule } from './login/login.module';
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
import { CoreSettingsModule } from './settings/settings.module';
@ -41,6 +42,7 @@ import { CoreXAPIModule } from './xapi/xapi.module';
CoreUserModule,
CorePushNotificationsModule,
CoreXAPIModule,
CoreH5PModule,
],
})
export class CoreFeaturesModule {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,475 @@
// (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 { CoreFile } from '@services/file';
import { CoreFilepool } from '@services/filepool';
import { CoreSites } from '@services/sites';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import {
CoreH5PCore,
CoreH5PDependencyAsset,
CoreH5PContentDependencyData,
CoreH5PDependenciesFiles,
CoreH5PLibraryBasicData,
CoreH5PContentMainLibraryData,
} from './core';
import { CONTENTS_LIBRARIES_TABLE_NAME, CONTENT_TABLE_NAME, CoreH5PLibraryCachedAssetsDBRecord } from '../services/database/h5p';
import { CoreH5PLibraryBeingSaved } from './storage';
/**
* Equivalent to Moodle's implementation of H5PFileStorage.
*/
export class CoreH5PFileStorage {
static readonly CACHED_ASSETS_FOLDER_NAME = 'cachedassets';
/**
* Will concatenate all JavaScrips and Stylesheets into two files in order to improve page performance.
*
* @param files A set of all the assets required for content to display.
* @param key Hashed key for cached asset.
* @param folderName Name of the folder of the H5P package.
* @param siteId The site ID.
* @return Promise resolved when done.
*/
async cacheAssets(files: CoreH5PDependenciesFiles, key: string, folderName: string, siteId: string): Promise<void> {
const cachedAssetsPath = this.getCachedAssetsFolderPath(folderName, siteId);
// Treat each type in the assets.
await Promise.all(Object.keys(files).map(async (type) => {
const assets: CoreH5PDependencyAsset[] = files[type];
if (!assets || !assets.length) {
return;
}
// Create new file for cached assets.
const fileName = key + '.' + (type == 'scripts' ? 'js' : 'css');
const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsPath, fileName);
// Store concatenated content.
const content = await this.concatenateFiles(assets, type);
await CoreFile.instance.writeFile(path, content);
// Now update the files data.
files[type] = [
{
path: CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, fileName),
version: '',
},
];
}));
}
/**
* Adds all files of a type into one file.
*
* @param assets A list of files.
* @param type The type of files in assets. Either 'scripts' or 'styles'
* @return Promise resolved with all of the files content in one string.
*/
protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> {
const basePath = CoreFile.instance.convertFileSrc(CoreFile.instance.getBasePathInstant());
let content = '';
for (const i in assets) {
const asset = assets[i];
let fileContent = await CoreFile.instance.readFile(asset.path);
if (type == 'scripts') {
// No need to treat scripts, just append the content.
content += fileContent + ';\n';
continue;
}
// Rewrite relative URLs used inside stylesheets.
const matches = fileContent.match(/url\(['"]?([^"')]+)['"]?\)/ig);
const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes.
const treated = {};
if (matches && matches.length) {
matches.forEach((match) => {
let url = match.replace(/(url\(['"]?|['"]?\)$)/ig, '');
if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) {
return; // Not relative or already treated, skip.
}
const pathSplit = assetPath.split('/');
treated[url] = url;
/* Find "../" in the URL. If it exists, we have to remove "../" and switch the last folder in the
filepath for the first folder in the url. */
if (url.match(/^\.\.\//)) {
// Split and remove empty values.
const urlSplit = url.split('/').filter((i) => i);
// Remove the file name from the asset path.
pathSplit.pop();
// Remove the first element from the file URL: ../ .
urlSplit.shift();
// Put the url's first folder into the asset path.
pathSplit[pathSplit.length - 1] = urlSplit[0];
urlSplit.shift();
// Create the new URL and replace it in the file contents.
url = pathSplit.join('/') + '/' + urlSplit.join('/');
} else {
pathSplit[pathSplit.length - 1] = url; // Put the whole path to the end of the asset path.
url = pathSplit.join('/');
}
fileContent = fileContent.replace(
new RegExp(CoreTextUtils.instance.escapeForRegex(match), 'g'),
'url("' + CoreTextUtils.instance.concatenatePaths(basePath, url) + '")',
);
});
}
content += fileContent + '\n';
}
return content;
}
/**
* Delete cached assets from file system.
*
* @param libraryId Library identifier.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteCachedAssets(removedEntries: CoreH5PLibraryCachedAssetsDBRecord[], siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
const promises: Promise<void>[] = [];
removedEntries.forEach((entry) => {
const cachedAssetsFolder = this.getCachedAssetsFolderPath(entry.foldername, site.getId());
['js', 'css'].forEach((type) => {
const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsFolder, entry.hash + '.' + type);
promises.push(CoreFile.instance.removeFile(path));
});
});
// Ignore errors, maybe there's no cached asset of some type.
await CoreUtils.instance.ignoreErrors(CoreUtils.instance.allPromises(promises));
}
/**
* Deletes a content folder from the file system.
*
* @param folderName Folder name of the content.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentFolder(folderName: string, siteId: string): Promise<void> {
await CoreFile.instance.removeDir(this.getContentFolderPath(folderName, siteId));
}
/**
* Delete content indexes from filesystem.
*
* @param folderName Name of the folder of the H5P package.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentIndex(folderName: string, siteId: string): Promise<void> {
await CoreFile.instance.removeFile(this.getContentIndexPath(folderName, siteId));
}
/**
* Delete content indexes from filesystem.
*
* @param libraryId Library identifier.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentIndexesForLibrary(libraryId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
const db = site.getDb();
// Get the folder names of all the packages that use this library.
const query = 'SELECT DISTINCT hc.foldername ' +
'FROM ' + CONTENTS_LIBRARIES_TABLE_NAME + ' hcl ' +
'JOIN ' + CONTENT_TABLE_NAME + ' hc ON hcl.h5pid = hc.id ' +
'WHERE hcl.libraryid = ?';
const queryArgs = [libraryId];
const result = await db.execute(query, queryArgs);
await Array.from(result.rows).map(async (entry: {foldername: string}) => {
try {
// Delete the index.html.
await this.deleteContentIndex(entry.foldername, site.getId());
} catch (error) {
// Ignore errors.
}
});
}
/**
* Deletes a library from the file system.
*
* @param libraryData The library data.
* @param siteId Site ID.
* @param folderName Folder name. If not provided, it will be calculated.
* @return Promise resolved when done.
*/
async deleteLibraryFolder(
libraryData: CoreH5PLibraryBasicData | CoreH5PContentMainLibraryData,
siteId: string,
folderName?: string,
): Promise<void> {
await CoreFile.instance.removeDir(this.getLibraryFolderPath(libraryData, siteId, folderName));
}
/**
* Will check if there are cache assets available for content.
*
* @param key Hashed key for cached asset
* @return Promise resolved with the files.
*/
async getCachedAssets(key: string): Promise<{scripts?: CoreH5PDependencyAsset[]; styles?: CoreH5PDependencyAsset[]} | null> {
// Get JS and CSS cached assets if they exist.
const results = await Promise.all([
this.getCachedAsset(key, '.js'),
this.getCachedAsset(key, '.css'),
]);
const files = {
scripts: results[0],
styles: results[1],
};
return files.scripts || files.styles ? files : null;
}
/**
* Check if a cached asset file exists and, if so, return its data.
*
* @param key Key of the cached asset.
* @param extension Extension of the file to get.
* @return Promise resolved with the list of assets (only one), undefined if not found.
*/
protected async getCachedAsset(key: string, extension: string): Promise<CoreH5PDependencyAsset[] | undefined> {
try {
const path = CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, key + extension);
const size = await CoreFile.instance.getFileSize(path);
if (size > 0) {
return [
{
path: path,
version: '',
},
];
}
} catch (error) {
// Not found, nothing to do.
}
}
/**
* Get relative path to a content cached assets.
*
* @param folderName Name of the folder of the content the assets belong to.
* @param siteId Site ID.
* @return Path.
*/
getCachedAssetsFolderPath(folderName: string, siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(
this.getContentFolderPath(folderName, siteId),
CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME,
);
}
/**
* Get a content folder name given the package URL.
*
* @param fileUrl Package URL.
* @param siteId Site ID.
* @return Promise resolved with the folder name.
*/
async getContentFolderNameByUrl(fileUrl: string, siteId: string): Promise<string> {
const path = await CoreFilepool.instance.getFilePathByUrl(siteId, fileUrl);
const fileAndDir = CoreFile.instance.getFileAndDirectoryFromPath(path);
return CoreMimetypeUtils.instance.removeExtension(fileAndDir.name);
}
/**
* 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 CoreTextUtils.instance.concatenatePaths(
this.getExternalH5PFolderPath(siteId),
'packages/' + folderName + '/content',
);
}
/**
* Get the content index file.
*
* @param fileUrl URL of the H5P package.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the file URL if exists, rejected otherwise.
*/
async getContentIndexFileUrl(fileUrl: string, siteId?: string): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const folderName = await this.getContentFolderNameByUrl(fileUrl, siteId);
const file = await CoreFile.instance.getFile(this.getContentIndexPath(folderName, siteId));
return file.toURL();
}
/**
* Get the path to a content index.
*
* @param folderName Name of the folder of the H5P package.
* @param siteId The site ID.
* @return Folder path.
*/
getContentIndexPath(folderName: string, siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(this.getContentFolderPath(folderName, siteId), 'index.html');
}
/**
* Get the path to the folder that contains the H5P core libraries.
*
* @return Folder path.
*/
getCoreH5PPath(): string {
return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getWWWPath(), '/h5p/');
}
/**
* Get the path to the dependency.
*
* @param dependency Dependency library.
* @return The path to the dependency library
*/
getDependencyPath(dependency: CoreH5PContentDependencyData): string {
return 'libraries/' + dependency.machineName + '-' + dependency.majorVersion + '.' + dependency.minorVersion;
}
/**
* Get path to the folder containing H5P files extracted from packages.
*
* @param siteId The site ID.
* @return Folder path.
*/
getExternalH5PFolderPath(siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getSiteFolder(siteId), 'h5p');
}
/**
* Get libraries folder path.
*
* @param siteId The site ID.
* @return Folder path.
*/
getLibrariesFolderPath(siteId: string): string {
return CoreTextUtils.instance.concatenatePaths(this.getExternalH5PFolderPath(siteId), 'libraries');
}
/**
* 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: CoreH5PLibraryBasicData | CoreH5PContentMainLibraryData,
siteId: string,
folderName?: string,
): string {
if (!folderName) {
folderName = CoreH5PCore.libraryToString(libraryData, true);
}
return CoreTextUtils.instance.concatenatePaths(this.getLibrariesFolderPath(siteId), folderName);
}
/**
* 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.
*/
async saveContent(contentPath: string, folderName: string, siteId: string): Promise<void> {
const folderPath = this.getContentFolderPath(folderName, siteId);
// Delete existing content for this package.
await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(folderPath));
// Copy the new one.
await CoreFile.instance.moveDir(contentPath, folderPath);
}
/**
* Save a library in filesystem.
*
* @param libraryData Library data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async saveLibrary(libraryData: CoreH5PLibraryBeingSaved, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const folderPath = this.getLibraryFolderPath(libraryData, siteId);
// Delete existing library version.
try {
await CoreFile.instance.removeDir(folderPath);
} catch (error) {
// Ignore errors, maybe it doesn't exist.
}
if (libraryData.uploadDirectory) {
// Copy the new one.
await CoreFile.instance.moveDir(libraryData.uploadDirectory, folderPath, true);
}
}
}

View File

@ -0,0 +1,917 @@
// (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 { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreH5P } from '@features/h5p/services/h5p';
import {
CoreH5PCore,
CoreH5PDisplayOptionBehaviour,
CoreH5PContentDependencyData,
CoreH5PLibraryData,
CoreH5PLibraryAddonData,
CoreH5PContentDepsTreeDependency,
CoreH5PLibraryBasicData,
CoreH5PLibraryBasicDataWithPatch,
} from './core';
import {
CONTENT_TABLE_NAME,
LIBRARIES_CACHEDASSETS_TABLE_NAME,
CoreH5PLibraryCachedAssetsDBRecord,
LIBRARIES_TABLE_NAME,
LIBRARY_DEPENDENCIES_TABLE_NAME,
CONTENTS_LIBRARIES_TABLE_NAME,
CoreH5PContentDBRecord,
CoreH5PLibraryDBRecord,
CoreH5PLibraryDependencyDBRecord,
CoreH5PContentsLibraryDBRecord,
} from '../services/database/h5p';
import { CoreError } from '@classes/errors/error';
import { CoreH5PSemantics } from './content-validator';
import { CoreH5PContentBeingSaved, CoreH5PLibraryBeingSaved } from './storage';
import { CoreH5PLibraryAddTo } from './validator';
/**
* Equivalent to Moodle's implementation of H5PFrameworkInterface.
*/
export class CoreH5PFramework {
/**
* 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.
*/
async clearFilteredParameters(libraryIds: number[], siteId?: string): Promise<void> {
if (!libraryIds || !libraryIds.length) {
return;
}
const db = await CoreSites.instance.getSiteDb(siteId);
const whereAndParams = db.getInOrEqual(libraryIds);
whereAndParams[0] = 'mainlibraryid ' + whereAndParams[0];
await db.updateRecordsWhere(CONTENT_TABLE_NAME, { filtered: null }, whereAndParams[0], whereAndParams[1]);
}
/**
* Delete cached assets from DB.
*
* @param libraryId Library identifier.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the removed entries.
*/
async deleteCachedAssets(libraryId: number, siteId?: string): Promise<CoreH5PLibraryCachedAssetsDBRecord[]> {
const db = await CoreSites.instance.getSiteDb(siteId);
// Get all the hashes that use this library.
const entries = await db.getRecords<CoreH5PLibraryCachedAssetsDBRecord>(
LIBRARIES_CACHEDASSETS_TABLE_NAME,
{ libraryid: libraryId },
);
const hashes = entries.map((entry) => entry.hash);
if (hashes.length) {
// Delete the entries from DB.
await db.deleteRecordsList(LIBRARIES_CACHEDASSETS_TABLE_NAME, 'hash', hashes);
}
return entries;
}
/**
* Delete content data from DB.
*
* @param id Content ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentData(id: number, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
await Promise.all([
// Delete the content data.
db.deleteRecords(CONTENT_TABLE_NAME, { id }),
// Remove content library dependencies.
this.deleteLibraryUsage(id, siteId),
]);
}
/**
* Delete library data from DB.
*
* @param id Library ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteLibrary(id: number, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
await db.deleteRecords(LIBRARIES_TABLE_NAME, { 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.
*/
async deleteLibraryDependencies(libraryId: number, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
await db.deleteRecords(LIBRARY_DEPENDENCIES_TABLE_NAME, { libraryid: libraryId });
}
/**
* Delete what libraries a content item is using.
*
* @param id Package ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteLibraryUsage(id: number, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
await db.deleteRecords(CONTENTS_LIBRARIES_TABLE_NAME, { h5pid: id });
}
/**
* Get all conent data from DB.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the list of content data.
*/
async getAllContentData(siteId?: string): Promise<CoreH5PContentDBRecord[]> {
const db = await CoreSites.instance.getSiteDb(siteId);
return db.getAllRecords<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME);
}
/**
* Get conent data from DB.
*
* @param id Content ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the content data.
*/
async getContentData(id: number, siteId?: string): Promise<CoreH5PContentDBRecord> {
const db = await CoreSites.instance.getSiteDb(siteId);
return db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { id });
}
/**
* Get conent data from DB.
*
* @param fileUrl H5P file URL.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the content data.
*/
async getContentDataByUrl(fileUrl: string, siteId?: string): Promise<CoreH5PContentDBRecord> {
const site = await CoreSites.instance.getSite(siteId);
const db = site.getDb();
// Try to use the folder name, it should be more reliable than the URL.
const folderName = await CoreH5P.instance.h5pCore.h5pFS.getContentFolderNameByUrl(fileUrl, site.getId());
try {
return await db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { foldername: folderName });
} catch (error) {
// Cannot get folder name, the h5p file was probably deleted. Just use the URL.
return db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, { fileurl: fileUrl });
}
}
/**
* Get the latest library version.
*
* @param machineName The library's machine name.
* @return Promise resolved with the latest library version data.
*/
async getLatestLibraryVersion(machineName: string, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> {
const db = await CoreSites.instance.getSiteDb(siteId);
try {
const records = await db.getRecords<CoreH5PLibraryDBRecord>(
LIBRARIES_TABLE_NAME,
{ machinename: machineName },
'majorversion DESC, minorversion DESC, patchversion DESC',
'*',
0,
1,
);
if (records && records[0]) {
return this.parseLibDBData(records[0]);
}
} catch (error) {
// Library not found.
}
throw new CoreError(`Missing required library: ${machineName}`);
}
/**
* 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 async getLibrary(
machineName: string,
majorVersion?: string | number,
minorVersion?: string | number,
siteId?: string,
): Promise<CoreH5PLibraryParsedDBRecord> {
const db = await CoreSites.instance.getSiteDb(siteId);
const libraries = await db.getRecords<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, {
machinename: machineName,
majorversion: majorVersion,
minorversion: minorVersion,
});
if (!libraries.length) {
throw new CoreError('Libary not found.');
}
return this.parseLibDBData(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.
*/
getLibraryByData(libraryData: CoreH5PLibraryBasicData, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> {
return this.getLibrary(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId);
}
/**
* Get a library data stored in DB by ID.
*
* @param id Library ID.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the library data, rejected if not found.
*/
async getLibraryById(id: number, siteId?: string): Promise<CoreH5PLibraryParsedDBRecord> {
const db = await CoreSites.instance.getSiteDb(siteId);
const library = await db.getRecord<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, { id });
return this.parseLibDBData(library);
}
/**
* 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.
*/
async getLibraryId(
machineName: string,
majorVersion?: string | number,
minorVersion?: string | number,
siteId?: string,
): Promise<number | undefined> {
try {
const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId);
return library.id || undefined;
} catch (error) {
return undefined;
}
}
/**
* 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.
*/
getLibraryIdByData(libraryData: CoreH5PLibraryBasicData, siteId?: string): Promise<number | undefined> {
return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId);
}
/**
* Get the default behaviour for the display option defined.
*
* @param name Identifier for the setting.
* @param defaultValue Optional default value if settings is not set.
* @return Return the value for this display option.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getOption(name: string, defaultValue: unknown): unknown {
// For now, all them are disabled by default, so only will be rendered when defined in the display options.
return CoreH5PDisplayOptionBehaviour.CONTROLLED_BY_AUTHOR_DEFAULT_OFF;
}
/**
* Check whether the user has permission to execute an action.
*
* @param permission Permission to check.
* @param id H5P package id.
* @return Whether the user has permission to execute an action.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hasPermission(permission: number, id: number): boolean {
// H5P capabilities have not been introduced.
return true;
}
/**
* Determines if content slug is used.
*
* @param slug The content slug.
* @return Whether the content slug is used
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isContentSlugAvailable(slug: string): boolean {
// By default the slug should be available as it's currently generated as a unique value for each h5p content.
return true;
}
/**
* Check whether a library is a patched version of the one installed.
*
* @param library Library to check.
* @param dbData Installed library. If not supplied it will be calculated.
* @return Promise resolved with boolean: whether it's a patched library.
*/
async isPatchedLibrary(library: CoreH5PLibraryBasicDataWithPatch, dbData?: CoreH5PLibraryParsedDBRecord): Promise<boolean> {
if (!dbData) {
dbData = await this.getLibraryByData(library);
}
return library.patchVersion > dbData.patchversion;
}
/**
* 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: CoreH5PLibraryBeingSaved, key: string, searchParam: string = 'path'): string {
if (typeof libraryData[key] != 'undefined') {
const parameterValues: string[] = [];
libraryData[key].forEach((file) => {
for (const index in file) {
if (index === searchParam) {
parameterValues.push(file[index]);
}
}
});
return parameterValues.join(',');
}
return '';
}
/**
* Load addon libraries.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the addon libraries.
*/
async loadAddons(siteId?: string): Promise<CoreH5PLibraryAddonData[]> {
const db = await CoreSites.instance.getSiteDb(siteId);
const query = 'SELECT l1.id AS libraryId, l1.machinename AS machineName, ' +
'l1.majorversion AS majorVersion, l1.minorversion AS minorVersion, ' +
'l1.patchversion AS patchVersion, l1.addto AS addTo, ' +
'l1.preloadedjs AS preloadedJs, l1.preloadedcss AS preloadedCss ' +
'FROM ' + LIBRARIES_TABLE_NAME + ' l1 ' +
'JOIN ' + LIBRARIES_TABLE_NAME + ' l2 ON l1.machinename = l2.machinename AND (' +
'l1.majorversion < l2.majorversion OR (l1.majorversion = l2.majorversion AND ' +
'l1.minorversion < l2.minorversion)) ' +
'WHERE l1.addto IS NOT NULL AND l2.machinename IS NULL';
const result = await db.execute(query);
const addons: CoreH5PLibraryAddonData[] = [];
for (let i = 0; i < result.rows.length; i++) {
addons.push(this.parseLibAddonData(result.rows.item(i)));
}
return addons;
}
/**
* Load content data from DB.
*
* @param id Content ID.
* @param fileUrl H5P file URL. Required if id is not provided.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the content data.
*/
async loadContent(id?: number, fileUrl?: string, siteId?: string): Promise<CoreH5PFrameworkContentData> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
let contentData: CoreH5PContentDBRecord;
if (id) {
contentData = await this.getContentData(id, siteId);
} else if (fileUrl) {
contentData = await this.getContentDataByUrl(fileUrl, siteId);
} else {
throw new CoreError('No id or fileUrl supplied to loadContent.');
}
// Load the main library data.
const libData = await this.getLibraryById(contentData.mainlibraryid, siteId);
// Map the values to the names used by the H5P core (it's the same Moodle web does).
const content = {
id: contentData.id,
params: contentData.jsoncontent,
embedType: 'iframe', // Always use iframe.
disable: null,
folderName: contentData.foldername,
title: libData.title,
slug: CoreH5PCore.slugify(libData.title) + '-' + contentData.id,
filtered: contentData.filtered,
libraryId: libData.id,
libraryName: libData.machinename,
libraryMajorVersion: libData.majorversion,
libraryMinorVersion: libData.minorversion,
libraryEmbedTypes: libData.embedtypes,
libraryFullscreen: libData.fullscreen,
metadata: null,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const params = CoreTextUtils.instance.parseJSON<any>(contentData.jsoncontent);
if (!params.metadata) {
params.metadata = {};
}
content.metadata = params.metadata;
content.params = JSON.stringify(typeof params.params != 'undefined' && params.params != null ? params.params : params);
return content;
}
/**
* Load dependencies for the given content of the given type.
*
* @param id Content ID.
* @param type The dependency type.
* @return Content dependencies, indexed by machine name.
*/
async loadContentDependencies(
id: number,
type?: string,
siteId?: string,
): Promise<{[machineName: string]: CoreH5PContentDependencyData}> {
const db = await CoreSites.instance.getSiteDb(siteId);
let query = 'SELECT hl.id AS libraryId, hl.machinename AS machineName, ' +
'hl.majorversion AS majorVersion, hl.minorversion AS minorVersion, ' +
'hl.patchversion AS patchVersion, hl.preloadedcss AS preloadedCss, ' +
'hl.preloadedjs AS preloadedJs, hcl.dropcss AS dropCss, ' +
'hcl.dependencytype as dependencyType ' +
'FROM ' + CONTENTS_LIBRARIES_TABLE_NAME + ' hcl ' +
'JOIN ' + LIBRARIES_TABLE_NAME + ' hl ON hcl.libraryid = hl.id ' +
'WHERE hcl.h5pid = ?';
const queryArgs: (string | number)[] = [];
queryArgs.push(id);
if (type) {
query += ' AND hcl.dependencytype = ?';
queryArgs.push(type);
}
query += ' ORDER BY hcl.weight';
const result = await db.execute(query, queryArgs);
const dependencies: {[machineName: string]: CoreH5PContentDependencyData} = {};
for (let i = 0; i < result.rows.length; i++) {
const dependency = result.rows.item(i);
dependencies[dependency.machineName] = dependency;
}
return dependencies;
}
/**
* Loads a library and its dependencies.
*
* @param machineName The library's machine name.
* @param majorVersion The library's major version.
* @param minorVersion The library's minor version.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the library data.
*/
async loadLibrary(
machineName: string,
majorVersion: number,
minorVersion: number,
siteId?: string,
): Promise<CoreH5PLibraryData> {
// First get the library data from DB.
const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId);
const libraryData: CoreH5PLibraryData = {
libraryId: library.id,
title: library.title,
machineName: library.machinename,
majorVersion: library.majorversion,
minorVersion: library.minorversion,
patchVersion: library.patchversion,
runnable: library.runnable,
fullscreen: library.fullscreen,
embedTypes: library.embedtypes,
preloadedJs: library.preloadedjs || undefined,
preloadedCss: library.preloadedcss || undefined,
dropLibraryCss: library.droplibrarycss || undefined,
semantics: library.semantics || undefined,
preloadedDependencies: [],
dynamicDependencies: [],
editorDependencies: [],
};
// Now get the dependencies.
const sql = 'SELECT hl.id, hl.machinename, hl.majorversion, hl.minorversion, hll.dependencytype ' +
'FROM ' + LIBRARY_DEPENDENCIES_TABLE_NAME + ' hll ' +
'JOIN ' + LIBRARIES_TABLE_NAME + ' hl ON hll.requiredlibraryid = hl.id ' +
'WHERE hll.libraryid = ? ' +
'ORDER BY hl.id ASC';
const sqlParams = [
library.id,
];
const db = await CoreSites.instance.getSiteDb(siteId);
const result = await db.execute(sql, sqlParams);
for (let i = 0; i < result.rows.length; i++) {
const dependency: LibraryDependency = result.rows.item(i);
const key = dependency.dependencytype + 'Dependencies';
libraryData[key].push({
machineName: dependency.machinename,
majorVersion: dependency.majorversion,
minorVersion: dependency.minorversion,
});
}
return libraryData;
}
/**
* Parse library addon data.
*
* @param library Library addon data.
* @return Parsed library.
*/
parseLibAddonData(library: LibraryAddonDBData): CoreH5PLibraryAddonData {
const parsedLib = <CoreH5PLibraryAddonData> library;
parsedLib.addTo = CoreTextUtils.instance.parseJSON<CoreH5PLibraryAddTo | null>(library.addTo, null);
return parsedLib;
}
/**
* Parse library DB data.
*
* @param library Library DB data.
* @return Parsed library.
*/
protected parseLibDBData(library: CoreH5PLibraryDBRecord): CoreH5PLibraryParsedDBRecord {
return Object.assign(library, {
semantics: library.semantics ? CoreTextUtils.instance.parseJSON(library.semantics, null) : null,
addto: library.addto ? CoreTextUtils.instance.parseJSON(library.addto, null) : null,
});
}
/**
* Resets marked user data for the given content.
*
* @param contentId Content ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async resetContentUserData(conentId: number, siteId?: string): Promise<void> {
// Currently, we do not store user data for a content.
}
/**
* Stores hash keys for cached assets, aggregated JavaScripts and stylesheets, and connects it to libraries so that we
* know which cache file to delete when a library is updated.
*
* @param key Hash key for the given libraries.
* @param libraries List of dependencies used to create the key.
* @param folderName The name of the folder that contains the H5P.
* @param siteId The site ID.
* @return Promise resolved when done.
*/
async saveCachedAssets(
hash: string,
dependencies: {[machineName: string]: CoreH5PContentDependencyData},
folderName: string,
siteId?: string,
): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
await Promise.all(Object.keys(dependencies).map(async (key) => {
const data: Partial<CoreH5PLibraryCachedAssetsDBRecord> = {
hash: key,
libraryid: dependencies[key].libraryId,
foldername: folderName,
};
await db.insertRecord(LIBRARIES_CACHEDASSETS_TABLE_NAME, data);
}));
}
/**
* 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.
*/
async saveLibraryData(libraryData: CoreH5PLibraryBeingSaved, siteId?: string): Promise<void> {
// Some special properties needs some checking and converting before they can be saved.
const preloadedJS = this.libraryParameterValuesToCsv(libraryData, 'preloadedJs', 'path');
const preloadedCSS = this.libraryParameterValuesToCsv(libraryData, 'preloadedCss', 'path');
const dropLibraryCSS = this.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(', ');
}
const site = await CoreSites.instance.getSite(siteId);
const db = site.getDb();
const data: Partial<CoreH5PLibraryDBRecord> = {
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: typeof libraryData.semantics != 'undefined' ? JSON.stringify(libraryData.semantics) : null,
addto: typeof libraryData.addTo != 'undefined' ? JSON.stringify(libraryData.addTo) : null,
};
if (libraryData.libraryId) {
data.id = libraryData.libraryId;
}
await db.insertRecord(LIBRARIES_TABLE_NAME, data);
if (!data.id) {
// New library. Get its ID.
const entry = await db.getRecord<CoreH5PLibraryDBRecord>(LIBRARIES_TABLE_NAME, data);
libraryData.libraryId = entry.id;
} else {
// Updated libary. Remove old dependencies.
await 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.
*/
async saveLibraryDependencies(
libraryId: number,
dependencies: CoreH5PLibraryBasicData[],
dependencyType: string,
siteId?: string,
): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
await Promise.all(dependencies.map(async (dependency) => {
// Get the ID of the library.
const dependencyId = await this.getLibraryIdByData(dependency, siteId);
// Create the relation.
const entry: Partial<CoreH5PLibraryDependencyDBRecord> = {
libraryid: libraryId,
requiredlibraryid: dependencyId,
dependencytype: dependencyType,
};
await db.insertRecord(LIBRARY_DEPENDENCIES_TABLE_NAME, entry);
}));
}
/**
* Saves what libraries the content uses.
*
* @param id Id identifying the package.
* @param librariesInUse List of libraries the content uses.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async saveLibraryUsage(
id: number,
librariesInUse: {[key: string]: CoreH5PContentDepsTreeDependency},
siteId?: string,
): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
// Calculate the CSS to drop.
const dropLibraryCssList: Record<string, string> = {};
for (const key in librariesInUse) {
const dependency = librariesInUse[key];
if ('dropLibraryCss' in dependency.library && dependency.library.dropLibraryCss) {
const split = dependency.library.dropLibraryCss.split(', ');
split.forEach((css) => {
dropLibraryCssList[css] = css;
});
}
}
// Now save the uusage.
await Promise.all(Object.keys(librariesInUse).map((key) => {
const dependency = librariesInUse[key];
const data: Partial<CoreH5PContentsLibraryDBRecord> = {
h5pid: id,
libraryid: dependency.library.libraryId,
dependencytype: dependency.type,
dropcss: dropLibraryCssList[dependency.library.machineName] ? 1 : 0,
weight: dependency.weight,
};
return db.insertRecord(CONTENTS_LIBRARIES_TABLE_NAME, data);
}));
}
/**
* Save content data in DB and clear cache.
*
* @param content Content to save.
* @param folderName The name of the folder that contains the H5P.
* @param fileUrl The online URL of the package.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with content ID.
*/
async updateContent(content: CoreH5PContentBeingSaved, folderName: string, fileUrl: string, siteId?: string): Promise<number> {
const db = await CoreSites.instance.getSiteDb(siteId);
// If the libraryid declared in the package is empty, get the latest version.
if (content.library && typeof content.library.libraryId == 'undefined') {
const mainLibrary = await this.getLatestLibraryVersion(content.library.machineName, siteId);
content.library.libraryId = mainLibrary.id;
}
const data: Partial<CoreH5PContentDBRecord> = {
id: undefined,
jsoncontent: content.params,
mainlibraryid: content.library?.libraryId,
timemodified: Date.now(),
filtered: null,
foldername: folderName,
fileurl: fileUrl,
timecreated: undefined,
};
if (typeof content.id != 'undefined') {
data.id = content.id;
} else {
data.timecreated = data.timemodified;
}
await db.insertRecord(CONTENT_TABLE_NAME, data);
if (!data.id) {
// New content. Get its ID.
const entry = await db.getRecord<CoreH5PContentDBRecord>(CONTENT_TABLE_NAME, data);
content.id = entry.id;
}
return content.id!;
}
/**
* This will update selected fields on the given content.
*
* @param id Content identifier.
* @param fields Object with the fields to update.
* @param siteId Site ID. If not defined, current site.
*/
async updateContentFields(id: number, fields: Partial<CoreH5PContentDBRecord>, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
const data = Object.assign({}, fields);
await db.updateRecords(CONTENT_TABLE_NAME, data, { id });
}
}
/**
* Content data returned by loadContent.
*/
export type CoreH5PFrameworkContentData = {
id: number; // The id of the content.
params: string; // The content in json format.
embedType: string; // Embed type to use.
disable: number | null; // H5P Button display options.
folderName: string; // Name of the folder that contains the contents.
title: string; // Main library's title.
slug: string; // Lib title and ID slugified.
filtered: string | null; // Filtered version of json_content.
libraryId: number; // Main library's ID.
libraryName: string; // Main library's machine name.
libraryMajorVersion: number; // Main library's major version.
libraryMinorVersion: number; // Main library's minor version.
libraryEmbedTypes: string; // Main library's list of supported embed types.
libraryFullscreen: number; // Main library's display fullscreen button.
metadata: unknown; // Content metadata.
};
export type CoreH5PLibraryParsedDBRecord = Omit<CoreH5PLibraryDBRecord, 'semantics'|'addto'> & {
semantics: CoreH5PSemantics[] | null;
addto: CoreH5PLibraryAddTo | null;
};
type LibraryDependency = {
id: number;
machinename: string;
majorversion: number;
minorversion: number;
dependencytype: string;
};
type LibraryAddonDBData = Omit<CoreH5PLibraryAddonData, 'addTo'> & {
addTo: string;
};

View File

@ -0,0 +1,255 @@
// (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 { FileEntry } from '@ionic-native/file';
import { CoreFile, CoreFileProvider } from '@services/file';
import { CoreSites } from '@services/sites';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreUser } from '@features/user/services/user';
import { CoreH5P } from '../services/h5p';
import { CoreH5PCore, CoreH5PDisplayOptions } from './core';
import { Translate } from '@singletons';
import { CoreError } from '@classes/errors/error';
/**
* Equivalent to Moodle's H5P helper class.
*/
export class CoreH5PHelper {
/**
* Convert the number representation of display options into an object.
*
* @param displayOptions Number representing display options.
* @return Object with display options.
*/
static decodeDisplayOptions(displayOptions: number): CoreH5PDisplayOptions {
const displayOptionsObject = CoreH5P.instance.h5pCore.getDisplayOptionsAsObject(displayOptions);
const config: CoreH5PDisplayOptions = {
export: false, // Don't allow downloading in the app.
embed: false, // Don't display the embed button in the app.
copyright: CoreUtils.instance.notNullOrUndefined(displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]) ?
displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] : false,
icon: CoreUtils.instance.notNullOrUndefined(displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_ABOUT]) ?
displayOptionsObject[CoreH5PCore.DISPLAY_OPTION_ABOUT] : false,
};
config.frame = config.copyright || config.export || config.embed;
return config;
}
/**
* Get the core H5P assets, including all core H5P JavaScript and CSS.
*
* @return Array core H5P assets.
*/
static async getCoreAssets(
siteId?: string,
): Promise<{settings: CoreH5PCoreSettings; cssRequires: string[]; jsRequires: string[]}> {
// Get core settings.
const settings = await CoreH5PHelper.getCoreSettings(siteId);
settings.core = {
styles: [],
scripts: [],
};
settings.loadedJs = [];
settings.loadedCss = [];
const libUrl = CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath();
const cssRequires: string[] = [];
const jsRequires: string[] = [];
// Add core stylesheets.
CoreH5PCore.STYLES.forEach((style) => {
settings.core!.styles.push(libUrl + style);
cssRequires.push(libUrl + style);
});
// Add core JavaScript.
CoreH5PCore.getScripts().forEach((script) => {
settings.core!.scripts.push(script);
jsRequires.push(script);
});
return { settings, cssRequires, jsRequires };
}
/**
* Get the settings needed by the H5P library.
*
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the settings.
*/
static async getCoreSettings(siteId?: string): Promise<CoreH5PCoreSettings> {
const site = await CoreSites.instance.getSite(siteId);
const userId = site.getUserId();
const user = await CoreUtils.instance.ignoreErrors(CoreUser.instance.getProfile(userId, undefined, false, siteId));
if (!user || !user.email) {
throw new CoreError(Translate.instance.instant('core.h5p.errorgetemail'));
}
const basePath = CoreFile.instance.getBasePathInstant();
const ajaxPaths = {
xAPIResult: '',
contentUserData: '',
};
return {
baseUrl: CoreFile.instance.getWWWPath(),
url: CoreFile.instance.convertFileSrc(
CoreTextUtils.instance.concatenatePaths(
basePath,
CoreH5P.instance.h5pCore.h5pFS.getExternalH5PFolderPath(site.getId()),
),
),
urlLibraries: CoreFile.instance.convertFileSrc(
CoreTextUtils.instance.concatenatePaths(
basePath,
CoreH5P.instance.h5pCore.h5pFS.getLibrariesFolderPath(site.getId()),
),
),
postUserStatistics: false,
ajax: ajaxPaths,
saveFreq: false,
siteUrl: site.getURL(),
l10n: {
H5P: CoreH5P.instance.h5pCore.getLocalization(), // eslint-disable-line @typescript-eslint/naming-convention
},
user: { name: site.getInfo()!.fullname, mail: user.email },
hubIsEnabled: false,
reportingIsEnabled: false,
crossorigin: null,
libraryConfig: null,
pluginCacheBuster: '',
libraryUrl: CoreTextUtils.instance.concatenatePaths(CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(), 'js'),
};
}
/**
* Extract and store an H5P file.
* 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.
* @param onProgress Function to call on progress.
* @return Promise resolved when done.
*/
static async saveH5P(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreH5PSaveOnProgress): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Notify that the unzip is starting.
onProgress && onProgress({ message: 'core.unzipping' });
const queueId = siteId + ':saveH5P:' + fileUrl;
await CoreH5P.instance.queueRunner.run(queueId, () => CoreH5PHelper.performSave(fileUrl, file, siteId, onProgress));
}
/**
* Extract and store an H5P file.
*
* @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.
* @param onProgress Function to call on progress.
* @return Promise resolved when done.
*/
protected static async performSave(
fileUrl: string,
file: FileEntry,
siteId?: string,
onProgress?: CoreH5PSaveOnProgress,
): Promise<void> {
const folderName = CoreMimetypeUtils.instance.removeExtension(file.name);
const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName);
// Unzip the file.
await CoreFile.instance.unzipFile(file.toURL(), destFolder, onProgress);
try {
// Notify that the unzip is starting.
onProgress && onProgress({ message: 'core.storingfiles' });
// Read the contents of the unzipped dir, process them and store them.
const contents = await CoreFile.instance.getDirectoryContents(destFolder);
const filesData = await CoreH5P.instance.h5pValidator.processH5PFiles(destFolder, contents);
const content = await CoreH5P.instance.h5pStorage.savePackage(filesData, folderName, fileUrl, false, siteId);
// Create the content player.
const contentData = await CoreH5P.instance.h5pCore.loadContent(content.id, undefined, siteId);
const embedType = CoreH5PCore.determineEmbedType(contentData.embedType, contentData.library.embedTypes);
await CoreH5P.instance.h5pPlayer.createContentIndex(content.id!, fileUrl, contentData, embedType, siteId);
} finally {
// Remove tmp folder.
try {
await CoreFile.instance.removeDir(destFolder);
} catch (error) {
// Ignore errors, it will be deleted eventually.
}
}
}
}
/**
* Core settings for H5P.
*/
export type CoreH5PCoreSettings = {
baseUrl: string;
url: string;
urlLibraries: string;
postUserStatistics: boolean;
ajax: {
xAPIResult: string;
contentUserData: string;
};
saveFreq: boolean;
siteUrl: string;
l10n: {
H5P: {[name: string]: string}; // eslint-disable-line @typescript-eslint/naming-convention
};
user: {
name: string;
mail: string;
};
hubIsEnabled: boolean;
reportingIsEnabled: boolean;
crossorigin: null;
libraryConfig: null;
pluginCacheBuster: string;
libraryUrl: string;
core?: {
styles: string[];
scripts: string[];
};
loadedJs?: string[];
loadedCss?: string[];
};
export type CoreH5PSaveOnProgress = (event?: ProgressEvent | { message: string }) => void;

View File

@ -0,0 +1,41 @@
// (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 { CoreH5PLibraryMetadataSettings } from './validator';
/**
* Equivalent to H5P's H5PMetadata class.
*/
export class CoreH5PMetadata {
/**
* 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.
*/
static boolifyAndEncodeSettings(metadataSettings: CoreH5PLibraryMetadataSettings): 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);
}
}

View File

@ -0,0 +1,420 @@
// (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 { CoreFile } from '@services/file';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { CoreXAPI } from '@features/xapi/services/xapi';
import { CoreH5P } from '../services/h5p';
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core';
import { CoreH5PCoreSettings, CoreH5PHelper } from './helper';
import { CoreH5PStorage } from './storage';
/**
* Equivalent to Moodle's H5P player class.
*/
export class CoreH5PPlayer {
constructor(
protected h5pCore: CoreH5PCore,
protected h5pStorage: CoreH5PStorage,
) { }
/**
* Calculate the URL to the site H5P player.
*
* @param siteUrl Site URL.
* @param fileUrl File URL.
* @param displayOptions Display options.
* @param component Component to send xAPI events to.
* @return URL.
*/
calculateOnlinePlayerUrl(siteUrl: string, fileUrl: string, displayOptions?: CoreH5PDisplayOptions, component?: string): string {
fileUrl = CoreH5P.instance.treatH5PUrl(fileUrl, siteUrl);
const params = this.getUrlParamsFromDisplayOptions(displayOptions);
params.url = encodeURIComponent(fileUrl);
if (component) {
params.component = component;
}
return CoreUrlUtils.instance.addParamsToUrl(CoreTextUtils.instance.concatenatePaths(siteUrl, '/h5p/embed.php'), params);
}
/**
* Create the index.html to render an H5P package.
* Part of the code of this function is equivalent to Moodle's add_assets_to_page function.
*
* @param id Content ID.
* @param h5pUrl The URL of the H5P file.
* @param content Content data.
* @param embedType Embed type. The app will always use 'iframe'.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the URL of the index file.
*/
async createContentIndex(
id: number,
h5pUrl: string,
content: CoreH5PContentData,
embedType: string,
siteId?: string,
): Promise<string> {
const site = await CoreSites.instance.getSite(siteId);
const contentId = this.getContentId(id);
const basePath = CoreFile.instance.getBasePathInstant();
const contentUrl = CoreFile.instance.convertFileSrc(
CoreTextUtils.instance.concatenatePaths(
basePath,
this.h5pCore.h5pFS.getContentFolderPath(content.folderName, site.getId()),
),
);
// Create the settings needed for the content.
const contentSettings = {
library: CoreH5PCore.libraryToString(content.library),
fullScreen: content.library.fullscreen,
exportUrl: '', // We'll never display the download button, so we don't need the exportUrl.
embedCode: this.getEmbedCode(site.getURL(), h5pUrl, true),
resizeCode: this.getResizeCode(),
title: content.slug,
displayOptions: {},
url: '', // It will be filled using dynamic params if needed.
contentUrl: contentUrl,
metadata: content.metadata,
contentUserData: [
{
state: '{}',
},
],
};
// Get the core H5P assets, needed by the H5P classes to render the H5P content.
const result = await this.getAssets(id, content, embedType, site.getId());
result.settings.contents[contentId] = Object.assign(result.settings.contents[contentId], contentSettings);
const indexPath = this.h5pCore.h5pFS.getContentIndexPath(content.folderName, site.getId());
let html = '<html><head><title>' + content.title + '</title>' +
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
// Include the required CSS.
result.cssRequires.forEach((cssUrl) => {
html += '<link rel="stylesheet" type="text/css" href="' + cssUrl + '">';
});
// Add the settings.
html += '<script type="text/javascript">var H5PIntegration = ' +
JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>';
// Add our own script to handle the params.
html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths(
this.h5pCore.h5pFS.getCoreH5PPath(),
'moodle/js/params.js',
) + '"></script>';
html += '</head><body>';
// Include the required JS at the beginning of the body, like Moodle web does.
// Load the embed.js to allow communication with the parent window.
html += '<script type="text/javascript" src="' +
CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/embed.js') + '"></script>';
result.jsRequires.forEach((jsUrl) => {
html += '<script type="text/javascript" src="' + jsUrl + '"></script>';
});
html += '<div class="h5p-iframe-wrapper">' +
'<iframe id="h5p-iframe-' + id + '" class="h5p-iframe" data-content-id="' + id + '"' +
'style="height:1px; min-width: 100%" src="about:blank"></iframe>' +
'</div></body>';
const fileEntry = await CoreFile.instance.writeFile(indexPath, html);
return fileEntry.toURL();
}
/**
* Delete all content indexes of all sites from filesystem.
*
* @return Promise resolved when done.
*/
async deleteAllContentIndexes(): Promise<void> {
const siteIds = await CoreSites.instance.getSitesIds();
await Promise.all(siteIds.map((siteId) => this.deleteAllContentIndexesForSite(siteId)));
}
/**
* Delete all content indexes for a certain site from filesystem.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteAllContentIndexesForSite(siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (!siteId) {
return;
}
const records = await this.h5pCore.h5pFramework.getAllContentData(siteId);
await Promise.all(records.map(async (record) => {
await CoreUtils.instance.ignoreErrors(this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId!));
}));
}
/**
* Delete all package content data.
*
* @param fileUrl File URL.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteContentByUrl(fileUrl: string, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId);
await CoreUtils.instance.allPromises([
this.h5pCore.h5pFramework.deleteContentData(data.id, siteId),
this.h5pCore.h5pFS.deleteContentFolder(data.foldername, siteId),
]);
}
/**
* Get the assets of a package.
*
* @param id Content id.
* @param content Content data.
* @param embedType Embed type.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the assets.
*/
protected async getAssets(
id: number,
content: CoreH5PContentData,
embedType: string,
siteId?: string,
): Promise<{settings: AssetsSettings; cssRequires: string[]; jsRequires: string[]}> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Get core assets.
const coreAssets = await CoreH5PHelper.getCoreAssets(siteId);
const contentId = this.getContentId(id);
const settings = <AssetsSettings> coreAssets.settings;
settings.contents = settings.contents || {};
settings.contents[contentId] = settings.contents[contentId] || {};
settings.moodleLibraryPaths = await this.h5pCore.getDependencyRoots(id);
/* The filterParameters function should be called before getting the dependency files because it rebuilds content
dependency cache. */
settings.contents[contentId].jsonContent = await this.h5pCore.filterParameters(content, siteId);
const files = await this.getDependencyFiles(id, content.folderName, siteId);
// H5P checks the embedType in here, but we'll always use iframe so there's no need to do it.
// JavaScripts and stylesheets will be loaded through h5p.js.
settings.contents[contentId].scripts = this.h5pCore.getAssetsUrls(files.scripts);
settings.contents[contentId].styles = this.h5pCore.getAssetsUrls(files.styles);
return {
settings: settings,
cssRequires: coreAssets.cssRequires,
jsRequires: coreAssets.jsRequires,
};
}
/**
* Get the identifier for the H5P content. This identifier is different than the ID stored in the DB.
*
* @param id Package ID.
* @return Content identifier.
*/
protected getContentId(id: number): string {
return 'cid-' + id;
}
/**
* Get the content index file.
*
* @param fileUrl URL of the H5P package.
* @param displayOptions Display options.
* @param component Component to send xAPI events to.
* @param contextId Context ID where the H5P is. Required for tracking.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the file URL if exists, rejected otherwise.
*/
async getContentIndexFileUrl(
fileUrl: string,
displayOptions?: CoreH5PDisplayOptions,
component?: string,
contextId?: number,
siteId?: string,
): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId);
// Add display options and component to the URL.
const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId);
displayOptions = this.h5pCore.fixDisplayOptions(displayOptions || {}, data.id);
const params: Record<string, string> = {
displayOptions: JSON.stringify(displayOptions),
component: component || '',
};
if (contextId) {
params.trackingUrl = await CoreXAPI.instance.getUrl(contextId, 'activity', siteId);
}
return CoreUrlUtils.instance.addParamsToUrl(path, params);
}
/**
* Finds library dependencies files of a certain package.
*
* @param id Content id.
* @param folderName Name of the folder of the content.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the files.
*/
protected async getDependencyFiles(id: number, folderName: string, siteId?: string): Promise<CoreH5PDependenciesFiles> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const preloadedDeps = await CoreH5P.instance.h5pCore.loadContentDependencies(id, 'preloaded', siteId);
return this.h5pCore.getDependenciesFiles(
preloadedDeps,
folderName,
this.h5pCore.h5pFS.getExternalH5PFolderPath(siteId),
siteId,
);
}
/**
* Get display options from a URL params.
*
* @param params URL params.
* @return Display options as object.
*/
getDisplayOptionsFromUrlParams(params?: {[name: string]: string}): CoreH5PDisplayOptions {
const displayOptions: CoreH5PDisplayOptions = {};
if (!params) {
return displayOptions;
}
displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = false; // Never allow downloading in the app.
displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = false; // Never show the embed option in the app.
displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] =
CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]);
displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] ||
displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] || displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT];
displayOptions[CoreH5PCore.DISPLAY_OPTION_ABOUT] =
!!this.h5pCore.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_ABOUT, true);
return displayOptions;
}
/**
* Embed code for settings.
*
* @param siteUrl The site URL.
* @param h5pUrl The URL of the .h5p file.
* @param embedEnabled Whether the option to embed the H5P content is enabled.
* @return The HTML code to reuse this H5P content in a different place.
*/
protected getEmbedCode(siteUrl: string, h5pUrl: string, embedEnabled?: boolean): string {
if (!embedEnabled) {
return '';
}
return '<iframe src="' + this.getEmbedUrl(siteUrl, h5pUrl) + '" allowfullscreen="allowfullscreen"></iframe>';
}
/**
* Get the encoded URL for embeding an H5P content.
*
* @param siteUrl The site URL.
* @param h5pUrl The URL of the .h5p file.
* @return The embed URL.
*/
protected getEmbedUrl(siteUrl: string, h5pUrl: string): string {
return CoreTextUtils.instance.concatenatePaths(siteUrl, '/h5p/embed.php') + '?url=' + h5pUrl;
}
/**
* Resizing script for settings.
*
* @return The HTML code with the resize script.
*/
protected getResizeCode(): string {
return '<script src="' + this.getResizerScriptUrl() + '"></script>';
}
/**
* Get the URL to the resizer script.
*
* @return URL.
*/
getResizerScriptUrl(): string {
return CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'js/h5p-resizer.js');
}
/**
* Get online player URL params from display options.
*
* @param options Display options.
* @return Object with URL params.
*/
getUrlParamsFromDisplayOptions(options?: CoreH5PDisplayOptions): {[name: string]: string} {
const params: {[name: string]: string} = {};
if (!options) {
return params;
}
params[CoreH5PCore.DISPLAY_OPTION_FRAME] = options[CoreH5PCore.DISPLAY_OPTION_FRAME] ? '1' : '0';
params[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = options[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] ? '1' : '0';
params[CoreH5PCore.DISPLAY_OPTION_EMBED] = options[CoreH5PCore.DISPLAY_OPTION_EMBED] ? '1' : '0';
params[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = options[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] ? '1' : '0';
return params;
}
}
type AssetsSettings = CoreH5PCoreSettings & {
contents: {
[contentId: string]: {
jsonContent: string | null;
scripts: string[];
styles: string[];
};
};
moodleLibraryPaths: {
[libString: string]: string;
};
};

View File

@ -0,0 +1,233 @@
// (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 { CoreFile, CoreFileProvider } from '@services/file';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreH5PCore, CoreH5PLibraryBasicData } from './core';
import { CoreH5PFramework } from './framework';
import { CoreH5PMetadata } from './metadata';
import {
CoreH5PLibrariesJsonData,
CoreH5PLibraryJsonData,
CoreH5PLibraryMetadataSettings,
CoreH5PMainJSONFilesData,
} from './validator';
/**
* Equivalent to H5P's H5PStorage class.
*/
export class CoreH5PStorage {
constructor(
protected h5pCore: CoreH5PCore,
protected h5pFramework: CoreH5PFramework,
) { }
/**
* Save libraries.
*
* @param librariesJsonData Data about libraries.
* @param folderName Name of the folder of the H5P package.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
protected async saveLibraries(librariesJsonData: CoreH5PLibrariesJsonData, folderName: string, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// 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.
await CoreFile.instance.createDir(this.h5pCore.h5pFS.getLibrariesFolderPath(siteId));
const libraryIds: number[] = [];
// Go through libraries that came with this package.
await Promise.all(Object.keys(librariesJsonData).map(async (libString) => {
const libraryData: CoreH5PLibraryBeingSaved = librariesJsonData[libString];
// Find local library identifier.
const dbData = await CoreUtils.instance.ignoreErrors(this.h5pFramework.getLibraryByData(libraryData));
if (dbData) {
// Library already installed.
libraryData.libraryId = dbData.id;
const isNewPatch = await this.h5pFramework.isPatchedLibrary(libraryData, dbData);
if (!isNewPatch) {
// 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 ?
CoreH5PMetadata.boolifyAndEncodeSettings(<CoreH5PLibraryMetadataSettings> libraryData.metadataSettings) : undefined;
// Save the library data in DB.
await this.h5pFramework.saveLibraryData(libraryData, siteId);
// Now save it in FS.
try {
await this.h5pCore.h5pFS.saveLibrary(libraryData, siteId);
} catch (error) {
if (libraryData.libraryId) {
// An error occurred, delete the DB data because the lib FS data has been deleted.
await this.h5pFramework.deleteLibrary(libraryData.libraryId, siteId);
}
throw error;
}
if (typeof libraryData.libraryId != 'undefined') {
const promises: Promise<void>[] = [];
// Remove all indexes of contents that use this library.
promises.push(this.h5pCore.h5pFS.deleteContentIndexesForLibrary(libraryData.libraryId, siteId));
if (this.h5pCore.aggregateAssets) {
// Remove cached assets that use this library.
const removedEntries = await this.h5pFramework.deleteCachedAssets(libraryData.libraryId, siteId);
await this.h5pCore.h5pFS.deleteCachedAssets(removedEntries, siteId);
}
await CoreUtils.instance.allPromises(promises);
}
}));
// Go through the libraries again to save dependencies.
await Promise.all(Object.keys(librariesJsonData).map(async (libString) => {
const libraryData: CoreH5PLibraryBeingSaved = librariesJsonData[libString];
if (!libraryData.saveDependencies || !libraryData.libraryId) {
return;
}
const libId = libraryData.libraryId;
libraryIds.push(libId);
// Remove any old dependencies.
await this.h5pFramework.deleteLibraryDependencies(libId, siteId);
// Insert the different new ones.
const promises: Promise<void>[] = [];
if (typeof libraryData.preloadedDependencies != 'undefined') {
promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.preloadedDependencies, 'preloaded'));
}
if (typeof libraryData.dynamicDependencies != 'undefined') {
promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.dynamicDependencies, 'dynamic'));
}
if (typeof libraryData.editorDependencies != 'undefined') {
promises.push(this.h5pFramework.saveLibraryDependencies(libId, libraryData.editorDependencies, 'editor'));
}
await Promise.all(promises);
}));
// Make sure dependencies, parameter filtering and export files get regenerated for content who uses these libraries.
if (libraryIds.length) {
await this.h5pFramework.clearFilteredParameters(libraryIds, siteId);
}
}
/**
* Save content data in DB and clear cache.
*
* @param content Content to save.
* @param folderName The name of the folder that contains the H5P.
* @param fileUrl The online URL of the package.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the content data.
*/
async savePackage(
data: CoreH5PMainJSONFilesData,
folderName: string,
fileUrl: string,
skipContent?: boolean,
siteId?: string,
): Promise<CoreH5PContentBeingSaved> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (this.h5pCore.mayUpdateLibraries()) {
// Save the libraries that were processed.
await this.saveLibraries(data.librariesJsonData, folderName, siteId);
}
const content: CoreH5PContentBeingSaved = {};
if (!skipContent) {
// Find main library version.
if (data.mainJsonData.preloadedDependencies) {
const mainLib = data.mainJsonData.preloadedDependencies.find((dependency) =>
dependency.machineName === data.mainJsonData.mainLibrary);
if (mainLib) {
const id = await this.h5pFramework.getLibraryIdByData(mainLib);
content.library = Object.assign(mainLib, { libraryId: id });
}
}
content.params = JSON.stringify(data.contentJsonData);
// Save the content data in DB.
await this.h5pCore.saveContent(content, folderName, fileUrl, siteId);
// Save the content files in their right place in FS.
const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName);
const contentPath = CoreTextUtils.instance.concatenatePaths(destFolder, 'content');
try {
await this.h5pCore.h5pFS.saveContent(contentPath, folderName, siteId);
} catch (error) {
// An error occurred, delete the DB data because the content files have been deleted.
await this.h5pFramework.deleteContentData(content.id!, siteId);
throw error;
}
}
return content;
}
}
/**
* Library to save.
*/
export type CoreH5PLibraryBeingSaved = Omit<CoreH5PLibraryJsonData, 'metadataSettings'> & {
libraryId?: number; // Library ID in the DB.
saveDependencies?: boolean; // Whether to save dependencies.
metadataSettings?: CoreH5PLibraryMetadataSettings | string; // Encoded metadata settings.
};
/**
* Data about a content being saved.
*/
export type CoreH5PContentBeingSaved = {
id?: number;
params?: string;
library?: CoreH5PContentLibrary;
};
export type CoreH5PContentLibrary = CoreH5PLibraryBasicData & {
libraryId?: number; // Library ID in the DB.
};

View File

@ -0,0 +1,328 @@
// (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 { CoreFile, CoreFileFormat } from '@services/file';
import { CoreTextUtils } from '@services/utils/text';
import { CoreH5PSemantics } from './content-validator';
import { CoreH5PCore, CoreH5PLibraryBasicData } from './core';
/**
* Equivalent to H5P's H5PValidator class.
*/
export class CoreH5PValidator {
/**
* Get library data.
* 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.
* @return Promise resolved with library data.
*/
protected async getLibraryData(libDir: DirectoryEntry, libPath: string): Promise<CoreH5PLibraryJsonData> {
// Read the required files.
const results = await Promise.all([
this.readLibraryJsonFile(libPath),
this.readLibrarySemanticsFile(libPath),
this.readLibraryLanguageFiles(libPath),
this.libraryHasIcon(libPath),
]);
const libraryData: CoreH5PLibraryJsonData = results[0];
libraryData.semantics = results[1];
libraryData.language = results[2];
libraryData.hasIcon = results[3];
return libraryData;
}
/**
* Get library data for all libraries in an H5P package.
*
* @param packagePath The path to the package folder.
* @param entries List of files and directories in the root of the package folder.
* @retun Promise resolved with the libraries data.
*/
protected async getPackageLibrariesData(
packagePath: string,
entries: (DirectoryEntry | FileEntry)[],
): Promise<CoreH5PLibrariesJsonData> {
const libraries: CoreH5PLibrariesJsonData = {};
await Promise.all(entries.map(async (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 = CoreTextUtils.instance.concatenatePaths(packagePath, entry.name);
const libraryData = await this.getLibraryData(<DirectoryEntry> entry, libDirPath);
libraryData.uploadDirectory = libDirPath;
libraries[CoreH5PCore.libraryToString(libraryData)] = libraryData;
}));
return libraries;
}
/**
* Check if the library has an icon file.
*
* @param libPath Path to the directory where the library files are.
* @return Promise resolved with boolean: whether the library has an icon file.
*/
protected async libraryHasIcon(libPath: string): Promise<boolean> {
const path = CoreTextUtils.instance.concatenatePaths(libPath, 'icon.svg');
try {
// Check if the file exists.
await CoreFile.instance.getFile(path);
return true;
} catch (error) {
return false;
}
}
/**
* Process libraries from an H5P library, getting the required data to save them.
* This code is inspired on 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 packagePath The path to the package folder.
* @param entries List of files and directories in the root of the package folder.
* @return Promise resolved when done.
*/
async processH5PFiles(packagePath: string, entries: (DirectoryEntry | FileEntry)[]): Promise<CoreH5PMainJSONFilesData> {
// Read the needed files.
const results = await Promise.all([
this.readH5PJsonFile(packagePath),
this.readH5PContentJsonFile(packagePath),
this.getPackageLibrariesData(packagePath, entries),
]);
return {
librariesJsonData: results[2],
mainJsonData: results[0],
contentJsonData: results[1],
};
}
/**
* Read content.json file and return its parsed contents.
*
* @param packagePath The path to the package folder.
* @return Promise resolved with the parsed file contents.
*/
protected readH5PContentJsonFile(packagePath: string): Promise<unknown> {
const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'content/content.json');
return CoreFile.instance.readFile(path, CoreFileFormat.FORMATJSON);
}
/**
* Read h5p.json file and return its parsed contents.
*
* @param packagePath The path to the package folder.
* @return Promise resolved with the parsed file contents.
*/
protected readH5PJsonFile(packagePath: string): Promise<CoreH5PMainJSONData> {
const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'h5p.json');
return CoreFile.instance.readFile(path, CoreFileFormat.FORMATJSON);
}
/**
* Read library.json file and return its parsed contents.
*
* @param libPath Path to the directory where the library files are.
* @return Promise resolved with the parsed file contents.
*/
protected readLibraryJsonFile(libPath: string): Promise<CoreH5PLibraryMainJsonData> {
const path = CoreTextUtils.instance.concatenatePaths(libPath, 'library.json');
return CoreFile.instance.readFile<CoreH5PLibraryMainJsonData>(path, CoreFileFormat.FORMATJSON);
}
/**
* Read all language files and return their contents indexed by language code.
*
* @param libPath Path to the directory where the library files are.
* @return Promise resolved with the language data.
*/
protected async readLibraryLanguageFiles(libPath: string): Promise<CoreH5PLibraryLangsJsonData | undefined> {
try {
const path = CoreTextUtils.instance.concatenatePaths(libPath, 'language');
const langIndex: CoreH5PLibraryLangsJsonData = {};
// Read all the files in the language directory.
const entries = await CoreFile.instance.getDirectoryContents(path);
await Promise.all(entries.map(async (entry) => {
const langFilePath = CoreTextUtils.instance.concatenatePaths(path, entry.name);
try {
const langFileData = await CoreFile.instance.readFile<CoreH5PLibraryLangJsonData>(
langFilePath,
CoreFileFormat.FORMATJSON,
);
const parts = entry.name.split('.'); // The language code is in parts[0].
langIndex[parts[0]] = langFileData;
} catch (error) {
// Ignore this language.
}
}));
return langIndex;
} catch (error) {
// Probably doesn't exist, ignore.
}
}
/**
* Read semantics.json file and return its parsed contents.
*
* @param libPath Path to the directory where the library files are.
* @return Promise resolved with the parsed file contents.
*/
protected async readLibrarySemanticsFile(libPath: string): Promise<CoreH5PSemantics[] | undefined> {
try {
const path = CoreTextUtils.instance.concatenatePaths(libPath, 'semantics.json');
return await CoreFile.instance.readFile<CoreH5PSemantics[]>(path, CoreFileFormat.FORMATJSON);
} catch (error) {
// Probably doesn't exist, ignore.
}
}
}
/**
* Data of the main JSON H5P files.
*/
export type CoreH5PMainJSONFilesData = {
contentJsonData: unknown; // Contents of content.json file.
librariesJsonData: CoreH5PLibrariesJsonData; // JSON data about each library.
mainJsonData: CoreH5PMainJSONData; // Contents of h5p.json file.
};
/**
* Data stored in h5p.json file of a content. More info in https://h5p.org/documentation/developers/json-file-definitions
*/
export type CoreH5PMainJSONData = {
title: string; // Title of the content.
mainLibrary: string; // The main H5P library for this content.
language: string; // Language code.
preloadedDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
embedTypes?: ('div' | 'iframe')[]; // List of possible ways to embed the package in the page.
authors?: { // The name and role of the content authors
name: string;
role: string;
}[];
source?: string; // The source (a URL) of the licensed material.
license?: string; // A code for the content license.
licenseVersion?: string; // The version of the license above as a string.
licenseExtras?: string; // Any additional information about the license.
yearFrom?: string; // If a license is valid for a certain period of time, this represents the start year (as a string).
yearTo?: string; // If a license is valid for a certain period of time, this represents the end year (as a string).
changes?: { // The changelog.
date: string;
author: string;
log: string;
}[];
authorComments?: string; // Comments for the editor of the content.
};
/**
* All JSON data for libraries of a package.
*/
export type CoreH5PLibrariesJsonData = {[libString: string]: CoreH5PLibraryJsonData};
/**
* All JSON data for a library, including semantics and language.
*/
export type CoreH5PLibraryJsonData = CoreH5PLibraryMainJsonData & {
semantics?: CoreH5PSemantics[]; // Data in semantics.json.
language?: CoreH5PLibraryLangsJsonData; // Language JSON data.
hasIcon?: boolean; // Whether the library has an icon.
uploadDirectory?: string; // Path where the lib is stored.
};
/**
* Data stored in library.json file of a library. More info in https://h5p.org/library-definition
*/
export type CoreH5PLibraryMainJsonData = {
title: string; // The human readable name of this library.
machineName: string; // The library machine name.
majorVersion: number; // Major version.
minorVersion: number; // Minor version.
patchVersion: number; // Patch version.
runnable: number; // Whether or not this library is runnable.
coreApi?: { // Required version of H5P Core API.
majorVersion: number;
minorVersion: number;
};
author?: string; // The name of the library author.
license?: string; // A code for the content license.
description?: string; // Textual description of the library.
preloadedDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
dynamicDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
editorDependencies?: CoreH5PLibraryBasicData[]; // Dependencies.
preloadedJs?: { path: string }[]; // List of path to the javascript files required for the library.
preloadedCss?: { path: string }[]; // List of path to the CSS files to be loaded with the library.
embedTypes?: ('div' | 'iframe')[]; // List of possible ways to embed the package in the page.
fullscreen?: number; // Enables the integrated full-screen button.
metadataSettings?: CoreH5PLibraryMetadataSettings; // Metadata settings.
addTo?: CoreH5PLibraryAddTo;
};
/**
* Library metadata settings.
*/
export type CoreH5PLibraryMetadataSettings = {
disable?: boolean | number;
disableExtraTitleField?: boolean | number;
};
/**
* Library plugin configuration data.
*/
export type CoreH5PLibraryAddTo = {
content?: {
types?: {
text?: {
regex?: string;
};
}[];
};
};
/**
* Data stored in all languages JSON file of a library.
*/
export type CoreH5PLibraryLangsJsonData = {[code: string]: CoreH5PLibraryLangJsonData};
/**
* Data stored in each language JSON file of a library.
*/
export type CoreH5PLibraryLangJsonData = {
semantics?: CoreH5PSemantics[];
};

View File

@ -0,0 +1,51 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import {
CONTENT_TABLE_NAME,
LIBRARIES_TABLE_NAME,
LIBRARY_DEPENDENCIES_TABLE_NAME,
CONTENTS_LIBRARIES_TABLE_NAME,
LIBRARIES_CACHEDASSETS_TABLE_NAME,
} from './services/database/h5p';
@NgModule({
imports: [
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [
CONTENT_TABLE_NAME,
LIBRARIES_TABLE_NAME,
LIBRARY_DEPENDENCIES_TABLE_NAME,
CONTENTS_LIBRARIES_TABLE_NAME,
LIBRARIES_CACHEDASSETS_TABLE_NAME,
],
multi: true,
},
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
// @todo
},
},
],
})
export class CoreH5PModule {}

View File

@ -0,0 +1,308 @@
// (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 { CoreSiteSchema } from '@services/sites';
/**
* Database variables for CoreH5PProvider service.
*/
// DB table names.
export const CONTENT_TABLE_NAME = 'h5p_content'; // H5P content.
export const LIBRARIES_TABLE_NAME = 'h5p_libraries'; // Installed libraries.
export const LIBRARY_DEPENDENCIES_TABLE_NAME = 'h5p_library_dependencies'; // Library dependencies.
export const CONTENTS_LIBRARIES_TABLE_NAME = 'h5p_contents_libraries'; // Which library is used in which content.
export const LIBRARIES_CACHEDASSETS_TABLE_NAME = 'h5p_libraries_cachedassets'; // H5P cached library assets.
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreH5PProvider',
version: 1,
canBeCleared: [
CONTENT_TABLE_NAME,
LIBRARIES_TABLE_NAME,
LIBRARY_DEPENDENCIES_TABLE_NAME,
CONTENTS_LIBRARIES_TABLE_NAME,
LIBRARIES_CACHEDASSETS_TABLE_NAME,
],
tables: [
{
name: CONTENT_TABLE_NAME,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
autoIncrement: true,
},
{
name: 'jsoncontent',
type: 'TEXT',
notNull: true,
},
{
name: 'mainlibraryid',
type: 'INTEGER',
notNull: true,
},
{
name: 'foldername',
type: 'TEXT',
notNull: true,
},
{
name: 'fileurl',
type: 'TEXT',
notNull: true,
},
{
name: 'filtered',
type: 'TEXT',
},
{
name: 'timecreated',
type: 'INTEGER',
notNull: true,
},
{
name: 'timemodified',
type: 'INTEGER',
notNull: true,
},
],
},
{
name: LIBRARIES_TABLE_NAME,
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: LIBRARY_DEPENDENCIES_TABLE_NAME,
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,
},
],
},
{
name: CONTENTS_LIBRARIES_TABLE_NAME,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
autoIncrement: true,
},
{
name: 'h5pid',
type: 'INTEGER',
notNull: true,
},
{
name: 'libraryid',
type: 'INTEGER',
notNull: true,
},
{
name: 'dependencytype',
type: 'TEXT',
notNull: true,
},
{
name: 'dropcss',
type: 'INTEGER',
notNull: true,
},
{
name: 'weight',
type: 'INTEGER',
notNull: true,
},
],
},
{
name: LIBRARIES_CACHEDASSETS_TABLE_NAME,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
autoIncrement: true,
},
{
name: 'libraryid',
type: 'INTEGER',
notNull: true,
},
{
name: 'hash',
type: 'TEXT',
notNull: true,
},
{
name: 'foldername',
type: 'TEXT',
notNull: true,
},
],
},
],
};
/**
* Structure of content data stored in DB.
*/
export type CoreH5PContentDBRecord = {
id: number; // The id of the content.
jsoncontent: string; // The content in json format.
mainlibraryid: number; // The library we first instantiate for this node.
foldername: string; // Name of the folder that contains the contents.
fileurl: string; // The online URL of the H5P package.
filtered: string | null; // Filtered version of json_content.
timecreated: number; // Time created.
timemodified: number; // Time modified.
};
/**
* Structure of library data stored in DB.
*/
export type CoreH5PLibraryDBRecord = {
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 | null; // Comma separated list of scripts to load.
preloadedcss?: string | null; // Comma separated list of stylesheets to load.
droplibrarycss?: string | null; // Libraries that should not have CSS included if this lib is used. Comma separated list.
semantics?: string | null; // The semantics definition.
addto?: string | null; // Plugin configuration data.
};
/**
* Structure of library dependencies stored in DB.
*/
export type CoreH5PLibraryDependencyDBRecord = {
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.
};
/**
* Structure of library used by a content stored in DB.
*/
export type CoreH5PContentsLibraryDBRecord = {
id: number;
h5pid: number;
libraryid: number;
dependencytype: string;
dropcss: number;
weight: number;
};
/**
* Structure of library cached assets stored in DB.
*/
export type CoreH5PLibraryCachedAssetsDBRecord = {
id: number; // Id.
libraryid: number; // The id of an H5P library.
hash: string; // The hash to identify the cached asset.
foldername: string; // Name of the folder that contains the contents.
};

View File

@ -0,0 +1,248 @@
// (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';
import { CoreSites } from '@services/sites';
import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreQueueRunner } from '@classes/queue-runner';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreH5PCore } from '../classes/core';
import { CoreH5PFramework } from '../classes/framework';
import { CoreH5PPlayer } from '../classes/player';
import { CoreH5PStorage } from '../classes/storage';
import { CoreH5PValidator } from '../classes/validator';
import { makeSingleton } from '@singletons';
import { CoreError } from '@classes/errors/error';
/**
* Service to provide H5P functionalities.
*/
@Injectable({ providedIn: 'root' })
export class CoreH5PProvider {
h5pCore: CoreH5PCore;
h5pFramework: CoreH5PFramework;
h5pPlayer: CoreH5PPlayer;
h5pStorage: CoreH5PStorage;
h5pValidator: CoreH5PValidator;
queueRunner: CoreQueueRunner;
protected readonly ROOT_CACHE_KEY = 'CoreH5P:';
constructor() {
this.queueRunner = new CoreQueueRunner(1);
this.h5pValidator = new CoreH5PValidator();
this.h5pFramework = new CoreH5PFramework();
this.h5pCore = new CoreH5PCore(this.h5pFramework);
this.h5pStorage = new CoreH5PStorage(this.h5pCore, this.h5pFramework);
this.h5pPlayer = new CoreH5PPlayer(this.h5pCore, this.h5pStorage);
}
/**
* Returns whether or not WS to get trusted H5P file is available.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if ws is available, false otherwise.
* @since 3.8
*/
async canGetTrustedH5PFile(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
return this.canGetTrustedH5PFileInSite(site);
}
/**
* Returns whether or not WS to get trusted H5P file is available in a certain site.
*
* @param site Site. If not defined, current site.
* @return Promise resolved with true if ws is available, false otherwise.
* @since 3.8
*/
canGetTrustedH5PFileInSite(site?: CoreSite): boolean {
site = site || CoreSites.instance.getCurrentSite();
return !!(site?.wsAvailable('core_h5p_get_trusted_h5p_file'));
}
/**
* Get a trusted H5P file.
*
* @param url The file URL.
* @param options Options.
* @param ignoreCache Whether to ignore cache.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the file data.
*/
async getTrustedH5PFile(
url: string,
options?: CoreH5PGetTrustedFileOptions,
ignoreCache?: boolean,
siteId?: string,
): Promise<CoreWSExternalFile> {
options = options || {};
const site = await CoreSites.instance.getSite(siteId);
const data: CoreH5pGetTrustedH5pFileWSParams = {
url: this.treatH5PUrl(url, site.getURL()),
frame: options.frame ? 1 : 0,
export: options.export ? 1 : 0,
embed: options.embed ? 1 : 0,
copyright: options.copyright ? 1 : 0,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getTrustedH5PFileCacheKey(url),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
const result: CoreH5PGetTrustedH5PFileResult = await site.read('core_h5p_get_trusted_h5p_file', data, preSets);
if (result.warnings && result.warnings.length) {
throw result.warnings[0];
}
if (result.files && result.files.length) {
return result.files[0];
}
throw new CoreError('File not found');
}
/**
* Get cache key for trusted H5P file WS calls.
*
* @param url The file URL.
* @return Cache key.
*/
protected getTrustedH5PFileCacheKey(url: string): string {
return this.getTrustedH5PFilePrefixCacheKey() + url;
}
/**
* Get prefixed cache key for trusted H5P file WS calls.
*
* @return Cache key.
*/
protected getTrustedH5PFilePrefixCacheKey(): string {
return this.ROOT_CACHE_KEY + 'trustedH5PFile:';
}
/**
* Invalidates all trusted H5P file WS calls.
*
* @param siteId Site ID (empty for current site).
* @return Promise resolved when the data is invalidated.
*/
async invalidateAllGetTrustedH5PFile(siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKeyStartingWith(this.getTrustedH5PFilePrefixCacheKey());
}
/**
* Invalidates get trusted H5P file WS call.
*
* @param url The URL of the file.
* @param siteId Site ID (empty for current site).
* @return Promise resolved when the data is invalidated.
*/
async invalidateGetTrustedH5PFile(url: string, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url));
}
/**
* Check whether H5P offline is disabled.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether is disabled.
*/
async isOfflineDisabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
return this.isOfflineDisabledInSite(site);
}
/**
* Check whether H5P offline is disabled.
*
* @param site Site instance. If not defined, current site.
* @return Whether is disabled.
*/
isOfflineDisabledInSite(site?: CoreSite): boolean {
site = site || CoreSites.instance.getCurrentSite();
return !!(site?.isFeatureDisabled('NoDelegate_H5POffline'));
}
/**
* Treat an H5P url before sending it to WS.
*
* @param url H5P file URL.
* @param siteUrl Site URL.
* @return Treated url.
*/
treatH5PUrl(url: string, siteUrl: string): string {
if (url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, '/webservice/pluginfile.php')) === 0) {
url = url.replace('/webservice/pluginfile', '/pluginfile');
}
return CoreUrlUtils.instance.removeUrlParams(url);
}
}
export class CoreH5P extends makeSingleton(CoreH5PProvider) {}
/**
* Params of core_h5p_get_trusted_h5p_file WS.
*/
export type CoreH5pGetTrustedH5pFileWSParams = {
url: string; // H5P file url.
frame?: number; // The frame allow to show the bar options below the content.
export?: number; // The export allow to download the package.
embed?: number; // The embed allow to copy the code to your site.
copyright?: number; // The copyright option.
};
/**
* Options for core_h5p_get_trusted_h5p_file.
*/
export type CoreH5PGetTrustedFileOptions = {
frame?: boolean; // Whether to show the bar options below the content.
export?: boolean; // Whether to allow to download the package.
embed?: boolean; // Whether to allow to copy the code to your site.
copyright?: boolean; // The copyright option.
};
/**
* Result of core_h5p_get_trusted_h5p_file.
*/
export type CoreH5PGetTrustedH5PFileResult = {
files: CoreWSExternalFile[]; // Files.
warnings: CoreWSExternalWarning[]; // List of warnings.
};

View File

@ -70,10 +70,25 @@ export const enum CoreFileFormat {
export class CoreFileProvider {
// Formats to read a file.
/**
* @deprecated since 3.9.5, use CoreFileFormat directly.
*/
static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT;
/**
* @deprecated since 3.9.5, use CoreFileFormat directly.
*/
static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL;
/**
* @deprecated since 3.9.5, use CoreFileFormat directly.
*/
static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING;
/**
* @deprecated since 3.9.5, use CoreFileFormat directly.
*/
static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER;
/**
* @deprecated since 3.9.5, use CoreFileFormat directly.
*/
static readonly FORMATJSON = CoreFileFormat.FORMATJSON;
// Folders.
@ -460,19 +475,25 @@ export class CoreFileProvider {
* @param format Format to read the file.
* @return Promise to be resolved when the file is read.
*/
readFile(path: string, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
readFile(
path: string,
format?: CoreFileFormat.FORMATTEXT | CoreFileFormat.FORMATDATAURL | CoreFileFormat.FORMATBINARYSTRING,
): Promise<string>;
readFile(path: string, format: CoreFileFormat.FORMATARRAYBUFFER): Promise<ArrayBuffer>;
readFile<T = unknown>(path: string, format: CoreFileFormat.FORMATJSON): Promise<T>;
readFile(path: string, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Read file ' + path + ' with format ' + format);
switch (format) {
case CoreFileProvider.FORMATDATAURL:
case CoreFileFormat.FORMATDATAURL:
return File.instance.readAsDataURL(this.basePath, path);
case CoreFileProvider.FORMATBINARYSTRING:
case CoreFileFormat.FORMATBINARYSTRING:
return File.instance.readAsBinaryString(this.basePath, path);
case CoreFileProvider.FORMATARRAYBUFFER:
case CoreFileFormat.FORMATARRAYBUFFER:
return File.instance.readAsArrayBuffer(this.basePath, path);
case CoreFileProvider.FORMATJSON:
case CoreFileFormat.FORMATJSON:
return File.instance.readAsText(this.basePath, path).then((text) => {
const parsed = CoreTextUtils.instance.parseJSON(text, null);
@ -494,8 +515,8 @@ export class CoreFileProvider {
* @param format Format to read the file.
* @return Promise to be resolved when the file is read.
*/
readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
format = format || CoreFileProvider.FORMATTEXT;
readFileData(fileData: IFile, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
format = format || CoreFileFormat.FORMATTEXT;
this.logger.debug('Read file from file data with format ' + format);
return new Promise((resolve, reject): void => {
@ -503,7 +524,7 @@ export class CoreFileProvider {
reader.onloadend = (event): void => {
if (event.target?.result !== undefined && event.target.result !== null) {
if (format == CoreFileProvider.FORMATJSON) {
if (format == CoreFileFormat.FORMATJSON) {
// Convert to object.
const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null);
@ -535,13 +556,13 @@ export class CoreFileProvider {
}, 3000);
switch (format) {
case CoreFileProvider.FORMATDATAURL:
case CoreFileFormat.FORMATDATAURL:
reader.readAsDataURL(fileData);
break;
case CoreFileProvider.FORMATBINARYSTRING:
case CoreFileFormat.FORMATBINARYSTRING:
reader.readAsBinaryString(fileData);
break;
case CoreFileProvider.FORMATARRAYBUFFER:
case CoreFileFormat.FORMATARRAYBUFFER:
reader.readAsArrayBuffer(fileData);
break;
default:

View File

@ -23,7 +23,7 @@ import { timeout } from 'rxjs/operators';
import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http';
import { CoreApp } from '@services/app';
import { CoreFile, CoreFileProvider } from '@services/file';
import { CoreFile, CoreFileFormat } from '@services/file';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
@ -855,9 +855,9 @@ export class CoreWSProvider {
// Use the cordova plugin.
if (url.indexOf('file://') === 0) {
// We cannot load local files using the http native plugin. Use file provider instead.
const format = options.responseType == 'json' ? CoreFileProvider.FORMATJSON : CoreFileProvider.FORMATTEXT;
const content = await CoreFile.instance.readFile(url, format);
const content = options.responseType == 'json' ?
await CoreFile.instance.readFile<T>(url, CoreFileFormat.FORMATJSON) :
await CoreFile.instance.readFile(url, CoreFileFormat.FORMATTEXT);
return new HttpResponse<T>({
body: <T> content,