992 lines
38 KiB
TypeScript
992 lines
38 KiB
TypeScript
// (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 '@providers/sites';
|
||
import { CoreTextUtils } from '@providers/utils/text';
|
||
import { CoreUtils } from '@providers/utils/utils';
|
||
import { CoreH5P } from '../providers/h5p';
|
||
import { CoreH5PFileStorage } from './file-storage';
|
||
import { CoreH5PFramework } from './framework';
|
||
import { CoreH5PContentValidator } from './content-validator';
|
||
import { Md5 } from 'ts-md5/dist/md5';
|
||
import { Translate } from '@singletons/core.singletons';
|
||
|
||
/**
|
||
* Equivalent to H5P's H5PCore class.
|
||
*/
|
||
export class CoreH5PCore {
|
||
|
||
static STYLES = [
|
||
'styles/h5p.css',
|
||
'styles/h5p-confirmation-dialog.css',
|
||
'styles/h5p-core-button.css'
|
||
];
|
||
static SCRIPTS = [
|
||
'js/jquery.js',
|
||
'js/h5p.js',
|
||
'js/h5p-event-dispatcher.js',
|
||
'js/h5p-x-api-event.js',
|
||
'js/h5p-x-api.js',
|
||
'js/h5p-content-type.js',
|
||
'js/h5p-confirmation-dialog.js',
|
||
'js/h5p-action-bar.js',
|
||
'js/request-queue.js',
|
||
];
|
||
static ADMIN_SCRIPTS = [
|
||
'js/jquery.js',
|
||
'js/h5p-utils.js',
|
||
];
|
||
|
||
// Disable flags
|
||
static DISABLE_NONE = 0;
|
||
static DISABLE_FRAME = 1;
|
||
static DISABLE_DOWNLOAD = 2;
|
||
static DISABLE_EMBED = 4;
|
||
static DISABLE_COPYRIGHT = 8;
|
||
static DISABLE_ABOUT = 16;
|
||
|
||
static DISPLAY_OPTION_FRAME = 'frame';
|
||
static DISPLAY_OPTION_DOWNLOAD = 'export';
|
||
static DISPLAY_OPTION_EMBED = 'embed';
|
||
static DISPLAY_OPTION_COPYRIGHT = 'copyright';
|
||
static DISPLAY_OPTION_ABOUT = 'icon';
|
||
static DISPLAY_OPTION_COPY = 'copy';
|
||
|
||
// Map to slugify characters.
|
||
static SLUGIFY_MAP = {
|
||
æ: 'ae', ø: 'oe', ö: 'o', ó: 'o', ô: 'o', Ò: 'oe', Õ: 'o', Ý: 'o', ý: 'y', ÿ: 'y', ā: 'y', ă: 'a', ą: 'a', œ: 'a', å: 'a',
|
||
ä: 'a', á: 'a', à: 'a', â: 'a', ã: 'a', ç: 'c', ć: 'c', ĉ: 'c', ċ: 'c', č: 'c', é: 'e', è: 'e', ê: 'e', ë: 'e', í: 'i',
|
||
ì: 'i', î: 'i', ï: 'i', ú: 'u', ñ: 'n', ü: 'u', ù: 'u', û: 'u', ß: 'es', ď: 'd', đ: 'd', ē: 'e', ĕ: 'e', ė: 'e', ę: 'e',
|
||
ě: 'e', ĝ: 'g', ğ: 'g', ġ: 'g', ģ: 'g', ĥ: 'h', ħ: 'h', ĩ: 'i', ī: 'i', ĭ: 'i', į: 'i', ı: 'i', ij: 'ij', ĵ: 'j', ķ: 'k',
|
||
ĺ: 'l', ļ: 'l', ľ: 'l', ŀ: 'l', ł: 'l', ń: 'n', ņ: 'n', ň: 'n', ʼn: 'n', ō: 'o', ŏ: 'o', ő: 'o', ŕ: 'r', ŗ: 'r', ř: 'r',
|
||
ś: 's', ŝ: 's', ş: 's', š: 's', ţ: 't', ť: 't', ŧ: 't', ũ: 'u', ū: 'u', ŭ: 'u', ů: 'u', ű: 'u', ų: 'u', ŵ: 'w', ŷ: 'y',
|
||
ź: 'z', ż: 'z', ž: 'z', ſ: 's', ƒ: 'f', ơ: 'o', ư: 'u', ǎ: 'a', ǐ: 'i', ǒ: 'o', ǔ: 'u', ǖ: 'u', ǘ: 'u', ǚ: 'u', ǜ: 'u',
|
||
ǻ: 'a', ǽ: 'ae', ǿ: 'oe'
|
||
};
|
||
|
||
aggregateAssets = true;
|
||
|
||
h5pFS: CoreH5PFileStorage;
|
||
|
||
constructor(public h5pFramework: CoreH5PFramework) {
|
||
this.h5pFS = new CoreH5PFileStorage();
|
||
}
|
||
|
||
/**
|
||
* Determine the correct embed type to use.
|
||
*
|
||
* @param Embed type of the content.
|
||
* @param Embed type of the main library.
|
||
* @return Either 'div' or 'iframe'.
|
||
*/
|
||
static determineEmbedType(contentEmbedType: string, libraryEmbedTypes: string): string {
|
||
// Detect content embed type.
|
||
let embedType = contentEmbedType.toLowerCase().indexOf('div') != -1 ? 'div' : 'iframe';
|
||
|
||
if (libraryEmbedTypes) {
|
||
// Check that embed type is available for library
|
||
const embedTypes = libraryEmbedTypes.toLowerCase();
|
||
|
||
if (embedTypes.indexOf(embedType) == -1) {
|
||
// Not available, pick default.
|
||
embedType = embedTypes.indexOf('div') != -1 ? 'div' : 'iframe';
|
||
}
|
||
}
|
||
|
||
return embedType;
|
||
}
|
||
|
||
/**
|
||
* Filter content run parameters and rebuild content dependency cache.
|
||
*
|
||
* @param content Content data.
|
||
* @param siteId Site ID. If not defined, current site.
|
||
* @return Promise resolved with the filtered params, resolved with null if error.
|
||
*/
|
||
async filterParameters(content: CoreH5PContentData, siteId?: string): Promise<string> {
|
||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||
|
||
if (content.filtered) {
|
||
return content.filtered;
|
||
}
|
||
|
||
if (typeof content.library == 'undefined' || typeof content.params == 'undefined') {
|
||
return null;
|
||
}
|
||
|
||
const params = {
|
||
library: CoreH5PCore.libraryToString(content.library),
|
||
params: CoreTextUtils.instance.parseJSON(content.params, false),
|
||
};
|
||
|
||
if (!params.params) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const validator = new CoreH5PContentValidator(siteId);
|
||
|
||
// Validate the main library and its dependencies.
|
||
await validator.validateLibrary(params, {options: [params.library]});
|
||
|
||
// Handle addons.
|
||
const addons = await this.h5pFramework.loadAddons(siteId);
|
||
|
||
// Validate addons.
|
||
for (const i in addons) {
|
||
const addon = addons[i];
|
||
const addTo = addon.addTo;
|
||
|
||
if (addTo && addTo.content && addTo.content.types && addTo.content.types.length) {
|
||
for (let i = 0; i < addTo.content.types.length; i++) {
|
||
const type = addTo.content.types[i];
|
||
|
||
if (type && type.text && type.text.regex && this.textAddonMatches(params.params, type.text.regex)) {
|
||
|
||
await validator.addon(addon);
|
||
|
||
// An addon shall only be added once.
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update content dependencies.
|
||
content.dependencies = validator.getDependencies();
|
||
|
||
const paramsStr = JSON.stringify(params.params);
|
||
|
||
// Sometimes the parameters are filtered before content has been created
|
||
if (content.id) {
|
||
// Update library usage.
|
||
try {
|
||
await this.h5pFramework.deleteLibraryUsage(content.id, siteId);
|
||
} catch (error) {
|
||
// Ignore errors.
|
||
}
|
||
|
||
await this.h5pFramework.saveLibraryUsage(content.id, content.dependencies, siteId);
|
||
|
||
if (!content.slug) {
|
||
content.slug = this.generateContentSlug(content);
|
||
}
|
||
|
||
// Cache.
|
||
await this.h5pFramework.updateContentFields(content.id, {
|
||
filtered: paramsStr,
|
||
slug: content.slug,
|
||
}, siteId);
|
||
}
|
||
|
||
return paramsStr;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Recursive. Goes through the dependency tree for the given library and
|
||
* adds all the dependencies to the given array in a flat format.
|
||
*
|
||
* @param dependencies Object where to save the dependencies.
|
||
* @param library The library to find all dependencies for.
|
||
* @param nextWeight An integer determining the order of the libraries when they are loaded.
|
||
* @param editor Used internally to force all preloaded sub dependencies of an editor dependency to be editor dependencies.
|
||
* @param siteId Site ID. If not defined, current site.
|
||
* @return Promise resolved with the next weight.
|
||
*/
|
||
async findLibraryDependencies(dependencies: {[key: string]: CoreH5PContentDepsTreeDependency},
|
||
library: CoreH5PLibraryData | CoreH5PLibraryAddonData, nextWeight: number = 1, editor: boolean = false,
|
||
siteId?: string): Promise<number> {
|
||
|
||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||
|
||
const types = ['dynamic', 'preloaded', 'editor'];
|
||
|
||
for (const i in types) {
|
||
|
||
let type = types[i];
|
||
const property = type + 'Dependencies';
|
||
|
||
if (!library[property]) {
|
||
continue; // Skip, no such dependencies.
|
||
}
|
||
|
||
if (type === 'preloaded' && editor) {
|
||
// All preloaded dependencies of an editor library is set to editor.
|
||
type = 'editor';
|
||
}
|
||
|
||
for (const j in library[property]) {
|
||
|
||
const dependency: CoreH5PLibraryBasicData = library[property][j];
|
||
|
||
const dependencyKey = type + '-' + dependency.machineName;
|
||
if (dependencies[dependencyKey]) {
|
||
continue; // Skip, already have this.
|
||
}
|
||
|
||
// Get the dependency library data and its subdependencies.
|
||
const dependencyLibrary = await this.loadLibrary(dependency.machineName, dependency.majorVersion,
|
||
dependency.minorVersion, siteId);
|
||
|
||
dependencies[dependencyKey] = {
|
||
library: dependencyLibrary,
|
||
type: type
|
||
};
|
||
|
||
// Get all its subdependencies.
|
||
const weight = await this.findLibraryDependencies(dependencies, dependencyLibrary, nextWeight, type === 'editor',
|
||
siteId);
|
||
|
||
nextWeight = weight;
|
||
dependencies[dependencyKey].weight = nextWeight++;
|
||
}
|
||
}
|
||
|
||
return nextWeight;
|
||
}
|
||
|
||
/**
|
||
* Validate and fix display options, updating them if needed.
|
||
*
|
||
* @param displayOptions The display options to validate.
|
||
* @param id Package ID.
|
||
*/
|
||
fixDisplayOptions(displayOptions: CoreH5PDisplayOptions, id: number): CoreH5PDisplayOptions {
|
||
|
||
// Never allow downloading in the app.
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = false;
|
||
|
||
// Embed - force setting it if always on or always off. In web, this is done when storing in DB.
|
||
const embed = this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_EMBED, CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW);
|
||
if (embed == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW || embed == CoreH5PDisplayOptionBehaviour.NEVER_SHOW) {
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = (embed == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW);
|
||
}
|
||
|
||
if (!this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_FRAME, true)) {
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = false;
|
||
} else {
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = this.setDisplayOptionOverrides(
|
||
CoreH5PCore.DISPLAY_OPTION_EMBED, CoreH5PPermission.EMBED_H5P, id,
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED]);
|
||
|
||
if (this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_COPYRIGHT, true) == false) {
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = false;
|
||
}
|
||
}
|
||
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_COPY] = this.h5pFramework.hasPermission(CoreH5PPermission.COPY_H5P, id);
|
||
|
||
return displayOptions;
|
||
}
|
||
|
||
/**
|
||
* Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}.
|
||
*
|
||
* @param libraryString On the form {machineName} {majorVersion}.{minorVersion}
|
||
* @return Object with keys machineName, majorVersion and minorVersion. Null if string is not parsable.
|
||
*/
|
||
generateContentSlug(content: CoreH5PContentData): string {
|
||
|
||
let slug = CoreH5PCore.slugify(content.title);
|
||
let available: boolean = null;
|
||
|
||
while (!available) {
|
||
if (available === false) {
|
||
// If not available, add number suffix.
|
||
const matches = slug.match(/(.+-)([0-9]+)$/);
|
||
if (matches) {
|
||
slug = matches[1] + (Number(matches[2]) + 1);
|
||
} else {
|
||
slug += '-2';
|
||
}
|
||
}
|
||
|
||
available = this.h5pFramework.isContentSlugAvailable(slug);
|
||
}
|
||
|
||
return slug;
|
||
}
|
||
|
||
/**
|
||
* Combines path with version.
|
||
*
|
||
* @param assets List of assets to get their URLs.
|
||
* @param assetsFolderPath The path of the folder where the assets are.
|
||
* @return List of urls.
|
||
*/
|
||
getAssetsUrls(assets: CoreH5PDependencyAsset[], assetsFolderPath: string = ''): string[] {
|
||
const urls = [];
|
||
|
||
assets.forEach((asset) => {
|
||
let url = asset.path;
|
||
|
||
// Add URL prefix if not external.
|
||
if (asset.path.indexOf('://') == -1 && assetsFolderPath) {
|
||
url = CoreTextUtils.instance.concatenatePaths(assetsFolderPath, url);
|
||
}
|
||
|
||
// Add version if set.
|
||
if (asset.version) {
|
||
url += asset.version;
|
||
}
|
||
|
||
urls.push(url);
|
||
});
|
||
|
||
return urls;
|
||
}
|
||
|
||
/**
|
||
* Return file paths for all dependencies files.
|
||
*
|
||
* @param dependencies The dependencies to get the files.
|
||
* @param folderName Name of the folder of the content.
|
||
* @param prefix Make paths relative to another dir.
|
||
* @param siteId The site ID. If not defined, current site.
|
||
* @return Promise resolved with the files.
|
||
*/
|
||
async getDependenciesFiles(dependencies: {[machineName: string]: CoreH5PContentDependencyData}, folderName: string,
|
||
prefix: string = '', siteId?: string): Promise<CoreH5PDependenciesFiles> {
|
||
|
||
// Build files list for assets.
|
||
const files: CoreH5PDependenciesFiles = {
|
||
scripts: [],
|
||
styles: [],
|
||
};
|
||
|
||
// Avoid caching empty files.
|
||
if (!Object.keys(dependencies).length) {
|
||
return files;
|
||
}
|
||
|
||
let cachedAssetsHash: string;
|
||
let cachedAssets: {scripts?: CoreH5PDependencyAsset[], styles?: CoreH5PDependencyAsset[]};
|
||
|
||
if (this.aggregateAssets) {
|
||
// Get aggregated files for assets.
|
||
cachedAssetsHash = CoreH5PCore.getDependenciesHash(dependencies);
|
||
|
||
cachedAssets = await this.h5pFS.getCachedAssets(cachedAssetsHash);
|
||
|
||
if (cachedAssets) {
|
||
// Cached assets found, return them.
|
||
return Object.assign(files, cachedAssets);
|
||
}
|
||
}
|
||
|
||
// No cached assets, use content dependencies.
|
||
for (const key in dependencies) {
|
||
const dependency = dependencies[key];
|
||
|
||
if (!dependency.path) {
|
||
dependency.path = this.h5pFS.getDependencyPath(dependency);
|
||
dependency.preloadedJs = (<string> dependency.preloadedJs).split(',');
|
||
dependency.preloadedCss = (<string> dependency.preloadedCss).split(',');
|
||
}
|
||
|
||
dependency.version = '?ver=' + dependency.majorVersion + '.' + dependency.minorVersion + '.' + dependency.patchVersion;
|
||
|
||
this.getDependencyAssets(dependency, 'preloadedJs', files.scripts, prefix);
|
||
this.getDependencyAssets(dependency, 'preloadedCss', files.styles, prefix);
|
||
}
|
||
|
||
if (this.aggregateAssets) {
|
||
// Aggregate and store assets.
|
||
await this.h5pFS.cacheAssets(files, cachedAssetsHash, folderName, siteId);
|
||
|
||
// Keep track of which libraries have been cached in case they are updated.
|
||
await this.h5pFramework.saveCachedAssets(cachedAssetsHash, dependencies, folderName, siteId);
|
||
}
|
||
|
||
return files;
|
||
}
|
||
|
||
/**
|
||
* Get the hash of a list of dependencies.
|
||
*
|
||
* @param dependencies Dependencies.
|
||
* @return Hash.
|
||
*/
|
||
static getDependenciesHash(dependencies: {[machineName: string]: CoreH5PContentDependencyData}): string {
|
||
// Build hash of dependencies.
|
||
const toHash = [];
|
||
|
||
// Use unique identifier for each library version.
|
||
for (const name in dependencies) {
|
||
const dep = dependencies[name];
|
||
toHash.push(dep.machineName + '-' + dep.majorVersion + '.' + dep.minorVersion + '.' + dep.patchVersion);
|
||
}
|
||
|
||
// Sort in case the same dependencies comes in a different order.
|
||
toHash.sort((a, b) => {
|
||
return a.localeCompare(b);
|
||
});
|
||
|
||
// Calculate hash.
|
||
return <string> Md5.hashAsciiStr(toHash.join(''));
|
||
}
|
||
|
||
/**
|
||
* Get the paths to the content dependencies.
|
||
*
|
||
* @param id The H5P content ID.
|
||
* @param siteId The site ID. If not defined, current site.
|
||
* @return Promise resolved with an object containing the path of each content dependency.
|
||
*/
|
||
async getDependencyRoots(id: number, siteId?: string): Promise<{[libString: string]: string}> {
|
||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||
|
||
const roots = {};
|
||
|
||
const dependencies = await this.h5pFramework.loadContentDependencies(id, undefined, siteId);
|
||
|
||
for (const machineName in dependencies) {
|
||
const dependency = dependencies[machineName];
|
||
const folderName = CoreH5PCore.libraryToString(dependency, true);
|
||
|
||
roots[folderName] = this.h5pFS.getLibraryFolderPath(dependency, siteId, folderName);
|
||
}
|
||
|
||
return roots;
|
||
}
|
||
|
||
/**
|
||
* Get all dependency assets of the given type.
|
||
*
|
||
* @param dependency The dependency.
|
||
* @param type Type of assets to get.
|
||
* @param assets Array where to store the assets.
|
||
* @param prefix Make paths relative to another dir.
|
||
*/
|
||
protected getDependencyAssets(dependency: CoreH5PContentDependencyData, type: string, assets: CoreH5PDependencyAsset[],
|
||
prefix: string = ''): void {
|
||
|
||
// Check if dependency has any files of this type
|
||
if (!dependency[type] || dependency[type][0] === '') {
|
||
return;
|
||
}
|
||
|
||
// Check if we should skip CSS.
|
||
if (type === 'preloadedCss' && CoreUtils.instance.isTrueOrOne(dependency.dropCss)) {
|
||
return;
|
||
}
|
||
|
||
for (const key in dependency[type]) {
|
||
const file = dependency[type][key];
|
||
|
||
assets.push({
|
||
path: prefix + '/' + dependency.path + '/' + (typeof file != 'string' ? file.path : file).trim(),
|
||
version: dependency.version
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Convert display options to an object.
|
||
*
|
||
* @param disable Display options as a number.
|
||
* @return Display options as object.
|
||
*/
|
||
getDisplayOptionsAsObject(disable: number): CoreH5PDisplayOptions {
|
||
const displayOptions: CoreH5PDisplayOptions = {};
|
||
|
||
// tslint:disable: no-bitwise
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = !(disable & CoreH5PCore.DISABLE_FRAME);
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = !(disable & CoreH5PCore.DISABLE_DOWNLOAD);
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = !(disable & CoreH5PCore.DISABLE_EMBED);
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = !(disable & CoreH5PCore.DISABLE_COPYRIGHT);
|
||
displayOptions[CoreH5PCore.DISPLAY_OPTION_ABOUT] = !!this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_ABOUT, true);
|
||
|
||
return displayOptions;
|
||
}
|
||
|
||
/**
|
||
* Determine display option visibility when viewing H5P
|
||
*
|
||
* @param disable The display options as a number.
|
||
* @param id Package ID.
|
||
* @return Display options as object.
|
||
*/
|
||
getDisplayOptionsForView(disable: number, id: number): CoreH5PDisplayOptions {
|
||
return this.fixDisplayOptions(this.getDisplayOptionsAsObject(disable), id);
|
||
}
|
||
|
||
/**
|
||
* Provide localization for the Core JS.
|
||
*
|
||
* @return Object with the translations.
|
||
*/
|
||
getLocalization(): {[name: string]: string} {
|
||
return {
|
||
fullscreen: Translate.instance.instant('core.h5p.fullscreen'),
|
||
disableFullscreen: Translate.instance.instant('core.h5p.disablefullscreen'),
|
||
download: Translate.instance.instant('core.h5p.download'),
|
||
copyrights: Translate.instance.instant('core.h5p.copyright'),
|
||
embed: Translate.instance.instant('core.h5p.embed'),
|
||
size: Translate.instance.instant('core.h5p.size'),
|
||
showAdvanced: Translate.instance.instant('core.h5p.showadvanced'),
|
||
hideAdvanced: Translate.instance.instant('core.h5p.hideadvanced'),
|
||
advancedHelp: Translate.instance.instant('core.h5p.resizescript'),
|
||
copyrightInformation: Translate.instance.instant('core.h5p.copyright'),
|
||
close: Translate.instance.instant('core.h5p.close'),
|
||
title: Translate.instance.instant('core.h5p.title'),
|
||
author: Translate.instance.instant('core.h5p.author'),
|
||
year: Translate.instance.instant('core.h5p.year'),
|
||
source: Translate.instance.instant('core.h5p.source'),
|
||
license: Translate.instance.instant('core.h5p.license'),
|
||
thumbnail: Translate.instance.instant('core.h5p.thumbnail'),
|
||
noCopyrights: Translate.instance.instant('core.h5p.nocopyright'),
|
||
reuse: Translate.instance.instant('core.h5p.reuse'),
|
||
reuseContent: Translate.instance.instant('core.h5p.reuseContent'),
|
||
reuseDescription: Translate.instance.instant('core.h5p.reuseDescription'),
|
||
downloadDescription: Translate.instance.instant('core.h5p.downloadtitle'),
|
||
copyrightsDescription: Translate.instance.instant('core.h5p.copyrighttitle'),
|
||
embedDescription: Translate.instance.instant('core.h5p.embedtitle'),
|
||
h5pDescription: Translate.instance.instant('core.h5p.h5ptitle'),
|
||
contentChanged: Translate.instance.instant('core.h5p.contentchanged'),
|
||
startingOver: Translate.instance.instant('core.h5p.startingover'),
|
||
by: Translate.instance.instant('core.h5p.by'),
|
||
showMore: Translate.instance.instant('core.h5p.showmore'),
|
||
showLess: Translate.instance.instant('core.h5p.showless'),
|
||
subLevel: Translate.instance.instant('core.h5p.sublevel'),
|
||
confirmDialogHeader: Translate.instance.instant('core.h5p.confirmdialogheader'),
|
||
confirmDialogBody: Translate.instance.instant('core.h5p.confirmdialogbody'),
|
||
cancelLabel: Translate.instance.instant('core.h5p.cancellabel'),
|
||
confirmLabel: Translate.instance.instant('core.h5p.confirmlabel'),
|
||
licenseU: Translate.instance.instant('core.h5p.undisclosed'),
|
||
licenseCCBY: Translate.instance.instant('core.h5p.ccattribution'),
|
||
licenseCCBYSA: Translate.instance.instant('core.h5p.ccattributionsa'),
|
||
licenseCCBYND: Translate.instance.instant('core.h5p.ccattributionnd'),
|
||
licenseCCBYNC: Translate.instance.instant('core.h5p.ccattributionnc'),
|
||
licenseCCBYNCSA: Translate.instance.instant('core.h5p.ccattributionncsa'),
|
||
licenseCCBYNCND: Translate.instance.instant('core.h5p.ccattributionncnd'),
|
||
licenseCC40: Translate.instance.instant('core.h5p.licenseCC40'),
|
||
licenseCC30: Translate.instance.instant('core.h5p.licenseCC30'),
|
||
licenseCC25: Translate.instance.instant('core.h5p.licenseCC25'),
|
||
licenseCC20: Translate.instance.instant('core.h5p.licenseCC20'),
|
||
licenseCC10: Translate.instance.instant('core.h5p.licenseCC10'),
|
||
licenseGPL: Translate.instance.instant('core.h5p.licenseGPL'),
|
||
licenseV3: Translate.instance.instant('core.h5p.licenseV3'),
|
||
licenseV2: Translate.instance.instant('core.h5p.licenseV2'),
|
||
licenseV1: Translate.instance.instant('core.h5p.licenseV1'),
|
||
licensePD: Translate.instance.instant('core.h5p.pd'),
|
||
licenseCC010: Translate.instance.instant('core.h5p.licenseCC010'),
|
||
licensePDM: Translate.instance.instant('core.h5p.pdm'),
|
||
licenseC: Translate.instance.instant('core.h5p.copyrightstring'),
|
||
contentType: Translate.instance.instant('core.h5p.contenttype'),
|
||
licenseExtras: Translate.instance.instant('core.h5p.licenseextras'),
|
||
changes: Translate.instance.instant('core.h5p.changelog'),
|
||
contentCopied: Translate.instance.instant('core.h5p.contentCopied'),
|
||
connectionLost: Translate.instance.instant('core.h5p.connectionLost'),
|
||
connectionReestablished: Translate.instance.instant('core.h5p.connectionReestablished'),
|
||
resubmitScores: Translate.instance.instant('core.h5p.resubmitScores'),
|
||
offlineDialogHeader: Translate.instance.instant('core.h5p.offlineDialogHeader'),
|
||
offlineDialogBody: Translate.instance.instant('core.h5p.offlineDialogBody'),
|
||
offlineDialogRetryMessage: Translate.instance.instant('core.h5p.offlineDialogRetryMessage'),
|
||
offlineDialogRetryButtonLabel: Translate.instance.instant('core.h5p.offlineDialogRetryButtonLabel'),
|
||
offlineSuccessfulSubmit: Translate.instance.instant('core.h5p.offlineSuccessfulSubmit'),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Get core JavaScript files.
|
||
*
|
||
* @return array The array containg urls of the core JavaScript files:
|
||
*/
|
||
static getScripts(): string[] {
|
||
const libUrl = CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath();
|
||
const urls = [];
|
||
|
||
CoreH5PCore.SCRIPTS.forEach((script) => {
|
||
urls.push(libUrl + script);
|
||
});
|
||
|
||
urls.push(CoreTextUtils.instance.concatenatePaths(libUrl, 'moodle/js/h5p_overrides.js'));
|
||
|
||
return urls;
|
||
}
|
||
|
||
/**
|
||
* Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}.
|
||
*
|
||
* @param libraryString On the form {machineName} {majorVersion}.{minorVersion}
|
||
* @return Object with keys machineName, majorVersion and minorVersion. Null if string is not parsable.
|
||
*/
|
||
static libraryFromString(libraryString: string): {machineName: string, majorVersion: number, minorVersion: number} {
|
||
|
||
const matches = libraryString.match(/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i);
|
||
|
||
if (matches && matches.length >= 4) {
|
||
return {
|
||
machineName: matches[1],
|
||
majorVersion: Number(matches[2]),
|
||
minorVersion: Number(matches[3])
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}.
|
||
*
|
||
* @param libraryData Library data.
|
||
* @param folderName Use hyphen instead of space in returned string.
|
||
* @return String on the form {machineName} {majorVersion}.{minorVersion}.
|
||
*/
|
||
static libraryToString(libraryData: any, folderName?: boolean): string {
|
||
return (libraryData.machineName ? libraryData.machineName : libraryData.name) + (folderName ? '-' : ' ') +
|
||
libraryData.majorVersion + '.' + libraryData.minorVersion;
|
||
}
|
||
|
||
/**
|
||
* 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<CoreH5PContentData> {
|
||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||
|
||
const content = await this.h5pFramework.loadContent(id, fileUrl, siteId);
|
||
|
||
// Validate metadata.
|
||
const validator = new CoreH5PContentValidator(siteId);
|
||
|
||
content.metadata = await validator.validateMetadata(content.metadata);
|
||
|
||
return {
|
||
id: content.id,
|
||
params: content.params,
|
||
embedType: content.embedType,
|
||
disable: content.disable,
|
||
folderName: content.folderName,
|
||
title: content.title,
|
||
slug: content.slug,
|
||
filtered: content.filtered,
|
||
libraryMajorVersion: content.libraryMajorVersion,
|
||
libraryMinorVersion: content.libraryMinorVersion,
|
||
metadata: content.metadata,
|
||
library: {
|
||
id: content.libraryId,
|
||
name: content.libraryName,
|
||
majorVersion: content.libraryMajorVersion,
|
||
minorVersion: content.libraryMinorVersion,
|
||
embedTypes: content.libraryEmbedTypes,
|
||
fullscreen: content.libraryFullscreen,
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
loadContentDependencies(id: number, type?: string, siteId?: string)
|
||
: Promise<{[machineName: string]: CoreH5PContentDependencyData}> {
|
||
|
||
return this.h5pFramework.loadContentDependencies(id, type, siteId);
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
loadLibrary(machineName: string, majorVersion: number, minorVersion: number, siteId?: string): Promise<CoreH5PLibraryData> {
|
||
return this.h5pFramework.loadLibrary(machineName, majorVersion, minorVersion, siteId);
|
||
}
|
||
|
||
/**
|
||
* Check if the current user has permission to update and install new libraries.
|
||
*
|
||
* @return Whether has permissions.
|
||
*/
|
||
mayUpdateLibraries(): boolean {
|
||
// In the app the installation only affects current user, so the user always has permissions.
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 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 saveContent(content: any, folderName: string, fileUrl: string, siteId?: string): Promise<number> {
|
||
|
||
content.id = await this.h5pFramework.updateContent(content, folderName, fileUrl, siteId);
|
||
|
||
// Some user data for content has to be reset when the content changes.
|
||
await this.h5pFramework.resetContentUserData(content.id, siteId);
|
||
|
||
return content.id;
|
||
}
|
||
|
||
/**
|
||
* Helper function used to figure out embed and download behaviour.
|
||
*
|
||
* @param optionName The option name.
|
||
* @param permission The permission.
|
||
* @param id The package ID.
|
||
* @param value Default value.
|
||
* @return The value to use.
|
||
*/
|
||
setDisplayOptionOverrides(optionName: string, permission: number, id: number, value: boolean): boolean {
|
||
const behaviour = this.h5pFramework.getOption(optionName, CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW);
|
||
|
||
// If never show globally, force hide
|
||
if (behaviour == CoreH5PDisplayOptionBehaviour.NEVER_SHOW) {
|
||
value = false;
|
||
} else if (behaviour == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW) {
|
||
// If always show or permissions say so, force show
|
||
value = true;
|
||
} else if (behaviour == CoreH5PDisplayOptionBehaviour.CONTROLLED_BY_PERMISSIONS) {
|
||
value = this.h5pFramework.hasPermission(permission, id);
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* Convert strings of text into simple kebab case slugs. Based on H5PCore::slugify.
|
||
*
|
||
* @param input The string to slugify.
|
||
* @return Slugified text.
|
||
*/
|
||
static slugify(input: string): string {
|
||
input = input || '';
|
||
|
||
input = input.toLowerCase();
|
||
|
||
// Replace common chars.
|
||
let newInput = '';
|
||
for (let i = 0; i < input.length; i++) {
|
||
const char = input[i];
|
||
|
||
newInput += CoreH5PCore.SLUGIFY_MAP[char] || char;
|
||
}
|
||
|
||
// Replace everything else.
|
||
newInput = newInput.replace(/[^a-z0-9]/g, '-');
|
||
|
||
// Prevent double hyphen
|
||
newInput = newInput.replace(/-{2,}/g, '-');
|
||
|
||
// Prevent hyphen in beginning or end.
|
||
newInput = newInput.replace(/(^-+|-+$)/g, '');
|
||
|
||
// Prevent too long slug.
|
||
if (newInput.length > 91) {
|
||
newInput = newInput.substr(0, 92);
|
||
}
|
||
|
||
// Prevent empty slug
|
||
if (newInput === '') {
|
||
newInput = 'interactive';
|
||
}
|
||
|
||
return newInput;
|
||
}
|
||
|
||
/**
|
||
* Determine if params contain any match.
|
||
*
|
||
* @param params Parameters.
|
||
* @param pattern Regular expression to identify pattern.
|
||
* @return True if params matches pattern.
|
||
*/
|
||
protected textAddonMatches(params: any, pattern: string): boolean {
|
||
|
||
if (typeof params == 'string') {
|
||
if (params.match(pattern)) {
|
||
return true;
|
||
}
|
||
} else if (typeof params == 'object') {
|
||
for (const key in params) {
|
||
const value = params[key];
|
||
|
||
if (this.textAddonMatches(value, pattern)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display options behaviour constants.
|
||
*/
|
||
export class CoreH5PDisplayOptionBehaviour {
|
||
static NEVER_SHOW = 0;
|
||
static CONTROLLED_BY_AUTHOR_DEFAULT_ON = 1;
|
||
static CONTROLLED_BY_AUTHOR_DEFAULT_OFF = 2;
|
||
static ALWAYS_SHOW = 3;
|
||
static CONTROLLED_BY_PERMISSIONS = 4;
|
||
}
|
||
|
||
/**
|
||
* Permission constants.
|
||
*/
|
||
export class CoreH5PPermission {
|
||
static DOWNLOAD_H5P = 0;
|
||
static EMBED_H5P = 1;
|
||
static CREATE_RESTRICTED = 2;
|
||
static UPDATE_LIBRARIES = 3;
|
||
static INSTALL_RECOMMENDED = 4;
|
||
static COPY_H5P = 4;
|
||
}
|
||
|
||
/**
|
||
* Display options as object.
|
||
*/
|
||
export type CoreH5PDisplayOptions = {
|
||
frame?: boolean;
|
||
export?: boolean;
|
||
embed?: boolean;
|
||
copyright?: boolean;
|
||
icon?: boolean;
|
||
copy?: boolean;
|
||
};
|
||
|
||
/**
|
||
* Dependency asset.
|
||
*/
|
||
export type CoreH5PDependencyAsset = {
|
||
path: string; // Path to the asset.
|
||
version: string; // Dependency version.
|
||
};
|
||
|
||
/**
|
||
* Dependencies files.
|
||
*/
|
||
export type CoreH5PDependenciesFiles = {
|
||
scripts: CoreH5PDependencyAsset[]; // JS scripts.
|
||
styles: CoreH5PDependencyAsset[]; // CSS files.
|
||
};
|
||
|
||
/**
|
||
* Content data, including main library data.
|
||
*/
|
||
export type CoreH5PContentData = {
|
||
id: number; // The id of the content.
|
||
params: string; // The content in json format.
|
||
embedType: string; // Embed type to use.
|
||
disable: number; // 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; // Filtered version of json_content.
|
||
libraryMajorVersion: number; // Main library's major version.
|
||
libraryMinorVersion: number; // Main library's minor version.
|
||
metadata: any; // Content metadata.
|
||
library: { // Main library data.
|
||
id: number; // The id of the library.
|
||
name: string; // The library machine name.
|
||
majorVersion: number; // Major version.
|
||
minorVersion: number; // Minor version.
|
||
embedTypes: string; // List of supported embed types.
|
||
fullscreen: number; // Display fullscreen button.
|
||
};
|
||
dependencies?: {[key: string]: CoreH5PContentDepsTreeDependency}; // Dependencies. Calculated in filterParameters.
|
||
};
|
||
|
||
/**
|
||
* Content dependency data.
|
||
*/
|
||
export type CoreH5PContentDependencyData = {
|
||
libraryId: number; // The id of the library if it is an existing library.
|
||
machineName: string; // The library machineName.
|
||
majorVersion: number; // The The library's majorVersion.
|
||
minorVersion: number; // The The library's minorVersion.
|
||
patchVersion: number; // The The library's patchVersion.
|
||
preloadedJs?: string | string[]; // Comma separated string with js file paths. If already parsed, list of paths.
|
||
preloadedCss?: string | string[]; // Comma separated string with css file paths. If already parsed, list of paths.
|
||
dropCss?: string; // CSV of machine names.
|
||
dependencyType: string; // The dependency type.
|
||
path?: string; // Path to the dependency. Calculated in getDependenciesFiles.
|
||
version?: string; // Version of the dependency. Calculated in getDependenciesFiles.
|
||
};
|
||
|
||
/**
|
||
* Data for each content dependency in the dependency tree.
|
||
*/
|
||
export type CoreH5PContentDepsTreeDependency = {
|
||
library: CoreH5PLibraryData | CoreH5PLibraryAddonData; // Library data.
|
||
type: string; // Dependency type.
|
||
weight?: number; // An integer determining the order of the libraries when they are loaded.
|
||
};
|
||
|
||
/**
|
||
* Library data.
|
||
*/
|
||
export type CoreH5PLibraryData = {
|
||
libraryId: number; // The id of the library.
|
||
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; // Can this library be started by the module? I.e. not a dependency.
|
||
fullscreen: number; // Display fullscreen button.
|
||
embedTypes: string; // List of supported embed types.
|
||
preloadedJs?: string; // Comma separated list of scripts to load.
|
||
preloadedCss?: string; // Comma separated list of stylesheets to load.
|
||
dropLibraryCss?: string; // List of libraries that should not have CSS included if this library is used. Comma separated list.
|
||
semantics?: any; // The semantics definition. If it's a string, it's in json format.
|
||
preloadedDependencies: CoreH5PLibraryBasicData[]; // Dependencies.
|
||
dynamicDependencies: CoreH5PLibraryBasicData[]; // Dependencies.
|
||
editorDependencies: CoreH5PLibraryBasicData[]; // Dependencies.
|
||
};
|
||
|
||
/**
|
||
* Library basic data.
|
||
*/
|
||
export type CoreH5PLibraryBasicData = {
|
||
machineName: string; // The library machine name.
|
||
majorVersion: number; // Major version.
|
||
minorVersion: number; // Minor version.
|
||
};
|
||
|
||
/**
|
||
* "Addon" data (library).
|
||
*/
|
||
export type CoreH5PLibraryAddonData = {
|
||
libraryId: number; // The id of the library.
|
||
machineName: string; // The library machine name.
|
||
majorVersion: number; // Major version.
|
||
minorVersion: number; // Minor version.
|
||
patchVersion: number; // Patch version.
|
||
preloadedJs?: string; // Comma separated list of scripts to load.
|
||
preloadedCss?: string; // Comma separated list of stylesheets to load.
|
||
addTo?: any; // Plugin configuration data.
|
||
};
|