MOBILE-3406 h5p: Refactor code into different classes

main
Dani Palou 2020-05-04 11:15:58 +02:00
parent 4f9adba63e
commit 724e0cc292
18 changed files with 3693 additions and 3286 deletions

View File

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTextUtils } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtils } from '@providers/utils/utils';
import { CoreH5PProvider, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from '../providers/h5p'; import { CoreH5P } from '../providers/h5p';
import { CoreH5PUtilsProvider } from '../providers/utils'; import { Translate } from '@singletons/core.singletons';
import { TranslateService } from '@ngx-translate/core'; import { CoreH5PCore, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from './core';
/** /**
* Equivalent to Moodle's H5PContentValidator, but without some of the validations. * Equivalent to H5P's H5PContentValidator, but without some of the validations.
* It's also used to build the dependency list. * It's also used to build the dependency list.
*/ */
export class CoreH5PContentValidator { export class CoreH5PContentValidator {
@ -43,17 +43,12 @@ export class CoreH5PContentValidator {
protected libraries: {[libString: string]: CoreH5PLibraryData} = {}; protected libraries: {[libString: string]: CoreH5PLibraryData} = {};
protected dependencies: {[key: string]: CoreH5PContentDepsTreeDependency} = {}; protected dependencies: {[key: string]: CoreH5PContentDepsTreeDependency} = {};
protected relativePathRegExp = /^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/; protected relativePathRegExp = /^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/;
protected allowedHtml: {[tag: string]: any} = {}; protected allowedHtml: {[tag: string]: string} = {};
protected allowedStyles: RegExp[]; protected allowedStyles: RegExp[];
protected metadataSemantics: any[]; protected metadataSemantics: any[];
protected copyrightSemantics: any; protected copyrightSemantics: any;
constructor(protected h5pProvider: CoreH5PProvider, constructor(protected siteId: string) { }
protected h5pUtils: CoreH5PUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider,
protected translate: TranslateService,
protected siteId: string) { }
/** /**
* Add Addon library. * Add Addon library.
@ -61,24 +56,23 @@ export class CoreH5PContentValidator {
* @param library The addon library to add. * @param library The addon library to add.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
addon(library: CoreH5PLibraryAddonData): Promise<void> { async addon(library: CoreH5PLibraryAddonData): Promise<void> {
const depKey = 'preloaded-' + library.machineName; const depKey = 'preloaded-' + library.machineName;
this.dependencies[depKey] = { this.dependencies[depKey] = {
library: library, library: library,
type: 'preloaded' type: 'preloaded',
}; };
return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => { this.nextWeight = await CoreH5P.instance.h5pCore.findLibraryDependencies(this.dependencies, library, this.nextWeight);
this.nextWeight = weight;
this.dependencies[depKey].weight = this.nextWeight++; this.dependencies[depKey].weight = this.nextWeight++;
});
} }
/** /**
* Get the flat dependency tree. * Get the flat dependency tree.
* *
* @return array * @return Dependencies.
*/ */
getDependencies(): {[key: string]: CoreH5PContentDepsTreeDependency} { getDependencies(): {[key: string]: CoreH5PContentDepsTreeDependency} {
return this.dependencies; return this.dependencies;
@ -92,7 +86,7 @@ export class CoreH5PContentValidator {
*/ */
validateMetadata(metadata: any): Promise<any> { validateMetadata(metadata: any): Promise<any> {
const semantics = this.getMetadataSemantics(); const semantics = this.getMetadataSemantics();
const group = this.utils.clone(metadata || {}); const group = CoreUtils.instance.clone(metadata || {});
// Stop complaining about "invalid selected option in select" for old content without license chosen. // Stop complaining about "invalid selected option in select" for old content without license chosen.
if (typeof group.license == 'undefined') { if (typeof group.license == 'undefined') {
@ -135,7 +129,7 @@ export class CoreH5PContentValidator {
tags.push('s'); tags.push('s');
} }
tags = this.utils.uniqueArray(tags); tags = CoreUtils.instance.uniqueArray(tags);
// Determine allowed style tags // Determine allowed style tags
const stylePatterns: RegExp[] = []; const stylePatterns: RegExp[] = [];
@ -168,7 +162,7 @@ export class CoreH5PContentValidator {
text = this.filterXss(text, tags, stylePatterns); text = this.filterXss(text, tags, stylePatterns);
} else { } else {
// Filter text to plain text. // Filter text to plain text.
text = this.textUtils.escapeHTML(text); text = CoreTextUtils.instance.escapeHTML(text);
} }
// Check if string is within allowed length. // Check if string is within allowed length.
@ -213,8 +207,8 @@ export class CoreH5PContentValidator {
} }
// Check if number is within allowed bounds even if step value is set. // Check if number is within allowed bounds even if step value is set.
if (typeof semantics.step != 'undefined') { if (typeof semantics.step != 'undefined') {
const testNumber = num - (typeof semantics.min != 'undefined' ? semantics.min : 0), const testNumber = num - (typeof semantics.min != 'undefined' ? semantics.min : 0);
rest = testNumber % semantics.step; const rest = testNumber % semantics.step;
if (rest !== 0) { if (rest !== 0) {
num -= rest; num -= rest;
} }
@ -245,8 +239,8 @@ export class CoreH5PContentValidator {
* @return Validated select. * @return Validated select.
*/ */
validateSelect(select: any, semantics: any): any { validateSelect(select: any, semantics: any): any {
const optional = semantics.optional, const optional = semantics.optional;
options = {}; const options = {};
let strict = false; let strict = false;
if (semantics.options && semantics.options.length) { if (semantics.options && semantics.options.length) {
@ -273,7 +267,7 @@ export class CoreH5PContentValidator {
if (strict && !optional && !options[value]) { if (strict && !optional && !options[value]) {
delete select[key]; delete select[key];
} else { } else {
select[key] = this.textUtils.escapeHTML(value); select[key] = CoreTextUtils.instance.escapeHTML(value);
} }
} }
} else { } else {
@ -285,7 +279,7 @@ export class CoreH5PContentValidator {
if (strict && !optional && !options[select]) { if (strict && !optional && !options[select]) {
select = semantics.options[0].value; select = semantics.options[0].value;
} }
select = this.textUtils.escapeHTML(select); select = CoreTextUtils.instance.escapeHTML(select);
} }
return select; return select;
@ -299,11 +293,10 @@ export class CoreH5PContentValidator {
* @param semantics Semantics. * @param semantics Semantics.
* @return Validated list. * @return Validated list.
*/ */
validateList(list: any, semantics: any): Promise<any[]> { async validateList(list: any, semantics: any): Promise<any[]> {
const field = semantics.field, const field = semantics.field;
fn = this[this.typeMap[field.type]].bind(this); const fn = this[this.typeMap[field.type]].bind(this);
let promise = Promise.resolve(), // Use a chain of promises so the order is kept. let keys = Object.keys(list);
keys = Object.keys(list);
// Check that list is not longer than allowed length. // Check that list is not longer than allowed length.
if (typeof semantics.max != 'undefined') { if (typeof semantics.max != 'undefined') {
@ -311,27 +304,25 @@ export class CoreH5PContentValidator {
} }
// Validate each element in list. // Validate each element in list.
keys.forEach((key) => { for (const i in keys) {
const key = keys[i];
if (isNaN(parseInt(key, 10))) { if (isNaN(parseInt(key, 10))) {
// It's an object and the key isn't an integer. Delete it. // It's an object and the key isn't an integer. Delete it.
delete list[key]; delete list[key];
} else { } else {
promise = promise.then(() => { const val = await fn(list[key], field);
return Promise.resolve(fn(list[key], field)).then((val) => {
if (val === null) { if (val === null) {
list.splice(key, 1); list.splice(key, 1);
} else { } else {
list[key] = val; list[key] = val;
} }
});
});
} }
}); }
return promise.then(() => {
if (!Array.isArray(list)) { if (!Array.isArray(list)) {
list = this.utils.objectToArray(list); list = CoreUtils.instance.objectToArray(list);
} }
if (!list.length) { if (!list.length) {
@ -339,7 +330,6 @@ export class CoreH5PContentValidator {
} }
return list; return list;
});
} }
/** /**
@ -350,7 +340,7 @@ export class CoreH5PContentValidator {
* @param typeValidKeys List of valid keys. * @param typeValidKeys List of valid keys.
* @return Promise resolved with the validated file. * @return Promise resolved with the validated file.
*/ */
protected validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise<any> { protected async validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise<any> {
// Do not allow to use files from other content folders. // Do not allow to use files from other content folders.
const matches = file.path.match(this.relativePathRegExp); const matches = file.path.match(this.relativePathRegExp);
if (matches && matches.length) { if (matches && matches.length) {
@ -363,9 +353,9 @@ export class CoreH5PContentValidator {
} }
// Make sure path and mime does not have any special chars // Make sure path and mime does not have any special chars
file.path = this.textUtils.escapeHTML(file.path); file.path = CoreTextUtils.instance.escapeHTML(file.path);
if (file.mime) { if (file.mime) {
file.mime = this.textUtils.escapeHTML(file.mime); file.mime = CoreTextUtils.instance.escapeHTML(file.mime);
} }
// Remove attributes that should not exist, they may contain JSON escape code. // Remove attributes that should not exist, they may contain JSON escape code.
@ -373,7 +363,7 @@ export class CoreH5PContentValidator {
if (semantics.extraAttributes) { if (semantics.extraAttributes) {
validKeys = validKeys.concat(semantics.extraAttributes); validKeys = validKeys.concat(semantics.extraAttributes);
} }
validKeys = this.utils.uniqueArray(validKeys); validKeys = CoreUtils.instance.uniqueArray(validKeys);
this.filterParams(file, validKeys); this.filterParams(file, validKeys);
@ -386,7 +376,7 @@ export class CoreH5PContentValidator {
} }
if (file.codecs) { if (file.codecs) {
file.codecs = this.textUtils.escapeHTML(file.codecs); file.codecs = CoreTextUtils.instance.escapeHTML(file.codecs);
} }
if (typeof file.bitrate != 'undefined') { if (typeof file.bitrate != 'undefined') {
@ -399,17 +389,15 @@ export class CoreH5PContentValidator {
} else { } else {
this.filterParams(file.quality, ['level', 'label']); this.filterParams(file.quality, ['level', 'label']);
file.quality.level = parseInt(file.quality.level); file.quality.level = parseInt(file.quality.level);
file.quality.label = this.textUtils.escapeHTML(file.quality.label); file.quality.label = CoreTextUtils.instance.escapeHTML(file.quality.label);
} }
} }
if (typeof file.copyright != 'undefined') { if (typeof file.copyright != 'undefined') {
return this.validateGroup(file.copyright, this.getCopyrightSemantics()).then(() => { await this.validateGroup(file.copyright, this.getCopyrightSemantics());
return file;
});
} }
return Promise.resolve(file); return file;
} }
/** /**
@ -441,18 +429,13 @@ export class CoreH5PContentValidator {
* @param semantics Semantics. * @param semantics Semantics.
* @return Promise resolved with the validated file. * @return Promise resolved with the validated file.
*/ */
validateVideo(video: any, semantics: any): Promise<any> { async validateVideo(video: any, semantics: any): Promise<any> {
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
for (const key in video) { for (const key in video) {
promise = promise.then(() => { await this.validateFilelike(video[key], semantics, ['width', 'height', 'codecs', 'quality', 'bitrate']);
return this.validateFilelike(video[key], semantics, ['width', 'height', 'codecs', 'quality', 'bitrate']);
});
} }
return promise.then(() => {
return video; return video;
});
} }
/** /**
@ -462,18 +445,13 @@ export class CoreH5PContentValidator {
* @param semantics Semantics. * @param semantics Semantics.
* @return Promise resolved with the validated file. * @return Promise resolved with the validated file.
*/ */
validateAudio(audio: any, semantics: any): Promise<any> { async validateAudio(audio: any, semantics: any): Promise<any> {
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
for (const key in audio) { for (const key in audio) {
promise = promise.then(() => { await this.validateFilelike(audio[key], semantics);
return this.validateFilelike(audio[key], semantics);
});
} }
return promise.then(() => {
return audio; return audio;
});
} }
/** /**
@ -483,19 +461,19 @@ export class CoreH5PContentValidator {
* @param group Group. * @param group Group.
* @param semantics Semantics. * @param semantics Semantics.
* @param flatten Whether to flatten. * @param flatten Whether to flatten.
* @return Promise resolved when done.
*/ */
validateGroup(group: any, semantics: any, flatten: boolean = true): Promise<any> { async validateGroup(group: any, semantics: any, flatten: boolean = true): Promise<any> {
// Groups with just one field are compressed in the editor to only output he child content. // Groups with just one field are compressed in the editor to only output the child content.
const isSubContent = semantics.isSubContent === true; const isSubContent = semantics.isSubContent === true;
if (semantics.fields.length == 1 && flatten && !isSubContent) { if (semantics.fields.length == 1 && flatten && !isSubContent) {
const field = semantics.fields[0], const field = semantics.fields[0];
fn = this[this.typeMap[field.type]].bind(this); const fn = this[this.typeMap[field.type]].bind(this);
return Promise.resolve(fn(group, field)); return fn(group, field);
} else { } else {
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
for (const key in group) { for (const key in group) {
// If subContentId is set, keep value // If subContentId is set, keep value
@ -504,9 +482,9 @@ export class CoreH5PContentValidator {
} }
// Find semantics for name=key. // Find semantics for name=key.
let found = false, let found = false;
fn = null, let fn = null;
field = null; let field = null;
for (let i = 0; i < semantics.fields.length; i++) { for (let i = 0; i < semantics.fields.length; i++) {
field = semantics.fields[i]; field = semantics.fields[i];
@ -522,23 +500,19 @@ export class CoreH5PContentValidator {
} }
if (found && fn) { if (found && fn) {
promise = promise.then(() => { const val = await fn(group[key], field);
return Promise.resolve(fn(group[key], field)).then((val) => {
group[key] = val; group[key] = val;
if (val === null) { if (val === null) {
delete group[key]; delete group[key];
} }
});
});
} else { } else {
// Something exists in content that does not have a corresponding semantics field. Remove it. // Something exists in content that does not have a corresponding semantics field. Remove it.
delete group.key; delete group.key;
} }
} }
return promise.then(() => {
return group; return group;
});
} }
} }
@ -551,43 +525,32 @@ export class CoreH5PContentValidator {
* @param semantics Semantics. * @param semantics Semantics.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
validateLibrary(value: any, semantics: any): Promise<any> { async validateLibrary(value: any, semantics: any): Promise<any> {
if (!value.library) { if (!value.library) {
return Promise.resolve(); return;
} }
let promise;
if (!this.libraries[value.library]) { if (!this.libraries[value.library]) {
const libSpec = this.h5pUtils.libraryFromString(value.library); // Load the library and store it in the index of libraries.
const libSpec = CoreH5PCore.libraryFromString(value.library);
promise = this.h5pProvider.loadLibrary(libSpec.machineName, libSpec.majorVersion, libSpec.minorVersion, this.siteId) this.libraries[value.library] = await CoreH5P.instance.h5pCore.loadLibrary(libSpec.machineName, libSpec.majorVersion,
.then((library) => { libSpec.minorVersion, this.siteId);
this.libraries[value.library] = library;
return library;
});
} else {
promise = Promise.resolve(this.libraries[value.library]);
} }
return promise.then((library) => { const library = this.libraries[value.library];
// Validate parameters.
return this.validateGroup(value.params, {type: 'group', fields: library.semantics}, false).then((validated) => {
value.params = validated; // Validate parameters.
value.params = await this.validateGroup(value.params, {type: 'group', fields: library.semantics}, false);
// Validate subcontent's metadata // Validate subcontent's metadata
if (value.metadata) { if (value.metadata) {
return this.validateMetadata(value.metadata).then((res) => { value.metadata = await this.validateMetadata(value.metadata);
value.metadata = res;
});
} }
}).then(() => {
let validKeys = ['library', 'params', 'subContentId', 'metadata']; let validKeys = ['library', 'params', 'subContentId', 'metadata'];
if (semantics.extraAttributes) { if (semantics.extraAttributes) {
validKeys = this.utils.uniqueArray(validKeys.concat(semantics.extraAttributes)); validKeys = CoreUtils.instance.uniqueArray(validKeys.concat(semantics.extraAttributes));
} }
this.filterParams(value, validKeys); this.filterParams(value, validKeys);
@ -605,17 +568,14 @@ export class CoreH5PContentValidator {
type: 'preloaded' type: 'preloaded'
}; };
return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => { this.nextWeight = await CoreH5P.instance.h5pCore.findLibraryDependencies(this.dependencies, library, this.nextWeight);
this.nextWeight = weight;
this.dependencies[depKey].weight = this.nextWeight++; this.dependencies[depKey].weight = this.nextWeight++;
return value; return value;
});
} else { } else {
return value; return value;
} }
});
});
} }
/** /**
@ -689,7 +649,7 @@ export class CoreH5PContentValidator {
protected filterXssSplit(m: string[], store: boolean = false): string { protected filterXssSplit(m: string[], store: boolean = false): string {
if (store) { if (store) {
this.allowedHtml = this.utils.arrayToObject(m); this.allowedHtml = CoreUtils.instance.arrayToObject(m);
return ''; return '';
} }
@ -710,9 +670,9 @@ export class CoreH5PContentValidator {
return ''; return '';
} }
const slash = matches[1] ? matches[1].trim() : '', const slash = matches[1] ? matches[1].trim() : '';
attrList = matches[3] || '', const attrList = matches[3] || '';
comment = matches[4] || ''; const comment = matches[4] || '';
let elem = matches[2] || ''; let elem = matches[2] || '';
if (comment) { if (comment) {
@ -733,8 +693,8 @@ export class CoreH5PContentValidator {
} }
// Is there a closing XHTML slash at the end of the attributes? // Is there a closing XHTML slash at the end of the attributes?
const newAttrList = attrList.replace(/(\s?)\/\s*$/g, '$1'), const newAttrList = attrList.replace(/(\s?)\/\s*$/g, '$1');
xhtmlSlash = attrList != newAttrList ? ' /' : ''; const xhtmlSlash = attrList != newAttrList ? ' /' : '';
// Clean up attributes. // Clean up attributes.
let attr2 = this.filterXssAttributes(newAttrList, let attr2 = this.filterXssAttributes(newAttrList,
@ -760,9 +720,9 @@ export class CoreH5PContentValidator {
while (attr.length != 0) { while (attr.length != 0) {
// Was the last operation successful? // Was the last operation successful?
let working = 0, let working = 0;
matches, let matches;
thisVal; let thisVal;
switch (mode) { switch (mode) {
case 0: case 0:
@ -877,10 +837,10 @@ export class CoreH5PContentValidator {
filterXssBadProtocol(str: string, decode: boolean = true): string { filterXssBadProtocol(str: string, decode: boolean = true): string {
// Get the plain text representation of the attribute value (i.e. its meaning). // Get the plain text representation of the attribute value (i.e. its meaning).
if (decode) { if (decode) {
str = this.textUtils.decodeHTMLEntities(str); str = CoreTextUtils.instance.decodeHTMLEntities(str);
} }
return this.textUtils.escapeHTML(this.stripDangerousProtocols(str)); return CoreTextUtils.instance.escapeHTML(this.stripDangerousProtocols(str));
} }
/** /**
@ -939,92 +899,92 @@ export class CoreH5PContentValidator {
{ {
name: 'title', name: 'title',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.title'), label: Translate.instance.instant('core.h5p.title'),
placeholder: 'La Gioconda' placeholder: 'La Gioconda'
}, },
{ {
name: 'license', name: 'license',
type: 'select', type: 'select',
label: this.translate.instant('core.h5p.license'), label: Translate.instance.instant('core.h5p.license'),
default: 'U', default: 'U',
options: [ options: [
{ {
value: 'U', value: 'U',
label: this.translate.instant('core.h5p.undisclosed') label: Translate.instance.instant('core.h5p.undisclosed')
}, },
{ {
type: 'optgroup', type: 'optgroup',
label: this.translate.instant('core.h5p.creativecommons'), label: Translate.instance.instant('core.h5p.creativecommons'),
options: [ options: [
{ {
value: 'CC BY', value: 'CC BY',
label: this.translate.instant('core.h5p.ccattribution'), label: Translate.instance.instant('core.h5p.ccattribution'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-SA', value: 'CC BY-SA',
label: this.translate.instant('core.h5p.ccattributionsa'), label: Translate.instance.instant('core.h5p.ccattributionsa'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-ND', value: 'CC BY-ND',
label: this.translate.instant('core.h5p.ccattributionnd'), label: Translate.instance.instant('core.h5p.ccattributionnd'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-NC', value: 'CC BY-NC',
label: this.translate.instant('core.h5p.ccattributionnc'), label: Translate.instance.instant('core.h5p.ccattributionnc'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-NC-SA', value: 'CC BY-NC-SA',
label: this.translate.instant('core.h5p.ccattributionncsa'), label: Translate.instance.instant('core.h5p.ccattributionncsa'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-NC-ND', value: 'CC BY-NC-ND',
label: this.translate.instant('core.h5p.ccattributionncnd'), label: Translate.instance.instant('core.h5p.ccattributionncnd'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC0 1.0', value: 'CC0 1.0',
label: this.translate.instant('core.h5p.ccpdd') label: Translate.instance.instant('core.h5p.ccpdd')
}, },
{ {
value: 'CC PDM', value: 'CC PDM',
label: this.translate.instant('core.h5p.pdm') label: Translate.instance.instant('core.h5p.pdm')
}, },
] ]
}, },
{ {
value: 'GNU GPL', value: 'GNU GPL',
label: this.translate.instant('core.h5p.gpl') label: Translate.instance.instant('core.h5p.gpl')
}, },
{ {
value: 'PD', value: 'PD',
label: this.translate.instant('core.h5p.pd') label: Translate.instance.instant('core.h5p.pd')
}, },
{ {
value: 'ODC PDDL', value: 'ODC PDDL',
label: this.translate.instant('core.h5p.pddl') label: Translate.instance.instant('core.h5p.pddl')
}, },
{ {
value: 'C', value: 'C',
label: this.translate.instant('core.h5p.copyrightstring') label: Translate.instance.instant('core.h5p.copyrightstring')
} }
] ]
}, },
{ {
name: 'licenseVersion', name: 'licenseVersion',
type: 'select', type: 'select',
label: this.translate.instant('core.h5p.licenseversion'), label: Translate.instance.instant('core.h5p.licenseversion'),
options: ccVersions, options: ccVersions,
optional: true optional: true
}, },
{ {
name: 'yearFrom', name: 'yearFrom',
type: 'number', type: 'number',
label: this.translate.instant('core.h5p.yearsfrom'), label: Translate.instance.instant('core.h5p.yearsfrom'),
placeholder: '1991', placeholder: '1991',
min: '-9999', min: '-9999',
max: '9999', max: '9999',
@ -1033,7 +993,7 @@ export class CoreH5PContentValidator {
{ {
name: 'yearTo', name: 'yearTo',
type: 'number', type: 'number',
label: this.translate.instant('core.h5p.yearsto'), label: Translate.instance.instant('core.h5p.yearsto'),
placeholder: '1992', placeholder: '1992',
min: '-9999', min: '-9999',
max: '9999', max: '9999',
@ -1042,7 +1002,7 @@ export class CoreH5PContentValidator {
{ {
name: 'source', name: 'source',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.source'), label: Translate.instance.instant('core.h5p.source'),
placeholder: 'https://', placeholder: 'https://',
optional: true optional: true
}, },
@ -1054,7 +1014,7 @@ export class CoreH5PContentValidator {
type: 'group', type: 'group',
fields: [ fields: [
{ {
label: this.translate.instant('core.h5p.authorname'), label: Translate.instance.instant('core.h5p.authorname'),
name: 'name', name: 'name',
optional: true, optional: true,
type: 'text' type: 'text'
@ -1062,24 +1022,24 @@ export class CoreH5PContentValidator {
{ {
name: 'role', name: 'role',
type: 'select', type: 'select',
label: this.translate.instant('core.h5p.authorrole'), label: Translate.instance.instant('core.h5p.authorrole'),
default: 'Author', default: 'Author',
options: [ options: [
{ {
value: 'Author', value: 'Author',
label: this.translate.instant('core.h5p.author') label: Translate.instance.instant('core.h5p.author')
}, },
{ {
value: 'Editor', value: 'Editor',
label: this.translate.instant('core.h5p.editor') label: Translate.instance.instant('core.h5p.editor')
}, },
{ {
value: 'Licensee', value: 'Licensee',
label: this.translate.instant('core.h5p.licensee') label: Translate.instance.instant('core.h5p.licensee')
}, },
{ {
value: 'Originator', value: 'Originator',
label: this.translate.instant('core.h5p.originator') label: Translate.instance.instant('core.h5p.originator')
} }
] ]
} }
@ -1090,9 +1050,9 @@ export class CoreH5PContentValidator {
name: 'licenseExtras', name: 'licenseExtras',
type: 'text', type: 'text',
widget: 'textarea', widget: 'textarea',
label: this.translate.instant('core.h5p.licenseextras'), label: Translate.instance.instant('core.h5p.licenseextras'),
optional: true, optional: true,
description: this.translate.instant('core.h5p.additionallicenseinfo') description: Translate.instance.instant('core.h5p.additionallicenseinfo')
}, },
{ {
name: 'changes', name: 'changes',
@ -1100,26 +1060,26 @@ export class CoreH5PContentValidator {
field: { field: {
name: 'change', name: 'change',
type: 'group', type: 'group',
label: this.translate.instant('core.h5p.changelog'), label: Translate.instance.instant('core.h5p.changelog'),
fields: [ fields: [
{ {
name: 'date', name: 'date',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.date'), label: Translate.instance.instant('core.h5p.date'),
optional: true optional: true
}, },
{ {
name: 'author', name: 'author',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.changedby'), label: Translate.instance.instant('core.h5p.changedby'),
optional: true optional: true
}, },
{ {
name: 'log', name: 'log',
type: 'text', type: 'text',
widget: 'textarea', widget: 'textarea',
label: this.translate.instant('core.h5p.changedescription'), label: Translate.instance.instant('core.h5p.changedescription'),
placeholder: this.translate.instant('core.h5p.changeplaceholder'), placeholder: Translate.instance.instant('core.h5p.changeplaceholder'),
optional: true optional: true
} }
] ]
@ -1129,8 +1089,8 @@ export class CoreH5PContentValidator {
name: 'authorComments', name: 'authorComments',
type: 'text', type: 'text',
widget: 'textarea', widget: 'textarea',
label: this.translate.instant('core.h5p.authorcomments'), label: Translate.instance.instant('core.h5p.authorcomments'),
description: this.translate.instant('core.h5p.authorcommentsdescription'), description: Translate.instance.instant('core.h5p.authorcommentsdescription'),
optional: true optional: true
}, },
{ {
@ -1164,33 +1124,33 @@ export class CoreH5PContentValidator {
this.copyrightSemantics = { this.copyrightSemantics = {
name: 'copyright', name: 'copyright',
type: 'group', type: 'group',
label: this.translate.instant('core.h5p.copyrightinfo'), label: Translate.instance.instant('core.h5p.copyrightinfo'),
fields: [ fields: [
{ {
name: 'title', name: 'title',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.title'), label: Translate.instance.instant('core.h5p.title'),
placeholder: 'La Gioconda', placeholder: 'La Gioconda',
optional: true optional: true
}, },
{ {
name: 'author', name: 'author',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.author'), label: Translate.instance.instant('core.h5p.author'),
placeholder: 'Leonardo da Vinci', placeholder: 'Leonardo da Vinci',
optional: true optional: true
}, },
{ {
name: 'year', name: 'year',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.years'), label: Translate.instance.instant('core.h5p.years'),
placeholder: '1503 - 1517', placeholder: '1503 - 1517',
optional: true optional: true
}, },
{ {
name: 'source', name: 'source',
type: 'text', type: 'text',
label: this.translate.instant('core.h5p.source'), label: Translate.instance.instant('core.h5p.source'),
placeholder: 'http://en.wikipedia.org/wiki/Mona_Lisa', placeholder: 'http://en.wikipedia.org/wiki/Mona_Lisa',
optional: true, optional: true,
regexp: { regexp: {
@ -1201,64 +1161,64 @@ export class CoreH5PContentValidator {
{ {
name: 'license', name: 'license',
type: 'select', type: 'select',
label: this.translate.instant('core.h5p.license'), label: Translate.instance.instant('core.h5p.license'),
default: 'U', default: 'U',
options: [ options: [
{ {
value: 'U', value: 'U',
label: this.translate.instant('core.h5p.undisclosed') label: Translate.instance.instant('core.h5p.undisclosed')
}, },
{ {
value: 'CC BY', value: 'CC BY',
label: this.translate.instant('core.h5p.ccattribution'), label: Translate.instance.instant('core.h5p.ccattribution'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-SA', value: 'CC BY-SA',
label: this.translate.instant('core.h5p.ccattributionsa'), label: Translate.instance.instant('core.h5p.ccattributionsa'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-ND', value: 'CC BY-ND',
label: this.translate.instant('core.h5p.ccattributionnd'), label: Translate.instance.instant('core.h5p.ccattributionnd'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-NC', value: 'CC BY-NC',
label: this.translate.instant('core.h5p.ccattributionnc'), label: Translate.instance.instant('core.h5p.ccattributionnc'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-NC-SA', value: 'CC BY-NC-SA',
label: this.translate.instant('core.h5p.ccattributionncsa'), label: Translate.instance.instant('core.h5p.ccattributionncsa'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'CC BY-NC-ND', value: 'CC BY-NC-ND',
label: this.translate.instant('core.h5p.ccattributionncnd'), label: Translate.instance.instant('core.h5p.ccattributionncnd'),
versions: ccVersions versions: ccVersions
}, },
{ {
value: 'GNU GPL', value: 'GNU GPL',
label: this.translate.instant('core.h5p.licenseGPL'), label: Translate.instance.instant('core.h5p.licenseGPL'),
versions: [ versions: [
{ {
value: 'v3', value: 'v3',
label: this.translate.instant('core.h5p.licenseV3') label: Translate.instance.instant('core.h5p.licenseV3')
}, },
{ {
value: 'v2', value: 'v2',
label: this.translate.instant('core.h5p.licenseV2') label: Translate.instance.instant('core.h5p.licenseV2')
}, },
{ {
value: 'v1', value: 'v1',
label: this.translate.instant('core.h5p.licenseV1') label: Translate.instance.instant('core.h5p.licenseV1')
} }
] ]
}, },
{ {
value: 'PD', value: 'PD',
label: this.translate.instant('core.h5p.pd'), label: Translate.instance.instant('core.h5p.pd'),
versions: [ versions: [
{ {
value: '-', value: '-',
@ -1266,24 +1226,24 @@ export class CoreH5PContentValidator {
}, },
{ {
value: 'CC0 1.0', value: 'CC0 1.0',
label: this.translate.instant('core.h5p.licenseCC010U') label: Translate.instance.instant('core.h5p.licenseCC010U')
}, },
{ {
value: 'CC PDM', value: 'CC PDM',
label: this.translate.instant('core.h5p.pdm') label: Translate.instance.instant('core.h5p.pdm')
} }
] ]
}, },
{ {
value: 'C', value: 'C',
label: this.translate.instant('core.h5p.copyrightstring') label: Translate.instance.instant('core.h5p.copyrightstring')
} }
] ]
}, },
{ {
name: 'version', name: 'version',
type: 'select', type: 'select',
label: this.translate.instant('core.h5p.licenseversion'), label: Translate.instance.instant('core.h5p.licenseversion'),
options: [] options: []
} }
] ]
@ -1301,23 +1261,23 @@ export class CoreH5PContentValidator {
return [ return [
{ {
value: '4.0', value: '4.0',
label: this.translate.instant('core.h5p.licenseCC40') label: Translate.instance.instant('core.h5p.licenseCC40')
}, },
{ {
value: '3.0', value: '3.0',
label: this.translate.instant('core.h5p.licenseCC30') label: Translate.instance.instant('core.h5p.licenseCC30')
}, },
{ {
value: '2.5', value: '2.5',
label: this.translate.instant('core.h5p.licenseCC25') label: Translate.instance.instant('core.h5p.licenseCC25')
}, },
{ {
value: '2.0', value: '2.0',
label: this.translate.instant('core.h5p.licenseCC20') label: Translate.instance.instant('core.h5p.licenseCC20')
}, },
{ {
value: '1.0', value: '1.0',
label: this.translate.instant('core.h5p.licenseCC10') label: Translate.instance.instant('core.h5p.licenseCC10')
} }
]; ];
} }

View File

@ -0,0 +1,989 @@
// (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);
});
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.
};

View File

@ -0,0 +1,457 @@
// (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 '@providers/file';
import { CoreFilepool } from '@providers/filepool';
import { CoreSites } from '@providers/sites';
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreUtils } from '@providers/utils/utils';
import { CoreH5PProvider } from '../providers/h5p';
import { CoreH5PCore, CoreH5PDependencyAsset, CoreH5PContentDependencyData, CoreH5PDependenciesFiles } from './core';
import { CoreH5PLibrariesCachedAssetsDBData } from './framework';
/**
* Equivalent to Moodle's implementation of H5PFileStorage.
*/
export class CoreH5PFileStorage {
static 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: string = await CoreFile.instance.readFile(asset.path);
if (type == 'scripts') {
// No need to treat scripts, just append the content.
content += fileContent + ';\n';
} else {
// 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(/^\.\.\//)) {
const urlSplit = url.split('/').filter((i) => {
return i; // Remove empty values.
});
// 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: CoreH5PLibrariesCachedAssetsDBData[], siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
const promises = [];
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));
});
});
try {
await CoreUtils.instance.allPromises(promises);
} catch (error) {
// Ignore errors, maybe there's no cached asset of some type.
}
}
/**
* 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 ' + CoreH5PProvider.CONTENTS_LIBRARIES_TABLE + ' hcl ' +
'JOIN ' + CoreH5PProvider.CONTENT_TABLE + ' hc ON hcl.h5pid = hc.id ' +
'WHERE hcl.libraryid = ?';
const queryArgs = [];
queryArgs.push(libraryId);
const result = await db.execute(query, queryArgs);
await Array.from(result.rows).map(async (entry: {foldername: string}) => {
try {
// Delete the index.html.
await CoreFile.instance.removeFile(this.getContentIndexPath(entry.foldername, site.getId()));
} catch (error) {
// Ignore errors.
}
});
}
/**
* Deletes a library from the file system.
*
* @param libraryData The library data.
* @param folderName Folder name. If not provided, it will be calculated.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteLibraryFolder(libraryData: any, folderName?: string, siteId?: 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[]}> {
// 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[]> {
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: any, 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.
try {
await CoreFile.instance.removeDir(folderPath);
} catch (error) {
// Ignore errors, maybe it doesn't exist.
}
// 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: any, siteId?: string): Promise<void> {
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.
}
// Copy the new one.
await CoreFile.instance.moveDir(libraryData.uploadDirectory, folderPath, true);
}
}

View File

@ -0,0 +1,902 @@
// (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 { CoreH5P, CoreH5PProvider } from '../providers/h5p';
import {
CoreH5PCore, CoreH5PDisplayOptionBehaviour, CoreH5PContentDependencyData, CoreH5PLibraryData, CoreH5PLibraryAddonData,
CoreH5PContentDepsTreeDependency
} from './core';
/**
* 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];
return db.updateRecordsWhere(CoreH5PProvider.CONTENT_TABLE, { 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<CoreH5PLibrariesCachedAssetsDBData[]> {
const db = await CoreSites.instance.getSiteDb(siteId);
// Get all the hashes that use this library.
const entries = await db.getRecords(CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, {libraryid: libraryId});
const hashes = entries.map((entry) => entry.hash);
if (hashes.length) {
// Delete the entries from DB.
await db.deleteRecordsList(CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, '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(CoreH5PProvider.CONTENT_TABLE, {id: 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(CoreH5PProvider.LIBRARIES_TABLE, {id: id});
}
/**
* Delete all dependencies belonging to given library.
*
* @param libraryId Library ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteLibraryDependencies(libraryId: number, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
await db.deleteRecords(CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE, {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(CoreH5PProvider.CONTENTS_LIBRARIES_TABLE, {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<CoreH5PContentDBData[]> {
const db = await CoreSites.instance.getSiteDb(siteId);
return db.getAllRecords(CoreH5PProvider.CONTENT_TABLE);
}
/**
* 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<CoreH5PContentDBData> {
const db = await CoreSites.instance.getSiteDb(siteId);
return db.getRecord(CoreH5PProvider.CONTENT_TABLE, {id: 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<CoreH5PContentDBData> {
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 {
const contentData = await db.getRecord(CoreH5PProvider.CONTENT_TABLE, {foldername: folderName});
return contentData;
} catch (error) {
// Cannot get folder name, the h5p file was probably deleted. Just use the URL.
return db.getRecord(CoreH5PProvider.CONTENT_TABLE, {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<CoreH5PLibraryDBData> {
const db = await CoreSites.instance.getSiteDb(siteId);
try {
const records = await db.getRecords(CoreH5PProvider.LIBRARIES_TABLE, {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 `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<CoreH5PLibraryDBData> {
const db = await CoreSites.instance.getSiteDb(siteId);
const conditions = {
machinename: machineName,
majorversion: undefined,
minorversion: undefined,
};
if (typeof majorVersion != 'undefined') {
conditions.majorversion = majorVersion;
}
if (typeof minorVersion != 'undefined') {
conditions.minorversion = minorVersion;
}
const libraries = await db.getRecords(CoreH5PProvider.LIBRARIES_TABLE, conditions);
if (!libraries.length) {
throw '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: any, siteId?: string): Promise<CoreH5PLibraryDBData> {
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<CoreH5PLibraryDBData> {
const db = await CoreSites.instance.getSiteDb(siteId);
const library = await db.getRecord(CoreH5PProvider.LIBRARIES_TABLE, {id: 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> {
try {
const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId);
return (library && library.id) || null;
} catch (error) {
return null;
}
}
/**
* Get a library ID. If not found, return null.
*
* @param libraryData Library data.
* @param siteId The site ID. If not defined, current site.
* @return Promise resolved with the library ID, null if not found.
*/
getLibraryIdByData(libraryData: any, siteId?: string): Promise<number> {
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.
*/
getOption(name: string, defaultValue: any = false): any {
// 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.
*/
hasPermission(permission: number, id: number): boolean {
// H5P capabilities have not been introduced.
return null;
}
/**
* Determines if content slug is used.
*
* @param slug The content slug.
* @return Whether the content slug is used
*/
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: any, dbData?: CoreH5PLibraryDBData): 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: any, key: string, searchParam: string = 'path'): string {
if (typeof libraryData[key] != 'undefined') {
const parameterValues = [];
libraryData[key].forEach((file) => {
for (const index in file) {
if (index === searchParam) {
parameterValues.push(file[index]);
}
}
});
return parameterValues.join(',');
}
return '';
}
/**
* 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 ' + CoreH5PProvider.LIBRARIES_TABLE + ' l1 ' +
'JOIN ' + CoreH5PProvider.LIBRARIES_TABLE + ' 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 = [];
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: CoreH5PContentDBData;
if (id) {
contentData = await this.getContentData(id, siteId);
} else if (fileUrl) {
contentData = await this.getContentDataByUrl(fileUrl, siteId);
} else {
throw '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,
};
const params = CoreTextUtils.instance.parseJSON(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 ' + CoreH5PProvider.CONTENTS_LIBRARIES_TABLE + ' hcl ' +
'JOIN ' + CoreH5PProvider.LIBRARIES_TABLE + ' hl ON hcl.libraryid = hl.id ' +
'WHERE hcl.h5pid = ?';
const queryArgs = [];
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 = {};
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,
preloadedCss: library.preloadedcss,
dropLibraryCss: library.droplibrarycss,
semantics: library.semantics,
preloadedDependencies: [],
dynamicDependencies: [],
editorDependencies: []
};
// Now get the dependencies.
const sql = 'SELECT hl.id, hl.machinename, hl.majorversion, hl.minorversion, hll.dependencytype ' +
'FROM ' + CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE + ' hll ' +
'JOIN ' + CoreH5PProvider.LIBRARIES_TABLE + ' 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 = 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: any): CoreH5PLibraryAddonData {
library.addto = CoreTextUtils.instance.parseJSON(library.addto, null);
return library;
}
/**
* Parse library DB data.
*
* @param library Library DB data.
* @return Parsed library.
*/
protected parseLibDBData(library: any): CoreH5PLibraryDBData {
library.semantics = CoreTextUtils.instance.parseJSON(library.semantics, null);
library.addto = CoreTextUtils.instance.parseJSON(library.addto, null);
return library;
}
/**
* 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.
*/
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 = {
hash: key,
libraryid: dependencies[key].libraryId,
foldername: folderName,
};
await db.insertRecord(CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, 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: any, 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 = {
id: undefined,
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(CoreH5PProvider.LIBRARIES_TABLE, data);
if (!data.id) {
// New library. Get its ID.
const entry = await db.getRecord(CoreH5PProvider.LIBRARIES_TABLE, 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: any[], 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 = {
libraryid: libraryId,
requiredlibraryid: dependencyId,
dependencytype: dependencyType
};
await db.insertRecord(CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE, 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 = {};
for (const key in librariesInUse) {
const dependency = librariesInUse[key];
if ((<CoreH5PLibraryData> dependency.library).dropLibraryCss) {
const split = (<CoreH5PLibraryData> 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 = {
h5pid: id,
libraryId: dependency.library.libraryId,
dependencytype: dependency.type,
dropcss: dropLibraryCssList[dependency.library.machineName] ? 1 : 0,
weight: dependency.weight,
};
return db.insertRecord(CoreH5PProvider.CONTENTS_LIBRARIES_TABLE, 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: any, 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 (typeof content.library.libraryId == 'undefined') {
const mainLibrary = await this.getLatestLibraryVersion(content.library.machineName, siteId);
content.library.libraryId = mainLibrary.id;
}
const data: CoreH5PContentDBData = {
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(CoreH5PProvider.CONTENT_TABLE, data);
if (!data.id) {
// New content. Get its ID.
const entry = await db.getRecord(CoreH5PProvider.CONTENT_TABLE, 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: {[name: string]: any}, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
const data = Object.assign({}, fields);
delete data.slug; // Slug isn't stored in DB.
await db.updateRecords(CoreH5PProvider.CONTENT_TABLE, data, {id: 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; // 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.
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: any; // Content metadata.
};
/**
* Content data stored in DB.
*/
export type CoreH5PContentDBData = {
id: number; // The id of the content.
jsoncontent: string; // The content in json format.
mainlibraryid: number; // The library we first instantiate for this node.
foldername: string; // Name of the folder that contains the contents.
fileurl: string; // The online URL of the H5P package.
filtered: string; // Filtered version of json_content.
timecreated: number; // Time created.
timemodified: number; // Time modified.
};
/**
* Library data stored in DB.
*/
export type CoreH5PLibraryDBData = {
id: number; // The id of the library.
machinename: string; // The library machine name.
title: string; // The human readable name of this library.
majorversion: number; // Major version.
minorversion: number; // Minor version.
patchversion: number; // Patch version.
runnable: number; // Can this library be started by the module? I.e. not a dependency.
fullscreen: number; // Display fullscreen button.
embedtypes: string; // List of supported embed types.
preloadedjs?: string; // Comma separated list of scripts to load.
preloadedcss?: string; // Comma separated list of stylesheets to load.
droplibrarycss?: string; // List of libraries that should not have CSS included if this library is used. Comma separated list.
semantics?: any; // The semantics definition.
addto?: any; // Plugin configuration data.
};
/**
* Library dependencies stored in DB.
*/
export type CoreH5PLibraryDependenciesDBData = {
id: number; // Id.
libraryid: number; // The id of an H5P library.
requiredlibraryid: number; // The dependent library to load.
dependencytype: string; // Type: preloaded, dynamic, or editor.
};
/**
* Library cached assets stored in DB.
*/
export type CoreH5PLibrariesCachedAssetsDBData = {
id: number; // Id.
libraryid: number; // The id of an H5P library.
hash: string; // The hash to identify the cached asset.
foldername: string; // Name of the folder that contains the contents.
};

View File

@ -0,0 +1,145 @@
// (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 '@providers/file';
import { CoreSites } from '@providers/sites';
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreH5P } from '../providers/h5p';
import { CoreH5PCore } from './core';
import { FileEntry } from '@ionic-native/file';
/**
* Equivalent to Moodle's H5P helper class.
*/
export class CoreH5PHelper {
/**
* Get the core H5P assets, including all core H5P JavaScript and CSS.
*
* @return Array core H5P assets.
*/
static async getCoreAssets(siteId?: string): Promise<{settings: any, 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: settings, cssRequires: cssRequires, jsRequires: 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<any> {
const site = await CoreSites.instance.getSite(siteId);
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(),
},
user: [],
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.
* @return Promise resolved when done.
*/
static async saveH5P(fileUrl: string, file: FileEntry, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Unzip the file.
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);
try {
// 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.
}
}
}
}

View File

@ -0,0 +1,39 @@
// (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.
/**
* 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: any): string {
// Convert metadataSettings values to boolean.
if (typeof metadataSettings.disable != 'undefined') {
metadataSettings.disable = metadataSettings.disable === 1;
}
if (typeof metadataSettings.disableExtraTitleField != 'undefined') {
metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1;
}
return JSON.stringify(metadataSettings);
}
}

View File

@ -0,0 +1,326 @@
// (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 '@providers/file';
import { CoreSites } from '@providers/sites';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreUrlUtils } from '@providers/utils/url';
import { CoreUtils } from '@providers/utils/utils';
import { CoreH5P } from '../providers/h5p';
import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core';
import { CoreH5PHelper } from './helper';
import { CoreH5PStorage } from './storage';
/**
* Equivalent to Moodle's H5P player class.
*/
export class CoreH5PPlayer {
constructor(protected h5pCore: CoreH5PCore,
protected h5pStorage: CoreH5PStorage) { }
/**
* 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: this.getEmbedUrl(site.getURL(), h5pUrl),
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, siteId);
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 display options.
html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths(
this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/displayoptions.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();
const records = await this.h5pCore.h5pFramework.getAllContentData(siteId);
await Promise.all(records.map(async (record) => {
try {
await this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId);
} catch (err) {
// Ignore errors, maybe the file doesn't exist.
}
}));
}
/**
* 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: any, 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 = 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 urlParams URL params.
* @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, urlParams?: {[name: string]: string}, siteId?: string): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId);
// Add display options to the URL.
const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId);
const options = this.h5pCore.fixDisplayOptions(this.getDisplayOptionsFromUrlParams(urlParams), data.id);
return CoreUrlUtils.instance.addParamsToUrl(path, options, undefined, true);
}
/**
* 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> {
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] =
CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD]);
displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] =
CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_EMBED]);
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');
}
}

View File

@ -0,0 +1,202 @@
// (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 '@providers/file';
import { CoreSites } from '@providers/sites';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreUtils } from '@providers/utils/utils';
import { CoreH5PCore } from './core';
import { CoreH5PFramework, CoreH5PLibraryDBData } from './framework';
import { CoreH5PMetadata } from './metadata';
import { 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: any, 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 = [];
// Go through libraries that came with this package.
await Promise.all(Object.keys(librariesJsonData).map(async (libString) => {
const libraryData = librariesJsonData[libString];
// Find local library identifier.
let dbData: CoreH5PLibraryDBData;
try {
dbData = await this.h5pFramework.getLibraryByData(libraryData);
} catch (error) {
// Not found.
}
if (dbData) {
// Library already installed.
libraryData.libraryId = dbData.id;
if (!this.h5pFramework.isPatchedLibrary(libraryData, dbData)) {
// 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(libraryData.metadataSettings) : null;
// 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) {
// 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 = [];
// 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 = librariesJsonData[libString];
if (!libraryData.saveDependencies) {
return;
}
libraryIds.push(libraryData.libraryId);
// Remove any old dependencies.
await this.h5pFramework.deleteLibraryDependencies(libraryData.libraryId, siteId);
// Insert the different new ones.
const promises = [];
if (typeof libraryData.preloadedDependencies != 'undefined') {
promises.push(this.h5pFramework.saveLibraryDependencies(libraryData.libraryId, libraryData.preloadedDependencies,
'preloaded'));
}
if (typeof libraryData.dynamicDependencies != 'undefined') {
promises.push(this.h5pFramework.saveLibraryDependencies(libraryData.libraryId, libraryData.dynamicDependencies,
'dynamic'));
}
if (typeof libraryData.editorDependencies != 'undefined') {
promises.push(this.h5pFramework.saveLibraryDependencies(libraryData.libraryId, 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<any> {
if (this.h5pCore.mayUpdateLibraries()) {
// Save the libraries that were processed.
await this.saveLibraries(data.librariesJsonData, folderName, siteId);
}
const content: any = {};
if (!skipContent) {
// Find main library version.
if (data.mainJsonData.preloadedDependencies) {
const mainLib = data.mainJsonData.preloadedDependencies.find((dependency) => {
return dependency.machineName === data.mainJsonData.mainLibrary;
});
if (mainLib) {
const id = await this.h5pFramework.getLibraryIdByData(mainLib);
mainLib.libraryId = id;
content.library = mainLib;
}
}
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;
}
}

View File

@ -0,0 +1,220 @@
// (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 '@providers/file';
import { CoreTextUtils } from '@providers/utils/text';
import { CoreH5PCore } 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<any> {
// Read the required files.
const results = await Promise.all([
this.readLibraryJsonFile(libPath),
this.readLibrarySemanticsFile(libPath),
this.readLibraryLanguageFiles(libPath),
this.libraryHasIcon(libPath),
]);
const libraryData = 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<{[libString: string]: any}> {
const libraries: {[libString: string]: any} = {};
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<any> {
const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'content/content.json');
return CoreFile.instance.readFile(path, CoreFileProvider.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<any> {
const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'h5p.json');
return CoreFile.instance.readFile(path, CoreFileProvider.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<any> {
const path = CoreTextUtils.instance.concatenatePaths(libPath, 'library.json');
return CoreFile.instance.readFile(path, CoreFileProvider.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<{[code: string]: any}> {
try {
const path = CoreTextUtils.instance.concatenatePaths(libPath, 'language');
const langIndex: {[code: string]: any} = {};
// 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(langFilePath, CoreFileProvider.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<any> {
try {
const path = CoreTextUtils.instance.concatenatePaths(libPath, 'semantics.json');
const result = await CoreFile.instance.readFile(path, CoreFileProvider.FORMATJSON);
return result;
} catch (error) {
// Probably doesn't exist, ignore.
}
}
}
/**
* Data of the main JSON H5P files.
*/
export type CoreH5PMainJSONFilesData = {
contentJsonData: any; // Contents of content.json file.
librariesJsonData: {[libString: string]: any}; // Some data about each library.
mainJsonData: any; // Contents of h5p.json file.
};

View File

@ -13,21 +13,21 @@
// limitations under the License. // limitations under the License.
import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges, SimpleChange } from '@angular/core'; import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges, SimpleChange } from '@angular/core';
import { CoreAppProvider } from '@providers/app'; import { CoreApp } from '@providers/app';
import { CoreEventsProvider } from '@providers/events'; import { CoreEvents } from '@providers/events';
import { CoreFileProvider } from '@providers/file'; import { CoreFile } from '@providers/file';
import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreFilepool } from '@providers/filepool';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLogger } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSites } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtils } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtils } from '@providers/utils/url';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreH5P } from '@core/h5p/providers/h5p';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreH5PProvider } from '@core/h5p/providers/h5p';
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreFileHelper } from '@providers/file-helper';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
import { CoreH5PCore } from '../../classes/core';
import { CoreH5PHelper } from '../../classes/helper';
/** /**
* Component to render an H5P package. * Component to render an H5P package.
@ -55,26 +55,13 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
protected urlParams; protected urlParams;
protected logger; protected logger;
constructor(loggerProvider: CoreLoggerProvider, constructor(public elementRef: ElementRef,
public elementRef: ElementRef, protected pluginFileDelegate: CorePluginFileDelegate) {
protected sitesProvider: CoreSitesProvider,
protected urlUtils: CoreUrlUtilsProvider,
protected utils: CoreUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected h5pProvider: CoreH5PProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected eventsProvider: CoreEventsProvider,
protected appProvider: CoreAppProvider,
protected domUtils: CoreDomUtilsProvider,
protected pluginFileDelegate: CorePluginFileDelegate,
protected fileProvider: CoreFileProvider,
protected fileHelper: CoreFileHelperProvider) {
this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent'); this.logger = CoreLogger.instance.getInstance('CoreH5PPlayerComponent');
this.site = sitesProvider.getCurrentSite(); this.site = CoreSites.instance.getCurrentSite();
this.siteId = this.site.getId(); this.siteId = this.site.getId();
this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles() && this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
!this.h5pProvider.isOfflineDisabledInSite();
} }
/** /**
@ -99,90 +86,102 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
* *
* @param e Event. * @param e Event.
*/ */
play(e: MouseEvent): void { async play(e: MouseEvent): Promise<void> {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.loading = true; this.loading = true;
let promise; let localUrl: string;
if (this.canDownload && this.fileHelper.isStateDownloaded(this.state)) { if (this.canDownload && CoreFileHelper.instance.isStateDownloaded(this.state)) {
// Package is downloaded, use the local URL. // Package is downloaded, use the local URL.
promise = this.h5pProvider.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId).catch(() => { try {
localUrl = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId);
} catch (error) {
// Index file doesn't exist, probably deleted because a lib was updated. Try to create it again. // Index file doesn't exist, probably deleted because a lib was updated. Try to create it again.
return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.urlParams.url).then((path) => { try {
return this.fileProvider.getFile(path); const path = await CoreFilepool.instance.getInternalUrlByUrl(this.siteId, this.urlParams.url);
}).then((file) => {
return this.h5pProvider.extractH5PFile(this.urlParams.url, file, this.siteId); const file = await CoreFile.instance.getFile(path);
}).then(() => {
await CoreH5PHelper.saveH5P(this.urlParams.url, file, this.siteId);
// File treated. Try to get the index file URL again. // File treated. Try to get the index file URL again.
return this.h5pProvider.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId); localUrl = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.urlParams.url, this.urlParams,
}); this.siteId);
}).catch((error) => { } catch (error) {
// Still failing. Delete the H5P package? // Still failing. Delete the H5P package?
this.logger.error('Error loading downloaded index:', error, this.src); this.logger.error('Error loading downloaded index:', error, this.src);
}); }
} else { }
promise = Promise.resolve();
} }
promise.then((url) => { try {
if (url) { if (localUrl) {
// Local package. // Local package.
this.playerSrc = url; this.playerSrc = localUrl;
} else { } else {
// Never allow downloading in the app. This will only work if the user is allowed to change the params. // Never allow downloading in the app. This will only work if the user is allowed to change the params.
const src = this.src && this.src.replace(CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=1', const src = this.src && this.src.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1',
CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=0'); CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=0');
// Get auto-login URL so the user is automatically authenticated. // Get auto-login URL so the user is automatically authenticated.
return this.sitesProvider.getCurrentSite().getAutoLoginUrl(src, false).then((url) => { const url = await CoreSites.instance.getCurrentSite().getAutoLoginUrl(src, false);
// Add the preventredirect param so the user can authenticate. // Add the preventredirect param so the user can authenticate.
this.playerSrc = this.urlUtils.addParamsToUrl(url, {preventredirect: false}); this.playerSrc = CoreUrlUtils.instance.addParamsToUrl(url, {preventredirect: false});
});
} }
}).finally(() => { } finally {
this.addResizerScript(); this.addResizerScript();
this.loading = false; this.loading = false;
this.showPackage = true; this.showPackage = true;
if (this.canDownload && (this.state == CoreConstants.OUTDATED || this.state == CoreConstants.NOT_DOWNLOADED)) { if (this.canDownload && (this.state == CoreConstants.OUTDATED || this.state == CoreConstants.NOT_DOWNLOADED)) {
// Download the package in background if the size is low. // Download the package in background if the size is low.
this.attemptDownloadInBg().catch((error) => { try {
this.attemptDownloadInBg();
} catch (error) {
this.logger.error('Error downloading H5P in background', error); this.logger.error('Error downloading H5P in background', error);
});
} }
}); }
}
} }
/** /**
* Download the package. * Download the package.
*
* @return Promise resolved when done.
*/ */
download(e: Event): void { async download(e: Event): Promise<void> {
e && e.preventDefault(); e && e.preventDefault();
e && e.stopPropagation(); e && e.stopPropagation();
if (!this.appProvider.isOnline()) { if (!CoreApp.instance.isOnline()) {
this.domUtils.showErrorModal('core.networkerrormsg', true); CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true);
return; return;
} }
try {
// Get the file size and ask the user to confirm. // Get the file size and ask the user to confirm.
this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId).then((size) => { const size = await this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId);
return this.domUtils.confirmDownloadSize({ size: size, total: true }).then(() => {
await CoreDomUtils.instance.confirmDownloadSize({ size: size, total: true });
// User confirmed, add to the queue. // User confirmed, add to the queue.
return this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId); await CoreFilepool.instance.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
}, () => {
// User cancelled. } catch (error) {
}); if (CoreDomUtils.instance.isCanceledError(error)) {
}).catch((error) => { // User cancelled, stop.
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); return;
}
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
this.calculateState(); this.calculateState();
}); }
} }
/** /**
@ -190,21 +189,18 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected attemptDownloadInBg(): Promise<any> { protected async attemptDownloadInBg(): Promise<void> {
if (this.urlParams && this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() && if (this.urlParams && this.src && this.siteCanDownload && CoreH5P.instance.canGetTrustedH5PFileInSite() &&
this.appProvider.isOnline()) { CoreApp.instance.isOnline()) {
// Get the file size. // Get the file size.
return this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId).then((size) => { const size = await this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId);
if (this.filepoolProvider.shouldDownload(size)) { if (CoreFilepool.instance.shouldDownload(size)) {
// Download the file in background. // Download the file in background.
this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId); CoreFilepool.instance.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
} }
});
} }
return Promise.resolve();
} }
/** /**
@ -219,29 +215,33 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
const script = document.createElement('script'); const script = document.createElement('script');
script.id = 'core-h5p-resizer-script'; script.id = 'core-h5p-resizer-script';
script.type = 'text/javascript'; script.type = 'text/javascript';
script.src = this.h5pProvider.getResizerScriptUrl(); script.src = CoreH5P.instance.h5pPlayer.getResizerScriptUrl();
document.head.appendChild(script); document.head.appendChild(script);
} }
/** /**
* Check if the package can be downloaded. * Check if the package can be downloaded.
*
* @return Promise resolved when done.
*/ */
protected checkCanDownload(): void { protected async checkCanDownload(): Promise<void> {
this.observer && this.observer.off(); this.observer && this.observer.off();
this.urlParams = this.urlUtils.extractUrlParams(this.src); this.urlParams = CoreUrlUtils.instance.extractUrlParams(this.src);
if (this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) { if (this.src && this.siteCanDownload && CoreH5P.instance.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) {
this.calculateState(); this.calculateState();
// Listen for changes in the state. // Listen for changes in the state.
this.filepoolProvider.getFileEventNameByUrl(this.siteId, this.urlParams.url).then((eventName) => { try {
this.observer = this.eventsProvider.on(eventName, () => { const eventName = await CoreFilepool.instance.getFileEventNameByUrl(this.siteId, this.urlParams.url);
this.observer = CoreEvents.instance.on(eventName, () => {
this.calculateState(); this.calculateState();
}); });
}).catch(() => { } catch (error) {
// An error probably means the file cannot be downloaded or we cannot check it (offline). // An error probably means the file cannot be downloaded or we cannot check it (offline).
}); }
} else { } else {
this.calculating = false; this.calculating = false;
@ -254,19 +254,22 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
* Calculate state of the file. * Calculate state of the file.
* *
* @param fileUrl The H5P file URL. * @param fileUrl The H5P file URL.
* @return Promise resolved when done.
*/ */
protected calculateState(): void { protected async calculateState(): Promise<void> {
this.calculating = true; this.calculating = true;
// Get the status of the file. // Get the status of the file.
this.filepoolProvider.getFileStateByUrl(this.siteId, this.urlParams.url).then((state) => { try {
const state = await CoreFilepool.instance.getFileStateByUrl(this.siteId, this.urlParams.url);
this.canDownload = true; this.canDownload = true;
this.state = state; this.state = state;
}).catch((error) => { } catch (error) {
this.canDownload = false; this.canDownload = false;
}).finally(() => { } finally {
this.calculating = false; this.calculating = false;
}); }
} }
/** /**

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -13,16 +13,15 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreFileProvider } from '@providers/file';
import { CorePluginFileHandler } from '@providers/plugin-file-delegate'; import { CorePluginFileHandler } from '@providers/plugin-file-delegate';
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreMimetypeUtils } from '@providers/utils/mimetype';
import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtils } from '@providers/utils/url';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtils } from '@providers/utils/utils';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreH5P } from './h5p';
import { CoreH5PProvider } from './h5p';
import { CoreWSExternalFile } from '@providers/ws'; import { CoreWSExternalFile } from '@providers/ws';
import { FileEntry } from '@ionic-native/file'; import { FileEntry } from '@ionic-native/file';
import { TranslateService } from '@ngx-translate/core'; import { Translate } from '@singletons/core.singletons';
import { CoreH5PHelper } from '../classes/helper';
/** /**
* Handler to treat H5P files. * Handler to treat H5P files.
@ -31,14 +30,6 @@ import { TranslateService } from '@ngx-translate/core';
export class CoreH5PPluginFileHandler implements CorePluginFileHandler { export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
name = 'CoreH5PPluginFileHandler'; name = 'CoreH5PPluginFileHandler';
constructor(protected urlUtils: CoreUrlUtilsProvider,
protected mimeUtils: CoreMimetypeUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider,
protected fileProvider: CoreFileProvider,
protected h5pProvider: CoreH5PProvider,
protected translate: TranslateService) { }
/** /**
* React to a file being deleted. * React to a file being deleted.
* *
@ -49,7 +40,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
*/ */
fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> { fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> {
// If an h5p file is deleted, remove the contents folder. // If an h5p file is deleted, remove the contents folder.
return this.h5pProvider.deleteContentByUrl(fileUrl, siteId); return CoreH5P.instance.h5pPlayer.deleteContentByUrl(fileUrl, siteId);
} }
/** /**
@ -60,7 +51,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
* @return Promise resolved with the file to use. Rejected if cannot download. * @return Promise resolved with the file to use. Rejected if cannot download.
*/ */
getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile> { getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile> {
return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId); return CoreH5P.instance.getTrustedH5PFile(file.fileurl, {}, false, siteId);
} }
/** /**
@ -75,7 +66,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
const urls = []; const urls = [];
for (let i = 0; i < iframes.length; i++) { for (let i = 0; i < iframes.length; i++) {
const params = this.urlUtils.extractUrlParams(iframes[i].src); const params = CoreUrlUtils.instance.extractUrlParams(iframes[i].src);
if (params.url) { if (params.url) {
urls.push(params.url); urls.push(params.url);
@ -92,17 +83,19 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the size. * @return Promise resolved with the size.
*/ */
getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> { async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId).then((file) => { try {
return file.filesize; const trustedFile = await CoreH5P.instance.getTrustedH5PFile(file.fileurl, {}, false, siteId);
}).catch((error): any => {
if (this.utils.isWebServiceError(error)) { return trustedFile.filesize;
} catch (error) {
if (CoreUtils.instance.isWebServiceError(error)) {
// WS returned an error, it means it cannot be downloaded. // WS returned an error, it means it cannot be downloaded.
return 0; return 0;
} }
return Promise.reject(error); throw error;
}); }
} }
/** /**
@ -111,7 +104,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
* @return Whether or not the handler is enabled on a site level. * @return Whether or not the handler is enabled on a site level.
*/ */
isEnabled(): boolean | Promise<boolean> { isEnabled(): boolean | Promise<boolean> {
return this.h5pProvider.canGetTrustedH5PFileInSite(); return CoreH5P.instance.canGetTrustedH5PFileInSite();
} }
/** /**
@ -122,12 +115,12 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
* @return Promise resolved with a boolean and a reason why it isn't downloadable if needed. * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed.
*/ */
async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<{downloadable: boolean, reason?: string}> { async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<{downloadable: boolean, reason?: string}> {
const offlineDisabled = await this.h5pProvider.isOfflineDisabled(siteId); const offlineDisabled = await CoreH5P.instance.isOfflineDisabled(siteId);
if (offlineDisabled) { if (offlineDisabled) {
return { return {
downloadable: false, downloadable: false,
reason: this.translate.instant('core.h5p.offlinedisabled'), reason: Translate.instance.instant('core.h5p.offlinedisabled'),
}; };
} else { } else {
return { return {
@ -143,7 +136,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
* @return Whether the file should be treated by this handler. * @return Whether the file should be treated by this handler.
*/ */
shouldHandleFile(file: CoreWSExternalFile): boolean { shouldHandleFile(file: CoreWSExternalFile): boolean {
return this.mimeUtils.guessExtensionFromUrl(file.fileurl) == 'h5p'; return CoreMimetypeUtils.instance.guessExtensionFromUrl(file.fileurl) == 'h5p';
} }
/** /**
@ -154,7 +147,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> { treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<void> {
return this.h5pProvider.extractH5PFile(fileUrl, file, siteId); return CoreH5PHelper.saveH5P(fileUrl, file, siteId);
} }
} }

View File

@ -1,429 +0,0 @@
// (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 { TranslateService } from '@ngx-translate/core';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreH5PContentDependencyData, CoreH5PDependencyAsset } from './h5p';
import { Md5 } from 'ts-md5/dist/md5';
/**
* Utils service with helper functions for H5P.
*/
@Injectable()
export class CoreH5PUtilsProvider {
// Map to slugify characters.
protected 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'
};
constructor(private translate: TranslateService,
private textUtils: CoreTextUtilsProvider) { }
/**
* The metadataSettings field in libraryJson uses 1 for true and 0 for false.
* Here we are converting these to booleans, and also doing JSON encoding.
*
* @param metadataSettings Settings.
* @return Stringified settings.
*/
boolifyAndEncodeMetadataSettings(metadataSettings: any): string {
// Convert metadataSettings values to boolean.
if (typeof metadataSettings.disable != 'undefined') {
metadataSettings.disable = metadataSettings.disable === 1;
}
if (typeof metadataSettings.disableExtraTitleField != 'undefined') {
metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1;
}
return JSON.stringify(metadataSettings);
}
/**
* 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'.
*/
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;
}
/**
* 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 = this.textUtils.concatenatePaths(assetsFolderPath, url);
}
// Add version if set.
if (asset.version) {
url += asset.version;
}
urls.push(url);
});
return urls;
}
/**
* Get the hash of a list of dependencies.
*
* @param dependencies Dependencies.
* @return Hash.
*/
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(''));
}
/**
* Provide localization for the Core JS.
*
* @return Object with the translations.
*/
getLocalization(): any {
return {
fullscreen: this.translate.instant('core.h5p.fullscreen'),
disableFullscreen: this.translate.instant('core.h5p.disablefullscreen'),
download: this.translate.instant('core.h5p.download'),
copyrights: this.translate.instant('core.h5p.copyright'),
embed: this.translate.instant('core.h5p.embed'),
size: this.translate.instant('core.h5p.size'),
showAdvanced: this.translate.instant('core.h5p.showadvanced'),
hideAdvanced: this.translate.instant('core.h5p.hideadvanced'),
advancedHelp: this.translate.instant('core.h5p.resizescript'),
copyrightInformation: this.translate.instant('core.h5p.copyright'),
close: this.translate.instant('core.h5p.close'),
title: this.translate.instant('core.h5p.title'),
author: this.translate.instant('core.h5p.author'),
year: this.translate.instant('core.h5p.year'),
source: this.translate.instant('core.h5p.source'),
license: this.translate.instant('core.h5p.license'),
thumbnail: this.translate.instant('core.h5p.thumbnail'),
noCopyrights: this.translate.instant('core.h5p.nocopyright'),
reuse: this.translate.instant('core.h5p.reuse'),
reuseContent: this.translate.instant('core.h5p.reuseContent'),
reuseDescription: this.translate.instant('core.h5p.reuseDescription'),
downloadDescription: this.translate.instant('core.h5p.downloadtitle'),
copyrightsDescription: this.translate.instant('core.h5p.copyrighttitle'),
embedDescription: this.translate.instant('core.h5p.embedtitle'),
h5pDescription: this.translate.instant('core.h5p.h5ptitle'),
contentChanged: this.translate.instant('core.h5p.contentchanged'),
startingOver: this.translate.instant('core.h5p.startingover'),
by: this.translate.instant('core.h5p.by'),
showMore: this.translate.instant('core.h5p.showmore'),
showLess: this.translate.instant('core.h5p.showless'),
subLevel: this.translate.instant('core.h5p.sublevel'),
confirmDialogHeader: this.translate.instant('core.h5p.confirmdialogheader'),
confirmDialogBody: this.translate.instant('core.h5p.confirmdialogbody'),
cancelLabel: this.translate.instant('core.h5p.cancellabel'),
confirmLabel: this.translate.instant('core.h5p.confirmlabel'),
licenseU: this.translate.instant('core.h5p.undisclosed'),
licenseCCBY: this.translate.instant('core.h5p.ccattribution'),
licenseCCBYSA: this.translate.instant('core.h5p.ccattributionsa'),
licenseCCBYND: this.translate.instant('core.h5p.ccattributionnd'),
licenseCCBYNC: this.translate.instant('core.h5p.ccattributionnc'),
licenseCCBYNCSA: this.translate.instant('core.h5p.ccattributionncsa'),
licenseCCBYNCND: this.translate.instant('core.h5p.ccattributionncnd'),
licenseCC40: this.translate.instant('core.h5p.licenseCC40'),
licenseCC30: this.translate.instant('core.h5p.licenseCC30'),
licenseCC25: this.translate.instant('core.h5p.licenseCC25'),
licenseCC20: this.translate.instant('core.h5p.licenseCC20'),
licenseCC10: this.translate.instant('core.h5p.licenseCC10'),
licenseGPL: this.translate.instant('core.h5p.licenseGPL'),
licenseV3: this.translate.instant('core.h5p.licenseV3'),
licenseV2: this.translate.instant('core.h5p.licenseV2'),
licenseV1: this.translate.instant('core.h5p.licenseV1'),
licensePD: this.translate.instant('core.h5p.pd'),
licenseCC010: this.translate.instant('core.h5p.licenseCC010'),
licensePDM: this.translate.instant('core.h5p.pdm'),
licenseC: this.translate.instant('core.h5p.copyrightstring'),
contentType: this.translate.instant('core.h5p.contenttype'),
licenseExtras: this.translate.instant('core.h5p.licenseextras'),
changes: this.translate.instant('core.h5p.changelog'),
contentCopied: this.translate.instant('core.h5p.contentCopied'),
connectionLost: this.translate.instant('core.h5p.connectionLost'),
connectionReestablished: this.translate.instant('core.h5p.connectionReestablished'),
resubmitScores: this.translate.instant('core.h5p.resubmitScores'),
offlineDialogHeader: this.translate.instant('core.h5p.offlineDialogHeader'),
offlineDialogBody: this.translate.instant('core.h5p.offlineDialogBody'),
offlineDialogRetryMessage: this.translate.instant('core.h5p.offlineDialogRetryMessage'),
offlineDialogRetryButtonLabel: this.translate.instant('core.h5p.offlineDialogRetryButtonLabel'),
offlineSuccessfulSubmit: this.translate.instant('core.h5p.offlineSuccessfulSubmit'),
};
}
/**
* 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.
*/
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;
}
/**
* Convert list of library parameter values to csv.
*
* @param libraryData Library data as found in library.json files.
* @param key Key that should be found in libraryData.
* @param searchParam The library parameter (Default: 'path').
* @return Library parameter values separated by ', '
*/
libraryParameterValuesToCsv(libraryData: any, key: string, searchParam: string = 'path'): string {
if (typeof libraryData[key] != 'undefined') {
const parameterValues = [];
libraryData[key].forEach((file) => {
for (const index in file) {
if (index === searchParam) {
parameterValues.push(file[index]);
}
}
});
return parameterValues.join(',');
}
return '';
}
/**
* Convert strings of text into simple kebab case slugs. Based on H5PCore::slugify.
*
* @param input The string to slugify.
* @return Slugified text.
*/
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 += this.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.
*/
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;
}
}

View File

@ -43,7 +43,7 @@ export class CoreViewerQRScannerPage {
this.closeModal(text); this.closeModal(text);
}).catch((error) => { }).catch((error) => {
if (!error.coreCanceled) { if (!this.domUtils.isCanceledError(error)) {
// Show error and stop scanning. // Show error and stop scanning.
this.domUtils.showErrorModalDefault(error, 'An error occurred.'); this.domUtils.showErrorModalDefault(error, 'An error occurred.');
this.utils.stopScanQR(); this.utils.stopScanQR();

View File

@ -53,7 +53,7 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
const versionApplied: number = await this.configProvider.get(this.VERSION_APPLIED, 0); const versionApplied: number = await this.configProvider.get(this.VERSION_APPLIED, 0);
if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) { if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
promises.push(CoreH5P.instance.deleteAllContentIndexes()); promises.push(CoreH5P.instance.h5pPlayer.deleteAllContentIndexes());
} }
try { try {

View File

@ -669,7 +669,7 @@ export class CoreDomUtilsProvider {
} }
// We received an object instead of a string. Search for common properties. // We received an object instead of a string. Search for common properties.
if (error.coreCanceled) { if (this.isCanceledError(error)) {
// It's a canceled error, don't display an error. // It's a canceled error, don't display an error.
return null; return null;
} }
@ -716,6 +716,16 @@ export class CoreDomUtilsProvider {
return this.instances[id]; return this.instances[id];
} }
/**
* Check whether an error is an error caused because the user canceled a showConfirm.
*
* @param error Error to check.
* @return Whether it's a canceled error.
*/
isCanceledError(error: any): boolean {
return error && error.coreCanceled;
}
/** /**
* Wait an element to exists using the findFunction. * Wait an element to exists using the findFunction.
* *
@ -1314,7 +1324,7 @@ export class CoreDomUtilsProvider {
* @return Promise resolved with the alert modal. * @return Promise resolved with the alert modal.
*/ */
showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise<Alert> { showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise<Alert> {
if (error && error.coreCanceled) { if (this.isCanceledError(error)) {
// It's a canceled error, don't display an error. // It's a canceled error, don't display an error.
return; return;
} }

View File

@ -1543,7 +1543,7 @@ export class CoreUtilsProvider {
} else if (typeof data != 'undefined') { } else if (typeof data != 'undefined') {
this.qrScanData.deferred.resolve(data); this.qrScanData.deferred.resolve(data);
} else { } else {
this.qrScanData.deferred.reject({coreCanceled: true}); this.qrScanData.deferred.reject(this.domUtils.createCanceledError());
} }
delete this.qrScanData; delete this.qrScanData;