diff --git a/src/core/h5p/classes/content-validator.ts b/src/core/h5p/classes/content-validator.ts index 146fb10c1..e00f53dc4 100644 --- a/src/core/h5p/classes/content-validator.ts +++ b/src/core/h5p/classes/content-validator.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreH5PProvider, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from '../providers/h5p'; -import { CoreH5PUtilsProvider } from '../providers/utils'; -import { TranslateService } from '@ngx-translate/core'; +import { CoreTextUtils } from '@providers/utils/text'; +import { CoreUtils } from '@providers/utils/utils'; +import { CoreH5P } from '../providers/h5p'; +import { Translate } from '@singletons/core.singletons'; +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. */ export class CoreH5PContentValidator { @@ -43,17 +43,12 @@ export class CoreH5PContentValidator { protected libraries: {[libString: string]: CoreH5PLibraryData} = {}; protected dependencies: {[key: string]: CoreH5PContentDepsTreeDependency} = {}; protected relativePathRegExp = /^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/; - protected allowedHtml: {[tag: string]: any} = {}; + protected allowedHtml: {[tag: string]: string} = {}; protected allowedStyles: RegExp[]; protected metadataSemantics: any[]; protected copyrightSemantics: any; - constructor(protected h5pProvider: CoreH5PProvider, - protected h5pUtils: CoreH5PUtilsProvider, - protected textUtils: CoreTextUtilsProvider, - protected utils: CoreUtilsProvider, - protected translate: TranslateService, - protected siteId: string) { } + constructor(protected siteId: string) { } /** * Add Addon library. @@ -61,24 +56,23 @@ export class CoreH5PContentValidator { * @param library The addon library to add. * @return Promise resolved when done. */ - addon(library: CoreH5PLibraryAddonData): Promise { + async addon(library: CoreH5PLibraryAddonData): Promise { const depKey = 'preloaded-' + library.machineName; this.dependencies[depKey] = { library: library, - type: 'preloaded' + type: 'preloaded', }; - return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => { - this.nextWeight = weight; - this.dependencies[depKey].weight = this.nextWeight++; - }); + this.nextWeight = await CoreH5P.instance.h5pCore.findLibraryDependencies(this.dependencies, library, this.nextWeight); + + this.dependencies[depKey].weight = this.nextWeight++; } /** * Get the flat dependency tree. * - * @return array + * @return Dependencies. */ getDependencies(): {[key: string]: CoreH5PContentDepsTreeDependency} { return this.dependencies; @@ -92,7 +86,7 @@ export class CoreH5PContentValidator { */ validateMetadata(metadata: any): Promise { 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. if (typeof group.license == 'undefined') { @@ -135,7 +129,7 @@ export class CoreH5PContentValidator { tags.push('s'); } - tags = this.utils.uniqueArray(tags); + tags = CoreUtils.instance.uniqueArray(tags); // Determine allowed style tags const stylePatterns: RegExp[] = []; @@ -168,7 +162,7 @@ export class CoreH5PContentValidator { text = this.filterXss(text, tags, stylePatterns); } else { // Filter text to plain text. - text = this.textUtils.escapeHTML(text); + text = CoreTextUtils.instance.escapeHTML(text); } // 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. if (typeof semantics.step != 'undefined') { - const testNumber = num - (typeof semantics.min != 'undefined' ? semantics.min : 0), - rest = testNumber % semantics.step; + const testNumber = num - (typeof semantics.min != 'undefined' ? semantics.min : 0); + const rest = testNumber % semantics.step; if (rest !== 0) { num -= rest; } @@ -245,8 +239,8 @@ export class CoreH5PContentValidator { * @return Validated select. */ validateSelect(select: any, semantics: any): any { - const optional = semantics.optional, - options = {}; + const optional = semantics.optional; + const options = {}; let strict = false; if (semantics.options && semantics.options.length) { @@ -273,7 +267,7 @@ export class CoreH5PContentValidator { if (strict && !optional && !options[value]) { delete select[key]; } else { - select[key] = this.textUtils.escapeHTML(value); + select[key] = CoreTextUtils.instance.escapeHTML(value); } } } else { @@ -285,7 +279,7 @@ export class CoreH5PContentValidator { if (strict && !optional && !options[select]) { select = semantics.options[0].value; } - select = this.textUtils.escapeHTML(select); + select = CoreTextUtils.instance.escapeHTML(select); } return select; @@ -299,11 +293,10 @@ export class CoreH5PContentValidator { * @param semantics Semantics. * @return Validated list. */ - validateList(list: any, semantics: any): Promise { - const field = semantics.field, - fn = this[this.typeMap[field.type]].bind(this); - let promise = Promise.resolve(), // Use a chain of promises so the order is kept. - keys = Object.keys(list); + async validateList(list: any, semantics: any): Promise { + const field = semantics.field; + const fn = this[this.typeMap[field.type]].bind(this); + let keys = Object.keys(list); // Check that list is not longer than allowed length. if (typeof semantics.max != 'undefined') { @@ -311,35 +304,32 @@ export class CoreH5PContentValidator { } // Validate each element in list. - keys.forEach((key) => { + for (const i in keys) { + const key = keys[i]; + if (isNaN(parseInt(key, 10))) { // It's an object and the key isn't an integer. Delete it. delete list[key]; } else { - promise = promise.then(() => { - return Promise.resolve(fn(list[key], field)).then((val) => { - if (val === null) { - list.splice(key, 1); - } else { - list[key] = val; - } - }); - }); + const val = await fn(list[key], field); + + if (val === null) { + list.splice(key, 1); + } else { + list[key] = val; + } } - }); + } - return promise.then(() => { + if (!Array.isArray(list)) { + list = CoreUtils.instance.objectToArray(list); + } - if (!Array.isArray(list)) { - list = this.utils.objectToArray(list); - } + if (!list.length) { + return null; + } - if (!list.length) { - return null; - } - - return list; - }); + return list; } /** @@ -350,7 +340,7 @@ export class CoreH5PContentValidator { * @param typeValidKeys List of valid keys. * @return Promise resolved with the validated file. */ - protected validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise { + protected async validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise { // Do not allow to use files from other content folders. const matches = file.path.match(this.relativePathRegExp); if (matches && matches.length) { @@ -363,9 +353,9 @@ export class CoreH5PContentValidator { } // 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) { - 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. @@ -373,7 +363,7 @@ export class CoreH5PContentValidator { if (semantics.extraAttributes) { validKeys = validKeys.concat(semantics.extraAttributes); } - validKeys = this.utils.uniqueArray(validKeys); + validKeys = CoreUtils.instance.uniqueArray(validKeys); this.filterParams(file, validKeys); @@ -386,7 +376,7 @@ export class CoreH5PContentValidator { } if (file.codecs) { - file.codecs = this.textUtils.escapeHTML(file.codecs); + file.codecs = CoreTextUtils.instance.escapeHTML(file.codecs); } if (typeof file.bitrate != 'undefined') { @@ -399,17 +389,15 @@ export class CoreH5PContentValidator { } else { this.filterParams(file.quality, ['level', 'label']); 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') { - return this.validateGroup(file.copyright, this.getCopyrightSemantics()).then(() => { - return file; - }); + await this.validateGroup(file.copyright, this.getCopyrightSemantics()); } - return Promise.resolve(file); + return file; } /** @@ -441,18 +429,13 @@ export class CoreH5PContentValidator { * @param semantics Semantics. * @return Promise resolved with the validated file. */ - validateVideo(video: any, semantics: any): Promise { - let promise = Promise.resolve(); // Use a chain of promises so the order is kept. + async validateVideo(video: any, semantics: any): Promise { for (const key in video) { - promise = promise.then(() => { - return this.validateFilelike(video[key], semantics, ['width', 'height', 'codecs', 'quality', 'bitrate']); - }); + await 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. * @return Promise resolved with the validated file. */ - validateAudio(audio: any, semantics: any): Promise { - let promise = Promise.resolve(); // Use a chain of promises so the order is kept. + async validateAudio(audio: any, semantics: any): Promise { for (const key in audio) { - promise = promise.then(() => { - return this.validateFilelike(audio[key], semantics); - }); + await this.validateFilelike(audio[key], semantics); } - return promise.then(() => { - return audio; - }); + return audio; } /** @@ -483,19 +461,19 @@ export class CoreH5PContentValidator { * @param group Group. * @param semantics Semantics. * @param flatten Whether to flatten. + * @return Promise resolved when done. */ - validateGroup(group: any, semantics: any, flatten: boolean = true): Promise { - // Groups with just one field are compressed in the editor to only output he child content. + async validateGroup(group: any, semantics: any, flatten: boolean = true): Promise { + // Groups with just one field are compressed in the editor to only output the child content. const isSubContent = semantics.isSubContent === true; if (semantics.fields.length == 1 && flatten && !isSubContent) { - const field = semantics.fields[0], - fn = this[this.typeMap[field.type]].bind(this); + const field = semantics.fields[0]; + const fn = this[this.typeMap[field.type]].bind(this); - return Promise.resolve(fn(group, field)); + return fn(group, field); } else { - let promise = Promise.resolve(); // Use a chain of promises so the order is kept. for (const key in group) { // If subContentId is set, keep value @@ -504,9 +482,9 @@ export class CoreH5PContentValidator { } // Find semantics for name=key. - let found = false, - fn = null, - field = null; + let found = false; + let fn = null; + let field = null; for (let i = 0; i < semantics.fields.length; i++) { field = semantics.fields[i]; @@ -522,23 +500,19 @@ export class CoreH5PContentValidator { } if (found && fn) { - promise = promise.then(() => { - return Promise.resolve(fn(group[key], field)).then((val) => { - group[key] = val; - if (val === null) { - delete group[key]; - } - }); - }); + const val = await fn(group[key], field); + + group[key] = val; + if (val === null) { + delete group[key]; + } } else { // Something exists in content that does not have a corresponding semantics field. Remove it. delete group.key; } } - return promise.then(() => { - return group; - }); + return group; } } @@ -551,71 +525,57 @@ export class CoreH5PContentValidator { * @param semantics Semantics. * @return Promise resolved when done. */ - validateLibrary(value: any, semantics: any): Promise { + async validateLibrary(value: any, semantics: any): Promise { if (!value.library) { - return Promise.resolve(); + return; } - let promise; - 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) - .then((library) => { - this.libraries[value.library] = library; - - return library; - }); - } else { - promise = Promise.resolve(this.libraries[value.library]); + this.libraries[value.library] = await CoreH5P.instance.h5pCore.loadLibrary(libSpec.machineName, libSpec.majorVersion, + libSpec.minorVersion, this.siteId); } - return promise.then((library) => { - // Validate parameters. - return this.validateGroup(value.params, {type: 'group', fields: library.semantics}, false).then((validated) => { + const library = this.libraries[value.library]; - value.params = validated; + // Validate parameters. + value.params = await this.validateGroup(value.params, {type: 'group', fields: library.semantics}, false); - // Validate subcontent's metadata - if (value.metadata) { - return this.validateMetadata(value.metadata).then((res) => { - value.metadata = res; - }); - } - }).then(() => { + // Validate subcontent's metadata + if (value.metadata) { + value.metadata = await this.validateMetadata(value.metadata); + } - let validKeys = ['library', 'params', 'subContentId', 'metadata']; - if (semantics.extraAttributes) { - validKeys = this.utils.uniqueArray(validKeys.concat(semantics.extraAttributes)); - } + let validKeys = ['library', 'params', 'subContentId', 'metadata']; + if (semantics.extraAttributes) { + validKeys = CoreUtils.instance.uniqueArray(validKeys.concat(semantics.extraAttributes)); + } - this.filterParams(value, validKeys); + this.filterParams(value, validKeys); - if (value.subContentId && - !value.subContentId.match(/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/)) { - delete value.subContentId; - } + if (value.subContentId && + !value.subContentId.match(/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/)) { + delete value.subContentId; + } - // Find all dependencies for this library. - const depKey = 'preloaded-' + library.machineName; - if (!this.dependencies[depKey]) { - this.dependencies[depKey] = { - library: library, - type: 'preloaded' - }; + // Find all dependencies for this library. + const depKey = 'preloaded-' + library.machineName; + if (!this.dependencies[depKey]) { + this.dependencies[depKey] = { + library: library, + type: 'preloaded' + }; - return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => { - this.nextWeight = weight; - this.dependencies[depKey].weight = this.nextWeight++; + this.nextWeight = await CoreH5P.instance.h5pCore.findLibraryDependencies(this.dependencies, library, this.nextWeight); - return value; - }); - } else { - return value; - } - }); - }); + this.dependencies[depKey].weight = this.nextWeight++; + + return value; + } else { + return value; + } } /** @@ -689,7 +649,7 @@ export class CoreH5PContentValidator { protected filterXssSplit(m: string[], store: boolean = false): string { if (store) { - this.allowedHtml = this.utils.arrayToObject(m); + this.allowedHtml = CoreUtils.instance.arrayToObject(m); return ''; } @@ -710,9 +670,9 @@ export class CoreH5PContentValidator { return ''; } - const slash = matches[1] ? matches[1].trim() : '', - attrList = matches[3] || '', - comment = matches[4] || ''; + const slash = matches[1] ? matches[1].trim() : ''; + const attrList = matches[3] || ''; + const comment = matches[4] || ''; let elem = matches[2] || ''; if (comment) { @@ -733,8 +693,8 @@ export class CoreH5PContentValidator { } // Is there a closing XHTML slash at the end of the attributes? - const newAttrList = attrList.replace(/(\s?)\/\s*$/g, '$1'), - xhtmlSlash = attrList != newAttrList ? ' /' : ''; + const newAttrList = attrList.replace(/(\s?)\/\s*$/g, '$1'); + const xhtmlSlash = attrList != newAttrList ? ' /' : ''; // Clean up attributes. let attr2 = this.filterXssAttributes(newAttrList, @@ -760,9 +720,9 @@ export class CoreH5PContentValidator { while (attr.length != 0) { // Was the last operation successful? - let working = 0, - matches, - thisVal; + let working = 0; + let matches; + let thisVal; switch (mode) { case 0: @@ -877,10 +837,10 @@ export class CoreH5PContentValidator { filterXssBadProtocol(str: string, decode: boolean = true): string { // Get the plain text representation of the attribute value (i.e. its meaning). 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)); } /** @@ -892,11 +852,11 @@ export class CoreH5PContentValidator { protected stripDangerousProtocols(uri: string): string { const allowedProtocols = { - ftp: true, - http: true, - https: true, - mailto: true - }; + ftp: true, + http: true, + https: true, + mailto: true + }; let before; // Iteratively remove any invalid protocol found. @@ -939,92 +899,92 @@ export class CoreH5PContentValidator { { name: 'title', type: 'text', - label: this.translate.instant('core.h5p.title'), + label: Translate.instance.instant('core.h5p.title'), placeholder: 'La Gioconda' }, { name: 'license', type: 'select', - label: this.translate.instant('core.h5p.license'), + label: Translate.instance.instant('core.h5p.license'), default: 'U', options: [ { value: 'U', - label: this.translate.instant('core.h5p.undisclosed') + label: Translate.instance.instant('core.h5p.undisclosed') }, { type: 'optgroup', - label: this.translate.instant('core.h5p.creativecommons'), + label: Translate.instance.instant('core.h5p.creativecommons'), options: [ { value: 'CC BY', - label: this.translate.instant('core.h5p.ccattribution'), + label: Translate.instance.instant('core.h5p.ccattribution'), versions: ccVersions }, { value: 'CC BY-SA', - label: this.translate.instant('core.h5p.ccattributionsa'), + label: Translate.instance.instant('core.h5p.ccattributionsa'), versions: ccVersions }, { value: 'CC BY-ND', - label: this.translate.instant('core.h5p.ccattributionnd'), + label: Translate.instance.instant('core.h5p.ccattributionnd'), versions: ccVersions }, { value: 'CC BY-NC', - label: this.translate.instant('core.h5p.ccattributionnc'), + label: Translate.instance.instant('core.h5p.ccattributionnc'), versions: ccVersions }, { value: 'CC BY-NC-SA', - label: this.translate.instant('core.h5p.ccattributionncsa'), + label: Translate.instance.instant('core.h5p.ccattributionncsa'), versions: ccVersions }, { value: 'CC BY-NC-ND', - label: this.translate.instant('core.h5p.ccattributionncnd'), + label: Translate.instance.instant('core.h5p.ccattributionncnd'), versions: ccVersions }, { value: 'CC0 1.0', - label: this.translate.instant('core.h5p.ccpdd') + label: Translate.instance.instant('core.h5p.ccpdd') }, { value: 'CC PDM', - label: this.translate.instant('core.h5p.pdm') + label: Translate.instance.instant('core.h5p.pdm') }, ] }, { value: 'GNU GPL', - label: this.translate.instant('core.h5p.gpl') + label: Translate.instance.instant('core.h5p.gpl') }, { value: 'PD', - label: this.translate.instant('core.h5p.pd') + label: Translate.instance.instant('core.h5p.pd') }, { value: 'ODC PDDL', - label: this.translate.instant('core.h5p.pddl') + label: Translate.instance.instant('core.h5p.pddl') }, { value: 'C', - label: this.translate.instant('core.h5p.copyrightstring') + label: Translate.instance.instant('core.h5p.copyrightstring') } ] }, { name: 'licenseVersion', type: 'select', - label: this.translate.instant('core.h5p.licenseversion'), + label: Translate.instance.instant('core.h5p.licenseversion'), options: ccVersions, optional: true }, { name: 'yearFrom', type: 'number', - label: this.translate.instant('core.h5p.yearsfrom'), + label: Translate.instance.instant('core.h5p.yearsfrom'), placeholder: '1991', min: '-9999', max: '9999', @@ -1033,7 +993,7 @@ export class CoreH5PContentValidator { { name: 'yearTo', type: 'number', - label: this.translate.instant('core.h5p.yearsto'), + label: Translate.instance.instant('core.h5p.yearsto'), placeholder: '1992', min: '-9999', max: '9999', @@ -1042,7 +1002,7 @@ export class CoreH5PContentValidator { { name: 'source', type: 'text', - label: this.translate.instant('core.h5p.source'), + label: Translate.instance.instant('core.h5p.source'), placeholder: 'https://', optional: true }, @@ -1054,7 +1014,7 @@ export class CoreH5PContentValidator { type: 'group', fields: [ { - label: this.translate.instant('core.h5p.authorname'), + label: Translate.instance.instant('core.h5p.authorname'), name: 'name', optional: true, type: 'text' @@ -1062,24 +1022,24 @@ export class CoreH5PContentValidator { { name: 'role', type: 'select', - label: this.translate.instant('core.h5p.authorrole'), + label: Translate.instance.instant('core.h5p.authorrole'), default: 'Author', options: [ { value: 'Author', - label: this.translate.instant('core.h5p.author') + label: Translate.instance.instant('core.h5p.author') }, { value: 'Editor', - label: this.translate.instant('core.h5p.editor') + label: Translate.instance.instant('core.h5p.editor') }, { value: 'Licensee', - label: this.translate.instant('core.h5p.licensee') + label: Translate.instance.instant('core.h5p.licensee') }, { 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', type: 'text', widget: 'textarea', - label: this.translate.instant('core.h5p.licenseextras'), + label: Translate.instance.instant('core.h5p.licenseextras'), optional: true, - description: this.translate.instant('core.h5p.additionallicenseinfo') + description: Translate.instance.instant('core.h5p.additionallicenseinfo') }, { name: 'changes', @@ -1100,26 +1060,26 @@ export class CoreH5PContentValidator { field: { name: 'change', type: 'group', - label: this.translate.instant('core.h5p.changelog'), + label: Translate.instance.instant('core.h5p.changelog'), fields: [ { name: 'date', type: 'text', - label: this.translate.instant('core.h5p.date'), + label: Translate.instance.instant('core.h5p.date'), optional: true }, { name: 'author', type: 'text', - label: this.translate.instant('core.h5p.changedby'), + label: Translate.instance.instant('core.h5p.changedby'), optional: true }, { name: 'log', type: 'text', widget: 'textarea', - label: this.translate.instant('core.h5p.changedescription'), - placeholder: this.translate.instant('core.h5p.changeplaceholder'), + label: Translate.instance.instant('core.h5p.changedescription'), + placeholder: Translate.instance.instant('core.h5p.changeplaceholder'), optional: true } ] @@ -1129,8 +1089,8 @@ export class CoreH5PContentValidator { name: 'authorComments', type: 'text', widget: 'textarea', - label: this.translate.instant('core.h5p.authorcomments'), - description: this.translate.instant('core.h5p.authorcommentsdescription'), + label: Translate.instance.instant('core.h5p.authorcomments'), + description: Translate.instance.instant('core.h5p.authorcommentsdescription'), optional: true }, { @@ -1164,33 +1124,33 @@ export class CoreH5PContentValidator { this.copyrightSemantics = { name: 'copyright', type: 'group', - label: this.translate.instant('core.h5p.copyrightinfo'), + label: Translate.instance.instant('core.h5p.copyrightinfo'), fields: [ { name: 'title', type: 'text', - label: this.translate.instant('core.h5p.title'), + label: Translate.instance.instant('core.h5p.title'), placeholder: 'La Gioconda', optional: true }, { name: 'author', type: 'text', - label: this.translate.instant('core.h5p.author'), + label: Translate.instance.instant('core.h5p.author'), placeholder: 'Leonardo da Vinci', optional: true }, { name: 'year', type: 'text', - label: this.translate.instant('core.h5p.years'), + label: Translate.instance.instant('core.h5p.years'), placeholder: '1503 - 1517', optional: true }, { name: 'source', type: 'text', - label: this.translate.instant('core.h5p.source'), + label: Translate.instance.instant('core.h5p.source'), placeholder: 'http://en.wikipedia.org/wiki/Mona_Lisa', optional: true, regexp: { @@ -1201,64 +1161,64 @@ export class CoreH5PContentValidator { { name: 'license', type: 'select', - label: this.translate.instant('core.h5p.license'), + label: Translate.instance.instant('core.h5p.license'), default: 'U', options: [ { value: 'U', - label: this.translate.instant('core.h5p.undisclosed') + label: Translate.instance.instant('core.h5p.undisclosed') }, { value: 'CC BY', - label: this.translate.instant('core.h5p.ccattribution'), + label: Translate.instance.instant('core.h5p.ccattribution'), versions: ccVersions }, { value: 'CC BY-SA', - label: this.translate.instant('core.h5p.ccattributionsa'), + label: Translate.instance.instant('core.h5p.ccattributionsa'), versions: ccVersions }, { value: 'CC BY-ND', - label: this.translate.instant('core.h5p.ccattributionnd'), + label: Translate.instance.instant('core.h5p.ccattributionnd'), versions: ccVersions }, { value: 'CC BY-NC', - label: this.translate.instant('core.h5p.ccattributionnc'), + label: Translate.instance.instant('core.h5p.ccattributionnc'), versions: ccVersions }, { value: 'CC BY-NC-SA', - label: this.translate.instant('core.h5p.ccattributionncsa'), + label: Translate.instance.instant('core.h5p.ccattributionncsa'), versions: ccVersions }, { value: 'CC BY-NC-ND', - label: this.translate.instant('core.h5p.ccattributionncnd'), + label: Translate.instance.instant('core.h5p.ccattributionncnd'), versions: ccVersions }, { value: 'GNU GPL', - label: this.translate.instant('core.h5p.licenseGPL'), + label: Translate.instance.instant('core.h5p.licenseGPL'), versions: [ { value: 'v3', - label: this.translate.instant('core.h5p.licenseV3') + label: Translate.instance.instant('core.h5p.licenseV3') }, { value: 'v2', - label: this.translate.instant('core.h5p.licenseV2') + label: Translate.instance.instant('core.h5p.licenseV2') }, { value: 'v1', - label: this.translate.instant('core.h5p.licenseV1') + label: Translate.instance.instant('core.h5p.licenseV1') } ] }, { value: 'PD', - label: this.translate.instant('core.h5p.pd'), + label: Translate.instance.instant('core.h5p.pd'), versions: [ { value: '-', @@ -1266,24 +1226,24 @@ export class CoreH5PContentValidator { }, { value: 'CC0 1.0', - label: this.translate.instant('core.h5p.licenseCC010U') + label: Translate.instance.instant('core.h5p.licenseCC010U') }, { value: 'CC PDM', - label: this.translate.instant('core.h5p.pdm') + label: Translate.instance.instant('core.h5p.pdm') } ] }, { value: 'C', - label: this.translate.instant('core.h5p.copyrightstring') + label: Translate.instance.instant('core.h5p.copyrightstring') } ] }, { name: 'version', type: 'select', - label: this.translate.instant('core.h5p.licenseversion'), + label: Translate.instance.instant('core.h5p.licenseversion'), options: [] } ] @@ -1301,23 +1261,23 @@ export class CoreH5PContentValidator { return [ { value: '4.0', - label: this.translate.instant('core.h5p.licenseCC40') + label: Translate.instance.instant('core.h5p.licenseCC40') }, { value: '3.0', - label: this.translate.instant('core.h5p.licenseCC30') + label: Translate.instance.instant('core.h5p.licenseCC30') }, { value: '2.5', - label: this.translate.instant('core.h5p.licenseCC25') + label: Translate.instance.instant('core.h5p.licenseCC25') }, { value: '2.0', - label: this.translate.instant('core.h5p.licenseCC20') + label: Translate.instance.instant('core.h5p.licenseCC20') }, { value: '1.0', - label: this.translate.instant('core.h5p.licenseCC10') + label: Translate.instance.instant('core.h5p.licenseCC10') } ]; } diff --git a/src/core/h5p/classes/core.ts b/src/core/h5p/classes/core.ts new file mode 100644 index 000000000..20984c8b2 --- /dev/null +++ b/src/core/h5p/classes/core.ts @@ -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 { + 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 { + + 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 { + + // 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 = ( dependency.preloadedJs).split(','); + dependency.preloadedCss = ( 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 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 { + 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 { + 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 { + + 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. +}; diff --git a/src/core/h5p/classes/file-storage.ts b/src/core/h5p/classes/file-storage.ts new file mode 100644 index 000000000..42b39f175 --- /dev/null +++ b/src/core/h5p/classes/file-storage.ts @@ -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 { + + 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 { + 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 { + + 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 { + 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 { + 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 { + + 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 { + 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 { + + 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 { + 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 { + 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 { + 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 { + 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); + } +} diff --git a/src/core/h5p/classes/framework.ts b/src/core/h5p/classes/framework.ts new file mode 100644 index 000000000..c8cfd0ea1 --- /dev/null +++ b/src/core/h5p/classes/framework.ts @@ -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 { + + 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 { + + 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 { + + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + + 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 { + + 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 { + 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 { + 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 { + + 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 { + 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 { + 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 { + + 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 { + 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 { + + // 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 { + // 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 { + + 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 { + // 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 { + + 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 { + + const db = await CoreSites.instance.getSiteDb(siteId); + + // Calculate the CSS to drop. + const dropLibraryCssList = {}; + + for (const key in librariesInUse) { + const dependency = librariesInUse[key]; + + if (( dependency.library).dropLibraryCss) { + const split = ( dependency.library).dropLibraryCss.split(', '); + + split.forEach((css) => { + dropLibraryCssList[css] = css; + }); + } + } + + // Now save the uusage. + await Promise.all(Object.keys(librariesInUse).map((key) => { + const dependency = librariesInUse[key]; + const data = { + 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 { + + 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 { + + 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. +}; diff --git a/src/core/h5p/classes/helper.ts b/src/core/h5p/classes/helper.ts new file mode 100644 index 000000000..dd6484cf2 --- /dev/null +++ b/src/core/h5p/classes/helper.ts @@ -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 { + + 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 { + 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. + } + } + } +} diff --git a/src/core/h5p/classes/metadata.ts b/src/core/h5p/classes/metadata.ts new file mode 100644 index 000000000..6615e8c7a --- /dev/null +++ b/src/core/h5p/classes/metadata.ts @@ -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); + } + +} diff --git a/src/core/h5p/classes/player.ts b/src/core/h5p/classes/player.ts new file mode 100644 index 000000000..c4f461ef0 --- /dev/null +++ b/src/core/h5p/classes/player.ts @@ -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 { + + 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 = '' + content.title + '' + + ''; + + // Include the required CSS. + result.cssRequires.forEach((cssUrl) => { + html += ''; + }); + + // Add the settings. + html += ''; + + // Add our own script to handle the display options. + html += ''; + + html += ''; + + // 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 += ''; + + result.jsRequires.forEach((jsUrl) => { + html += ''; + }); + + html += '
' + + '' + + '
'; + + 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 { + 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 { + 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 { + 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 { + 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 { + + 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 ''; + } + + /** + * 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 ''; + } + + /** + * Get the URL to the resizer script. + * + * @return URL. + */ + getResizerScriptUrl(): string { + return CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'js/h5p-resizer.js'); + } +} diff --git a/src/core/h5p/classes/storage.ts b/src/core/h5p/classes/storage.ts new file mode 100644 index 000000000..56da67fb7 --- /dev/null +++ b/src/core/h5p/classes/storage.ts @@ -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 { + 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 { + + 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; + } +} diff --git a/src/core/h5p/classes/validator.ts b/src/core/h5p/classes/validator.ts new file mode 100644 index 000000000..ced64f99a --- /dev/null +++ b/src/core/h5p/classes/validator.ts @@ -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 { + + // 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( 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 { + 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 { + + // 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 { + 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 { + 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 { + 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 { + 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. +}; diff --git a/src/core/h5p/components/h5p-player/h5p-player.ts b/src/core/h5p/components/h5p-player/h5p-player.ts index 25153b69c..bdace5d86 100644 --- a/src/core/h5p/components/h5p-player/h5p-player.ts +++ b/src/core/h5p/components/h5p-player/h5p-player.ts @@ -13,21 +13,21 @@ // limitations under the License. import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges, SimpleChange } from '@angular/core'; -import { CoreAppProvider } from '@providers/app'; -import { CoreEventsProvider } from '@providers/events'; -import { CoreFileProvider } from '@providers/file'; -import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider } from '@providers/sites'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreUrlUtilsProvider } from '@providers/utils/url'; -import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreH5PProvider } from '@core/h5p/providers/h5p'; +import { CoreApp } from '@providers/app'; +import { CoreEvents } from '@providers/events'; +import { CoreFile } from '@providers/file'; +import { CoreFilepool } from '@providers/filepool'; +import { CoreLogger } from '@providers/logger'; +import { CoreSites } from '@providers/sites'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreUrlUtils } from '@providers/utils/url'; +import { CoreH5P } from '@core/h5p/providers/h5p'; 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 { CoreSite } from '@classes/site'; +import { CoreH5PCore } from '../../classes/core'; +import { CoreH5PHelper } from '../../classes/helper'; /** * Component to render an H5P package. @@ -55,26 +55,13 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { protected urlParams; protected logger; - constructor(loggerProvider: CoreLoggerProvider, - public elementRef: ElementRef, - 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) { + constructor(public elementRef: ElementRef, + protected pluginFileDelegate: CorePluginFileDelegate) { - this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent'); - this.site = sitesProvider.getCurrentSite(); + this.logger = CoreLogger.instance.getInstance('CoreH5PPlayerComponent'); + this.site = CoreSites.instance.getCurrentSite(); this.siteId = this.site.getId(); - this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles() && - !this.h5pProvider.isOfflineDisabledInSite(); + this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite(); } /** @@ -99,90 +86,102 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { * * @param e Event. */ - play(e: MouseEvent): void { + async play(e: MouseEvent): Promise { e.preventDefault(); e.stopPropagation(); 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. - 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. - return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.urlParams.url).then((path) => { - return this.fileProvider.getFile(path); - }).then((file) => { - return this.h5pProvider.extractH5PFile(this.urlParams.url, file, this.siteId); - }).then(() => { + try { + const path = await CoreFilepool.instance.getInternalUrlByUrl(this.siteId, this.urlParams.url); + + const file = await CoreFile.instance.getFile(path); + + await CoreH5PHelper.saveH5P(this.urlParams.url, file, this.siteId); + // File treated. Try to get the index file URL again. - return this.h5pProvider.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId); - }); - }).catch((error) => { - // Still failing. Delete the H5P package? - this.logger.error('Error loading downloaded index:', error, this.src); - }); - } else { - promise = Promise.resolve(); + localUrl = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.urlParams.url, this.urlParams, + this.siteId); + } catch (error) { + // Still failing. Delete the H5P package? + this.logger.error('Error loading downloaded index:', error, this.src); + } + } } - promise.then((url) => { - if (url) { + try { + if (localUrl) { // Local package. - this.playerSrc = url; + this.playerSrc = localUrl; } else { // 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', - CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=0'); + const src = this.src && this.src.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1', + CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=0'); // Get auto-login URL so the user is automatically authenticated. - return this.sitesProvider.getCurrentSite().getAutoLoginUrl(src, false).then((url) => { - // Add the preventredirect param so the user can authenticate. - this.playerSrc = this.urlUtils.addParamsToUrl(url, {preventredirect: false}); - }); + const url = await CoreSites.instance.getCurrentSite().getAutoLoginUrl(src, false); + + // Add the preventredirect param so the user can authenticate. + this.playerSrc = CoreUrlUtils.instance.addParamsToUrl(url, {preventredirect: false}); } - }).finally(() => { + } finally { + this.addResizerScript(); this.loading = false; this.showPackage = true; if (this.canDownload && (this.state == CoreConstants.OUTDATED || this.state == CoreConstants.NOT_DOWNLOADED)) { // 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); - }); + } } - }); + } } /** * Download the package. + * + * @return Promise resolved when done. */ - download(e: Event): void { + async download(e: Event): Promise { e && e.preventDefault(); e && e.stopPropagation(); - if (!this.appProvider.isOnline()) { - this.domUtils.showErrorModal('core.networkerrormsg', true); + if (!CoreApp.instance.isOnline()) { + CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); return; } - // Get the file size and ask the user to confirm. - this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId).then((size) => { - return this.domUtils.confirmDownloadSize({ size: size, total: true }).then(() => { + try { + // Get the file size and ask the user to confirm. + const size = await this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId); - // User confirmed, add to the queue. - return this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId); - }, () => { - // User cancelled. - }); - }).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + await CoreDomUtils.instance.confirmDownloadSize({ size: size, total: true }); + + // User confirmed, add to the queue. + await CoreFilepool.instance.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId); + + } catch (error) { + if (CoreDomUtils.instance.isCanceledError(error)) { + // User cancelled, stop. + return; + } + + CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); this.calculateState(); - }); + } } /** @@ -190,21 +189,18 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { * * @return Promise resolved when done. */ - protected attemptDownloadInBg(): Promise { - if (this.urlParams && this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() && - this.appProvider.isOnline()) { + protected async attemptDownloadInBg(): Promise { + if (this.urlParams && this.src && this.siteCanDownload && CoreH5P.instance.canGetTrustedH5PFileInSite() && + CoreApp.instance.isOnline()) { // 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)) { - // Download the file in background. - this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId); - } - }); + if (CoreFilepool.instance.shouldDownload(size)) { + // Download the file in background. + 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'); script.id = 'core-h5p-resizer-script'; script.type = 'text/javascript'; - script.src = this.h5pProvider.getResizerScriptUrl(); + script.src = CoreH5P.instance.h5pPlayer.getResizerScriptUrl(); document.head.appendChild(script); } /** * Check if the package can be downloaded. + * + * @return Promise resolved when done. */ - protected checkCanDownload(): void { + protected async checkCanDownload(): Promise { 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(); // Listen for changes in the state. - this.filepoolProvider.getFileEventNameByUrl(this.siteId, this.urlParams.url).then((eventName) => { - this.observer = this.eventsProvider.on(eventName, () => { + try { + const eventName = await CoreFilepool.instance.getFileEventNameByUrl(this.siteId, this.urlParams.url); + + this.observer = CoreEvents.instance.on(eventName, () => { this.calculateState(); }); - }).catch(() => { + } catch (error) { // An error probably means the file cannot be downloaded or we cannot check it (offline). - }); + } } else { this.calculating = false; @@ -254,19 +254,22 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy { * Calculate state of the file. * * @param fileUrl The H5P file URL. + * @return Promise resolved when done. */ - protected calculateState(): void { + protected async calculateState(): Promise { this.calculating = true; // 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.state = state; - }).catch((error) => { + } catch (error) { this.canDownload = false; - }).finally(() => { + } finally { this.calculating = false; - }); + } } /** diff --git a/src/core/h5p/h5p.module.ts b/src/core/h5p/h5p.module.ts index 95874d33d..834278fc5 100644 --- a/src/core/h5p/h5p.module.ts +++ b/src/core/h5p/h5p.module.ts @@ -15,14 +15,12 @@ import { NgModule } from '@angular/core'; import { CoreH5PComponentsModule } from './components/components.module'; import { CoreH5PProvider } from './providers/h5p'; -import { CoreH5PUtilsProvider } from './providers/utils'; import { CoreH5PPluginFileHandler } from './providers/pluginfile-handler'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; // List of providers (without handlers). export const CORE_H5P_PROVIDERS: any[] = [ CoreH5PProvider, - CoreH5PUtilsProvider ]; @NgModule({ @@ -32,7 +30,6 @@ export const CORE_H5P_PROVIDERS: any[] = [ ], providers: [ CoreH5PProvider, - CoreH5PUtilsProvider, CoreH5PPluginFileHandler ], exports: [] diff --git a/src/core/h5p/providers/h5p.ts b/src/core/h5p/providers/h5p.ts index 821509cda..bc35ac91a 100644 --- a/src/core/h5p/providers/h5p.ts +++ b/src/core/h5p/providers/h5p.ts @@ -13,20 +13,19 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreFileProvider } from '@providers/file'; -import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreLogger } from '@providers/logger'; +import { CoreSites, CoreSiteSchema } from '@providers/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; -import { CoreUrlUtilsProvider } from '@providers/utils/url'; -import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreH5PUtilsProvider } from './utils'; -import { CoreH5PContentValidator } from '../classes/content-validator'; -import { TranslateService } from '@ngx-translate/core'; -import { FileEntry } from '@ionic-native/file'; +import { CoreTextUtils } from '@providers/utils/text'; +import { CoreUrlUtils } from '@providers/utils/url'; + +import { CoreH5PCore } from '../classes/core'; +import { CoreH5PFramework } from '../classes/framework'; +import { CoreH5PPlayer } from '../classes/player'; +import { CoreH5PStorage } from '../classes/storage'; +import { CoreH5PValidator } from '../classes/validator'; + import { makeSingleton } from '@singletons/core.singletons'; /** @@ -35,59 +34,32 @@ import { makeSingleton } from '@singletons/core.singletons'; @Injectable() export class CoreH5PProvider { - 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', - ]; + // DB table names. + static CONTENT_TABLE = 'h5p_content'; // H5P content. + static LIBRARIES_TABLE = 'h5p_libraries'; // Installed libraries. + static LIBRARY_DEPENDENCIES_TABLE = 'h5p_library_dependencies'; // Library dependencies. + static CONTENTS_LIBRARIES_TABLE = 'h5p_contents_libraries'; // Which library is used in which content. + static LIBRARIES_CACHEDASSETS_TABLE = 'h5p_libraries_cachedassets'; // H5P cached library assets. - // 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'; - - protected CONTENT_TABLE = 'h5p_content'; // H5P content. - protected LIBRARIES_TABLE = 'h5p_libraries'; // Installed libraries. - protected LIBRARY_DEPENDENCIES_TABLE = 'h5p_library_dependencies'; // Library dependencies. - protected CONTENTS_LIBRARIES_TABLE = 'h5p_contents_libraries'; // Which library is used in which content. - protected LIBRARIES_CACHEDASSETS_TABLE = 'h5p_libraries_cachedassets'; // H5P cached library assets. - protected aggregateAssets = true; // Save all the assets from one package into a single file. + h5pCore: CoreH5PCore; + h5pFramework: CoreH5PFramework; + h5pPlayer: CoreH5PPlayer; + h5pStorage: CoreH5PStorage; + h5pValidator: CoreH5PValidator; protected siteSchema: CoreSiteSchema = { name: 'CoreH5PProvider', version: 1, canBeCleared: [ - this.CONTENT_TABLE, this.LIBRARIES_TABLE, this.LIBRARY_DEPENDENCIES_TABLE, this.CONTENTS_LIBRARIES_TABLE, - this.LIBRARIES_CACHEDASSETS_TABLE + CoreH5PProvider.CONTENT_TABLE, + CoreH5PProvider.LIBRARIES_TABLE, + CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE, + CoreH5PProvider.CONTENTS_LIBRARIES_TABLE, + CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, ], tables: [ { - name: this.CONTENT_TABLE, + name: CoreH5PProvider.CONTENT_TABLE, columns: [ { name: 'id', @@ -132,7 +104,7 @@ export class CoreH5PProvider { ] }, { - name: this.LIBRARIES_TABLE, + name: CoreH5PProvider.LIBRARIES_TABLE, columns: [ { name: 'id', @@ -203,7 +175,7 @@ export class CoreH5PProvider { ] }, { - name: this.LIBRARY_DEPENDENCIES_TABLE, + name: CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE, columns: [ { name: 'id', @@ -229,7 +201,7 @@ export class CoreH5PProvider { ] }, { - name: this.CONTENTS_LIBRARIES_TABLE, + name: CoreH5PProvider.CONTENTS_LIBRARIES_TABLE, columns: [ { name: 'id', @@ -265,7 +237,7 @@ export class CoreH5PProvider { ] }, { - name: this.LIBRARIES_CACHEDASSETS_TABLE, + name: CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, columns: [ { name: 'id', @@ -294,65 +266,19 @@ export class CoreH5PProvider { }; protected ROOT_CACHE_KEY = 'CoreH5P:'; - protected logger; - constructor(logger: CoreLoggerProvider, - private sitesProvider: CoreSitesProvider, - private textUtils: CoreTextUtilsProvider, - private fileProvider: CoreFileProvider, - private mimeUtils: CoreMimetypeUtilsProvider, - private h5pUtils: CoreH5PUtilsProvider, - private filepoolProvider: CoreFilepoolProvider, - private utils: CoreUtilsProvider, - private urlUtils: CoreUrlUtilsProvider, - private translate: TranslateService) { + constructor() { - this.logger = logger.getInstance('CoreH5PProvider'); + this.logger = CoreLogger.instance.getInstance('CoreH5PProvider'); - this.sitesProvider.registerSiteSchema(this.siteSchema); - } + CoreSites.instance.registerSiteSchema(this.siteSchema); - /** - * 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. - */ - protected cacheAssets(files: {scripts: CoreH5PDependencyAsset[], styles: CoreH5PDependencyAsset[]}, key: string, - folderName: string, siteId: string): Promise { - - const promises = []; - - for (const type in files) { - const assets: CoreH5PDependencyAsset[] = files[type]; - - if (!assets || !assets.length) { - continue; - } - - // Create new file for cached assets. - const fileName = key + '.' + (type == 'scripts' ? 'js' : 'css'), - path = this.textUtils.concatenatePaths(this.getCachedAssetsFolderPath(folderName, siteId), fileName); - - // Store concatenated content. - promises.push(this.concatenateFiles(assets, type).then((content) => { - return this.fileProvider.writeFile(path, content); - }).then(() => { - // Now update the files data. - files[type] = [ - { - path: this.textUtils.concatenatePaths(this.getCachedAssetsFolderName(), fileName), - version: '' - } - ]; - })); - } - - return Promise.all(promises); + this.h5pValidator = new CoreH5PValidator(); + this.h5pFramework = new CoreH5PFramework(); + this.h5pCore = new CoreH5PCore(this.h5pFramework); + this.h5pStorage = new CoreH5PStorage(this.h5pCore, this.h5pFramework); + this.h5pPlayer = new CoreH5PPlayer(this.h5pCore, this.h5pStorage); } /** @@ -362,10 +288,10 @@ export class CoreH5PProvider { * @return Promise resolved with true if ws is available, false otherwise. * @since 3.8 */ - canGetTrustedH5PFile(siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - return this.canGetTrustedH5PFileInSite(site); - }); + async canGetTrustedH5PFile(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.canGetTrustedH5PFileInSite(site); } /** @@ -376,1440 +302,11 @@ export class CoreH5PProvider { * @since 3.8 */ canGetTrustedH5PFileInSite(site?: CoreSite): boolean { - site = site || this.sitesProvider.getCurrentSite(); + site = site || CoreSites.instance.getCurrentSite(); return site.wsAvailable('core_h5p_get_trusted_h5p_file'); } - /** - * Will clear filtered params for all the content that uses the specified libraries. - * This means that the content dependencies will have to be rebuilt and the parameters re-filtered. - * - * @param libraryIds Array of library ids. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - protected clearFilteredParameters(libraryIds: number[], siteId?: string): Promise { - - if (!libraryIds || !libraryIds.length) { - return Promise.resolve(); - } - - return this.sitesProvider.getSiteDb(siteId).then((db) => { - const whereAndParams = db.getInOrEqual(libraryIds); - whereAndParams[0] = 'mainlibraryid ' + whereAndParams[0]; - - return db.updateRecordsWhere(this.CONTENT_TABLE, { filtered: null }, whereAndParams[0], whereAndParams[1]); - }); - } - - /** - * 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 concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise { - const basePath = this.fileProvider.convertFileSrc(this.fileProvider.getBasePathInstant()); - let content = '', - promise = Promise.resolve(); // Use a chain of promises so the order is kept. - - assets.forEach((asset) => { - - promise = promise.then(() => { - return this.fileProvider.readFile(asset.path); - }).then((fileContent: string) => { - if (type == 'scripts') { - content += fileContent + ';\n'; - } else { - // Rewrite relative URLs used inside stylesheets. - const matches = fileContent.match(/url\([\'"]?([^"\')]+)[\'"]?\)/ig), - assetPath = asset.path.replace(/(^\/|\/$)/g, ''), // Path without start/end slashes. - 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(this.textUtils.escapeForRegex(match), 'g'), - 'url("' + this.textUtils.concatenatePaths(basePath, url) + '")'); - }); - } - - content += fileContent + '\n'; - } - }); - }); - - return promise.then(() => { - return content; - }); - } - - /** - * Create the index.html to render an H5P package. - * - * @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. - */ - createContentIndex(id: number, h5pUrl: string, content: CoreH5PContentData, embedType: string, siteId?: string) - : Promise { - - return this.sitesProvider.getSite(siteId).then((site) => { - - const contentId = this.getContentId(id), - basePath = this.fileProvider.getBasePathInstant(), - contentUrl = this.fileProvider.convertFileSrc(this.textUtils.concatenatePaths( - basePath, this.getContentFolderPath(content.folderName, site.getId()))); - - // Create the settings needed for the content. - const contentSettings = { - library: this.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. - return this.getAssets(id, content, embedType, site.getId()).then((result) => { - result.settings.contents[contentId] = Object.assign(result.settings.contents[contentId], contentSettings); - - const indexPath = this.getContentIndexPath(content.folderName, siteId); - let html = '' + content.title + '' + - ''; - - // Include the required CSS. - result.cssRequires.forEach((cssUrl) => { - html += ''; - }); - - // Add the settings. - html += ''; - - // Add our own script to handle the display options. - html += ''; - - html += ''; - - // 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 += ''; - - result.jsRequires.forEach((jsUrl) => { - html += ''; - }); - - html += '
' + - '' + - '
'; - - return this.fileProvider.writeFile(indexPath, html); - }).then((fileEntry) => { - return fileEntry.toURL(); - }); - }); - } - - /** - * Delete all content indexes of all sites from filesystem. - * - * @return Promise resolved when done. - */ - async deleteAllContentIndexes(): Promise { - const siteIds = await this.sitesProvider.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 { - - const site = await this.sitesProvider.getSite(siteId); - - const records = await site.getDb().getAllRecords(this.CONTENT_TABLE); - - const promises = records.map(async (record) => { - try { - await this.fileProvider.removeFile(this.getContentIndexPath(record.foldername, site.getId())); - } catch (err) { - // Ignore errors, maybe the file doesn't exist. - } - }); - - await Promise.all(promises); - } - - /** - * Delete cached assets from DB and filesystem. - * - * @param libraryId Library identifier. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - protected deleteCachedAssets(libraryId: number, siteId?: string): Promise { - - return this.sitesProvider.getSite(siteId).then((site) => { - const db = site.getDb(); - - // Get all the hashes that use this library. - return db.getRecords(this.LIBRARIES_CACHEDASSETS_TABLE, {libraryid: libraryId}).then((entries) => { - // Delete the files with these hashes. - const promises = [], - hashes = []; - - entries.forEach((entry) => { - hashes.push(entry.hash); - - const cachedAssetsFolder = this.getCachedAssetsFolderPath(entry.foldername, site.getId()); - - ['js', 'css'].forEach((type) => { - const path = this.textUtils.concatenatePaths(cachedAssetsFolder, entry.hash + '.' + type); - - promises.push(this.fileProvider.removeFile(path).catch(() => { - // Ignore errors, maybe there's no cached asset of this type. - })); - }); - }); - - return Promise.all(promises).then(() => { - return db.deleteRecordsList(this.LIBRARIES_CACHEDASSETS_TABLE, 'hash', hashes); - }); - }); - }); - } - - /** - * Delete all package content data. - * - * @param fileUrl File URL. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - deleteContentByUrl(fileUrl: string, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - return this.getContentDataByUrl(fileUrl, siteId).then((data) => { - const promises = []; - - promises.push(this.deleteContentData(data.id, siteId)); - - promises.push(this.deleteContentFolder(data.foldername, siteId)); - - return this.utils.allPromises(promises); - }); - } - - /** - * Delete content data from DB. - * - * @param id Content ID. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - deleteContentData(id: number, siteId?: string): Promise { - const promises = []; - - // Delete the content data. - promises.push(this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.deleteRecords(this.CONTENT_TABLE, {id: id}); - })); - - // Remove content library dependencies. - promises.push(this.deleteLibraryUsage(id, siteId)); - - return Promise.all(promises); - } - - /** - * Deletes a content folder from the file system. - * - * @param folderName Folder name of the content. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - deleteContentFolder(folderName: string, siteId?: string): Promise { - return this.fileProvider.removeDir(this.getContentFolderPath(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. - */ - protected deleteContentIndexesForLibrary(libraryId: number, siteId?: string): Promise { - - return this.sitesProvider.getSite(siteId).then((site) => { - const db = site.getDb(); - - // Get the folder names of all the packages that use this library. - const query = 'SELECT DISTINCT hc.foldername ' + - 'FROM ' + this.CONTENTS_LIBRARIES_TABLE + ' hcl ' + - 'JOIN ' + this.CONTENT_TABLE + ' hc ON hcl.h5pid = hc.id ' + - 'WHERE hcl.libraryid = ?', - queryArgs = []; - - queryArgs.push(libraryId); - - return db.execute(query, queryArgs).then((result) => { - const promises = []; - - for (let i = 0; i < result.rows.length; i++) { - const entry = result.rows.item(i); - - // Delete the index.html file. - promises.push(this.fileProvider.removeFile(this.getContentIndexPath(entry.foldername, site.getId())) - .catch(() => { - // Ignore errors. - })); - } - - return Promise.all(promises); - }); - }); - } - - /** - * Delete library data from DB. - * - * @param id Library ID. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - deleteLibraryData(id: number, siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.deleteRecords(this.LIBRARIES_TABLE, {id: id}); - }); - } - - /** - * Delete all dependencies belonging to given library. - * - * @param libraryId Library ID. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - deleteLibraryDependencies(libraryId: number, siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.deleteRecords(this.LIBRARY_DEPENDENCIES_TABLE, {libraryid: libraryId}); - }); - } - - /** - * Deletes a library from the file system. - * - * @param libraryData The library data. - * @param folderName Folder name. If not provided, it will be calculated. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - deleteLibraryFolder(libraryData: any, folderName?: string, siteId?: string): Promise { - return this.fileProvider.removeDir(this.getLibraryFolderPath(libraryData, siteId, folderName)); - } - - /** - * 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. - */ - deleteLibraryUsage(id: number, siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.deleteRecords(this.CONTENTS_LIBRARIES_TABLE, {h5pid: id}); - }); - } - - /** - * Extract an H5P file. Some of this code was copied from the isValidPackage function in Moodle's H5PValidator. - * This function won't validate most things because it should've been done by the server already. - * - * @param fileUrl The file URL used to download the file. - * @param file The file entry of the downloaded file. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - extractH5PFile(fileUrl: string, file: FileEntry, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - // Unzip the file. - const folderName = this.mimeUtils.removeExtension(file.name), - destFolder = this.textUtils.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName); - - // Unzip the file. - return this.fileProvider.unzipFile(file.toURL(), destFolder).then(() => { - // Read the contents of the unzipped dir. - return this.fileProvider.getDirectoryContents(destFolder); - }).then((contents) => { - return this.processH5PFiles(destFolder, contents).then((data) => { - const content: any = {}; - - // Save the libraries that were processed. - return this.saveLibraries(data.librariesJsonData, folderName, siteId).then(() => { - // Now treat contents. - - // Find main library version - for (const i in data.mainJsonData.preloadedDependencies) { - const dependency = data.mainJsonData.preloadedDependencies[i]; - - if (dependency.machineName === data.mainJsonData.mainLibrary) { - return this.getLibraryIdByData(dependency).then((id) => { - dependency.libraryId = id; - content.library = dependency; - }); - } - } - }).then(() => { - // Save the content data in DB. - content.params = JSON.stringify(data.contentJsonData); - - return this.saveContentData(content, folderName, fileUrl, siteId); - }).then(() => { - // Save the content files in their right place. - const contentPath = this.textUtils.concatenatePaths(destFolder, 'content'); - - return this.saveContentInFS(contentPath, folderName, siteId).catch((error) => { - // An error occurred, delete the DB data because the content data has been deleted. - return this.deleteContentData(content.id, siteId).catch(() => { - // Ignore errors. - }).then(() => { - return Promise.reject(error); - }); - }); - }).then(() => { - // Create the content player. - - return this.loadContentData(content.id, undefined, siteId).then((contentData) => { - const embedType = this.h5pUtils.determineEmbedType(contentData.embedType, contentData.library.embedTypes); - - return this.createContentIndex(content.id, fileUrl, contentData, embedType, siteId); - }); - }).finally(() => { - // Remove tmp folder. - return this.fileProvider.removeDir(destFolder).catch(() => { - // Ignore errors, it will be deleted eventually. - }); - }); - }); - }); - } - - /** - * 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. - */ - filterParameters(content: CoreH5PContentData, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - if (content.filtered) { - return Promise.resolve(content.filtered); - } - - if (typeof content.library == 'undefined' || typeof content.params == 'undefined') { - return Promise.resolve(null); - } - - const params = { - library: this.libraryToString(content.library), - params: this.textUtils.parseJSON(content.params, false) - }; - - if (!params.params) { - return null; - } - - const validator = new CoreH5PContentValidator(this, this.h5pUtils, this.textUtils, this.utils, this.translate, siteId); - - // Validate the main library and its dependencies. - return validator.validateLibrary(params, {options: [params.library]}).then(() => { - - // Handle addons. - return this.loadAddons(siteId); - }).then((addons) => { - // Validate addons. Use a chain of promises to calculate the weight properly. - let promise = Promise.resolve(); - - addons.forEach((addon) => { - 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.h5pUtils.textAddonMatches(params.params, type.text.regex)) { - - promise = promise.then(() => { - return validator.addon(addon); - }); - - // An addon shall only be added once. - break; - } - } - } - }); - - return promise; - }).then(() => { - // 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. - return this.deleteLibraryUsage(content.id, siteId).catch(() => { - // Ignore errors. - }).then(() => { - return this.saveLibraryUsage(content.id, content.dependencies, siteId); - }).then(() => { - if (!content.slug) { - content.slug = this.h5pUtils.slugify(content.title); - } - - // Cache. - return this.updateContentFields(content.id, {filtered: paramsStr}, siteId).then(() => { - return paramsStr; - }); - }); - } - - return paramsStr; - }).catch(() => { - 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. - */ - findLibraryDependencies(dependencies: {[key: string]: CoreH5PContentDepsTreeDependency}, - library: CoreH5PLibraryData | CoreH5PLibraryAddonData, nextWeight: number = 1, editor: boolean = false, - siteId?: string): Promise { - - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - let promise = Promise.resolve(); // We need to create a chain of promises to calculate the weight properly. - - ['dynamic', 'preloaded', 'editor'].forEach((type) => { - const property = type + 'Dependencies'; - - if (!library[property]) { - return; // Skip, no such dependencies. - } - - if (type === 'preloaded' && editor) { - // All preloaded dependencies of an editor library is set to editor. - type = 'editor'; - } - - library[property].forEach((dependency: CoreH5PLibraryBasicData) => { - - promise = promise.then(() => { - const dependencyKey = type + '-' + dependency.machineName; - if (dependencies[dependencyKey]) { - return; // Skip, already have this. - } - - // Get the dependency library data and its subdependencies. - return this.loadLibrary(dependency.machineName, dependency.majorVersion, dependency.minorVersion, siteId) - .then((dependencyLibrary) => { - - dependencies[dependencyKey] = { - library: dependencyLibrary, - type: type - }; - - // Get all its subdependencies. - return this.findLibraryDependencies(dependencies, dependencyLibrary, nextWeight, type === 'editor', siteId); - }).then((weight) => { - nextWeight = weight; - dependencies[dependencyKey].weight = nextWeight++; - }); - }); - }); - }); - - return promise.then(() => { - 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[CoreH5PProvider.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.getOption(CoreH5PProvider.DISPLAY_OPTION_EMBED, CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW); - if (embed == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW || embed == CoreH5PDisplayOptionBehaviour.NEVER_SHOW) { - displayOptions[CoreH5PProvider.DISPLAY_OPTION_EMBED] = (embed == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW); - } - - if (!this.getOption(CoreH5PProvider.DISPLAY_OPTION_FRAME, true)) { - displayOptions[CoreH5PProvider.DISPLAY_OPTION_FRAME] = false; - } else { - displayOptions[CoreH5PProvider.DISPLAY_OPTION_EMBED] = this.setDisplayOptionOverrides( - CoreH5PProvider.DISPLAY_OPTION_EMBED, CoreH5PPermission.EMBED_H5P, id, - displayOptions[CoreH5PProvider.DISPLAY_OPTION_EMBED]); - - if (this.getOption(CoreH5PProvider.DISPLAY_OPTION_COPYRIGHT, true) == false) { - displayOptions[CoreH5PProvider.DISPLAY_OPTION_COPYRIGHT] = false; - } - } - - displayOptions[CoreH5PProvider.DISPLAY_OPTION_COPY] = this.hasPermission(CoreH5PPermission.COPY_H5P, id); - - return displayOptions; - } - - /** - * 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 getAssets(id: number, content: CoreH5PContentData, embedType: string, siteId?: string) - : Promise<{settings: any, cssRequires: string[], jsRequires: string[]}> { - - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - const cssRequires = [], - jsRequires = [], - contentId = this.getContentId(id); - let settings; - - return this.getCoreSettings(id, siteId).then((coreSettings) => { - settings = coreSettings; - - settings.core = { - styles: [], - scripts: [] - }; - settings.loadedJs = []; - settings.loadedCss = []; - - const libUrl = this.getCoreH5PPath(); - - // Add core stylesheets. - CoreH5PProvider.STYLES.forEach((style) => { - settings.core.styles.push(libUrl + style); - cssRequires.push(libUrl + style); - }); - - // Add core JavaScript. - this.getScripts().forEach((script) => { - settings.core.scripts.push(script); - jsRequires.push(script); - }); - - /* The filterParameters function should be called before getting the dependency files because it rebuilds content - dependency cache. */ - return this.filterParameters(content, siteId); - }).then((params) => { - settings.contents = settings.contents || {}; - settings.contents[contentId] = settings.contents[contentId] || {}; - settings.contents[contentId].jsonContent = params; - - return this.getContentDependencyFiles(id, content.folderName, siteId); - }).then((files) => { - - // 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.h5pUtils.getAssetsUrls(files.scripts); - settings.contents[contentId].styles = this.h5pUtils.getAssetsUrls(files.styles); - - return { - settings: settings, - cssRequires: cssRequires, - jsRequires: jsRequires - }; - }); - } - - /** - * Will check if there are cache assets available for content. - * - * @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 with the files. - */ - getCachedAssets(key: string, folderName: string, siteId: string) - : Promise<{scripts?: CoreH5PDependencyAsset[], styles?: CoreH5PDependencyAsset[]}> { - - const files: {scripts?: CoreH5PDependencyAsset[], styles?: CoreH5PDependencyAsset[]} = {}, - promises = [], - cachedAssetsName = this.getCachedAssetsFolderName(), - jsPath = this.textUtils.concatenatePaths(cachedAssetsName, key + '.js'), - cssPath = this.textUtils.concatenatePaths(cachedAssetsName, key + '.css'); - let found = false; - - promises.push(this.fileProvider.getFileSize(jsPath).then((size) => { - if (size > 0) { - found = true; - files.scripts = [ - { - path: jsPath, - version: '' - } - ]; - } - }).catch(() => { - // Not found. - })); - - promises.push(this.fileProvider.getFileSize(cssPath).then((size) => { - if (size > 0) { - found = true; - files.styles = [ - { - path: cssPath, - version: '' - } - ]; - } - }).catch(() => { - // Not found. - })); - - return Promise.all(promises).then(() => { - return found ? files : null; - }); - } - - /** - * Get folder name of the content cached assets. - * - * @return Name. - */ - getCachedAssetsFolderName(): string { - return 'cachedassets'; - } - - /** - * 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 this.textUtils.concatenatePaths(this.getContentFolderPath(folderName, siteId), this.getCachedAssetsFolderName()); - } - - /** - * 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 conent data from DB. - * - * @param id Content ID. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved with the content data. - */ - protected getContentData(id: number, siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.getRecord(this.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. - */ - protected getContentDataByUrl(fileUrl: string, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - const db = site.getDb(); - - // Try to use the folder name, it should be more reliable than the URL. - return this.getContentFolderNameByUrl(fileUrl, site.getId()).then((folderName) => { - - return db.getRecord(this.CONTENT_TABLE, {foldername: folderName}); - }, () => { - // Cannot get folder name, the h5p file was probably deleted. Just use the URL. - return db.getRecord(this.CONTENT_TABLE, {fileurl: fileUrl}); - }); - }); - } - - /** - * Get a package content path. - * - * @param folderName Name of the folder of the H5P package. - * @param siteId The site ID. - * @return Folder path. - */ - getContentFolderPath(folderName: string, siteId: string): string { - return this.textUtils.concatenatePaths(this.getExternalH5PFolderPath(siteId), 'packages/' + folderName + '/content'); - } - - /** - * 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. - */ - getContentIndexFileUrl(fileUrl: string, urlParams?: {[name: string]: string}, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - return this.getContentFolderNameByUrl(fileUrl, siteId).then((folderName) => { - return this.fileProvider.getFile(this.getContentIndexPath(folderName, siteId)); - }).then((file) => { - return file.toURL(); - }).then((url) => { - // Add display options to the URL. - return this.getContentDataByUrl(fileUrl, siteId).then((data) => { - const options = this.fixDisplayOptions(this.getDisplayOptionsFromUrlParams(urlParams), data.id); - - return this.urlUtils.addParamsToUrl(url, options, undefined, true); - }); - }); - - } - - /** - * 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 this.textUtils.concatenatePaths(this.getContentFolderPath(folderName, siteId), 'index.html'); - } - - /** - * Get a content folder name given the package URL. - * - * @param fileUrl Package URL. - * @param siteId Site ID. - * @return Promise resolved with the folder name. - */ - getContentFolderNameByUrl(fileUrl: string, siteId: string): Promise { - return this.filepoolProvider.getFilePathByUrl(siteId, fileUrl).then((path) => { - - const fileAndDir = this.fileProvider.getFileAndDirectoryFromPath(path); - - return this.mimeUtils.removeExtension(fileAndDir.name); - }); - } - - /** - * Get the path to the folder that contains the H5P core libraries. - * - * @return Folder path. - */ - getCoreH5PPath(): string { - return this.textUtils.concatenatePaths(this.fileProvider.getWWWPath(), '/h5p/'); - } - - /** - * Get the settings needed by the H5P library. - * - * @param id The H5P content ID. - * @param siteId The site ID. If not defined, current site. - * @return Promise resolved with the settings. - */ - getCoreSettings(id: number, siteId?: string): Promise { - - return this.sitesProvider.getSite(siteId).then((site) => { - - const basePath = this.fileProvider.getBasePathInstant(), - ajaxPaths: any = {}; - ajaxPaths.xAPIResult = ''; - ajaxPaths.contentUserData = ''; - - return { - baseUrl: this.fileProvider.getWWWPath(), - url: this.fileProvider.convertFileSrc(this.textUtils.concatenatePaths( - basePath, this.getExternalH5PFolderPath(site.getId()))), - urlLibraries: this.fileProvider.convertFileSrc(this.textUtils.concatenatePaths( - basePath, this.getLibrariesFolderPath(site.getId()))), - postUserStatistics: false, - ajax: ajaxPaths, - saveFreq: false, - siteUrl: site.getURL(), - l10n: { - H5P: this.h5pUtils.getLocalization() - }, - user: [], - hubIsEnabled: false, - reportingIsEnabled: false, - crossorigin: null, - libraryConfig: null, - pluginCacheBuster: '', - libraryUrl: this.textUtils.concatenatePaths(this.getCoreH5PPath(), 'js') - }; - }); - } - - /** - * 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 getContentDependencyFiles(id: number, folderName: string, siteId?: string) - : Promise<{scripts: CoreH5PDependencyAsset[], styles: CoreH5PDependencyAsset[]}> { - - return this.loadContentDependencies(id, 'preloaded', siteId).then((dependencies) => { - return this.getDependenciesFiles(dependencies, folderName, this.getExternalH5PFolderPath(siteId), siteId); - }); - } - - /** - * 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' && this.utils.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 - }); - } - } - - /** - * 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. - */ - protected getDependenciesFiles(dependencies: {[machineName: string]: CoreH5PContentDependencyData}, folderName: string, - prefix: string = '', siteId?: string): Promise<{scripts: CoreH5PDependencyAsset[], styles: CoreH5PDependencyAsset[]}> { - - // Build files list for assets. - const files = { - scripts: [], - styles: [] - }; - - // Avoid caching empty files. - if (!Object.keys(dependencies).length) { - return Promise.resolve(files); - } - - let promise, - cachedAssetsHash; - - if (this.aggregateAssets) { - // Get aggregated files for assets. - cachedAssetsHash = this.h5pUtils.getDependenciesHash(dependencies); - - promise = this.getCachedAssets(cachedAssetsHash, folderName, siteId); - } else { - promise = Promise.resolve(null); - } - - return promise.then((cachedAssets) => { - 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.getDependencyPath(dependency); - dependency.preloadedJs = ( dependency.preloadedJs).split(','); - dependency.preloadedCss = ( 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. - return this.cacheAssets(files, cachedAssetsHash, folderName, siteId).then(() => { - // Keep track of which libraries have been cached in case they are updated. - return this.saveCachedAssets(cachedAssetsHash, dependencies, folderName, siteId); - }).then(() => { - return files; - }); - } - - return files; - }); - } - - /** - * Get the path to the dependency. - * - * @param dependency Dependency library. - * @return The path to the dependency library - */ - protected getDependencyPath(dependency: CoreH5PContentDependencyData): string { - return 'libraries/' + dependency.machineName + '-' + dependency.majorVersion + '.' + dependency.minorVersion; - } - - /** - * 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. - */ - getDependencyRoots(id: number, siteId?: string): Promise<{[libString: string]: string}> { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - const roots = {}; - - return this.loadContentDependencies(id, undefined, siteId).then((dependencies) => { - - for (const machineName in dependencies) { - const dependency = dependencies[machineName], - folderName = this.libraryToString(dependency, true); - - roots[folderName] = this.getLibraryFolderPath(dependency, siteId, folderName); - } - - return roots; - }); - } - - /** - * 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[CoreH5PProvider.DISPLAY_OPTION_FRAME] = !(disable & CoreH5PProvider.DISABLE_FRAME); - displayOptions[CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD] = !(disable & CoreH5PProvider.DISABLE_DOWNLOAD); - displayOptions[CoreH5PProvider.DISPLAY_OPTION_EMBED] = !(disable & CoreH5PProvider.DISABLE_EMBED); - displayOptions[CoreH5PProvider.DISPLAY_OPTION_COPYRIGHT] = !(disable & CoreH5PProvider.DISABLE_COPYRIGHT); - displayOptions[CoreH5PProvider.DISPLAY_OPTION_ABOUT] = !!this.getOption(CoreH5PProvider.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); - } - - /** - * 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[CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD] = - this.utils.isTrueOrOne(params[CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD]); - displayOptions[CoreH5PProvider.DISPLAY_OPTION_EMBED] = - this.utils.isTrueOrOne(params[CoreH5PProvider.DISPLAY_OPTION_EMBED]); - displayOptions[CoreH5PProvider.DISPLAY_OPTION_COPYRIGHT] = - this.utils.isTrueOrOne(params[CoreH5PProvider.DISPLAY_OPTION_COPYRIGHT]); - displayOptions[CoreH5PProvider.DISPLAY_OPTION_FRAME] = displayOptions[CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD] || - displayOptions[CoreH5PProvider.DISPLAY_OPTION_EMBED] || displayOptions[CoreH5PProvider.DISPLAY_OPTION_COPYRIGHT]; - displayOptions[CoreH5PProvider.DISPLAY_OPTION_ABOUT] = !!this.getOption(CoreH5PProvider.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 ''; - } - - /** - * 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 this.textUtils.concatenatePaths(siteUrl, '/h5p/embed.php') + '?url=' + h5pUrl; - } - - /** - * Get path to the folder containing H5P files extracted from packages. - * - * @param siteId The site ID. - * @return Folder path. - */ - getExternalH5PFolderPath(siteId: string): string { - return this.textUtils.concatenatePaths(this.fileProvider.getSiteFolder(siteId), 'h5p'); - } - - /** - * Get library data. This code is based on the getLibraryData from Moodle's H5PValidator. - * This function won't validate most things because it should've been done by the server already. - * - * @param libDir Directory where the library files are. - * @param libPath Path to the directory where the library files are. - * @param h5pDir Path to the directory where this h5p files are. - * @return Library data. - */ - protected getLibraryData(libDir: DirectoryEntry, libPath: string, h5pDir: string): any { - const libraryJsonPath = this.textUtils.concatenatePaths(libPath, 'library.json'), - semanticsPath = this.textUtils.concatenatePaths(libPath, 'semantics.json'), - langPath = this.textUtils.concatenatePaths(libPath, 'language'), - iconPath = this.textUtils.concatenatePaths(libPath, 'icon.svg'), - promises = []; - let h5pData, - semanticsData, - langData, - hasIcon; - - // Read the library json file. - promises.push(this.fileProvider.readFile(libraryJsonPath, CoreFileProvider.FORMATJSON).then((data) => { - h5pData = data; - })); - - // Get library semantics if it exists. - promises.push(this.fileProvider.readFile(semanticsPath, CoreFileProvider.FORMATJSON).then((data) => { - semanticsData = data; - }).catch(() => { - // Probably doesn't exist, ignore. - })); - - // Get language data if it exists. - promises.push(this.fileProvider.getDirectoryContents(langPath).then((entries) => { - const subPromises = []; - langData = {}; - - entries.forEach((entry) => { - const langFilePath = this.textUtils.concatenatePaths(langPath, entry.name); - - subPromises.push(this.fileProvider.readFile(langFilePath, CoreFileProvider.FORMATJSON).then((data) => { - const parts = entry.name.split('.'); // The language code is in parts[0]. - langData[parts[0]] = data; - })); - }); - }).catch(() => { - // Probably doesn't exist, ignore. - })); - - // Check if it has icon. - promises.push(this.fileProvider.getFile(iconPath).then(() => { - hasIcon = true; - }).catch(() => { - hasIcon = false; - })); - - return Promise.all(promises).then(() => { - h5pData.semantics = semanticsData; - h5pData.language = langData; - h5pData.hasIcon = hasIcon; - - return h5pData; - }); - } - - /** - * Get a library data stored in DB. - * - * @param machineName Machine name. - * @param majorVersion Major version number. - * @param minorVersion Minor version number. - * @param siteId The site ID. If not defined, current site. - * @return Promise resolved with the library data, rejected if not found. - */ - protected getLibrary(machineName: string, majorVersion?: string | number, minorVersion?: string | number, siteId?: string) - : Promise { - - return this.sitesProvider.getSiteDb(siteId).then((db) => { - const conditions: any = { - machinename: machineName - }; - - if (typeof majorVersion != 'undefined') { - conditions.majorversion = majorVersion; - } - if (typeof minorVersion != 'undefined') { - conditions.minorversion = minorVersion; - } - - return db.getRecords(this.LIBRARIES_TABLE, conditions); - }).then((libraries): any => { - if (!libraries.length) { - return Promise.reject(null); - } - - 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. - */ - protected getLibraryByData(libraryData: any, siteId?: string): Promise { - 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. - */ - protected getLibraryById(id: number, siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.getRecord(this.LIBRARIES_TABLE, {id: id}).then((library) => { - 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. - */ - protected getLibraryId(machineName: string, majorVersion?: string | number, minorVersion?: string | number, siteId?: string) - : Promise { - - return this.getLibrary(machineName, majorVersion, minorVersion, siteId).then((library) => { - return (library && library.id) || null; - }).catch(() => { - return null; - }); - } - - /** - * Get a library ID. If not found, return null. - * - * @param libraryData Library data. - * @param siteId The site ID. If not defined, current site. - * @return Promise resolved with the library ID, null if not found. - */ - protected getLibraryIdByData(libraryData: any, siteId?: string): Promise { - return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId); - } - - /** - * Get libraries folder path. - * - * @param siteId The site ID. - * @return Folder path. - */ - getLibrariesFolderPath(siteId: string): string { - return this.textUtils.concatenatePaths(this.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 = this.libraryToString(libraryData, true); - } - - return this.textUtils.concatenatePaths(this.getLibrariesFolderPath(siteId), folderName); - } - - /** - * 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; - } - - /** - * Resizing script for settings. - * - * @return The HTML code with the resize script. - */ - protected getResizeCode(): string { - return ''; - } - - /** - * Get the URL to the resizer script. - * - * @return URL. - */ - getResizerScriptUrl(): string { - return this.textUtils.concatenatePaths(this.getCoreH5PPath(), 'js/h5p-resizer.js'); - } - - /** - * Get core JavaScript files. - * - * @return array The array containg urls of the core JavaScript files: - */ - getScripts(): string[] { - const libUrl = this.getCoreH5PPath(), - urls = []; - - CoreH5PProvider.SCRIPTS.forEach((script) => { - urls.push(libUrl + script); - }); - - return urls; - } - /** * Get a trusted H5P file. * @@ -1819,42 +316,41 @@ export class CoreH5PProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the file data. */ - getTrustedH5PFile(url: string, options?: CoreH5PGetTrustedFileOptions, ignoreCache?: boolean, siteId?: string) + async getTrustedH5PFile(url: string, options?: CoreH5PGetTrustedFileOptions, ignoreCache?: boolean, siteId?: string) : Promise { options = options || {}; - return this.sitesProvider.getSite(siteId).then((site) => { + const site = await CoreSites.instance.getSite(siteId); - const data = { - url: this.treatH5PUrl(url, site.getURL()), - frame: options.frame ? 1 : 0, - export: options.export ? 1 : 0, - embed: options.embed ? 1 : 0, - copyright: options.copyright ? 1 : 0, - }, - preSets: CoreSiteWSPreSets = { - cacheKey: this.getTrustedH5PFileCacheKey(url), - updateFrequency: CoreSite.FREQUENCY_RARELY - }; + const data = { + url: this.treatH5PUrl(url, site.getURL()), + frame: options.frame ? 1 : 0, + export: options.export ? 1 : 0, + embed: options.embed ? 1 : 0, + copyright: options.copyright ? 1 : 0, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getTrustedH5PFileCacheKey(url), + updateFrequency: CoreSite.FREQUENCY_RARELY + }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } - return site.read('core_h5p_get_trusted_h5p_file', data, preSets).then((result: CoreH5PGetTrustedH5PFileResult): any => { - if (result.warnings && result.warnings.length) { - return Promise.reject(result.warnings[0]); - } + const result: CoreH5PGetTrustedH5PFileResult = await site.read('core_h5p_get_trusted_h5p_file', data, preSets); - if (result.files && result.files.length) { - return result.files[0]; - } + if (result.warnings && result.warnings.length) { + throw result.warnings[0]; + } - return Promise.reject(null); - }); - }); + if (result.files && result.files.length) { + return result.files[0]; + } + + throw 'File not found'; } /** @@ -1876,28 +372,16 @@ export class CoreH5PProvider { return this.ROOT_CACHE_KEY + 'trustedH5PFile:'; } - /** - * 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; - } - /** * Invalidates all trusted H5P file WS calls. * * @param siteId Site ID (empty for current site). * @return Promise resolved when the data is invalidated. */ - invalidateAllGetTrustedH5PFile(siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - return site.invalidateWsCacheForKeyStartingWith(this.getTrustedH5PFilePrefixCacheKey()); - }); + async invalidateAllGetTrustedH5PFile(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getTrustedH5PFilePrefixCacheKey()); } /** @@ -1907,10 +391,10 @@ export class CoreH5PProvider { * @param siteId Site ID (empty for current site). * @return Promise resolved when the data is invalidated. */ - invalidateAvailableInContexts(url: string, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - return site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url)); - }); + async invalidateAvailableInContexts(url: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getTrustedH5PFileCacheKey(url)); } /** @@ -1920,7 +404,7 @@ export class CoreH5PProvider { * @return Promise resolved with boolean: whether is disabled. */ async isOfflineDisabled(siteId?: string): Promise { - const site = await this.sitesProvider.getSite(siteId); + const site = await CoreSites.instance.getSite(siteId); return this.isOfflineDisabledInSite(site); } @@ -1932,701 +416,11 @@ export class CoreH5PProvider { * @return Whether is disabled. */ isOfflineDisabledInSite(site?: CoreSite): boolean { - site = site || this.sitesProvider.getCurrentSite(); + site = site || CoreSites.instance.getCurrentSite(); return site.isFeatureDisabled('NoDelegate_H5POffline'); } - /** - * Performs actions required when a library has been installed. - * - * @param libraryId ID of library that was installed. - * @param siteId Site ID. - * @return Promise resolved when done. - */ - protected libraryInstalled(libraryId: number, siteId: string): Promise { - const promises = []; - - // Remove all indexes of contents that use this library. - promises.push(this.deleteContentIndexesForLibrary(libraryId, siteId)); - - if (this.aggregateAssets) { - // Remove cached assets that use this library. - promises.push(this.deleteCachedAssets(libraryId, siteId)); - } - - return this.utils.allPromises(promises); - } - - /** - * Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}. - * - * @param libraryData Library data. - * @param folderName Use hyphen instead of space in returned string. - * @return String on the form {machineName} {majorVersion}.{minorVersion}. - */ - protected libraryToString(libraryData: any, folderName?: boolean): string { - return (libraryData.machineName ? libraryData.machineName : libraryData.name) + (folderName ? '-' : ' ') + - libraryData.majorVersion + '.' + libraryData.minorVersion; - } - - /** - * Load addon libraries. - * - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved with the addon libraries. - */ - loadAddons(siteId?: string): Promise { - return this.sitesProvider.getSiteDb(siteId).then((db) => { - - 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 ' + this.LIBRARIES_TABLE + ' l1 ' + - 'JOIN ' + this.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'; - - return db.execute(query).then((result) => { - 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. - */ - protected loadContentData(id?: number, fileUrl?: string, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - let promise: Promise; - - if (id) { - promise = this.getContentData(id, siteId); - } else if (fileUrl) { - promise = this.getContentDataByUrl(fileUrl, siteId); - } else { - promise = Promise.reject(null); - } - - return promise.then((contentData) => { - - // Load the main library data. - return this.getLibraryById(contentData.mainlibraryid, siteId).then((libData) => { - - // Validate metadata. - const validator = new CoreH5PContentValidator(this, this.h5pUtils, this.textUtils, this.utils, this.translate, - siteId); - - // Validate empty metadata, like Moodle web does. - return validator.validateMetadata({}).then((metadata) => { - // Map the values to the names used by the H5P core (it's the same Moodle web does). - return { - id: contentData.id, - params: contentData.jsoncontent, - embedType: 'iframe', // Always use iframe. - disable: null, - folderName: contentData.foldername, - title: libData.title, - slug: this.h5pUtils.slugify(libData.title) + '-' + contentData.id, - filtered: contentData.filtered, - libraryMajorVersion: libData.majorversion, - libraryMinorVersion: libData.minorversion, - metadata: metadata, - library: { - id: libData.id, - name: libData.machinename, - majorVersion: libData.majorversion, - minorVersion: libData.minorversion, - embedTypes: libData.embedtypes, - fullscreen: libData.fullscreen - } - }; - }); - }); - }); - } - - /** - * 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.sitesProvider.getSiteDb(siteId).then((db) => { - 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 ' + this.CONTENTS_LIBRARIES_TABLE + ' hcl ' + - 'JOIN ' + this.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'; - - return db.execute(query, queryArgs).then((result) => { - 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. - */ - loadLibrary(machineName: string, majorVersion: number, minorVersion: number, siteId?: string): Promise { - - // First get the library data from DB. - return this.getLibrary(machineName, majorVersion, minorVersion, siteId).then((library) => { - 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 ' + this.LIBRARY_DEPENDENCIES_TABLE + ' hll ' + - 'JOIN ' + this.LIBRARIES_TABLE + ' hl ON hll.requiredlibraryid = hl.id ' + - 'WHERE hll.libraryid = ? ' + - 'ORDER BY hl.id ASC'; - - const sqlParams = [ - library.id - ]; - - return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.execute(sql, sqlParams).then((result) => { - - for (let i = 0; i < result.rows.length; i++) { - const dependency = result.rows.item(i), - 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 = this.textUtils.parseJSON(library.addto, null); - - return library; - } - - /** - * Parse library DB data. - * - * @param library Library DB data. - * @return Parsed library. - */ - parseLibDBData(library: any): CoreH5PLibraryDBData { - library.semantics = this.textUtils.parseJSON(library.semantics, null); - library.addto = this.textUtils.parseJSON(library.addto, null); - - return library; - } - - /** - * Process libraries from an H5P library, getting the required data to save them. - * This code was copied from the isValidPackage function in Moodle's H5PValidator. - * This function won't validate most things because it should've been done by the server already. - * - * @param fileUrl The file URL used to download the file. - * @param file The file entry of the downloaded file. - * @return Promise resolved when done. - */ - protected processH5PFiles(destFolder: string, entries: (DirectoryEntry | FileEntry)[]) - : Promise<{librariesJsonData: any, mainJsonData: any, contentJsonData: any}> { - - const promises = [], - libraries: any = {}; - let contentJsonData, - mainH5PData; - - // Read the h5p.json file. - const h5pJsonPath = this.textUtils.concatenatePaths(destFolder, 'h5p.json'); - promises.push(this.fileProvider.readFile(h5pJsonPath, CoreFileProvider.FORMATJSON).then((data) => { - mainH5PData = data; - })); - - // Read the content.json file. - const contentJsonPath = this.textUtils.concatenatePaths(destFolder, 'content/content.json'); - promises.push(this.fileProvider.readFile(contentJsonPath, CoreFileProvider.FORMATJSON).then((data) => { - contentJsonData = data; - })); - - // Treat libraries. - entries.forEach((entry) => { - if (entry.name[0] == '.' || entry.name[0] == '_' || entry.name == 'content' || entry.isFile) { - // Skip files, the content folder and any folder starting with a . or _. - return; - } - - const libDirPath = this.textUtils.concatenatePaths(destFolder, entry.name); - - promises.push(this.getLibraryData( entry, libDirPath, destFolder).then((libraryH5PData) => { - libraryH5PData.uploadDirectory = libDirPath; - libraries[this.libraryToString(libraryH5PData)] = libraryH5PData; - })); - }); - - return Promise.all(promises).then(() => { - return { - librariesJsonData: libraries, - mainJsonData: mainH5PData, - contentJsonData: contentJsonData - }; - }); - } - - /** - * 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. - */ - protected saveCachedAssets(hash: string, dependencies: {[machineName: string]: CoreH5PContentDependencyData}, - folderName: string, siteId?: string): Promise { - - return this.sitesProvider.getSiteDb(siteId).then((db) => { - const promises = []; - - for (const key in dependencies) { - const data = { - hash: key, - libraryid: dependencies[key].libraryId, - foldername: folderName - }; - - promises.push(db.insertRecord(this.LIBRARIES_CACHEDASSETS_TABLE, data)); - } - - return Promise.all(promises); - }); - } - - /** - * 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. - */ - protected saveContentData(content: any, folderName: string, fileUrl: string, siteId?: string): Promise { - // Save in DB. - return this.sitesProvider.getSiteDb(siteId).then((db) => { - - const data: any = { - jsoncontent: content.params, - mainlibraryid: content.library.libraryId, - timemodified: Date.now(), - filtered: null, - foldername: folderName, - fileurl: fileUrl - }; - - if (typeof content.id != 'undefined') { - data.id = content.id; - } else { - data.timecreated = data.timemodified; - } - - return db.insertRecord(this.CONTENT_TABLE, data).then(() => { - if (!data.id) { - // New content. Get its ID. - return db.getRecord(this.CONTENT_TABLE, data).then((entry) => { - content.id = entry.id; - }); - } - }); - }).then(() => { - // If resetContentUserData is implemented in the future, it should be called in here. - return content.id; - }); - } - - /** - * Save the content in filesystem. - * - * @param contentPath Path to the current content folder (tmp). - * @param folderName Name to put to the content folder. - * @param siteId Site ID. - * @return Promise resolved when done. - */ - protected saveContentInFS(contentPath: string, folderName: string, siteId: string): Promise { - const folderPath = this.getContentFolderPath(folderName, siteId); - - // Delete existing content for this package. - return this.fileProvider.removeDir(folderPath).catch(() => { - // Ignore errors, maybe it doesn't exist. - }).then(() => { - // Copy the new one. - return this.fileProvider.moveDir(contentPath, folderPath); - }); - } - - /** - * Save libraries. This code is based on the saveLibraries function from Moodle's H5PStorage. - * - * @param librariesJsonData Data about libraries. - * @param folderName Name of the folder of the H5P package. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - protected saveLibraries(librariesJsonData: any, folderName: string, siteId?: string): Promise { - siteId = siteId || this.sitesProvider.getCurrentSiteId(); - - const libraryIds = []; - - // First of all, try to create the dir where the libraries are stored. This way we don't have to do it for each lib. - return this.fileProvider.createDir(this.getLibrariesFolderPath(siteId)).then(() => { - const promises = []; - - // Go through libraries that came with this package. - for (const libString in librariesJsonData) { - const libraryData = librariesJsonData[libString]; - - // Find local library identifier. - promises.push(this.getLibraryByData(libraryData).catch(() => { - // Not found. - }).then((dbData) => { - if (dbData) { - // Library already installed. - libraryData.libraryId = dbData.id; - - if (libraryData.patchVersion <= dbData.patchversion) { - // Same or older version, no need to save. - libraryData.saveDependencies = false; - - return; - } - } - - libraryData.saveDependencies = true; - - // Convert metadataSettings values to boolean and json_encode it before saving. - libraryData.metadataSettings = libraryData.metadataSettings ? - this.h5pUtils.boolifyAndEncodeMetadataSettings(libraryData.metadataSettings) : null; - - // Save the library data in DB. - return this.saveLibraryData(libraryData, siteId).then(() => { - // Now save it in FS. - return this.saveLibraryInFS(libraryData, siteId).catch((error) => { - // An error occurred, delete the DB data because the lib FS data has been deleted. - return this.deleteLibraryData(libraryData.libraryId, siteId).catch(() => { - // Ignore errors. - }).then(() => { - return Promise.reject(error); - }); - }); - }).then(() => { - if (typeof libraryData.libraryId != 'undefined') { - return this.libraryInstalled(libraryData.libraryId, siteId); - } - }); - })); - } - - return Promise.all(promises); - }).then(() => { - // Go through the libraries again to save dependencies. - const promises = []; - - for (const libString in librariesJsonData) { - const libraryData = librariesJsonData[libString]; - if (!libraryData.saveDependencies) { - continue; - } - - libraryIds.push(libraryData.libraryId); - - // Remove any old dependencies. - promises.push(this.deleteLibraryDependencies(libraryData.libraryId).then(() => { - // Insert the different new ones. - const subPromises = []; - - if (typeof libraryData.preloadedDependencies != 'undefined') { - subPromises.push(this.saveLibraryDependencies(libraryData.libraryId, libraryData.preloadedDependencies, - 'preloaded')); - } - if (typeof libraryData.dynamicDependencies != 'undefined') { - subPromises.push(this.saveLibraryDependencies(libraryData.libraryId, libraryData.dynamicDependencies, - 'dynamic')); - } - if (typeof libraryData.editorDependencies != 'undefined') { - subPromises.push(this.saveLibraryDependencies(libraryData.libraryId, libraryData.editorDependencies, - 'editor')); - } - - return Promise.all(subPromises); - })); - } - - return Promise.all(promises); - }).then(() => { - // Make sure dependencies, parameter filtering and export files get regenerated for content who uses these libraries. - if (libraryIds.length) { - return this.clearFilteredParameters(libraryIds, siteId); - } - }); - } - - /** - * Save a library in filesystem. - * - * @param libraryData Library data. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - protected saveLibraryInFS(libraryData: any, siteId?: string): Promise { - const folderPath = this.getLibraryFolderPath(libraryData, siteId); - - // Delete existing library version. - return this.fileProvider.removeDir(folderPath).catch(() => { - // Ignore errors, maybe it doesn't exist. - }).then(() => { - // Copy the new one. - return this.fileProvider.moveDir(libraryData.uploadDirectory, folderPath, true); - }); - } - - /** - * Save library data in DB. - * - * @param libraryData Library data to save. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - protected saveLibraryData(libraryData: any, siteId?: string): Promise { - // Some special properties needs some checking and converting before they can be saved. - const preloadedJS = this.h5pUtils.libraryParameterValuesToCsv(libraryData, 'preloadedJs', 'path'), - preloadedCSS = this.h5pUtils.libraryParameterValuesToCsv(libraryData, 'preloadedCss', 'path'), - dropLibraryCSS = this.h5pUtils.libraryParameterValuesToCsv(libraryData, 'dropLibraryCss', 'machineName'); - - if (typeof libraryData.semantics == 'undefined') { - libraryData.semantics = ''; - } - if (typeof libraryData.fullscreen == 'undefined') { - libraryData.fullscreen = 0; - } - - let embedTypes = ''; - if (typeof libraryData.embedTypes != 'undefined') { - embedTypes = libraryData.embedTypes.join(', '); - } - - return this.sitesProvider.getSite(siteId).then((site) => { - const db = site.getDb(), - data: any = { - title: libraryData.title, - machinename: libraryData.machineName, - majorversion: libraryData.majorVersion, - minorversion: libraryData.minorVersion, - patchversion: libraryData.patchVersion, - runnable: libraryData.runnable, - fullscreen: libraryData.fullscreen, - embedtypes: embedTypes, - preloadedjs: preloadedJS, - preloadedcss: preloadedCSS, - droplibrarycss: dropLibraryCSS, - semantics: 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; - } - - return db.insertRecord(this.LIBRARIES_TABLE, data).then(() => { - if (!data.id) { - // New library. Get its ID. - return db.getRecord(this.LIBRARIES_TABLE, data).then((entry) => { - libraryData.libraryId = entry.id; - }); - } else { - // Updated libary. Remove old dependencies. - return this.deleteLibraryDependencies(data.id, site.getId()); - } - }); - }); - } - - /** - * Save what libraries a library is depending on. - * - * @param libraryId Library Id for the library we're saving dependencies for. - * @param dependencies List of dependencies as associative arrays containing machineName, majorVersion, minorVersion. - * @param dependencytype The type of dependency. - * @param siteId Site ID. If not defined, current site. - * @return Promise resolved when done. - */ - protected saveLibraryDependencies(libraryId: number, dependencies: any[], dependencyType: string, siteId?: string) - : Promise { - - return this.sitesProvider.getSiteDb(siteId).then((db) => { - - const promises = []; - - dependencies.forEach((dependency) => { - // Get the ID of the library. - promises.push(this.getLibraryIdByData(dependency, siteId).then((dependencyId) => { - // Create the relation. - const entry = { - libraryid: libraryId, - requiredlibraryid: dependencyId, - dependencytype: dependencyType - }; - - return db.insertRecord(this.LIBRARY_DEPENDENCIES_TABLE, entry); - })); - }); - - return Promise.all(promises); - }); - } - - /** - * 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. - */ - saveLibraryUsage(id: number, librariesInUse: {[key: string]: CoreH5PContentDepsTreeDependency}, siteId?: string) - : Promise { - - return this.sitesProvider.getSiteDb(siteId).then((db) => { - // Calculate the CSS to drop. - const dropLibraryCssList = {}, - promises = []; - - for (const key in librariesInUse) { - const dependency = librariesInUse[key]; - - if (( dependency.library).dropLibraryCss) { - const split = ( dependency.library).dropLibraryCss.split(', '); - - split.forEach((css) => { - dropLibraryCssList[css] = css; - }); - } - } - - for (const key in librariesInUse) { - const dependency = librariesInUse[key], - data = { - h5pid: id, - libraryId: dependency.library.libraryId, - dependencytype: dependency.type, - dropcss: dropLibraryCssList[dependency.library.machineName] ? 1 : 0, - weight: dependency.weight - }; - - promises.push(db.insertRecord(this.CONTENTS_LIBRARIES_TABLE, data)); - } - - return Promise.all(promises); - }); - - } - - /** - * 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.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.hasPermission(permission, id); - } - - return value; - } - /** * Treat an H5P url before sending it to WS. * @@ -2635,68 +429,16 @@ export class CoreH5PProvider { * @return Treated url. */ protected treatH5PUrl(url: string, siteUrl: string): string { - if (url.indexOf(this.textUtils.concatenatePaths(siteUrl, '/webservice/pluginfile.php')) === 0) { + if (url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, '/webservice/pluginfile.php')) === 0) { url = url.replace('/webservice/pluginfile', '/pluginfile'); } - return this.urlUtils.removeUrlParams(url); - } - - /** - * 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. - */ - protected updateContentFields(id: number, fields: any, siteId?: string): Promise { - - return this.sitesProvider.getSiteDb(siteId).then((db) => { - const data = Object.assign(fields); - delete data.slug; // Slug isn't stored in DB. - - return db.updateRecords(this.CONTENT_TABLE, data, {id: id}); - }); + return CoreUrlUtils.instance.removeUrlParams(url); } } export class CoreH5P extends makeSingleton(CoreH5PProvider) {} -/** - * 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; -}; - /** * Options for core_h5p_get_trusted_h5p_file. */ @@ -2714,152 +456,3 @@ export type CoreH5PGetTrustedH5PFileResult = { files: CoreWSExternalFile[]; // Files. warnings: CoreWSExternalWarning[]; // List of warnings. }; - -/** - * Dependency asset. - */ -export type CoreH5PDependencyAsset = { - path: string; // Path to the asset. - version: string; // Dependency version. -}; - -/** - * 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. -}; - -/** - * 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. -}; - -/** - * 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. -}; diff --git a/src/core/h5p/providers/pluginfile-handler.ts b/src/core/h5p/providers/pluginfile-handler.ts index 8f0ca98d7..88a92e525 100644 --- a/src/core/h5p/providers/pluginfile-handler.ts +++ b/src/core/h5p/providers/pluginfile-handler.ts @@ -13,16 +13,15 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreFileProvider } from '@providers/file'; import { CorePluginFileHandler } from '@providers/plugin-file-delegate'; -import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; -import { CoreUrlUtilsProvider } from '@providers/utils/url'; -import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreH5PProvider } from './h5p'; +import { CoreMimetypeUtils } from '@providers/utils/mimetype'; +import { CoreUrlUtils } from '@providers/utils/url'; +import { CoreUtils } from '@providers/utils/utils'; +import { CoreH5P } from './h5p'; import { CoreWSExternalFile } from '@providers/ws'; 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. @@ -31,14 +30,6 @@ import { TranslateService } from '@ngx-translate/core'; export class CoreH5PPluginFileHandler implements CorePluginFileHandler { 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. * @@ -49,7 +40,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler { */ fileDeleted(fileUrl: string, path: string, siteId?: string): Promise { // 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. */ getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise { - 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 = []; 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) { urls.push(params.url); @@ -92,17 +83,19 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the size. */ - getFileSize(file: CoreWSExternalFile, siteId?: string): Promise { - return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId).then((file) => { - return file.filesize; - }).catch((error): any => { - if (this.utils.isWebServiceError(error)) { + async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise { + try { + const trustedFile = await CoreH5P.instance.getTrustedH5PFile(file.fileurl, {}, false, siteId); + + return trustedFile.filesize; + } catch (error) { + if (CoreUtils.instance.isWebServiceError(error)) { // WS returned an error, it means it cannot be downloaded. 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. */ isEnabled(): boolean | Promise { - 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. */ 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) { return { downloadable: false, - reason: this.translate.instant('core.h5p.offlinedisabled'), + reason: Translate.instance.instant('core.h5p.offlinedisabled'), }; } else { return { @@ -143,7 +136,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler { * @return Whether the file should be treated by this handler. */ 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. * @return Promise resolved when done. */ - treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise { - return this.h5pProvider.extractH5PFile(fileUrl, file, siteId); + treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise { + return CoreH5PHelper.saveH5P(fileUrl, file, siteId); } } diff --git a/src/core/h5p/providers/utils.ts b/src/core/h5p/providers/utils.ts deleted file mode 100644 index 026b2b9b7..000000000 --- a/src/core/h5p/providers/utils.ts +++ /dev/null @@ -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 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; - } -} diff --git a/src/core/viewer/pages/qr-scanner/qr-scanner.ts b/src/core/viewer/pages/qr-scanner/qr-scanner.ts index 49a2060e7..13ca3cb6a 100644 --- a/src/core/viewer/pages/qr-scanner/qr-scanner.ts +++ b/src/core/viewer/pages/qr-scanner/qr-scanner.ts @@ -43,7 +43,7 @@ export class CoreViewerQRScannerPage { this.closeModal(text); }).catch((error) => { - if (!error.coreCanceled) { + if (!this.domUtils.isCanceledError(error)) { // Show error and stop scanning. this.domUtils.showErrorModalDefault(error, 'An error occurred.'); this.utils.stopScanQR(); diff --git a/src/providers/update-manager.ts b/src/providers/update-manager.ts index bf76dec22..493b5c9e2 100644 --- a/src/providers/update-manager.ts +++ b/src/providers/update-manager.ts @@ -53,7 +53,7 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { const versionApplied: number = await this.configProvider.get(this.VERSION_APPLIED, 0); if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) { - promises.push(CoreH5P.instance.deleteAllContentIndexes()); + promises.push(CoreH5P.instance.h5pPlayer.deleteAllContentIndexes()); } try { diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 9ef11f960..b88c0aec9 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -669,7 +669,7 @@ export class CoreDomUtilsProvider { } // 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. return null; } @@ -716,6 +716,16 @@ export class CoreDomUtilsProvider { 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. * @@ -1314,7 +1324,7 @@ export class CoreDomUtilsProvider { * @return Promise resolved with the alert modal. */ showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise { - if (error && error.coreCanceled) { + if (this.isCanceledError(error)) { // It's a canceled error, don't display an error. return; } diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 8e959d845..30752b8b2 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -1543,7 +1543,7 @@ export class CoreUtilsProvider { } else if (typeof data != 'undefined') { this.qrScanData.deferred.resolve(data); } else { - this.qrScanData.deferred.reject({coreCanceled: true}); + this.qrScanData.deferred.reject(this.domUtils.createCanceledError()); } delete this.qrScanData;