MOBILE-3666 h5p: Implement services and classes
parent
c83ff34ae0
commit
f1ac735abf
|
@ -1087,7 +1087,7 @@ export class SQLiteDB {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SQLiteDBRecordValues = {
|
export type SQLiteDBRecordValues = {
|
||||||
[key in string ]: SQLiteDBRecordValue | undefined;
|
[key in string ]: SQLiteDBRecordValue | undefined | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SQLiteDBQueryParams = {
|
export type SQLiteDBQueryParams = {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { CoreCourseModule } from './course/course.module';
|
||||||
import { CoreCoursesModule } from './courses/courses.module';
|
import { CoreCoursesModule } from './courses/courses.module';
|
||||||
import { CoreEmulatorModule } from './emulator/emulator.module';
|
import { CoreEmulatorModule } from './emulator/emulator.module';
|
||||||
import { CoreFileUploaderModule } from './fileuploader/fileuploader.module';
|
import { CoreFileUploaderModule } from './fileuploader/fileuploader.module';
|
||||||
|
import { CoreH5PModule } from './h5p/h5p.module';
|
||||||
import { CoreLoginModule } from './login/login.module';
|
import { CoreLoginModule } from './login/login.module';
|
||||||
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
|
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
|
||||||
import { CoreSettingsModule } from './settings/settings.module';
|
import { CoreSettingsModule } from './settings/settings.module';
|
||||||
|
@ -41,6 +42,7 @@ import { CoreXAPIModule } from './xapi/xapi.module';
|
||||||
CoreUserModule,
|
CoreUserModule,
|
||||||
CorePushNotificationsModule,
|
CorePushNotificationsModule,
|
||||||
CoreXAPIModule,
|
CoreXAPIModule,
|
||||||
|
CoreH5PModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreFeaturesModule {}
|
export class CoreFeaturesModule {}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
|
@ -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.
|
||||||
|
};
|
|
@ -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[];
|
||||||
|
};
|
|
@ -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 {}
|
|
@ -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.
|
||||||
|
};
|
|
@ -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.
|
||||||
|
};
|
|
@ -70,10 +70,25 @@ export const enum CoreFileFormat {
|
||||||
export class CoreFileProvider {
|
export class CoreFileProvider {
|
||||||
|
|
||||||
// Formats to read a file.
|
// Formats to read a file.
|
||||||
|
/**
|
||||||
|
* @deprecated since 3.9.5, use CoreFileFormat directly.
|
||||||
|
*/
|
||||||
static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT;
|
static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT;
|
||||||
|
/**
|
||||||
|
* @deprecated since 3.9.5, use CoreFileFormat directly.
|
||||||
|
*/
|
||||||
static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL;
|
static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL;
|
||||||
|
/**
|
||||||
|
* @deprecated since 3.9.5, use CoreFileFormat directly.
|
||||||
|
*/
|
||||||
static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING;
|
static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING;
|
||||||
|
/**
|
||||||
|
* @deprecated since 3.9.5, use CoreFileFormat directly.
|
||||||
|
*/
|
||||||
static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER;
|
static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER;
|
||||||
|
/**
|
||||||
|
* @deprecated since 3.9.5, use CoreFileFormat directly.
|
||||||
|
*/
|
||||||
static readonly FORMATJSON = CoreFileFormat.FORMATJSON;
|
static readonly FORMATJSON = CoreFileFormat.FORMATJSON;
|
||||||
|
|
||||||
// Folders.
|
// Folders.
|
||||||
|
@ -460,19 +475,25 @@ export class CoreFileProvider {
|
||||||
* @param format Format to read the file.
|
* @param format Format to read the file.
|
||||||
* @return Promise to be resolved when the file is read.
|
* @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.
|
// Remove basePath if it's in the path.
|
||||||
path = this.removeStartingSlash(path.replace(this.basePath, ''));
|
path = this.removeStartingSlash(path.replace(this.basePath, ''));
|
||||||
this.logger.debug('Read file ' + path + ' with format ' + format);
|
this.logger.debug('Read file ' + path + ' with format ' + format);
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case CoreFileProvider.FORMATDATAURL:
|
case CoreFileFormat.FORMATDATAURL:
|
||||||
return File.instance.readAsDataURL(this.basePath, path);
|
return File.instance.readAsDataURL(this.basePath, path);
|
||||||
case CoreFileProvider.FORMATBINARYSTRING:
|
case CoreFileFormat.FORMATBINARYSTRING:
|
||||||
return File.instance.readAsBinaryString(this.basePath, path);
|
return File.instance.readAsBinaryString(this.basePath, path);
|
||||||
case CoreFileProvider.FORMATARRAYBUFFER:
|
case CoreFileFormat.FORMATARRAYBUFFER:
|
||||||
return File.instance.readAsArrayBuffer(this.basePath, path);
|
return File.instance.readAsArrayBuffer(this.basePath, path);
|
||||||
case CoreFileProvider.FORMATJSON:
|
case CoreFileFormat.FORMATJSON:
|
||||||
return File.instance.readAsText(this.basePath, path).then((text) => {
|
return File.instance.readAsText(this.basePath, path).then((text) => {
|
||||||
const parsed = CoreTextUtils.instance.parseJSON(text, null);
|
const parsed = CoreTextUtils.instance.parseJSON(text, null);
|
||||||
|
|
||||||
|
@ -494,8 +515,8 @@ export class CoreFileProvider {
|
||||||
* @param format Format to read the file.
|
* @param format Format to read the file.
|
||||||
* @return Promise to be resolved when the file is read.
|
* @return Promise to be resolved when the file is read.
|
||||||
*/
|
*/
|
||||||
readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
|
readFileData(fileData: IFile, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
|
||||||
format = format || CoreFileProvider.FORMATTEXT;
|
format = format || CoreFileFormat.FORMATTEXT;
|
||||||
this.logger.debug('Read file from file data with format ' + format);
|
this.logger.debug('Read file from file data with format ' + format);
|
||||||
|
|
||||||
return new Promise((resolve, reject): void => {
|
return new Promise((resolve, reject): void => {
|
||||||
|
@ -503,7 +524,7 @@ export class CoreFileProvider {
|
||||||
|
|
||||||
reader.onloadend = (event): void => {
|
reader.onloadend = (event): void => {
|
||||||
if (event.target?.result !== undefined && event.target.result !== null) {
|
if (event.target?.result !== undefined && event.target.result !== null) {
|
||||||
if (format == CoreFileProvider.FORMATJSON) {
|
if (format == CoreFileFormat.FORMATJSON) {
|
||||||
// Convert to object.
|
// Convert to object.
|
||||||
const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null);
|
const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null);
|
||||||
|
|
||||||
|
@ -535,13 +556,13 @@ export class CoreFileProvider {
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case CoreFileProvider.FORMATDATAURL:
|
case CoreFileFormat.FORMATDATAURL:
|
||||||
reader.readAsDataURL(fileData);
|
reader.readAsDataURL(fileData);
|
||||||
break;
|
break;
|
||||||
case CoreFileProvider.FORMATBINARYSTRING:
|
case CoreFileFormat.FORMATBINARYSTRING:
|
||||||
reader.readAsBinaryString(fileData);
|
reader.readAsBinaryString(fileData);
|
||||||
break;
|
break;
|
||||||
case CoreFileProvider.FORMATARRAYBUFFER:
|
case CoreFileFormat.FORMATARRAYBUFFER:
|
||||||
reader.readAsArrayBuffer(fileData);
|
reader.readAsArrayBuffer(fileData);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { timeout } from 'rxjs/operators';
|
||||||
|
|
||||||
import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http';
|
import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreFile, CoreFileProvider } from '@services/file';
|
import { CoreFile, CoreFileFormat } from '@services/file';
|
||||||
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
|
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
|
||||||
|
@ -855,9 +855,9 @@ export class CoreWSProvider {
|
||||||
// Use the cordova plugin.
|
// Use the cordova plugin.
|
||||||
if (url.indexOf('file://') === 0) {
|
if (url.indexOf('file://') === 0) {
|
||||||
// We cannot load local files using the http native plugin. Use file provider instead.
|
// 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 = options.responseType == 'json' ?
|
||||||
|
await CoreFile.instance.readFile<T>(url, CoreFileFormat.FORMATJSON) :
|
||||||
const content = await CoreFile.instance.readFile(url, format);
|
await CoreFile.instance.readFile(url, CoreFileFormat.FORMATTEXT);
|
||||||
|
|
||||||
return new HttpResponse<T>({
|
return new HttpResponse<T>({
|
||||||
body: <T> content,
|
body: <T> content,
|
||||||
|
|
Loading…
Reference in New Issue