forked from EVOgeek/Vmeda.Online
		
	MOBILE-3406 h5p: Refactor code into different classes
This commit is contained in:
		
							parent
							
								
									4f9adba63e
								
							
						
					
					
						commit
						724e0cc292
					
				| @ -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<void> { | ||||
|     async addon(library: CoreH5PLibraryAddonData): Promise<void> { | ||||
|         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<any> { | ||||
|         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<any[]> { | ||||
|         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<any[]> { | ||||
|         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<any> { | ||||
|     protected async validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise<any> { | ||||
|         // 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<any> { | ||||
|         let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
 | ||||
|     async validateVideo(video: any, semantics: any): Promise<any> { | ||||
| 
 | ||||
|         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<any> { | ||||
|         let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
 | ||||
|     async validateAudio(audio: any, semantics: any): Promise<any> { | ||||
| 
 | ||||
|         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<any> { | ||||
|         // 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<any> { | ||||
|         // 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<any> { | ||||
|     async validateLibrary(value: any, semantics: any): Promise<any> { | ||||
|         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') | ||||
|             } | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
							
								
								
									
										989
									
								
								src/core/h5p/classes/core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										989
									
								
								src/core/h5p/classes/core.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,989 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSites } from '@providers/sites'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreH5P } from '../providers/h5p'; | ||||
| import { CoreH5PFileStorage } from './file-storage'; | ||||
| import { CoreH5PFramework } from './framework'; | ||||
| import { CoreH5PContentValidator } from './content-validator'; | ||||
| import { Md5 } from 'ts-md5/dist/md5'; | ||||
| import { Translate } from '@singletons/core.singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PCore class. | ||||
|  */ | ||||
| export class CoreH5PCore { | ||||
| 
 | ||||
|     static STYLES = [ | ||||
|         'styles/h5p.css', | ||||
|         'styles/h5p-confirmation-dialog.css', | ||||
|         'styles/h5p-core-button.css' | ||||
|     ]; | ||||
|     static SCRIPTS = [ | ||||
|         'js/jquery.js', | ||||
|         'js/h5p.js', | ||||
|         'js/h5p-event-dispatcher.js', | ||||
|         'js/h5p-x-api-event.js', | ||||
|         'js/h5p-x-api.js', | ||||
|         'js/h5p-content-type.js', | ||||
|         'js/h5p-confirmation-dialog.js', | ||||
|         'js/h5p-action-bar.js', | ||||
|         'js/request-queue.js', | ||||
|     ]; | ||||
|     static ADMIN_SCRIPTS = [ | ||||
|         'js/jquery.js', | ||||
|         'js/h5p-utils.js', | ||||
|     ]; | ||||
| 
 | ||||
|     // Disable flags
 | ||||
|     static DISABLE_NONE = 0; | ||||
|     static DISABLE_FRAME = 1; | ||||
|     static DISABLE_DOWNLOAD = 2; | ||||
|     static DISABLE_EMBED = 4; | ||||
|     static DISABLE_COPYRIGHT = 8; | ||||
|     static DISABLE_ABOUT = 16; | ||||
| 
 | ||||
|     static DISPLAY_OPTION_FRAME = 'frame'; | ||||
|     static DISPLAY_OPTION_DOWNLOAD = 'export'; | ||||
|     static DISPLAY_OPTION_EMBED = 'embed'; | ||||
|     static DISPLAY_OPTION_COPYRIGHT = 'copyright'; | ||||
|     static DISPLAY_OPTION_ABOUT = 'icon'; | ||||
|     static DISPLAY_OPTION_COPY = 'copy'; | ||||
| 
 | ||||
|     // Map to slugify characters.
 | ||||
|     static SLUGIFY_MAP = { | ||||
|         æ: 'ae', ø: 'oe', ö: 'o', ó: 'o', ô: 'o', Ò: 'oe', Õ: 'o', Ý: 'o', ý: 'y', ÿ: 'y', ā: 'y', ă: 'a', ą: 'a', œ: 'a', å: 'a', | ||||
|         ä: 'a', á: 'a', à: 'a', â: 'a', ã: 'a', ç: 'c', ć: 'c', ĉ: 'c', ċ: 'c', č: 'c', é: 'e', è: 'e', ê: 'e', ë: 'e', í: 'i', | ||||
|         ì: 'i', î: 'i', ï: 'i', ú: 'u', ñ: 'n', ü: 'u', ù: 'u', û: 'u', ß: 'es', ď: 'd', đ: 'd', ē: 'e', ĕ: 'e', ė: 'e', ę: 'e', | ||||
|         ě: 'e', ĝ: 'g', ğ: 'g', ġ: 'g', ģ: 'g', ĥ: 'h', ħ: 'h', ĩ: 'i', ī: 'i', ĭ: 'i', į: 'i', ı: 'i', ij: 'ij', ĵ: 'j', ķ: 'k', | ||||
|         ĺ: 'l', ļ: 'l', ľ: 'l', ŀ: 'l', ł: 'l', ń: 'n', ņ: 'n', ň: 'n', ʼn: 'n', ō: 'o', ŏ: 'o', ő: 'o', ŕ: 'r', ŗ: 'r', ř: 'r', | ||||
|         ś: 's', ŝ: 's', ş: 's', š: 's', ţ: 't', ť: 't', ŧ: 't', ũ: 'u', ū: 'u', ŭ: 'u', ů: 'u', ű: 'u', ų: 'u', ŵ: 'w', ŷ: 'y', | ||||
|         ź: 'z', ż: 'z', ž: 'z', ſ: 's', ƒ: 'f', ơ: 'o', ư: 'u', ǎ: 'a', ǐ: 'i', ǒ: 'o', ǔ: 'u', ǖ: 'u', ǘ: 'u', ǚ: 'u', ǜ: 'u', | ||||
|         ǻ: 'a', ǽ: 'ae', ǿ: 'oe' | ||||
|     }; | ||||
| 
 | ||||
|     aggregateAssets = true; | ||||
| 
 | ||||
|     h5pFS: CoreH5PFileStorage; | ||||
| 
 | ||||
|     constructor(public h5pFramework: CoreH5PFramework) { | ||||
|         this.h5pFS = new CoreH5PFileStorage(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determine the correct embed type to use. | ||||
|      * | ||||
|      * @param Embed type of the content. | ||||
|      * @param Embed type of the main library. | ||||
|      * @return Either 'div' or 'iframe'. | ||||
|      */ | ||||
|     static determineEmbedType(contentEmbedType: string, libraryEmbedTypes: string): string { | ||||
|         // Detect content embed type.
 | ||||
|         let embedType = contentEmbedType.toLowerCase().indexOf('div') != -1 ? 'div' : 'iframe'; | ||||
| 
 | ||||
|         if (libraryEmbedTypes) { | ||||
|             // Check that embed type is available for library
 | ||||
|             const embedTypes = libraryEmbedTypes.toLowerCase(); | ||||
| 
 | ||||
|             if (embedTypes.indexOf(embedType) == -1) { | ||||
|                 // Not available, pick default.
 | ||||
|                 embedType = embedTypes.indexOf('div') != -1 ? 'div' : 'iframe'; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return embedType; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Filter content run parameters and rebuild content dependency cache. | ||||
|      * | ||||
|      * @param content Content data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the filtered params, resolved with null if error. | ||||
|      */ | ||||
|     async filterParameters(content: CoreH5PContentData, siteId?: string): Promise<string> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (content.filtered) { | ||||
|             return content.filtered; | ||||
|         } | ||||
| 
 | ||||
|         if (typeof content.library == 'undefined' || typeof content.params == 'undefined') { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const params = { | ||||
|             library: CoreH5PCore.libraryToString(content.library), | ||||
|             params: CoreTextUtils.instance.parseJSON(content.params, false), | ||||
|         }; | ||||
| 
 | ||||
|         if (!params.params) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const validator = new CoreH5PContentValidator(siteId); | ||||
| 
 | ||||
|             // Validate the main library and its dependencies.
 | ||||
|             await validator.validateLibrary(params, {options: [params.library]}); | ||||
| 
 | ||||
|             // Handle addons.
 | ||||
|             const addons = await this.h5pFramework.loadAddons(siteId); | ||||
| 
 | ||||
|             // Validate addons.
 | ||||
|             for (const i in addons) { | ||||
|                 const addon = addons[i]; | ||||
|                 const addTo = addon.addTo; | ||||
| 
 | ||||
|                 if (addTo && addTo.content && addTo.content.types && addTo.content.types.length) { | ||||
|                     for (let i = 0; i < addTo.content.types.length; i++) { | ||||
|                         const type = addTo.content.types[i]; | ||||
| 
 | ||||
|                         if (type && type.text && type.text.regex && this.textAddonMatches(params.params, type.text.regex)) { | ||||
| 
 | ||||
|                             await validator.addon(addon); | ||||
| 
 | ||||
|                             // An addon shall only be added once.
 | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Update content dependencies.
 | ||||
|             content.dependencies = validator.getDependencies(); | ||||
| 
 | ||||
|             const paramsStr = JSON.stringify(params.params); | ||||
| 
 | ||||
|             // Sometimes the parameters are filtered before content has been created
 | ||||
|             if (content.id) { | ||||
|                 // Update library usage.
 | ||||
|                 try { | ||||
|                     await this.h5pFramework.deleteLibraryUsage(content.id, siteId); | ||||
|                 } catch (error) { | ||||
|                     // Ignore errors.
 | ||||
|                 } | ||||
| 
 | ||||
|                 await this.h5pFramework.saveLibraryUsage(content.id, content.dependencies, siteId); | ||||
| 
 | ||||
|                 if (!content.slug) { | ||||
|                     content.slug = this.generateContentSlug(content); | ||||
|                 } | ||||
| 
 | ||||
|                 // Cache.
 | ||||
|                 await this.h5pFramework.updateContentFields(content.id, { | ||||
|                     filtered: paramsStr, | ||||
|                     slug: content.slug, | ||||
|                 }, siteId); | ||||
|             } | ||||
| 
 | ||||
|             return paramsStr; | ||||
|         } catch (error) { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Recursive. Goes through the dependency tree for the given library and | ||||
|      * adds all the dependencies to the given array in a flat format. | ||||
|      * | ||||
|      * @param dependencies Object where to save the dependencies. | ||||
|      * @param library The library to find all dependencies for. | ||||
|      * @param nextWeight An integer determining the order of the libraries when they are loaded. | ||||
|      * @param editor Used internally to force all preloaded sub dependencies of an editor dependency to be editor dependencies. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the next weight. | ||||
|      */ | ||||
|     async findLibraryDependencies(dependencies: {[key: string]: CoreH5PContentDepsTreeDependency}, | ||||
|             library: CoreH5PLibraryData | CoreH5PLibraryAddonData, nextWeight: number = 1, editor: boolean = false, | ||||
|             siteId?: string): Promise<number> { | ||||
| 
 | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const types = ['dynamic', 'preloaded', 'editor']; | ||||
| 
 | ||||
|         for (const i in types) { | ||||
| 
 | ||||
|             let type = types[i]; | ||||
|             const property = type + 'Dependencies'; | ||||
| 
 | ||||
|             if (!library[property]) { | ||||
|                 continue; // Skip, no such dependencies.
 | ||||
|             } | ||||
| 
 | ||||
|             if (type === 'preloaded' && editor) { | ||||
|                 // All preloaded dependencies of an editor library is set to editor.
 | ||||
|                 type = 'editor'; | ||||
|             } | ||||
| 
 | ||||
|             for (const j in library[property]) { | ||||
| 
 | ||||
|                 const dependency: CoreH5PLibraryBasicData = library[property][j]; | ||||
| 
 | ||||
|                 const dependencyKey = type + '-' + dependency.machineName; | ||||
|                 if (dependencies[dependencyKey]) { | ||||
|                     continue; // Skip, already have this.
 | ||||
|                 } | ||||
| 
 | ||||
|                 // Get the dependency library data and its subdependencies.
 | ||||
|                 const dependencyLibrary = await this.loadLibrary(dependency.machineName, dependency.majorVersion, | ||||
|                         dependency.minorVersion, siteId); | ||||
| 
 | ||||
|                 dependencies[dependencyKey] = { | ||||
|                     library: dependencyLibrary, | ||||
|                     type: type | ||||
|                 }; | ||||
| 
 | ||||
|                 // Get all its subdependencies.
 | ||||
|                 const weight = await this.findLibraryDependencies(dependencies, dependencyLibrary, nextWeight, type === 'editor', | ||||
|                         siteId); | ||||
| 
 | ||||
|                 nextWeight = weight; | ||||
|                 dependencies[dependencyKey].weight = nextWeight++; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return nextWeight; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Validate and fix display options, updating them if needed. | ||||
|      * | ||||
|      * @param displayOptions The display options to validate. | ||||
|      * @param id Package ID. | ||||
|      */ | ||||
|     fixDisplayOptions(displayOptions: CoreH5PDisplayOptions, id: number): CoreH5PDisplayOptions { | ||||
| 
 | ||||
|         // Never allow downloading in the app.
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = false; | ||||
| 
 | ||||
|         // Embed - force setting it if always on or always off. In web, this is done when storing in DB.
 | ||||
|         const embed = this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_EMBED, CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW); | ||||
|         if (embed == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW || embed == CoreH5PDisplayOptionBehaviour.NEVER_SHOW) { | ||||
|             displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = (embed == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW); | ||||
|         } | ||||
| 
 | ||||
|         if (!this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_FRAME, true)) { | ||||
|             displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = false; | ||||
|         } else { | ||||
|             displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = this.setDisplayOptionOverrides( | ||||
|                     CoreH5PCore.DISPLAY_OPTION_EMBED, CoreH5PPermission.EMBED_H5P, id, | ||||
|                     displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED]); | ||||
| 
 | ||||
|             if (this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_COPYRIGHT, true) == false) { | ||||
|                 displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = false; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_COPY] = this.h5pFramework.hasPermission(CoreH5PPermission.COPY_H5P, id); | ||||
| 
 | ||||
|         return displayOptions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}. | ||||
|      * | ||||
|      * @param libraryString On the form {machineName} {majorVersion}.{minorVersion} | ||||
|      * @return Object with keys machineName, majorVersion and minorVersion. Null if string is not parsable. | ||||
|      */ | ||||
|     generateContentSlug(content: CoreH5PContentData): string { | ||||
| 
 | ||||
|         let slug = CoreH5PCore.slugify(content.title); | ||||
|         let available: boolean = null; | ||||
| 
 | ||||
|         while (!available) { | ||||
|             if (available === false) { | ||||
|                 // If not available, add number suffix.
 | ||||
|                 const matches = slug.match(/(.+-)([0-9]+)$/); | ||||
|                 if (matches) { | ||||
|                     slug = matches[1] + (Number(matches[2]) + 1); | ||||
|                 } else { | ||||
|                     slug +=  '-2'; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             available = this.h5pFramework.isContentSlugAvailable(slug); | ||||
|         } | ||||
| 
 | ||||
|         return slug; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Combines path with version. | ||||
|      * | ||||
|      * @param assets List of assets to get their URLs. | ||||
|      * @param assetsFolderPath The path of the folder where the assets are. | ||||
|      * @return List of urls. | ||||
|      */ | ||||
|     getAssetsUrls(assets: CoreH5PDependencyAsset[], assetsFolderPath: string = ''): string[] { | ||||
|         const urls = []; | ||||
| 
 | ||||
|         assets.forEach((asset) => { | ||||
|             let url = asset.path; | ||||
| 
 | ||||
|             // Add URL prefix if not external.
 | ||||
|             if (asset.path.indexOf('://') == -1 && assetsFolderPath) { | ||||
|                 url = CoreTextUtils.instance.concatenatePaths(assetsFolderPath, url); | ||||
|             } | ||||
| 
 | ||||
|             // Add version if set.
 | ||||
|             if (asset.version) { | ||||
|                 url += asset.version; | ||||
|             } | ||||
| 
 | ||||
|             urls.push(url); | ||||
|         }); | ||||
| 
 | ||||
|         return urls; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return file paths for all dependencies files. | ||||
|      * | ||||
|      * @param dependencies The dependencies to get the files. | ||||
|      * @param folderName Name of the folder of the content. | ||||
|      * @param prefix Make paths relative to another dir. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getDependenciesFiles(dependencies: {[machineName: string]: CoreH5PContentDependencyData}, folderName: string, | ||||
|             prefix: string = '', siteId?: string): Promise<CoreH5PDependenciesFiles> { | ||||
| 
 | ||||
|         // Build files list for assets.
 | ||||
|         const files: CoreH5PDependenciesFiles = { | ||||
|             scripts: [], | ||||
|             styles: [], | ||||
|         }; | ||||
| 
 | ||||
|         // Avoid caching empty files.
 | ||||
|         if (!Object.keys(dependencies).length) { | ||||
|             return files; | ||||
|         } | ||||
| 
 | ||||
|         let cachedAssetsHash: string; | ||||
|         let cachedAssets: {scripts?: CoreH5PDependencyAsset[], styles?: CoreH5PDependencyAsset[]}; | ||||
| 
 | ||||
|         if (this.aggregateAssets) { | ||||
|             // Get aggregated files for assets.
 | ||||
|             cachedAssetsHash = CoreH5PCore.getDependenciesHash(dependencies); | ||||
| 
 | ||||
|             cachedAssets = await this.h5pFS.getCachedAssets(cachedAssetsHash); | ||||
| 
 | ||||
|             if (cachedAssets) { | ||||
|                 // Cached assets found, return them.
 | ||||
|                 return Object.assign(files, cachedAssets); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // No cached assets, use content dependencies.
 | ||||
|         for (const key in dependencies) { | ||||
|             const dependency = dependencies[key]; | ||||
| 
 | ||||
|             if (!dependency.path) { | ||||
|                 dependency.path = this.h5pFS.getDependencyPath(dependency); | ||||
|                 dependency.preloadedJs = (<string> dependency.preloadedJs).split(','); | ||||
|                 dependency.preloadedCss = (<string> dependency.preloadedCss).split(','); | ||||
|             } | ||||
| 
 | ||||
|             dependency.version = '?ver=' + dependency.majorVersion + '.' + dependency.minorVersion + '.' + dependency.patchVersion; | ||||
| 
 | ||||
|             this.getDependencyAssets(dependency, 'preloadedJs', files.scripts, prefix); | ||||
|             this.getDependencyAssets(dependency, 'preloadedCss', files.styles, prefix); | ||||
|         } | ||||
| 
 | ||||
|         if (this.aggregateAssets) { | ||||
|             // Aggregate and store assets.
 | ||||
|             await this.h5pFS.cacheAssets(files, cachedAssetsHash, folderName, siteId); | ||||
| 
 | ||||
|             // Keep track of which libraries have been cached in case they are updated.
 | ||||
|             await this.h5pFramework.saveCachedAssets(cachedAssetsHash, dependencies, folderName, siteId); | ||||
|         } | ||||
| 
 | ||||
|         return files; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the hash of a list of dependencies. | ||||
|      * | ||||
|      * @param dependencies Dependencies. | ||||
|      * @return Hash. | ||||
|      */ | ||||
|     static getDependenciesHash(dependencies: {[machineName: string]: CoreH5PContentDependencyData}): string { | ||||
|         // Build hash of dependencies.
 | ||||
|         const toHash = []; | ||||
| 
 | ||||
|         // Use unique identifier for each library version.
 | ||||
|         for (const name in dependencies) { | ||||
|             const dep = dependencies[name]; | ||||
|             toHash.push(dep.machineName + '-' + dep.majorVersion + '.' + dep.minorVersion + '.' + dep.patchVersion); | ||||
|         } | ||||
| 
 | ||||
|         // Sort in case the same dependencies comes in a different order.
 | ||||
|         toHash.sort((a, b) => { | ||||
|             return a.localeCompare(b); | ||||
|         }); | ||||
| 
 | ||||
|         // Calculate hash.
 | ||||
|         return <string> Md5.hashAsciiStr(toHash.join('')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the paths to the content dependencies. | ||||
|      * | ||||
|      * @param id The H5P content ID. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with an object containing the path of each content dependency. | ||||
|      */ | ||||
|     async getDependencyRoots(id: number, siteId?: string): Promise<{[libString: string]: string}> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const roots = {}; | ||||
| 
 | ||||
|         const dependencies = await this.h5pFramework.loadContentDependencies(id, undefined, siteId); | ||||
| 
 | ||||
|         for (const machineName in dependencies) { | ||||
|             const dependency = dependencies[machineName]; | ||||
|             const folderName = CoreH5PCore.libraryToString(dependency, true); | ||||
| 
 | ||||
|             roots[folderName] = this.h5pFS.getLibraryFolderPath(dependency, siteId, folderName); | ||||
|         } | ||||
| 
 | ||||
|         return roots; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all dependency assets of the given type. | ||||
|      * | ||||
|      * @param dependency The dependency. | ||||
|      * @param type Type of assets to get. | ||||
|      * @param assets Array where to store the assets. | ||||
|      * @param prefix Make paths relative to another dir. | ||||
|      */ | ||||
|     protected getDependencyAssets(dependency: CoreH5PContentDependencyData, type: string, assets: CoreH5PDependencyAsset[], | ||||
|             prefix: string = ''): void { | ||||
| 
 | ||||
|         // Check if dependency has any files of this type
 | ||||
|         if (!dependency[type] || dependency[type][0] === '') { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Check if we should skip CSS.
 | ||||
|         if (type === 'preloadedCss' && CoreUtils.instance.isTrueOrOne(dependency.dropCss)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         for (const key in dependency[type]) { | ||||
|             const file = dependency[type][key]; | ||||
| 
 | ||||
|             assets.push({ | ||||
|                 path: prefix + '/' + dependency.path + '/' + (typeof file != 'string' ? file.path : file).trim(), | ||||
|                 version: dependency.version | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert display options to an object. | ||||
|      * | ||||
|      * @param disable Display options as a number. | ||||
|      * @return Display options as object. | ||||
|      */ | ||||
|     getDisplayOptionsAsObject(disable: number): CoreH5PDisplayOptions { | ||||
|         const displayOptions: CoreH5PDisplayOptions = {}; | ||||
| 
 | ||||
|         // tslint:disable: no-bitwise
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = !(disable & CoreH5PCore.DISABLE_FRAME); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = !(disable & CoreH5PCore.DISABLE_DOWNLOAD); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = !(disable & CoreH5PCore.DISABLE_EMBED); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = !(disable & CoreH5PCore.DISABLE_COPYRIGHT); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_ABOUT] = !!this.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_ABOUT, true); | ||||
| 
 | ||||
|         return displayOptions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determine display option visibility when viewing H5P | ||||
|      * | ||||
|      * @param disable The display options as a number. | ||||
|      * @param id Package ID. | ||||
|      * @return Display options as object. | ||||
|      */ | ||||
|     getDisplayOptionsForView(disable: number, id: number): CoreH5PDisplayOptions { | ||||
|         return this.fixDisplayOptions(this.getDisplayOptionsAsObject(disable), id); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Provide localization for the Core JS. | ||||
|      * | ||||
|      * @return Object with the translations. | ||||
|      */ | ||||
|     getLocalization(): {[name: string]: string} { | ||||
|         return { | ||||
|             fullscreen: Translate.instance.instant('core.h5p.fullscreen'), | ||||
|             disableFullscreen: Translate.instance.instant('core.h5p.disablefullscreen'), | ||||
|             download: Translate.instance.instant('core.h5p.download'), | ||||
|             copyrights: Translate.instance.instant('core.h5p.copyright'), | ||||
|             embed: Translate.instance.instant('core.h5p.embed'), | ||||
|             size: Translate.instance.instant('core.h5p.size'), | ||||
|             showAdvanced: Translate.instance.instant('core.h5p.showadvanced'), | ||||
|             hideAdvanced: Translate.instance.instant('core.h5p.hideadvanced'), | ||||
|             advancedHelp: Translate.instance.instant('core.h5p.resizescript'), | ||||
|             copyrightInformation: Translate.instance.instant('core.h5p.copyright'), | ||||
|             close: Translate.instance.instant('core.h5p.close'), | ||||
|             title: Translate.instance.instant('core.h5p.title'), | ||||
|             author: Translate.instance.instant('core.h5p.author'), | ||||
|             year: Translate.instance.instant('core.h5p.year'), | ||||
|             source: Translate.instance.instant('core.h5p.source'), | ||||
|             license: Translate.instance.instant('core.h5p.license'), | ||||
|             thumbnail: Translate.instance.instant('core.h5p.thumbnail'), | ||||
|             noCopyrights: Translate.instance.instant('core.h5p.nocopyright'), | ||||
|             reuse: Translate.instance.instant('core.h5p.reuse'), | ||||
|             reuseContent: Translate.instance.instant('core.h5p.reuseContent'), | ||||
|             reuseDescription: Translate.instance.instant('core.h5p.reuseDescription'), | ||||
|             downloadDescription: Translate.instance.instant('core.h5p.downloadtitle'), | ||||
|             copyrightsDescription: Translate.instance.instant('core.h5p.copyrighttitle'), | ||||
|             embedDescription: Translate.instance.instant('core.h5p.embedtitle'), | ||||
|             h5pDescription: Translate.instance.instant('core.h5p.h5ptitle'), | ||||
|             contentChanged: Translate.instance.instant('core.h5p.contentchanged'), | ||||
|             startingOver: Translate.instance.instant('core.h5p.startingover'), | ||||
|             by: Translate.instance.instant('core.h5p.by'), | ||||
|             showMore: Translate.instance.instant('core.h5p.showmore'), | ||||
|             showLess: Translate.instance.instant('core.h5p.showless'), | ||||
|             subLevel: Translate.instance.instant('core.h5p.sublevel'), | ||||
|             confirmDialogHeader: Translate.instance.instant('core.h5p.confirmdialogheader'), | ||||
|             confirmDialogBody: Translate.instance.instant('core.h5p.confirmdialogbody'), | ||||
|             cancelLabel: Translate.instance.instant('core.h5p.cancellabel'), | ||||
|             confirmLabel: Translate.instance.instant('core.h5p.confirmlabel'), | ||||
|             licenseU: Translate.instance.instant('core.h5p.undisclosed'), | ||||
|             licenseCCBY: Translate.instance.instant('core.h5p.ccattribution'), | ||||
|             licenseCCBYSA: Translate.instance.instant('core.h5p.ccattributionsa'), | ||||
|             licenseCCBYND: Translate.instance.instant('core.h5p.ccattributionnd'), | ||||
|             licenseCCBYNC: Translate.instance.instant('core.h5p.ccattributionnc'), | ||||
|             licenseCCBYNCSA: Translate.instance.instant('core.h5p.ccattributionncsa'), | ||||
|             licenseCCBYNCND: Translate.instance.instant('core.h5p.ccattributionncnd'), | ||||
|             licenseCC40: Translate.instance.instant('core.h5p.licenseCC40'), | ||||
|             licenseCC30: Translate.instance.instant('core.h5p.licenseCC30'), | ||||
|             licenseCC25: Translate.instance.instant('core.h5p.licenseCC25'), | ||||
|             licenseCC20: Translate.instance.instant('core.h5p.licenseCC20'), | ||||
|             licenseCC10: Translate.instance.instant('core.h5p.licenseCC10'), | ||||
|             licenseGPL: Translate.instance.instant('core.h5p.licenseGPL'), | ||||
|             licenseV3: Translate.instance.instant('core.h5p.licenseV3'), | ||||
|             licenseV2: Translate.instance.instant('core.h5p.licenseV2'), | ||||
|             licenseV1: Translate.instance.instant('core.h5p.licenseV1'), | ||||
|             licensePD: Translate.instance.instant('core.h5p.pd'), | ||||
|             licenseCC010: Translate.instance.instant('core.h5p.licenseCC010'), | ||||
|             licensePDM: Translate.instance.instant('core.h5p.pdm'), | ||||
|             licenseC: Translate.instance.instant('core.h5p.copyrightstring'), | ||||
|             contentType: Translate.instance.instant('core.h5p.contenttype'), | ||||
|             licenseExtras: Translate.instance.instant('core.h5p.licenseextras'), | ||||
|             changes: Translate.instance.instant('core.h5p.changelog'), | ||||
|             contentCopied: Translate.instance.instant('core.h5p.contentCopied'), | ||||
|             connectionLost: Translate.instance.instant('core.h5p.connectionLost'), | ||||
|             connectionReestablished: Translate.instance.instant('core.h5p.connectionReestablished'), | ||||
|             resubmitScores: Translate.instance.instant('core.h5p.resubmitScores'), | ||||
|             offlineDialogHeader: Translate.instance.instant('core.h5p.offlineDialogHeader'), | ||||
|             offlineDialogBody: Translate.instance.instant('core.h5p.offlineDialogBody'), | ||||
|             offlineDialogRetryMessage: Translate.instance.instant('core.h5p.offlineDialogRetryMessage'), | ||||
|             offlineDialogRetryButtonLabel: Translate.instance.instant('core.h5p.offlineDialogRetryButtonLabel'), | ||||
|             offlineSuccessfulSubmit: Translate.instance.instant('core.h5p.offlineSuccessfulSubmit'), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get core JavaScript files. | ||||
|      * | ||||
|      * @return array The array containg urls of the core JavaScript files: | ||||
|      */ | ||||
|     static getScripts(): string[] { | ||||
|         const libUrl = CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(); | ||||
|         const urls = []; | ||||
| 
 | ||||
|         CoreH5PCore.SCRIPTS.forEach((script) => { | ||||
|             urls.push(libUrl + script); | ||||
|         }); | ||||
| 
 | ||||
|         return urls; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}. | ||||
|      * | ||||
|      * @param libraryString On the form {machineName} {majorVersion}.{minorVersion} | ||||
|      * @return Object with keys machineName, majorVersion and minorVersion. Null if string is not parsable. | ||||
|      */ | ||||
|     static libraryFromString(libraryString: string): {machineName: string, majorVersion: number, minorVersion: number} { | ||||
| 
 | ||||
|         const matches = libraryString.match(/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i); | ||||
| 
 | ||||
|         if (matches && matches.length >= 4) { | ||||
|             return { | ||||
|                 machineName: matches[1], | ||||
|                 majorVersion: Number(matches[2]), | ||||
|                 minorVersion: Number(matches[3]) | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param folderName Use hyphen instead of space in returned string. | ||||
|      * @return String on the form {machineName} {majorVersion}.{minorVersion}. | ||||
|      */ | ||||
|     static libraryToString(libraryData: any, folderName?: boolean): string { | ||||
|         return (libraryData.machineName ? libraryData.machineName : libraryData.name) + (folderName ? '-' : ' ') + | ||||
|                 libraryData.majorVersion + '.' + libraryData.minorVersion; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load content data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param fileUrl H5P file URL. Required if id is not provided. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async loadContent(id?: number, fileUrl?: string, siteId?: string): Promise<CoreH5PContentData> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const content = await this.h5pFramework.loadContent(id, fileUrl, siteId); | ||||
| 
 | ||||
|         // Validate metadata.
 | ||||
|         const validator = new CoreH5PContentValidator(siteId); | ||||
| 
 | ||||
|         content.metadata = await validator.validateMetadata(content.metadata); | ||||
| 
 | ||||
|         return { | ||||
|             id: content.id, | ||||
|             params: content.params, | ||||
|             embedType: content.embedType, | ||||
|             disable: content.disable, | ||||
|             folderName: content.folderName, | ||||
|             title: content.title, | ||||
|             slug: content.slug, | ||||
|             filtered: content.filtered, | ||||
|             libraryMajorVersion: content.libraryMajorVersion, | ||||
|             libraryMinorVersion: content.libraryMinorVersion, | ||||
|             metadata: content.metadata, | ||||
|             library: { | ||||
|                 id: content.libraryId, | ||||
|                 name: content.libraryName, | ||||
|                 majorVersion: content.libraryMajorVersion, | ||||
|                 minorVersion: content.libraryMinorVersion, | ||||
|                 embedTypes: content.libraryEmbedTypes, | ||||
|                 fullscreen: content.libraryFullscreen, | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load dependencies for the given content of the given type. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param type The dependency type. | ||||
|      * @return Content dependencies, indexed by machine name. | ||||
|      */ | ||||
|     loadContentDependencies(id: number, type?: string, siteId?: string) | ||||
|             : Promise<{[machineName: string]: CoreH5PContentDependencyData}> { | ||||
| 
 | ||||
|         return this.h5pFramework.loadContentDependencies(id, type, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads a library and its dependencies. | ||||
|      * | ||||
|      * @param machineName The library's machine name. | ||||
|      * @param majorVersion The library's major version. | ||||
|      * @param minorVersion The library's minor version. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data. | ||||
|      */ | ||||
|     loadLibrary(machineName: string, majorVersion: number, minorVersion: number, siteId?: string): Promise<CoreH5PLibraryData> { | ||||
|         return this.h5pFramework.loadLibrary(machineName, majorVersion, minorVersion, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the current user has permission to update and install new libraries. | ||||
|      * | ||||
|      * @return Whether has permissions. | ||||
|      */ | ||||
|     mayUpdateLibraries(): boolean { | ||||
|         // In the app the installation only affects current user, so the user always has permissions.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save content data in DB and clear cache. | ||||
|      * | ||||
|      * @param content Content to save. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param fileUrl The online URL of the package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with content ID. | ||||
|      */ | ||||
|     async saveContent(content: any, folderName: string, fileUrl: string, siteId?: string): Promise<number> { | ||||
| 
 | ||||
|         content.id = await this.h5pFramework.updateContent(content, folderName, fileUrl, siteId); | ||||
| 
 | ||||
|         // Some user data for content has to be reset when the content changes.
 | ||||
|         await this.h5pFramework.resetContentUserData(content.id, siteId); | ||||
| 
 | ||||
|         return content.id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper function used to figure out embed and download behaviour. | ||||
|      * | ||||
|      * @param optionName The option name. | ||||
|      * @param permission The permission. | ||||
|      * @param id The package ID. | ||||
|      * @param value Default value. | ||||
|      * @return The value to use. | ||||
|      */ | ||||
|     setDisplayOptionOverrides(optionName: string, permission: number, id: number, value: boolean): boolean { | ||||
|         const behaviour = this.h5pFramework.getOption(optionName, CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW); | ||||
| 
 | ||||
|         // If never show globally, force hide
 | ||||
|         if (behaviour == CoreH5PDisplayOptionBehaviour.NEVER_SHOW) { | ||||
|             value = false; | ||||
|         } else if (behaviour == CoreH5PDisplayOptionBehaviour.ALWAYS_SHOW) { | ||||
|             // If always show or permissions say so, force show
 | ||||
|             value = true; | ||||
|         } else if (behaviour == CoreH5PDisplayOptionBehaviour.CONTROLLED_BY_PERMISSIONS) { | ||||
|             value = this.h5pFramework.hasPermission(permission, id); | ||||
|         } | ||||
| 
 | ||||
|         return value; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert strings of text into simple kebab case slugs. Based on H5PCore::slugify. | ||||
|      * | ||||
|      * @param input The string to slugify. | ||||
|      * @return Slugified text. | ||||
|      */ | ||||
|     static slugify(input: string): string { | ||||
|         input = input || ''; | ||||
| 
 | ||||
|         input = input.toLowerCase(); | ||||
| 
 | ||||
|         // Replace common chars.
 | ||||
|         let newInput = ''; | ||||
|         for (let i = 0; i < input.length; i++) { | ||||
|             const char = input[i]; | ||||
| 
 | ||||
|             newInput += CoreH5PCore.SLUGIFY_MAP[char] || char; | ||||
|         } | ||||
| 
 | ||||
|         // Replace everything else.
 | ||||
|         newInput = newInput.replace(/[^a-z0-9]/g, '-'); | ||||
| 
 | ||||
|         // Prevent double hyphen
 | ||||
|         newInput = newInput.replace(/-{2,}/g, '-'); | ||||
| 
 | ||||
|         // Prevent hyphen in beginning or end.
 | ||||
|         newInput = newInput.replace(/(^-+|-+$)/g, ''); | ||||
| 
 | ||||
|         // Prevent too long slug.
 | ||||
|         if (newInput.length > 91) { | ||||
|             newInput = newInput.substr(0, 92); | ||||
|         } | ||||
| 
 | ||||
|         // Prevent empty slug
 | ||||
|         if (newInput === '') { | ||||
|             newInput = 'interactive'; | ||||
|         } | ||||
| 
 | ||||
|         return newInput; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determine if params contain any match. | ||||
|      * | ||||
|      * @param params Parameters. | ||||
|      * @param pattern Regular expression to identify pattern. | ||||
|      * @return True if params matches pattern. | ||||
|      */ | ||||
|     protected textAddonMatches(params: any, pattern: string): boolean { | ||||
| 
 | ||||
|         if (typeof params == 'string') { | ||||
|             if (params.match(pattern)) { | ||||
|                 return true; | ||||
|             } | ||||
|         } else if (typeof params == 'object') { | ||||
|             for (const key in params) { | ||||
|                 const value = params[key]; | ||||
| 
 | ||||
|                 if (this.textAddonMatches(value, pattern)) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Display options behaviour constants. | ||||
|  */ | ||||
| export class CoreH5PDisplayOptionBehaviour { | ||||
|     static NEVER_SHOW = 0; | ||||
|     static CONTROLLED_BY_AUTHOR_DEFAULT_ON = 1; | ||||
|     static CONTROLLED_BY_AUTHOR_DEFAULT_OFF = 2; | ||||
|     static ALWAYS_SHOW = 3; | ||||
|     static CONTROLLED_BY_PERMISSIONS = 4; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Permission constants. | ||||
|  */ | ||||
| export class CoreH5PPermission { | ||||
|     static DOWNLOAD_H5P = 0; | ||||
|     static EMBED_H5P = 1; | ||||
|     static CREATE_RESTRICTED = 2; | ||||
|     static UPDATE_LIBRARIES = 3; | ||||
|     static INSTALL_RECOMMENDED = 4; | ||||
|     static COPY_H5P = 4; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Display options as object. | ||||
|  */ | ||||
| export type CoreH5PDisplayOptions = { | ||||
|     frame?: boolean; | ||||
|     export?: boolean; | ||||
|     embed?: boolean; | ||||
|     copyright?: boolean; | ||||
|     icon?: boolean; | ||||
|     copy?: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Dependency asset. | ||||
|  */ | ||||
| export type CoreH5PDependencyAsset = { | ||||
|     path: string; // Path to the asset.
 | ||||
|     version: string; // Dependency version.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Dependencies files. | ||||
|  */ | ||||
| export type CoreH5PDependenciesFiles = { | ||||
|     scripts: CoreH5PDependencyAsset[]; // JS scripts.
 | ||||
|     styles: CoreH5PDependencyAsset[]; // CSS files.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Content data, including main library data. | ||||
|  */ | ||||
| export type CoreH5PContentData = { | ||||
|     id: number; // The id of the content.
 | ||||
|     params: string; // The content in json format.
 | ||||
|     embedType: string; // Embed type to use.
 | ||||
|     disable: number; // H5P Button display options.
 | ||||
|     folderName: string; // Name of the folder that contains the contents.
 | ||||
|     title: string; // Main library's title.
 | ||||
|     slug: string; // Lib title and ID slugified.
 | ||||
|     filtered: string; // Filtered version of json_content.
 | ||||
|     libraryMajorVersion: number; // Main library's major version.
 | ||||
|     libraryMinorVersion: number; // Main library's minor version.
 | ||||
|     metadata: any; // Content metadata.
 | ||||
|     library: { // Main library data.
 | ||||
|         id: number; // The id of the library.
 | ||||
|         name: string; // The library machine name.
 | ||||
|         majorVersion: number; // Major version.
 | ||||
|         minorVersion: number; // Minor version.
 | ||||
|         embedTypes: string; // List of supported embed types.
 | ||||
|         fullscreen: number; // Display fullscreen button.
 | ||||
|     }; | ||||
|     dependencies?: {[key: string]: CoreH5PContentDepsTreeDependency}; // Dependencies. Calculated in filterParameters.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Content dependency data. | ||||
|  */ | ||||
| export type CoreH5PContentDependencyData = { | ||||
|     libraryId: number; // The id of the library if it is an existing library.
 | ||||
|     machineName: string; // The library machineName.
 | ||||
|     majorVersion: number; // The The library's majorVersion.
 | ||||
|     minorVersion: number; // The The library's minorVersion.
 | ||||
|     patchVersion: number; // The The library's patchVersion.
 | ||||
|     preloadedJs?: string | string[]; // Comma separated string with js file paths. If already parsed, list of paths.
 | ||||
|     preloadedCss?: string | string[]; // Comma separated string with css file paths. If already parsed, list of paths.
 | ||||
|     dropCss?: string; // CSV of machine names.
 | ||||
|     dependencyType: string; // The dependency type.
 | ||||
|     path?: string; // Path to the dependency. Calculated in getDependenciesFiles.
 | ||||
|     version?: string; // Version of the dependency. Calculated in getDependenciesFiles.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data for each content dependency in the dependency tree. | ||||
|  */ | ||||
| export type CoreH5PContentDepsTreeDependency = { | ||||
|     library: CoreH5PLibraryData | CoreH5PLibraryAddonData; // Library data.
 | ||||
|     type: string; // Dependency type.
 | ||||
|     weight?: number; // An integer determining the order of the libraries when they are loaded.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library data. | ||||
|  */ | ||||
| export type CoreH5PLibraryData = { | ||||
|     libraryId: number; // The id of the library.
 | ||||
|     title: string; // The human readable name of this library.
 | ||||
|     machineName: string; // The library machine name.
 | ||||
|     majorVersion: number; // Major version.
 | ||||
|     minorVersion: number; // Minor version.
 | ||||
|     patchVersion: number; // Patch version.
 | ||||
|     runnable: number; // Can this library be started by the module? I.e. not a dependency.
 | ||||
|     fullscreen: number; // Display fullscreen button.
 | ||||
|     embedTypes: string; // List of supported embed types.
 | ||||
|     preloadedJs?: string; // Comma separated list of scripts to load.
 | ||||
|     preloadedCss?: string; // Comma separated list of stylesheets to load.
 | ||||
|     dropLibraryCss?: string; // List of libraries that should not have CSS included if this library is used. Comma separated list.
 | ||||
|     semantics?: any; // The semantics definition. If it's a string, it's in json format.
 | ||||
|     preloadedDependencies: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     dynamicDependencies: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
|     editorDependencies: CoreH5PLibraryBasicData[]; // Dependencies.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library basic data. | ||||
|  */ | ||||
| export type CoreH5PLibraryBasicData = { | ||||
|     machineName: string; // The library machine name.
 | ||||
|     majorVersion: number; // Major version.
 | ||||
|     minorVersion: number; // Minor version.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * "Addon" data (library). | ||||
|  */ | ||||
| export type CoreH5PLibraryAddonData = { | ||||
|     libraryId: number; // The id of the library.
 | ||||
|     machineName: string; // The library machine name.
 | ||||
|     majorVersion: number; // Major version.
 | ||||
|     minorVersion: number; // Minor version.
 | ||||
|     patchVersion: number; // Patch version.
 | ||||
|     preloadedJs?: string; // Comma separated list of scripts to load.
 | ||||
|     preloadedCss?: string; // Comma separated list of stylesheets to load.
 | ||||
|     addTo?: any; // Plugin configuration data.
 | ||||
| }; | ||||
							
								
								
									
										457
									
								
								src/core/h5p/classes/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										457
									
								
								src/core/h5p/classes/file-storage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,457 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile } from '@providers/file'; | ||||
| import { CoreFilepool } from '@providers/filepool'; | ||||
| import { CoreSites } from '@providers/sites'; | ||||
| import { CoreMimetypeUtils } from '@providers/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreH5PProvider } from '../providers/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDependencyAsset, CoreH5PContentDependencyData, CoreH5PDependenciesFiles } from './core'; | ||||
| import { CoreH5PLibrariesCachedAssetsDBData } from './framework'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's implementation of H5PFileStorage. | ||||
|  */ | ||||
| export class CoreH5PFileStorage { | ||||
| 
 | ||||
|     static CACHED_ASSETS_FOLDER_NAME = 'cachedassets'; | ||||
| 
 | ||||
|     /** | ||||
|      * Will concatenate all JavaScrips and Stylesheets into two files in order to improve page performance. | ||||
|      * | ||||
|      * @param files A set of all the assets required for content to display. | ||||
|      * @param key Hashed key for cached asset. | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async cacheAssets(files: CoreH5PDependenciesFiles, key: string, folderName: string, siteId: string): Promise<void> { | ||||
| 
 | ||||
|         const cachedAssetsPath = this.getCachedAssetsFolderPath(folderName, siteId); | ||||
| 
 | ||||
|         // Treat each type in the assets.
 | ||||
|         await Promise.all(Object.keys(files).map(async (type) => { | ||||
| 
 | ||||
|             const assets: CoreH5PDependencyAsset[] = files[type]; | ||||
| 
 | ||||
|             if (!assets || !assets.length) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Create new file for cached assets.
 | ||||
|             const fileName = key + '.' + (type == 'scripts' ? 'js' : 'css'); | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsPath, fileName); | ||||
| 
 | ||||
|             // Store concatenated content.
 | ||||
|             const content = await this.concatenateFiles(assets, type); | ||||
| 
 | ||||
|             await CoreFile.instance.writeFile(path, content); | ||||
| 
 | ||||
|             // Now update the files data.
 | ||||
|             files[type] = [ | ||||
|                 { | ||||
|                     path: CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, fileName), | ||||
|                     version: '' | ||||
|                 } | ||||
|             ]; | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds all files of a type into one file. | ||||
|      * | ||||
|      * @param assets A list of files. | ||||
|      * @param type The type of files in assets. Either 'scripts' or 'styles' | ||||
|      * @return Promise resolved with all of the files content in one string. | ||||
|      */ | ||||
|     protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> { | ||||
|         const basePath = CoreFile.instance.convertFileSrc(CoreFile.instance.getBasePathInstant()); | ||||
|         let content = ''; | ||||
| 
 | ||||
|         for (const i in assets) { | ||||
|             const asset = assets[i]; | ||||
| 
 | ||||
|             let fileContent: string = await CoreFile.instance.readFile(asset.path); | ||||
| 
 | ||||
|             if (type == 'scripts') { | ||||
|                 // No need to treat scripts, just append the content.
 | ||||
|                 content += fileContent + ';\n'; | ||||
|             } else { | ||||
|                 // Rewrite relative URLs used inside stylesheets.
 | ||||
|                 const matches = fileContent.match(/url\([\'"]?([^"\')]+)[\'"]?\)/ig); | ||||
|                 const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes.
 | ||||
|                 const treated = {}; | ||||
| 
 | ||||
|                 if (matches && matches.length) { | ||||
|                     matches.forEach((match) => { | ||||
|                         let url = match.replace(/(url\(['"]?|['"]?\)$)/ig, ''); | ||||
| 
 | ||||
|                         if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) { | ||||
|                             return; // Not relative or already treated, skip.
 | ||||
|                         } | ||||
| 
 | ||||
|                         const pathSplit = assetPath.split('/'); | ||||
|                         treated[url] = url; | ||||
| 
 | ||||
|                         /* Find "../" in the URL. If it exists, we have to remove "../" and switch the last folder in the | ||||
|                            filepath for the first folder in the url. */ | ||||
|                         if (url.match(/^\.\.\//)) { | ||||
|                             const urlSplit = url.split('/').filter((i) => { | ||||
|                                 return i; // Remove empty values.
 | ||||
|                             }); | ||||
| 
 | ||||
|                             // Remove the file name from the asset path.
 | ||||
|                             pathSplit.pop(); | ||||
| 
 | ||||
|                             // Remove the first element from the file URL: ../ .
 | ||||
|                             urlSplit.shift(); | ||||
| 
 | ||||
|                             // Put the url's first folder into the asset path.
 | ||||
|                             pathSplit[pathSplit.length - 1] = urlSplit[0]; | ||||
|                             urlSplit.shift(); | ||||
| 
 | ||||
|                             // Create the new URL and replace it in the file contents.
 | ||||
|                             url = pathSplit.join('/') + '/' + urlSplit.join('/'); | ||||
| 
 | ||||
|                         } else { | ||||
|                             pathSplit[pathSplit.length - 1] = url; // Put the whole path to the end of the asset path.
 | ||||
|                             url = pathSplit.join('/'); | ||||
|                         } | ||||
| 
 | ||||
|                         fileContent = fileContent.replace(new RegExp(CoreTextUtils.instance.escapeForRegex(match), 'g'), | ||||
|                                     'url("' + CoreTextUtils.instance.concatenatePaths(basePath, url) + '")'); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 content += fileContent + '\n'; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete cached assets from file system. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteCachedAssets(removedEntries: CoreH5PLibrariesCachedAssetsDBData[], siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
|         const promises = []; | ||||
| 
 | ||||
|         removedEntries.forEach((entry) => { | ||||
| 
 | ||||
|             const cachedAssetsFolder = this.getCachedAssetsFolderPath(entry.foldername, site.getId()); | ||||
| 
 | ||||
|             ['js', 'css'].forEach((type) => { | ||||
|                 const path = CoreTextUtils.instance.concatenatePaths(cachedAssetsFolder, entry.hash + '.' + type); | ||||
| 
 | ||||
|                 promises.push(CoreFile.instance.removeFile(path)); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreUtils.instance.allPromises(promises); | ||||
|         } catch (error) { | ||||
|             // Ignore errors, maybe there's no cached asset of some type.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a content folder from the file system. | ||||
|      * | ||||
|      * @param folderName Folder name of the content. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentFolder(folderName: string, siteId?: string): Promise<void> { | ||||
|         await CoreFile.instance.removeDir(this.getContentFolderPath(folderName, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content indexes from filesystem. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentIndex(folderName: string, siteId?: string): Promise<void> { | ||||
|         await CoreFile.instance.removeFile(this.getContentIndexPath(folderName, siteId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content indexes from filesystem. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentIndexesForLibrary(libraryId: number, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
| 
 | ||||
|         // Get the folder names of all the packages that use this library.
 | ||||
|         const query = 'SELECT DISTINCT hc.foldername ' + | ||||
|                     'FROM ' + CoreH5PProvider.CONTENTS_LIBRARIES_TABLE + ' hcl ' + | ||||
|                     'JOIN ' + CoreH5PProvider.CONTENT_TABLE + ' hc ON hcl.h5pid = hc.id ' + | ||||
|                     'WHERE hcl.libraryid = ?'; | ||||
|         const queryArgs = []; | ||||
| 
 | ||||
|         queryArgs.push(libraryId); | ||||
| 
 | ||||
|         const result = await db.execute(query, queryArgs); | ||||
| 
 | ||||
|         await Array.from(result.rows).map(async (entry: {foldername: string}) => { | ||||
|             try { | ||||
|                 // Delete the index.html.
 | ||||
|                 await CoreFile.instance.removeFile(this.getContentIndexPath(entry.foldername, site.getId())); | ||||
|             } catch (error) { | ||||
|                 // Ignore errors.
 | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a library from the file system. | ||||
|      * | ||||
|      * @param libraryData The library data. | ||||
|      * @param folderName Folder name. If not provided, it will be calculated. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryFolder(libraryData: any, folderName?: string, siteId?: string): Promise<void> { | ||||
|         await CoreFile.instance.removeDir(this.getLibraryFolderPath(libraryData, siteId, folderName)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Will check if there are cache assets available for content. | ||||
|      * | ||||
|      * @param key Hashed key for cached asset | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     async getCachedAssets(key: string): Promise<{scripts?: CoreH5PDependencyAsset[], styles?: CoreH5PDependencyAsset[]}> { | ||||
| 
 | ||||
|         // Get JS and CSS cached assets if they exist.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.getCachedAsset(key, '.js'), | ||||
|             this.getCachedAsset(key, '.css'), | ||||
|         ]); | ||||
| 
 | ||||
|         const files = { | ||||
|             scripts: results[0], | ||||
|             styles: results[1], | ||||
|         }; | ||||
| 
 | ||||
|         return files.scripts || files.styles ? files : null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a cached asset file exists and, if so, return its data. | ||||
|      * | ||||
|      * @param key Key of the cached asset. | ||||
|      * @param extension Extension of the file to get. | ||||
|      * @return Promise resolved with the list of assets (only one), undefined if not found. | ||||
|      */ | ||||
|     protected async getCachedAsset(key: string, extension: string): Promise<CoreH5PDependencyAsset[]> { | ||||
| 
 | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME, key + extension); | ||||
| 
 | ||||
|             const size = await CoreFile.instance.getFileSize(path); | ||||
| 
 | ||||
|             if (size > 0) { | ||||
|                 return [ | ||||
|                     { | ||||
|                         path: path, | ||||
|                         version: '', | ||||
|                     }, | ||||
|                 ]; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             // Not found, nothing to do.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get relative path to a content cached assets. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the content the assets belong to. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Path. | ||||
|      */ | ||||
|     getCachedAssetsFolderPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths( | ||||
|                 this.getContentFolderPath(folderName, siteId), CoreH5PFileStorage.CACHED_ASSETS_FOLDER_NAME); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a content folder name given the package URL. | ||||
|      * | ||||
|      * @param fileUrl Package URL. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved with the folder name. | ||||
|      */ | ||||
|     async getContentFolderNameByUrl(fileUrl: string, siteId: string): Promise<string> { | ||||
|         const path = await CoreFilepool.instance.getFilePathByUrl(siteId, fileUrl); | ||||
| 
 | ||||
|         const fileAndDir = CoreFile.instance.getFileAndDirectoryFromPath(path); | ||||
| 
 | ||||
|         return CoreMimetypeUtils.instance.removeExtension(fileAndDir.name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a package content path. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getContentFolderPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths( | ||||
|                 this.getExternalH5PFolderPath(siteId), 'packages/' + folderName + '/content'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the content index file. | ||||
|      * | ||||
|      * @param fileUrl URL of the H5P package. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. | ||||
|      */ | ||||
|     async getContentIndexFileUrl(fileUrl: string, siteId?: string): Promise<string> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const folderName = await this.getContentFolderNameByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         const file = await CoreFile.instance.getFile(this.getContentIndexPath(folderName, siteId)); | ||||
| 
 | ||||
|         return file.toURL(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to a content index. | ||||
|      * | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getContentIndexPath(folderName: string, siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getContentFolderPath(folderName, siteId), 'index.html'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the folder that contains the H5P core libraries. | ||||
|      * | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getCoreH5PPath(): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getWWWPath(), '/h5p/'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the path to the dependency. | ||||
|      * | ||||
|      * @param dependency Dependency library. | ||||
|      * @return The path to the dependency library | ||||
|      */ | ||||
|     getDependencyPath(dependency: CoreH5PContentDependencyData): string { | ||||
|         return 'libraries/' + dependency.machineName + '-' + dependency.majorVersion + '.' + dependency.minorVersion; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get path to the folder containing H5P files extracted from packages. | ||||
|      * | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getExternalH5PFolderPath(siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(CoreFile.instance.getSiteFolder(siteId), 'h5p'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get libraries folder path. | ||||
|      * | ||||
|      * @param siteId The site ID. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getLibrariesFolderPath(siteId: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getExternalH5PFolderPath(siteId), 'libraries'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library's folder path. | ||||
|      * | ||||
|      * @param libraryData The library data. | ||||
|      * @param siteId The site ID. | ||||
|      * @param folderName Folder name. If not provided, it will be calculated. | ||||
|      * @return Folder path. | ||||
|      */ | ||||
|     getLibraryFolderPath(libraryData: any, siteId: string, folderName?: string): string { | ||||
|         if (!folderName) { | ||||
|             folderName = CoreH5PCore.libraryToString(libraryData, true); | ||||
|         } | ||||
| 
 | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.getLibrariesFolderPath(siteId), folderName); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save the content in filesystem. | ||||
|      * | ||||
|      * @param contentPath Path to the current content folder (tmp). | ||||
|      * @param folderName Name to put to the content folder. | ||||
|      * @param siteId Site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveContent(contentPath: string, folderName: string, siteId: string): Promise<void> { | ||||
|         const folderPath = this.getContentFolderPath(folderName, siteId); | ||||
| 
 | ||||
|         // Delete existing content for this package.
 | ||||
|         try { | ||||
|             await CoreFile.instance.removeDir(folderPath); | ||||
|         } catch (error) { | ||||
|             // Ignore errors, maybe it doesn't exist.
 | ||||
|         } | ||||
| 
 | ||||
|         // Copy the new one.
 | ||||
|         await CoreFile.instance.moveDir(contentPath, folderPath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save a library in filesystem. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibrary(libraryData: any, siteId?: string): Promise<void> { | ||||
|         const folderPath = this.getLibraryFolderPath(libraryData, siteId); | ||||
| 
 | ||||
|         // Delete existing library version.
 | ||||
|         try { | ||||
|             await CoreFile.instance.removeDir(folderPath); | ||||
|         } catch (error) { | ||||
|             // Ignore errors, maybe it doesn't exist.
 | ||||
|         } | ||||
| 
 | ||||
|         // Copy the new one.
 | ||||
|         await CoreFile.instance.moveDir(libraryData.uploadDirectory, folderPath, true); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										902
									
								
								src/core/h5p/classes/framework.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										902
									
								
								src/core/h5p/classes/framework.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,902 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreSites } from '@providers/sites'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreH5P, CoreH5PProvider } from '../providers/h5p'; | ||||
| import { | ||||
|     CoreH5PCore, CoreH5PDisplayOptionBehaviour, CoreH5PContentDependencyData, CoreH5PLibraryData, CoreH5PLibraryAddonData, | ||||
|     CoreH5PContentDepsTreeDependency | ||||
| } from './core'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's implementation of H5PFrameworkInterface. | ||||
|  */ | ||||
| export class CoreH5PFramework { | ||||
| 
 | ||||
|     /** | ||||
|      * Will clear filtered params for all the content that uses the specified libraries. | ||||
|      * This means that the content dependencies will have to be rebuilt and the parameters re-filtered. | ||||
|      * | ||||
|      * @param libraryIds Array of library ids. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async clearFilteredParameters(libraryIds: number[], siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         if (!libraryIds || !libraryIds.length) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const whereAndParams = db.getInOrEqual(libraryIds); | ||||
|         whereAndParams[0] = 'mainlibraryid ' + whereAndParams[0]; | ||||
| 
 | ||||
|         return db.updateRecordsWhere(CoreH5PProvider.CONTENT_TABLE, { filtered: null }, whereAndParams[0], whereAndParams[1]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete cached assets from DB. | ||||
|      * | ||||
|      * @param libraryId Library identifier. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the removed entries. | ||||
|      */ | ||||
|     async deleteCachedAssets(libraryId: number, siteId?: string): Promise<CoreH5PLibrariesCachedAssetsDBData[]> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // Get all the hashes that use this library.
 | ||||
|         const entries = await db.getRecords(CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, {libraryid: libraryId}); | ||||
| 
 | ||||
|         const hashes = entries.map((entry) => entry.hash); | ||||
| 
 | ||||
|         if (hashes.length) { | ||||
|             // Delete the entries from DB.
 | ||||
|             await db.deleteRecordsList(CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, 'hash', hashes); | ||||
|         } | ||||
| 
 | ||||
|         return entries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete content data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentData(id: number, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|             // Delete the content data.
 | ||||
|             db.deleteRecords(CoreH5PProvider.CONTENT_TABLE, {id: id}), | ||||
| 
 | ||||
|             // Remove content library dependencies.
 | ||||
|             this.deleteLibraryUsage(id, siteId), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete library data from DB. | ||||
|      * | ||||
|      * @param id Library ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibrary(id: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(CoreH5PProvider.LIBRARIES_TABLE, {id: id}); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all dependencies belonging to given library. | ||||
|      * | ||||
|      * @param libraryId Library ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryDependencies(libraryId: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE, {libraryid: libraryId}); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete what libraries a content item is using. | ||||
|      * | ||||
|      * @param id Package ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteLibraryUsage(id: number, siteId?: string): Promise<void> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await db.deleteRecords(CoreH5PProvider.CONTENTS_LIBRARIES_TABLE, {h5pid: id}); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all conent data from DB. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the list of content data. | ||||
|      */ | ||||
|     async getAllContentData(siteId?: string): Promise<CoreH5PContentDBData[]> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getAllRecords(CoreH5PProvider.CONTENT_TABLE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get conent data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async getContentData(id: number, siteId?: string): Promise<CoreH5PContentDBData> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         return db.getRecord(CoreH5PProvider.CONTENT_TABLE, {id: id}); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get conent data from DB. | ||||
|      * | ||||
|      * @param fileUrl H5P file URL. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async getContentDataByUrl(fileUrl: string, siteId?: string): Promise<CoreH5PContentDBData> { | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
| 
 | ||||
|         // Try to use the folder name, it should be more reliable than the URL.
 | ||||
|         const folderName = await CoreH5P.instance.h5pCore.h5pFS.getContentFolderNameByUrl(fileUrl, site.getId()); | ||||
| 
 | ||||
|         try { | ||||
|             const contentData = await db.getRecord(CoreH5PProvider.CONTENT_TABLE, {foldername: folderName}); | ||||
| 
 | ||||
|             return contentData; | ||||
|         } catch (error) { | ||||
|             // Cannot get folder name, the h5p file was probably deleted. Just use the URL.
 | ||||
|             return db.getRecord(CoreH5PProvider.CONTENT_TABLE, {fileurl: fileUrl}); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the latest library version. | ||||
|      * | ||||
|      * @param machineName The library's machine name. | ||||
|      * @return Promise resolved with the latest library version data. | ||||
|      */ | ||||
|     async getLatestLibraryVersion(machineName: string, siteId?: string): Promise<CoreH5PLibraryDBData> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         try { | ||||
|             const records = await db.getRecords(CoreH5PProvider.LIBRARIES_TABLE, {machinename: machineName}, | ||||
|                     'majorversion DESC, minorversion DESC, patchversion DESC', '*', 0, 1); | ||||
| 
 | ||||
|             if (records && records[0]) { | ||||
|                 return this.parseLibDBData(records[0]); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             // Library not found.
 | ||||
|         } | ||||
| 
 | ||||
|         throw `Missing required library: ${machineName}`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB. | ||||
|      * | ||||
|      * @param machineName Machine name. | ||||
|      * @param majorVersion Major version number. | ||||
|      * @param minorVersion Minor version number. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     protected async getLibrary(machineName: string, majorVersion?: string | number, minorVersion?: string | number, | ||||
|             siteId?: string): Promise<CoreH5PLibraryDBData> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const conditions = { | ||||
|             machinename: machineName, | ||||
|             majorversion: undefined, | ||||
|             minorversion: undefined, | ||||
|         }; | ||||
| 
 | ||||
|         if (typeof majorVersion != 'undefined') { | ||||
|             conditions.majorversion = majorVersion; | ||||
|         } | ||||
|         if (typeof minorVersion != 'undefined') { | ||||
|             conditions.minorversion = minorVersion; | ||||
|         } | ||||
| 
 | ||||
|         const libraries = await db.getRecords(CoreH5PProvider.LIBRARIES_TABLE, conditions); | ||||
| 
 | ||||
|         if (!libraries.length) { | ||||
|             throw 'Libary not found.'; | ||||
|         } | ||||
| 
 | ||||
|         return this.parseLibDBData(libraries[0]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     getLibraryByData(libraryData: any, siteId?: string): Promise<CoreH5PLibraryDBData> { | ||||
|         return this.getLibrary(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library data stored in DB by ID. | ||||
|      * | ||||
|      * @param id Library ID. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data, rejected if not found. | ||||
|      */ | ||||
|     async getLibraryById(id: number, siteId?: string): Promise<CoreH5PLibraryDBData> { | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const library = await db.getRecord(CoreH5PProvider.LIBRARIES_TABLE, {id: id}); | ||||
| 
 | ||||
|         return this.parseLibDBData(library); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library ID. If not found, return null. | ||||
|      * | ||||
|      * @param machineName Machine name. | ||||
|      * @param majorVersion Major version number. | ||||
|      * @param minorVersion Minor version number. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library ID, null if not found. | ||||
|      */ | ||||
|     async getLibraryId(machineName: string, majorVersion?: string | number, minorVersion?: string | number, siteId?: string) | ||||
|             : Promise<number> { | ||||
| 
 | ||||
|         try { | ||||
|             const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId); | ||||
| 
 | ||||
|             return (library && library.id) || null; | ||||
|         } catch (error) { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a library ID. If not found, return null. | ||||
|      * | ||||
|      * @param libraryData Library data. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library ID, null if not found. | ||||
|      */ | ||||
|     getLibraryIdByData(libraryData: any, siteId?: string): Promise<number> { | ||||
|         return this.getLibraryId(libraryData.machineName, libraryData.majorVersion, libraryData.minorVersion, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the default behaviour for the display option defined. | ||||
|      * | ||||
|      * @param name Identifier for the setting. | ||||
|      * @param defaultValue Optional default value if settings is not set. | ||||
|      * @return Return the value for this display option. | ||||
|      */ | ||||
|     getOption(name: string, defaultValue: any = false): any { | ||||
|         // For now, all them are disabled by default, so only will be rendered when defined in the display options.
 | ||||
|         return CoreH5PDisplayOptionBehaviour.CONTROLLED_BY_AUTHOR_DEFAULT_OFF; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the user has permission to execute an action. | ||||
|      * | ||||
|      * @param permission Permission to check. | ||||
|      * @param id H5P package id. | ||||
|      * @return Whether the user has permission to execute an action. | ||||
|      */ | ||||
|     hasPermission(permission: number, id: number): boolean { | ||||
|         // H5P capabilities have not been introduced.
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determines if content slug is used. | ||||
|      * | ||||
|      * @param slug The content slug. | ||||
|      * @return Whether the content slug is used | ||||
|      */ | ||||
|     isContentSlugAvailable(slug: string): boolean { | ||||
|         // By default the slug should be available as it's currently generated as a unique value for each h5p content.
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether a library is a patched version of the one installed. | ||||
|      * | ||||
|      * @param library Library to check. | ||||
|      * @param dbData Installed library. If not supplied it will be calculated. | ||||
|      * @return Promise resolved with boolean: whether it's a patched library. | ||||
|      */ | ||||
|     async isPatchedLibrary(library: any, dbData?: CoreH5PLibraryDBData): Promise<boolean> { | ||||
|         if (!dbData) { | ||||
|             dbData = await this.getLibraryByData(library); | ||||
|         } | ||||
| 
 | ||||
|         return library.patchVersion > dbData.patchversion; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert list of library parameter values to csv. | ||||
|      * | ||||
|      * @param libraryData Library data as found in library.json files. | ||||
|      * @param key Key that should be found in libraryData. | ||||
|      * @param searchParam The library parameter (Default: 'path'). | ||||
|      * @return Library parameter values separated by ', ' | ||||
|      */ | ||||
|     libraryParameterValuesToCsv(libraryData: any, key: string, searchParam: string = 'path'): string { | ||||
|         if (typeof libraryData[key] != 'undefined') { | ||||
|             const parameterValues = []; | ||||
| 
 | ||||
|             libraryData[key].forEach((file) => { | ||||
|                 for (const index in file) { | ||||
|                     if (index === searchParam) { | ||||
|                         parameterValues.push(file[index]); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             return parameterValues.join(','); | ||||
|         } | ||||
| 
 | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load addon libraries. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the addon libraries. | ||||
|      */ | ||||
|     async loadAddons(siteId?: string): Promise<CoreH5PLibraryAddonData[]> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const query = 'SELECT l1.id AS libraryId, l1.machinename AS machineName, ' + | ||||
|                         'l1.majorversion AS majorVersion, l1.minorversion AS minorVersion, ' + | ||||
|                         'l1.patchversion AS patchVersion, l1.addto AS addTo, ' + | ||||
|                         'l1.preloadedjs AS preloadedJs, l1.preloadedcss AS preloadedCss ' + | ||||
|                     'FROM ' + CoreH5PProvider.LIBRARIES_TABLE + ' l1 ' + | ||||
|                     'JOIN ' + CoreH5PProvider.LIBRARIES_TABLE + ' l2 ON l1.machinename = l2.machinename AND (' + | ||||
|                         'l1.majorversion < l2.majorversion OR (l1.majorversion = l2.majorversion AND ' + | ||||
|                         'l1.minorversion < l2.minorversion)) ' + | ||||
|                     'WHERE l1.addto IS NOT NULL AND l2.machinename IS NULL'; | ||||
| 
 | ||||
|         const result = await db.execute(query); | ||||
| 
 | ||||
|         const addons = []; | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             addons.push(this.parseLibAddonData(result.rows.item(i))); | ||||
|         } | ||||
| 
 | ||||
|         return addons; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load content data from DB. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param fileUrl H5P file URL. Required if id is not provided. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async loadContent(id?: number, fileUrl?: string, siteId?: string): Promise<CoreH5PFrameworkContentData> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         let contentData: CoreH5PContentDBData; | ||||
| 
 | ||||
|         if (id) { | ||||
|             contentData = await this.getContentData(id, siteId); | ||||
|         } else if (fileUrl) { | ||||
|             contentData = await this.getContentDataByUrl(fileUrl, siteId); | ||||
|         } else { | ||||
|             throw 'No id or fileUrl supplied to loadContent.'; | ||||
|         } | ||||
| 
 | ||||
|         // Load the main library data.
 | ||||
|         const libData = await this.getLibraryById(contentData.mainlibraryid, siteId); | ||||
| 
 | ||||
|         // Map the values to the names used by the H5P core (it's the same Moodle web does).
 | ||||
|         const content = { | ||||
|             id: contentData.id, | ||||
|             params: contentData.jsoncontent, | ||||
|             embedType: 'iframe', // Always use iframe.
 | ||||
|             disable: null, | ||||
|             folderName: contentData.foldername, | ||||
|             title: libData.title, | ||||
|             slug: CoreH5PCore.slugify(libData.title) + '-' + contentData.id, | ||||
|             filtered: contentData.filtered, | ||||
|             libraryId: libData.id, | ||||
|             libraryName: libData.machinename, | ||||
|             libraryMajorVersion: libData.majorversion, | ||||
|             libraryMinorVersion: libData.minorversion, | ||||
|             libraryEmbedTypes: libData.embedtypes, | ||||
|             libraryFullscreen: libData.fullscreen, | ||||
|             metadata: null, | ||||
|         }; | ||||
| 
 | ||||
|         const params = CoreTextUtils.instance.parseJSON(contentData.jsoncontent); | ||||
|         if (!params.metadata) { | ||||
|             params.metadata = {}; | ||||
|         } | ||||
|         content.metadata = params.metadata; | ||||
|         content.params = JSON.stringify(typeof params.params != 'undefined' && params.params != null ? params.params : params); | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load dependencies for the given content of the given type. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param type The dependency type. | ||||
|      * @return Content dependencies, indexed by machine name. | ||||
|      */ | ||||
|     async loadContentDependencies(id: number, type?: string, siteId?: string) | ||||
|             : Promise<{[machineName: string]: CoreH5PContentDependencyData}> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         let query = 'SELECT hl.id AS libraryId, hl.machinename AS machineName, ' + | ||||
|                         'hl.majorversion AS majorVersion, hl.minorversion AS minorVersion, ' + | ||||
|                         'hl.patchversion AS patchVersion, hl.preloadedcss AS preloadedCss, ' + | ||||
|                         'hl.preloadedjs AS preloadedJs, hcl.dropcss AS dropCss, ' + | ||||
|                         'hcl.dependencytype as dependencyType ' + | ||||
|                     'FROM ' + CoreH5PProvider.CONTENTS_LIBRARIES_TABLE + ' hcl ' + | ||||
|                     'JOIN ' + CoreH5PProvider.LIBRARIES_TABLE + ' hl ON hcl.libraryid = hl.id ' + | ||||
|                     'WHERE hcl.h5pid = ?'; | ||||
|         const queryArgs = []; | ||||
|         queryArgs.push(id); | ||||
| 
 | ||||
|         if (type) { | ||||
|             query += ' AND hcl.dependencytype = ?'; | ||||
|             queryArgs.push(type); | ||||
|         } | ||||
| 
 | ||||
|         query += ' ORDER BY hcl.weight'; | ||||
| 
 | ||||
|         const result = await db.execute(query, queryArgs); | ||||
| 
 | ||||
|         const dependencies = {}; | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             const dependency = result.rows.item(i); | ||||
| 
 | ||||
|             dependencies[dependency.machineName] = dependency; | ||||
|         } | ||||
| 
 | ||||
|         return dependencies; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads a library and its dependencies. | ||||
|      * | ||||
|      * @param machineName The library's machine name. | ||||
|      * @param majorVersion The library's major version. | ||||
|      * @param minorVersion The library's minor version. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the library data. | ||||
|      */ | ||||
|     async loadLibrary(machineName: string, majorVersion: number, minorVersion: number, siteId?: string) | ||||
|             : Promise<CoreH5PLibraryData> { | ||||
| 
 | ||||
|         // First get the library data from DB.
 | ||||
|         const library = await this.getLibrary(machineName, majorVersion, minorVersion, siteId); | ||||
| 
 | ||||
|         const libraryData: CoreH5PLibraryData = { | ||||
|             libraryId: library.id, | ||||
|             title: library.title, | ||||
|             machineName: library.machinename, | ||||
|             majorVersion: library.majorversion, | ||||
|             minorVersion: library.minorversion, | ||||
|             patchVersion: library.patchversion, | ||||
|             runnable: library.runnable, | ||||
|             fullscreen: library.fullscreen, | ||||
|             embedTypes: library.embedtypes, | ||||
|             preloadedJs: library.preloadedjs, | ||||
|             preloadedCss: library.preloadedcss, | ||||
|             dropLibraryCss: library.droplibrarycss, | ||||
|             semantics: library.semantics, | ||||
|             preloadedDependencies: [], | ||||
|             dynamicDependencies: [], | ||||
|             editorDependencies: [] | ||||
|         }; | ||||
| 
 | ||||
|         // Now get the dependencies.
 | ||||
|         const sql = 'SELECT hl.id, hl.machinename, hl.majorversion, hl.minorversion, hll.dependencytype ' + | ||||
|                 'FROM ' + CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE + ' hll ' + | ||||
|                 'JOIN ' + CoreH5PProvider.LIBRARIES_TABLE + ' hl ON hll.requiredlibraryid = hl.id ' + | ||||
|                 'WHERE hll.libraryid = ? ' + | ||||
|                 'ORDER BY hl.id ASC'; | ||||
| 
 | ||||
|         const sqlParams = [ | ||||
|             library.id, | ||||
|         ]; | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const result = await db.execute(sql, sqlParams); | ||||
| 
 | ||||
|         for (let i = 0; i < result.rows.length; i++) { | ||||
|             const dependency = result.rows.item(i); | ||||
|             const key = dependency.dependencytype + 'Dependencies'; | ||||
| 
 | ||||
|             libraryData[key].push({ | ||||
|                 machineName: dependency.machinename, | ||||
|                 majorVersion: dependency.majorversion, | ||||
|                 minorVersion: dependency.minorversion | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return libraryData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse library addon data. | ||||
|      * | ||||
|      * @param library Library addon data. | ||||
|      * @return Parsed library. | ||||
|      */ | ||||
|     parseLibAddonData(library: any): CoreH5PLibraryAddonData { | ||||
|         library.addto = CoreTextUtils.instance.parseJSON(library.addto, null); | ||||
| 
 | ||||
|         return library; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse library DB data. | ||||
|      * | ||||
|      * @param library Library DB data. | ||||
|      * @return Parsed library. | ||||
|      */ | ||||
|     protected parseLibDBData(library: any): CoreH5PLibraryDBData { | ||||
|         library.semantics = CoreTextUtils.instance.parseJSON(library.semantics, null); | ||||
|         library.addto = CoreTextUtils.instance.parseJSON(library.addto, null); | ||||
| 
 | ||||
|         return library; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resets marked user data for the given content. | ||||
|      * | ||||
|      * @param contentId Content ID. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async resetContentUserData(conentId: number, siteId?: string): Promise<void> { | ||||
|         // Currently, we do not store user data for a content.
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Stores hash keys for cached assets, aggregated JavaScripts and stylesheets, and connects it to libraries so that we | ||||
|      * know which cache file to delete when a library is updated. | ||||
|      * | ||||
|      * @param key Hash key for the given libraries. | ||||
|      * @param libraries List of dependencies used to create the key. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param siteId The site ID. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveCachedAssets(hash: string, dependencies: {[machineName: string]: CoreH5PContentDependencyData}, | ||||
|             folderName: string, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all(Object.keys(dependencies).map(async (key) => { | ||||
|             const data = { | ||||
|                 hash: key, | ||||
|                 libraryid: dependencies[key].libraryId, | ||||
|                 foldername: folderName, | ||||
|             }; | ||||
| 
 | ||||
|             await db.insertRecord(CoreH5PProvider.LIBRARIES_CACHEDASSETS_TABLE, data); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save library data in DB. | ||||
|      * | ||||
|      * @param libraryData Library data to save. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryData(libraryData: any, siteId?: string): Promise<void> { | ||||
|         // Some special properties needs some checking and converting before they can be saved.
 | ||||
|         const preloadedJS = this.libraryParameterValuesToCsv(libraryData, 'preloadedJs', 'path'); | ||||
|         const preloadedCSS = this.libraryParameterValuesToCsv(libraryData, 'preloadedCss', 'path'); | ||||
|         const dropLibraryCSS = this.libraryParameterValuesToCsv(libraryData, 'dropLibraryCss', 'machineName'); | ||||
| 
 | ||||
|         if (typeof libraryData.semantics == 'undefined') { | ||||
|             libraryData.semantics = ''; | ||||
|         } | ||||
|         if (typeof libraryData.fullscreen == 'undefined') { | ||||
|             libraryData.fullscreen = 0; | ||||
|         } | ||||
| 
 | ||||
|         let embedTypes = ''; | ||||
|         if (typeof libraryData.embedTypes != 'undefined') { | ||||
|             embedTypes = libraryData.embedTypes.join(', '); | ||||
|         } | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const db = site.getDb(); | ||||
|         const data = { | ||||
|             id: undefined, | ||||
|             title: libraryData.title, | ||||
|             machinename: libraryData.machineName, | ||||
|             majorversion: libraryData.majorVersion, | ||||
|             minorversion: libraryData.minorVersion, | ||||
|             patchversion: libraryData.patchVersion, | ||||
|             runnable: libraryData.runnable, | ||||
|             fullscreen: libraryData.fullscreen, | ||||
|             embedtypes: embedTypes, | ||||
|             preloadedjs: preloadedJS, | ||||
|             preloadedcss: preloadedCSS, | ||||
|             droplibrarycss: dropLibraryCSS, | ||||
|             semantics: typeof libraryData.semantics != 'undefined' ? JSON.stringify(libraryData.semantics) : null, | ||||
|             addto: typeof libraryData.addTo != 'undefined' ? JSON.stringify(libraryData.addTo) : null, | ||||
|         }; | ||||
| 
 | ||||
|         if (libraryData.libraryId) { | ||||
|             data.id = libraryData.libraryId; | ||||
|         } | ||||
| 
 | ||||
|         await db.insertRecord(CoreH5PProvider.LIBRARIES_TABLE, data); | ||||
| 
 | ||||
|         if (!data.id) { | ||||
|             // New library. Get its ID.
 | ||||
|             const entry = await db.getRecord(CoreH5PProvider.LIBRARIES_TABLE, data); | ||||
| 
 | ||||
|             libraryData.libraryId = entry.id; | ||||
|         } else { | ||||
|             // Updated libary. Remove old dependencies.
 | ||||
|             await this.deleteLibraryDependencies(data.id, site.getId()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save what libraries a library is depending on. | ||||
|      * | ||||
|      * @param libraryId Library Id for the library we're saving dependencies for. | ||||
|      * @param dependencies List of dependencies as associative arrays containing machineName, majorVersion, minorVersion. | ||||
|      * @param dependencytype The type of dependency. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryDependencies(libraryId: number, dependencies: any[], dependencyType: string, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         await Promise.all(dependencies.map(async (dependency) => { | ||||
|             // Get the ID of the library.
 | ||||
|             const dependencyId = await this.getLibraryIdByData(dependency, siteId); | ||||
| 
 | ||||
|             // Create the relation.
 | ||||
|             const entry = { | ||||
|                 libraryid: libraryId, | ||||
|                 requiredlibraryid: dependencyId, | ||||
|                 dependencytype: dependencyType | ||||
|             }; | ||||
| 
 | ||||
|             await db.insertRecord(CoreH5PProvider.LIBRARY_DEPENDENCIES_TABLE, entry); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Saves what libraries the content uses. | ||||
|      * | ||||
|      * @param id Id identifying the package. | ||||
|      * @param librariesInUse List of libraries the content uses. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async saveLibraryUsage(id: number, librariesInUse: {[key: string]: CoreH5PContentDepsTreeDependency}, siteId?: string) | ||||
|             : Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // Calculate the CSS to drop.
 | ||||
|         const dropLibraryCssList = {}; | ||||
| 
 | ||||
|         for (const key in librariesInUse) { | ||||
|             const dependency = librariesInUse[key]; | ||||
| 
 | ||||
|             if ((<CoreH5PLibraryData> dependency.library).dropLibraryCss) { | ||||
|                 const split = (<CoreH5PLibraryData> dependency.library).dropLibraryCss.split(', '); | ||||
| 
 | ||||
|                 split.forEach((css) => { | ||||
|                     dropLibraryCssList[css] = css; | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Now save the uusage.
 | ||||
|         await Promise.all(Object.keys(librariesInUse).map((key) => { | ||||
|             const dependency = librariesInUse[key]; | ||||
|             const data = { | ||||
|                 h5pid: id, | ||||
|                 libraryId: dependency.library.libraryId, | ||||
|                 dependencytype: dependency.type, | ||||
|                 dropcss: dropLibraryCssList[dependency.library.machineName] ? 1 : 0, | ||||
|                 weight: dependency.weight, | ||||
|             }; | ||||
| 
 | ||||
|             return db.insertRecord(CoreH5PProvider.CONTENTS_LIBRARIES_TABLE, data); | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save content data in DB and clear cache. | ||||
|      * | ||||
|      * @param content Content to save. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param fileUrl The online URL of the package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with content ID. | ||||
|      */ | ||||
|     async updateContent(content: any, folderName: string, fileUrl: string, siteId?: string): Promise<number> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         // If the libraryid declared in the package is empty, get the latest version.
 | ||||
|         if (typeof content.library.libraryId == 'undefined') { | ||||
|             const mainLibrary = await this.getLatestLibraryVersion(content.library.machineName, siteId); | ||||
| 
 | ||||
|             content.library.libraryId = mainLibrary.id; | ||||
|         } | ||||
| 
 | ||||
|         const data: CoreH5PContentDBData = { | ||||
|             id: undefined, | ||||
|             jsoncontent: content.params, | ||||
|             mainlibraryid: content.library.libraryId, | ||||
|             timemodified: Date.now(), | ||||
|             filtered: null, | ||||
|             foldername: folderName, | ||||
|             fileurl: fileUrl, | ||||
|             timecreated: undefined, | ||||
|         }; | ||||
| 
 | ||||
|         if (typeof content.id != 'undefined') { | ||||
|             data.id = content.id; | ||||
|         } else { | ||||
|             data.timecreated = data.timemodified; | ||||
|         } | ||||
| 
 | ||||
|         await db.insertRecord(CoreH5PProvider.CONTENT_TABLE, data); | ||||
| 
 | ||||
|         if (!data.id) { | ||||
|             // New content. Get its ID.
 | ||||
|             const entry = await db.getRecord(CoreH5PProvider.CONTENT_TABLE, data); | ||||
| 
 | ||||
|             content.id = entry.id; | ||||
|         } | ||||
| 
 | ||||
|         return content.id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This will update selected fields on the given content. | ||||
|      * | ||||
|      * @param id Content identifier. | ||||
|      * @param fields Object with the fields to update. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      */ | ||||
|     async updateContentFields(id: number, fields: {[name: string]: any}, siteId?: string): Promise<void> { | ||||
| 
 | ||||
|         const db = await CoreSites.instance.getSiteDb(siteId); | ||||
| 
 | ||||
|         const data = Object.assign({}, fields); | ||||
|         delete data.slug; // Slug isn't stored in DB.
 | ||||
| 
 | ||||
|         await db.updateRecords(CoreH5PProvider.CONTENT_TABLE, data, {id: id}); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Content data returned by loadContent. | ||||
|  */ | ||||
| export type CoreH5PFrameworkContentData = { | ||||
|     id: number; // The id of the content.
 | ||||
|     params: string; // The content in json format.
 | ||||
|     embedType: string; // Embed type to use.
 | ||||
|     disable: number; // H5P Button display options.
 | ||||
|     folderName: string; // Name of the folder that contains the contents.
 | ||||
|     title: string; // Main library's title.
 | ||||
|     slug: string; // Lib title and ID slugified.
 | ||||
|     filtered: string; // Filtered version of json_content.
 | ||||
|     libraryId: number; // Main library's ID.
 | ||||
|     libraryName: string; // Main library's machine name.
 | ||||
|     libraryMajorVersion: number; // Main library's major version.
 | ||||
|     libraryMinorVersion: number; // Main library's minor version.
 | ||||
|     libraryEmbedTypes: string; // Main library's list of supported embed types.
 | ||||
|     libraryFullscreen: number; // Main library's display fullscreen button.
 | ||||
|     metadata: any; // Content metadata.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Content data stored in DB. | ||||
|  */ | ||||
| export type CoreH5PContentDBData = { | ||||
|     id: number; // The id of the content.
 | ||||
|     jsoncontent: string; // The content in json format.
 | ||||
|     mainlibraryid: number; // The library we first instantiate for this node.
 | ||||
|     foldername: string; // Name of the folder that contains the contents.
 | ||||
|     fileurl: string; // The online URL of the H5P package.
 | ||||
|     filtered: string; // Filtered version of json_content.
 | ||||
|     timecreated: number; // Time created.
 | ||||
|     timemodified: number; // Time modified.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library data stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryDBData = { | ||||
|     id: number; // The id of the library.
 | ||||
|     machinename: string; // The library machine name.
 | ||||
|     title: string; // The human readable name of this library.
 | ||||
|     majorversion: number; // Major version.
 | ||||
|     minorversion: number; // Minor version.
 | ||||
|     patchversion: number; // Patch version.
 | ||||
|     runnable: number; // Can this library be started by the module? I.e. not a dependency.
 | ||||
|     fullscreen: number; // Display fullscreen button.
 | ||||
|     embedtypes: string; // List of supported embed types.
 | ||||
|     preloadedjs?: string; // Comma separated list of scripts to load.
 | ||||
|     preloadedcss?: string; // Comma separated list of stylesheets to load.
 | ||||
|     droplibrarycss?: string; // List of libraries that should not have CSS included if this library is used. Comma separated list.
 | ||||
|     semantics?: any; // The semantics definition.
 | ||||
|     addto?: any; // Plugin configuration data.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library dependencies stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibraryDependenciesDBData = { | ||||
|     id: number; // Id.
 | ||||
|     libraryid: number; // The id of an H5P library.
 | ||||
|     requiredlibraryid: number; // The dependent library to load.
 | ||||
|     dependencytype: string; // Type: preloaded, dynamic, or editor.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Library cached assets stored in DB. | ||||
|  */ | ||||
| export type CoreH5PLibrariesCachedAssetsDBData = { | ||||
|     id: number; // Id.
 | ||||
|     libraryid: number; // The id of an H5P library.
 | ||||
|     hash: string; // The hash to identify the cached asset.
 | ||||
|     foldername: string; // Name of the folder that contains the contents.
 | ||||
| }; | ||||
							
								
								
									
										145
									
								
								src/core/h5p/classes/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/core/h5p/classes/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile, CoreFileProvider } from '@providers/file'; | ||||
| import { CoreSites } from '@providers/sites'; | ||||
| import { CoreMimetypeUtils } from '@providers/utils/mimetype'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreH5P } from '../providers/h5p'; | ||||
| import { CoreH5PCore } from './core'; | ||||
| import { FileEntry } from '@ionic-native/file'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's H5P helper class. | ||||
|  */ | ||||
| export class CoreH5PHelper { | ||||
| 
 | ||||
|     /** | ||||
|      * Get the core H5P assets, including all core H5P JavaScript and CSS. | ||||
|      * | ||||
|      * @return Array core H5P assets. | ||||
|      */ | ||||
|     static async getCoreAssets(siteId?: string): Promise<{settings: any, cssRequires: string[], jsRequires: string[]}> { | ||||
| 
 | ||||
|         // Get core settings.
 | ||||
|         const settings = await CoreH5PHelper.getCoreSettings(siteId); | ||||
| 
 | ||||
|         settings.core = { | ||||
|             styles: [], | ||||
|             scripts: [] | ||||
|         }; | ||||
|         settings.loadedJs = []; | ||||
|         settings.loadedCss = []; | ||||
| 
 | ||||
|         const libUrl = CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(); | ||||
|         const cssRequires: string[] = []; | ||||
|         const jsRequires: string[] = []; | ||||
| 
 | ||||
|         // Add core stylesheets.
 | ||||
|         CoreH5PCore.STYLES.forEach((style) => { | ||||
|             settings.core.styles.push(libUrl + style); | ||||
|             cssRequires.push(libUrl + style); | ||||
|         }); | ||||
| 
 | ||||
|         // Add core JavaScript.
 | ||||
|         CoreH5PCore.getScripts().forEach((script) => { | ||||
|             settings.core.scripts.push(script); | ||||
|             jsRequires.push(script); | ||||
|         }); | ||||
| 
 | ||||
|         return {settings: settings, cssRequires: cssRequires, jsRequires: jsRequires}; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the settings needed by the H5P library. | ||||
|      * | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the settings. | ||||
|      */ | ||||
|     static async getCoreSettings(siteId?: string): Promise<any> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const basePath = CoreFile.instance.getBasePathInstant(); | ||||
|         const ajaxPaths = { | ||||
|             xAPIResult: '', | ||||
|             contentUserData: '', | ||||
|         }; | ||||
| 
 | ||||
|         return { | ||||
|             baseUrl: CoreFile.instance.getWWWPath(), | ||||
|             url: CoreFile.instance.convertFileSrc(CoreTextUtils.instance.concatenatePaths( | ||||
|                     basePath, CoreH5P.instance.h5pCore.h5pFS.getExternalH5PFolderPath(site.getId()))), | ||||
|             urlLibraries: CoreFile.instance.convertFileSrc(CoreTextUtils.instance.concatenatePaths( | ||||
|                     basePath, CoreH5P.instance.h5pCore.h5pFS.getLibrariesFolderPath(site.getId()))), | ||||
|             postUserStatistics: false, | ||||
|             ajax: ajaxPaths, | ||||
|             saveFreq: false, | ||||
|             siteUrl: site.getURL(), | ||||
|             l10n: { | ||||
|                 H5P: CoreH5P.instance.h5pCore.getLocalization(), | ||||
|             }, | ||||
|             user: [], | ||||
|             hubIsEnabled: false, | ||||
|             reportingIsEnabled: false, | ||||
|             crossorigin: null, | ||||
|             libraryConfig: null, | ||||
|             pluginCacheBuster: '', | ||||
|             libraryUrl: CoreTextUtils.instance.concatenatePaths(CoreH5P.instance.h5pCore.h5pFS.getCoreH5PPath(), 'js'), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Extract and store an H5P file. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param fileUrl The file URL used to download the file. | ||||
|      * @param file The file entry of the downloaded file. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     static async saveH5P(fileUrl: string, file: FileEntry, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Unzip the file.
 | ||||
|         const folderName = CoreMimetypeUtils.instance.removeExtension(file.name); | ||||
|         const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName); | ||||
| 
 | ||||
|         // Unzip the file.
 | ||||
|         await CoreFile.instance.unzipFile(file.toURL(), destFolder); | ||||
| 
 | ||||
|         try { | ||||
|             // Read the contents of the unzipped dir, process them and store them.
 | ||||
|             const contents = await CoreFile.instance.getDirectoryContents(destFolder); | ||||
| 
 | ||||
|             const filesData = await CoreH5P.instance.h5pValidator.processH5PFiles(destFolder, contents); | ||||
| 
 | ||||
|             const content = await CoreH5P.instance.h5pStorage.savePackage(filesData, folderName, fileUrl, false, siteId); | ||||
| 
 | ||||
|             // Create the content player.
 | ||||
|             const contentData = await CoreH5P.instance.h5pCore.loadContent(content.id, undefined, siteId); | ||||
| 
 | ||||
|             const embedType = CoreH5PCore.determineEmbedType(contentData.embedType, contentData.library.embedTypes); | ||||
| 
 | ||||
|             await CoreH5P.instance.h5pPlayer.createContentIndex(content.id, fileUrl, contentData, embedType, siteId); | ||||
|         } finally { | ||||
|             // Remove tmp folder.
 | ||||
|             try { | ||||
|                 await CoreFile.instance.removeDir(destFolder); | ||||
|             } catch (error) { | ||||
|                 // Ignore errors, it will be deleted eventually.
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										39
									
								
								src/core/h5p/classes/metadata.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/core/h5p/classes/metadata.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PMetadata class. | ||||
|  */ | ||||
| export class CoreH5PMetadata { | ||||
| 
 | ||||
|     /** | ||||
|      * The metadataSettings field in libraryJson uses 1 for true and 0 for false. | ||||
|      * Here we are converting these to booleans, and also doing JSON encoding. | ||||
|      * | ||||
|      * @param metadataSettings Settings. | ||||
|      * @return Stringified settings. | ||||
|      */ | ||||
|     static boolifyAndEncodeSettings(metadataSettings: any): string { | ||||
|         // Convert metadataSettings values to boolean.
 | ||||
|         if (typeof metadataSettings.disable != 'undefined') { | ||||
|             metadataSettings.disable = metadataSettings.disable === 1; | ||||
|         } | ||||
|         if (typeof metadataSettings.disableExtraTitleField != 'undefined') { | ||||
|             metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1; | ||||
|         } | ||||
| 
 | ||||
|         return JSON.stringify(metadataSettings); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										326
									
								
								src/core/h5p/classes/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								src/core/h5p/classes/player.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,326 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile } from '@providers/file'; | ||||
| import { CoreSites } from '@providers/sites'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreUrlUtils } from '@providers/utils/url'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreH5P } from '../providers/h5p'; | ||||
| import { CoreH5PCore, CoreH5PDisplayOptions, CoreH5PContentData, CoreH5PDependenciesFiles } from './core'; | ||||
| import { CoreH5PHelper } from './helper'; | ||||
| import { CoreH5PStorage } from './storage'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to Moodle's H5P player class. | ||||
|  */ | ||||
| export class CoreH5PPlayer { | ||||
| 
 | ||||
|     constructor(protected h5pCore: CoreH5PCore, | ||||
|             protected h5pStorage: CoreH5PStorage) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Create the index.html to render an H5P package. | ||||
|      * Part of the code of this function is equivalent to Moodle's add_assets_to_page function. | ||||
|      * | ||||
|      * @param id Content ID. | ||||
|      * @param h5pUrl The URL of the H5P file. | ||||
|      * @param content Content data. | ||||
|      * @param embedType Embed type. The app will always use 'iframe'. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the URL of the index file. | ||||
|      */ | ||||
|     async createContentIndex(id: number, h5pUrl: string, content: CoreH5PContentData, embedType: string, siteId?: string) | ||||
|             : Promise<string> { | ||||
| 
 | ||||
|         const site = await CoreSites.instance.getSite(siteId); | ||||
| 
 | ||||
|         const contentId = this.getContentId(id); | ||||
|         const basePath = CoreFile.instance.getBasePathInstant(); | ||||
|         const contentUrl = CoreFile.instance.convertFileSrc(CoreTextUtils.instance.concatenatePaths( | ||||
|                     basePath, this.h5pCore.h5pFS.getContentFolderPath(content.folderName, site.getId()))); | ||||
| 
 | ||||
|         // Create the settings needed for the content.
 | ||||
|         const contentSettings = { | ||||
|             library: CoreH5PCore.libraryToString(content.library), | ||||
|             fullScreen: content.library.fullscreen, | ||||
|             exportUrl: '', // We'll never display the download button, so we don't need the exportUrl.
 | ||||
|             embedCode: this.getEmbedCode(site.getURL(), h5pUrl, true), | ||||
|             resizeCode: this.getResizeCode(), | ||||
|             title: content.slug, | ||||
|             displayOptions: {}, | ||||
|             url: this.getEmbedUrl(site.getURL(), h5pUrl), | ||||
|             contentUrl: contentUrl, | ||||
|             metadata: content.metadata, | ||||
|             contentUserData: [ | ||||
|                 { | ||||
|                     state: '{}' | ||||
|                 } | ||||
|             ] | ||||
|         }; | ||||
| 
 | ||||
|         // Get the core H5P assets, needed by the H5P classes to render the H5P content.
 | ||||
|         const result = await this.getAssets(id, content, embedType, site.getId()); | ||||
| 
 | ||||
|         result.settings.contents[contentId] = Object.assign(result.settings.contents[contentId], contentSettings); | ||||
| 
 | ||||
|         const indexPath = this.h5pCore.h5pFS.getContentIndexPath(content.folderName, siteId); | ||||
|         let html = '<html><head><title>' + content.title + '</title>' + | ||||
|                 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'; | ||||
| 
 | ||||
|         // Include the required CSS.
 | ||||
|         result.cssRequires.forEach((cssUrl) => { | ||||
|             html += '<link rel="stylesheet" type="text/css" href="' + cssUrl + '">'; | ||||
|         }); | ||||
| 
 | ||||
|         // Add the settings.
 | ||||
|         html += '<script type="text/javascript">var H5PIntegration = ' + | ||||
|                 JSON.stringify(result.settings).replace(/\//g, '\\/') + '</script>'; | ||||
| 
 | ||||
|         // Add our own script to handle the display options.
 | ||||
|         html += '<script type="text/javascript" src="' + CoreTextUtils.instance.concatenatePaths( | ||||
|                 this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/displayoptions.js') + '"></script>'; | ||||
| 
 | ||||
|         html += '</head><body>'; | ||||
| 
 | ||||
|         // Include the required JS at the beginning of the body, like Moodle web does.
 | ||||
|         // Load the embed.js to allow communication with the parent window.
 | ||||
|         html += '<script type="text/javascript" src="' + | ||||
|                 CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'moodle/js/embed.js') + '"></script>'; | ||||
| 
 | ||||
|         result.jsRequires.forEach((jsUrl) => { | ||||
|             html += '<script type="text/javascript" src="' + jsUrl + '"></script>'; | ||||
|         }); | ||||
| 
 | ||||
|         html += '<div class="h5p-iframe-wrapper">' + | ||||
|                 '<iframe id="h5p-iframe-' + id + '" class="h5p-iframe" data-content-id="' + id + '"' + | ||||
|                     'style="height:1px; min-width: 100%" src="about:blank"></iframe>' + | ||||
|                 '</div></body>'; | ||||
| 
 | ||||
|         const fileEntry = await CoreFile.instance.writeFile(indexPath, html); | ||||
| 
 | ||||
|         return fileEntry.toURL(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all content indexes of all sites from filesystem. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteAllContentIndexes(): Promise<void> { | ||||
|         const siteIds = await CoreSites.instance.getSitesIds(); | ||||
| 
 | ||||
|         await Promise.all(siteIds.map((siteId) => this.deleteAllContentIndexesForSite(siteId))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all content indexes for a certain site from filesystem. | ||||
|      * | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteAllContentIndexesForSite(siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const records = await this.h5pCore.h5pFramework.getAllContentData(siteId); | ||||
| 
 | ||||
|         await Promise.all(records.map(async (record) => { | ||||
|             try { | ||||
|                 await this.h5pCore.h5pFS.deleteContentIndex(record.foldername, siteId); | ||||
|             } catch (err) { | ||||
|                 // Ignore errors, maybe the file doesn't exist.
 | ||||
|             } | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete all package content data. | ||||
|      * | ||||
|      * @param fileUrl File URL. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async deleteContentByUrl(fileUrl: string, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises([ | ||||
|             this.h5pCore.h5pFramework.deleteContentData(data.id, siteId), | ||||
| 
 | ||||
|             this.h5pCore.h5pFS.deleteContentFolder(data.foldername, siteId), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the assets of a package. | ||||
|      * | ||||
|      * @param id Content id. | ||||
|      * @param content Content data. | ||||
|      * @param embedType Embed type. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the assets. | ||||
|      */ | ||||
|     protected async getAssets(id: number, content: CoreH5PContentData, embedType: string, siteId?: string) | ||||
|             : Promise<{settings: any, cssRequires: string[], jsRequires: string[]}> { | ||||
| 
 | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Get core assets.
 | ||||
|         const coreAssets = await CoreH5PHelper.getCoreAssets(siteId); | ||||
| 
 | ||||
|         const contentId = this.getContentId(id); | ||||
|         const settings = coreAssets.settings; | ||||
|         settings.contents = settings.contents || {}; | ||||
|         settings.contents[contentId] = settings.contents[contentId] || {}; | ||||
| 
 | ||||
|         settings.moodleLibraryPaths = await this.h5pCore.getDependencyRoots(id); | ||||
| 
 | ||||
|         /* The filterParameters function should be called before getting the dependency files because it rebuilds content | ||||
|            dependency cache. */ | ||||
|         settings.contents[contentId].jsonContent = await this.h5pCore.filterParameters(content, siteId); | ||||
| 
 | ||||
|         const files = await this.getDependencyFiles(id, content.folderName, siteId); | ||||
| 
 | ||||
|         // H5P checks the embedType in here, but we'll always use iframe so there's no need to do it.
 | ||||
|         // JavaScripts and stylesheets will be loaded through h5p.js.
 | ||||
|         settings.contents[contentId].scripts = this.h5pCore.getAssetsUrls(files.scripts); | ||||
|         settings.contents[contentId].styles = this.h5pCore.getAssetsUrls(files.styles); | ||||
| 
 | ||||
|         return { | ||||
|             settings: settings, | ||||
|             cssRequires: coreAssets.cssRequires, | ||||
|             jsRequires: coreAssets.jsRequires, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the identifier for the H5P content. This identifier is different than the ID stored in the DB. | ||||
|      * | ||||
|      * @param id Package ID. | ||||
|      * @return Content identifier. | ||||
|      */ | ||||
|     protected getContentId(id: number): string { | ||||
|         return 'cid-' + id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the content index file. | ||||
|      * | ||||
|      * @param fileUrl URL of the H5P package. | ||||
|      * @param urlParams URL params. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the file URL if exists, rejected otherwise. | ||||
|      */ | ||||
|     async getContentIndexFileUrl(fileUrl: string, urlParams?: {[name: string]: string}, siteId?: string): Promise<string> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         const path = await this.h5pCore.h5pFS.getContentIndexFileUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         // Add display options to the URL.
 | ||||
|         const data = await this.h5pCore.h5pFramework.getContentDataByUrl(fileUrl, siteId); | ||||
| 
 | ||||
|         const options = this.h5pCore.fixDisplayOptions(this.getDisplayOptionsFromUrlParams(urlParams), data.id); | ||||
| 
 | ||||
|         return CoreUrlUtils.instance.addParamsToUrl(path, options, undefined, true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Finds library dependencies files of a certain package. | ||||
|      * | ||||
|      * @param id Content id. | ||||
|      * @param folderName Name of the folder of the content. | ||||
|      * @param siteId The site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the files. | ||||
|      */ | ||||
|     protected async getDependencyFiles(id: number, folderName: string, siteId?: string): Promise<CoreH5PDependenciesFiles> { | ||||
| 
 | ||||
|         const preloadedDeps = await CoreH5P.instance.h5pCore.loadContentDependencies(id, 'preloaded', siteId); | ||||
| 
 | ||||
|         return this.h5pCore.getDependenciesFiles(preloadedDeps, folderName, | ||||
|                 this.h5pCore.h5pFS.getExternalH5PFolderPath(siteId), siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get display options from a URL params. | ||||
|      * | ||||
|      * @param params URL params. | ||||
|      * @return Display options as object. | ||||
|      */ | ||||
|     getDisplayOptionsFromUrlParams(params: {[name: string]: string}): CoreH5PDisplayOptions { | ||||
|         const displayOptions: CoreH5PDisplayOptions = {}; | ||||
| 
 | ||||
|         if (!params) { | ||||
|             return displayOptions; | ||||
|         } | ||||
| 
 | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] = | ||||
|                 CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD]); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] = | ||||
|                 CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_EMBED]); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT] = | ||||
|                 CoreUtils.instance.isTrueOrOne(params[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]); | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_FRAME] = displayOptions[CoreH5PCore.DISPLAY_OPTION_DOWNLOAD] || | ||||
|                 displayOptions[CoreH5PCore.DISPLAY_OPTION_EMBED] || displayOptions[CoreH5PCore.DISPLAY_OPTION_COPYRIGHT]; | ||||
|         displayOptions[CoreH5PCore.DISPLAY_OPTION_ABOUT] = | ||||
|                 !!this.h5pCore.h5pFramework.getOption(CoreH5PCore.DISPLAY_OPTION_ABOUT, true); | ||||
| 
 | ||||
|         return displayOptions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Embed code for settings. | ||||
|      * | ||||
|      * @param siteUrl The site URL. | ||||
|      * @param h5pUrl The URL of the .h5p file. | ||||
|      * @param embedEnabled Whether the option to embed the H5P content is enabled. | ||||
|      * @return The HTML code to reuse this H5P content in a different place. | ||||
|      */ | ||||
|     protected getEmbedCode(siteUrl: string, h5pUrl: string, embedEnabled?: boolean): string { | ||||
|         if (!embedEnabled) { | ||||
|             return ''; | ||||
|         } | ||||
| 
 | ||||
|         return '<iframe src="' + this.getEmbedUrl(siteUrl, h5pUrl) + '" allowfullscreen="allowfullscreen"></iframe>'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the encoded URL for embeding an H5P content. | ||||
|      * | ||||
|      * @param siteUrl The site URL. | ||||
|      * @param h5pUrl The URL of the .h5p file. | ||||
|      * @return The embed URL. | ||||
|      */ | ||||
|     protected getEmbedUrl(siteUrl: string, h5pUrl: string): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(siteUrl, '/h5p/embed.php') + '?url=' + h5pUrl; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resizing script for settings. | ||||
|      * | ||||
|      * @return The HTML code with the resize script. | ||||
|      */ | ||||
|     protected getResizeCode(): string { | ||||
|         return '<script src="' + this.getResizerScriptUrl() + '"></script>'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the URL to the resizer script. | ||||
|      * | ||||
|      * @return URL. | ||||
|      */ | ||||
|     getResizerScriptUrl(): string { | ||||
|         return CoreTextUtils.instance.concatenatePaths(this.h5pCore.h5pFS.getCoreH5PPath(), 'js/h5p-resizer.js'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										202
									
								
								src/core/h5p/classes/storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								src/core/h5p/classes/storage.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,202 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile, CoreFileProvider } from '@providers/file'; | ||||
| import { CoreSites } from '@providers/sites'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreUtils } from '@providers/utils/utils'; | ||||
| import { CoreH5PCore } from './core'; | ||||
| import { CoreH5PFramework, CoreH5PLibraryDBData } from './framework'; | ||||
| import { CoreH5PMetadata } from './metadata'; | ||||
| import { CoreH5PMainJSONFilesData } from './validator'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PStorage class. | ||||
|  */ | ||||
| export class CoreH5PStorage { | ||||
| 
 | ||||
|     constructor(protected h5pCore: CoreH5PCore, | ||||
|             protected h5pFramework: CoreH5PFramework) { } | ||||
| 
 | ||||
|     /** | ||||
|      * Save libraries. | ||||
|      * | ||||
|      * @param librariesJsonData Data about libraries. | ||||
|      * @param folderName Name of the folder of the H5P package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async saveLibraries(librariesJsonData: any, folderName: string, siteId?: string): Promise<void> { | ||||
|         siteId = siteId || CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // First of all, try to create the dir where the libraries are stored. This way we don't have to do it for each lib.
 | ||||
|         await CoreFile.instance.createDir(this.h5pCore.h5pFS.getLibrariesFolderPath(siteId)); | ||||
| 
 | ||||
|         const libraryIds = []; | ||||
| 
 | ||||
|         // Go through libraries that came with this package.
 | ||||
|         await Promise.all(Object.keys(librariesJsonData).map(async (libString) => { | ||||
|             const libraryData = librariesJsonData[libString]; | ||||
| 
 | ||||
|             // Find local library identifier.
 | ||||
|             let dbData: CoreH5PLibraryDBData; | ||||
| 
 | ||||
|             try { | ||||
|                 dbData = await this.h5pFramework.getLibraryByData(libraryData); | ||||
|             } catch (error) { | ||||
|                 // Not found.
 | ||||
|             } | ||||
| 
 | ||||
|             if (dbData) { | ||||
|                 // Library already installed.
 | ||||
|                 libraryData.libraryId = dbData.id; | ||||
| 
 | ||||
|                 if (!this.h5pFramework.isPatchedLibrary(libraryData, dbData)) { | ||||
|                     // Same or older version, no need to save.
 | ||||
|                     libraryData.saveDependencies = false; | ||||
| 
 | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             libraryData.saveDependencies = true; | ||||
| 
 | ||||
|             // Convert metadataSettings values to boolean and json_encode it before saving.
 | ||||
|             libraryData.metadataSettings = libraryData.metadataSettings ? | ||||
|                     CoreH5PMetadata.boolifyAndEncodeSettings(libraryData.metadataSettings) : null; | ||||
| 
 | ||||
|             // Save the library data in DB.
 | ||||
|             await this.h5pFramework.saveLibraryData(libraryData, siteId); | ||||
| 
 | ||||
|             // Now save it in FS.
 | ||||
|             try { | ||||
|                 await this.h5pCore.h5pFS.saveLibrary(libraryData, siteId); | ||||
|             } catch (error) { | ||||
|                 // An error occurred, delete the DB data because the lib FS data has been deleted.
 | ||||
|                 await this.h5pFramework.deleteLibrary(libraryData.libraryId, siteId); | ||||
| 
 | ||||
|                 throw error; | ||||
|             } | ||||
| 
 | ||||
|             if (typeof libraryData.libraryId != 'undefined') { | ||||
|                 const promises = []; | ||||
| 
 | ||||
|                 // Remove all indexes of contents that use this library.
 | ||||
|                 promises.push(this.h5pCore.h5pFS.deleteContentIndexesForLibrary(libraryData.libraryId, siteId)); | ||||
| 
 | ||||
|                 if (this.h5pCore.aggregateAssets) { | ||||
|                     // Remove cached assets that use this library.
 | ||||
|                     const removedEntries = await this.h5pFramework.deleteCachedAssets(libraryData.libraryId, siteId); | ||||
| 
 | ||||
|                     await this.h5pCore.h5pFS.deleteCachedAssets(removedEntries, siteId); | ||||
|                 } | ||||
| 
 | ||||
|                 await CoreUtils.instance.allPromises(promises); | ||||
|             } | ||||
|         })); | ||||
| 
 | ||||
|         // Go through the libraries again to save dependencies.
 | ||||
|         await Promise.all(Object.keys(librariesJsonData).map(async (libString) => { | ||||
|             const libraryData = librariesJsonData[libString]; | ||||
| 
 | ||||
|             if (!libraryData.saveDependencies) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             libraryIds.push(libraryData.libraryId); | ||||
| 
 | ||||
|             // Remove any old dependencies.
 | ||||
|             await this.h5pFramework.deleteLibraryDependencies(libraryData.libraryId, siteId); | ||||
| 
 | ||||
|             // Insert the different new ones.
 | ||||
|             const promises = []; | ||||
| 
 | ||||
|             if (typeof libraryData.preloadedDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libraryData.libraryId, libraryData.preloadedDependencies, | ||||
|                         'preloaded')); | ||||
|             } | ||||
|             if (typeof libraryData.dynamicDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libraryData.libraryId, libraryData.dynamicDependencies, | ||||
|                         'dynamic')); | ||||
|             } | ||||
|             if (typeof libraryData.editorDependencies != 'undefined') { | ||||
|                 promises.push(this.h5pFramework.saveLibraryDependencies(libraryData.libraryId, libraryData.editorDependencies, | ||||
|                         'editor')); | ||||
|             } | ||||
| 
 | ||||
|             await Promise.all(promises); | ||||
|         })); | ||||
| 
 | ||||
|         // Make sure dependencies, parameter filtering and export files get regenerated for content who uses these libraries.
 | ||||
|         if (libraryIds.length) { | ||||
|             await this.h5pFramework.clearFilteredParameters(libraryIds, siteId); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save content data in DB and clear cache. | ||||
|      * | ||||
|      * @param content Content to save. | ||||
|      * @param folderName The name of the folder that contains the H5P. | ||||
|      * @param fileUrl The online URL of the package. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @return Promise resolved with the content data. | ||||
|      */ | ||||
|     async savePackage(data: CoreH5PMainJSONFilesData, folderName: string, fileUrl: string, skipContent?: boolean, siteId?: string) | ||||
|             : Promise<any> { | ||||
| 
 | ||||
|         if (this.h5pCore.mayUpdateLibraries()) { | ||||
|             // Save the libraries that were processed.
 | ||||
|             await this.saveLibraries(data.librariesJsonData, folderName, siteId); | ||||
|         } | ||||
| 
 | ||||
|         const content: any = {}; | ||||
| 
 | ||||
|         if (!skipContent) { | ||||
|             // Find main library version.
 | ||||
|            if (data.mainJsonData.preloadedDependencies) { | ||||
|                const mainLib = data.mainJsonData.preloadedDependencies.find((dependency) => { | ||||
|                    return dependency.machineName === data.mainJsonData.mainLibrary; | ||||
|                }); | ||||
| 
 | ||||
|                if (mainLib) { | ||||
|                     const id = await this.h5pFramework.getLibraryIdByData(mainLib); | ||||
| 
 | ||||
|                     mainLib.libraryId = id; | ||||
|                     content.library = mainLib; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             content.params = JSON.stringify(data.contentJsonData); | ||||
| 
 | ||||
|             // Save the content data in DB.
 | ||||
|             await this.h5pCore.saveContent(content, folderName, fileUrl, siteId); | ||||
| 
 | ||||
|             // Save the content files in their right place in FS.
 | ||||
|             const destFolder = CoreTextUtils.instance.concatenatePaths(CoreFileProvider.TMPFOLDER, 'h5p/' + folderName); | ||||
|             const contentPath = CoreTextUtils.instance.concatenatePaths(destFolder, 'content'); | ||||
| 
 | ||||
|             try { | ||||
|                 await this.h5pCore.h5pFS.saveContent(contentPath, folderName, siteId); | ||||
|             } catch (error) { | ||||
|                 // An error occurred, delete the DB data because the content files have been deleted.
 | ||||
|                 await this.h5pFramework.deleteContentData(content.id, siteId); | ||||
| 
 | ||||
|                 throw error; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return content; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										220
									
								
								src/core/h5p/classes/validator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/core/h5p/classes/validator.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,220 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { CoreFile, CoreFileProvider } from '@providers/file'; | ||||
| import { CoreTextUtils } from '@providers/utils/text'; | ||||
| import { CoreH5PCore } from './core'; | ||||
| 
 | ||||
| /** | ||||
|  * Equivalent to H5P's H5PValidator class. | ||||
|  */ | ||||
| export class CoreH5PValidator { | ||||
| 
 | ||||
|     /** | ||||
|      * Get library data. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param libDir Directory where the library files are. | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with library data. | ||||
|      */ | ||||
|     protected async getLibraryData(libDir: DirectoryEntry, libPath: string): Promise<any> { | ||||
| 
 | ||||
|         // Read the required files.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.readLibraryJsonFile(libPath), | ||||
|             this.readLibrarySemanticsFile(libPath), | ||||
|             this.readLibraryLanguageFiles(libPath), | ||||
|             this.libraryHasIcon(libPath), | ||||
|         ]); | ||||
| 
 | ||||
|         const libraryData = results[0]; | ||||
|         libraryData.semantics = results[1]; | ||||
|         libraryData.language = results[2]; | ||||
|         libraryData.hasIcon = results[3]; | ||||
| 
 | ||||
|         return libraryData; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get library data for all libraries in an H5P package. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @param entries List of files and directories in the root of the package folder. | ||||
|      * @retun Promise resolved with the libraries data. | ||||
|      */ | ||||
|     protected async getPackageLibrariesData(packagePath: string, entries: (DirectoryEntry | FileEntry)[]) | ||||
|             : Promise<{[libString: string]: any}> { | ||||
| 
 | ||||
|         const libraries: {[libString: string]: any} = {}; | ||||
| 
 | ||||
|         await Promise.all(entries.map(async (entry) => { | ||||
|             if (entry.name[0] == '.' || entry.name[0] == '_' || entry.name == 'content' || entry.isFile) { | ||||
|                 // Skip files, the content folder and any folder starting with a . or _.
 | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const libDirPath = CoreTextUtils.instance.concatenatePaths(packagePath, entry.name); | ||||
| 
 | ||||
|             const libraryData = await this.getLibraryData(<DirectoryEntry> entry, libDirPath); | ||||
| 
 | ||||
|             libraryData.uploadDirectory = libDirPath; | ||||
|             libraries[CoreH5PCore.libraryToString(libraryData)] = libraryData; | ||||
|         })); | ||||
| 
 | ||||
|         return libraries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the library has an icon file. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with boolean: whether the library has an icon file. | ||||
|      */ | ||||
|     protected async libraryHasIcon(libPath: string): Promise<boolean> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(libPath, 'icon.svg'); | ||||
| 
 | ||||
|         try { | ||||
|             // Check if the file exists.
 | ||||
|             await CoreFile.instance.getFile(path); | ||||
| 
 | ||||
|             return true; | ||||
|         } catch (error) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process libraries from an H5P library, getting the required data to save them. | ||||
|      * This code is inspired on the isValidPackage function in Moodle's H5PValidator. | ||||
|      * This function won't validate most things because it should've been done by the server already. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @param entries List of files and directories in the root of the package folder. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async processH5PFiles(packagePath: string, entries: (DirectoryEntry | FileEntry)[]): Promise<CoreH5PMainJSONFilesData> { | ||||
| 
 | ||||
|         // Read the needed files.
 | ||||
|         const results = await Promise.all([ | ||||
|             this.readH5PJsonFile(packagePath), | ||||
|             this.readH5PContentJsonFile(packagePath), | ||||
|             this.getPackageLibrariesData(packagePath, entries), | ||||
|         ]); | ||||
| 
 | ||||
|         return { | ||||
|             librariesJsonData: results[2], | ||||
|             mainJsonData: results[0], | ||||
|             contentJsonData: results[1], | ||||
|         }; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read content.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readH5PContentJsonFile(packagePath: string): Promise<any> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'content/content.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile(path, CoreFileProvider.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read h5p.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param packagePath The path to the package folder. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readH5PJsonFile(packagePath: string): Promise<any> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(packagePath, 'h5p.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile(path, CoreFileProvider.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read library.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected readLibraryJsonFile(libPath: string): Promise<any> { | ||||
|         const path = CoreTextUtils.instance.concatenatePaths(libPath, 'library.json'); | ||||
| 
 | ||||
|         return CoreFile.instance.readFile(path, CoreFileProvider.FORMATJSON); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read all language files and return their contents indexed by language code. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the language data. | ||||
|      */ | ||||
|     protected async readLibraryLanguageFiles(libPath: string): Promise<{[code: string]: any}> { | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(libPath, 'language'); | ||||
|             const langIndex: {[code: string]: any} = {}; | ||||
| 
 | ||||
|             // Read all the files in the language directory.
 | ||||
|             const entries = await CoreFile.instance.getDirectoryContents(path); | ||||
| 
 | ||||
|             await Promise.all(entries.map(async (entry) => { | ||||
|                 const langFilePath = CoreTextUtils.instance.concatenatePaths(path, entry.name); | ||||
| 
 | ||||
|                 try { | ||||
|                     const langFileData = await CoreFile.instance.readFile(langFilePath, CoreFileProvider.FORMATJSON); | ||||
| 
 | ||||
|                     const parts = entry.name.split('.'); // The language code is in parts[0].
 | ||||
|                     langIndex[parts[0]] = langFileData; | ||||
|                 } catch (error) { | ||||
|                     // Ignore this language.
 | ||||
|                 } | ||||
|             })); | ||||
| 
 | ||||
|             return langIndex; | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             // Probably doesn't exist, ignore.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read semantics.json file and return its parsed contents. | ||||
|      * | ||||
|      * @param libPath Path to the directory where the library files are. | ||||
|      * @return Promise resolved with the parsed file contents. | ||||
|      */ | ||||
|     protected async readLibrarySemanticsFile(libPath: string): Promise<any> { | ||||
|         try { | ||||
|             const path = CoreTextUtils.instance.concatenatePaths(libPath, 'semantics.json'); | ||||
| 
 | ||||
|             const result = await CoreFile.instance.readFile(path, CoreFileProvider.FORMATJSON); | ||||
| 
 | ||||
|             return result; | ||||
|         } catch (error) { | ||||
|             // Probably doesn't exist, ignore.
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Data of the main JSON H5P files. | ||||
|  */ | ||||
| export type CoreH5PMainJSONFilesData = { | ||||
|     contentJsonData: any; // Contents of content.json file.
 | ||||
|     librariesJsonData: {[libString: string]: any}; // Some data about each library.
 | ||||
|     mainJsonData: any; // Contents of h5p.json file.
 | ||||
| }; | ||||
| @ -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<void> { | ||||
|         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<void> { | ||||
|         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<any> { | ||||
|         if (this.urlParams && this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() && | ||||
|                 this.appProvider.isOnline()) { | ||||
|     protected async attemptDownloadInBg(): Promise<void> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         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; | ||||
|         }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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: [] | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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<any> { | ||||
|         // 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<CoreWSExternalFile> { | ||||
|         return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId); | ||||
|         return CoreH5P.instance.getTrustedH5PFile(file.fileurl, {}, false, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -75,7 +66,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler { | ||||
|         const urls = []; | ||||
| 
 | ||||
|         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<number> { | ||||
|         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<number> { | ||||
|         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<boolean> { | ||||
|         return this.h5pProvider.canGetTrustedH5PFileInSite(); | ||||
|         return CoreH5P.instance.canGetTrustedH5PFileInSite(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -122,12 +115,12 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler { | ||||
|      * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed. | ||||
|      */ | ||||
|     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<any> { | ||||
|         return this.h5pProvider.extractH5PFile(fileUrl, file, siteId); | ||||
|     treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<void> { | ||||
|         return CoreH5PHelper.saveH5P(fileUrl, file, siteId); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,429 +0,0 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| import { CoreH5PContentDependencyData, CoreH5PDependencyAsset } from './h5p'; | ||||
| import { Md5 } from 'ts-md5/dist/md5'; | ||||
| 
 | ||||
| /** | ||||
|  * Utils service with helper functions for H5P. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreH5PUtilsProvider { | ||||
| 
 | ||||
|     // Map to slugify characters.
 | ||||
|     protected SLUGIFY_MAP = { | ||||
|         æ: 'ae', | ||||
|         ø: 'oe', | ||||
|         ö: 'o', | ||||
|         ó: 'o', | ||||
|         ô: 'o', | ||||
|         Ò: 'oe', | ||||
|         Õ: 'o', | ||||
|         Ý: 'o', | ||||
|         ý: 'y', | ||||
|         ÿ: 'y', | ||||
|         ā: 'y', | ||||
|         ă: 'a', | ||||
|         ą: 'a', | ||||
|         œ: 'a', | ||||
|         å: 'a', | ||||
|         ä: 'a', | ||||
|         á: 'a', | ||||
|         à: 'a', | ||||
|         â: 'a', | ||||
|         ã: 'a', | ||||
|         ç: 'c', | ||||
|         ć: 'c', | ||||
|         ĉ: 'c', | ||||
|         ċ: 'c', | ||||
|         č: 'c', | ||||
|         é: 'e', | ||||
|         è: 'e', | ||||
|         ê: 'e', | ||||
|         ë: 'e', | ||||
|         í: 'i', | ||||
|         ì: 'i', | ||||
|         î: 'i', | ||||
|         ï: 'i', | ||||
|         ú: 'u', | ||||
|         ñ: 'n', | ||||
|         ü: 'u', | ||||
|         ù: 'u', | ||||
|         û: 'u', | ||||
|         ß: 'es', | ||||
|         ď: 'd', | ||||
|         đ: 'd', | ||||
|         ē: 'e', | ||||
|         ĕ: 'e', | ||||
|         ė: 'e', | ||||
|         ę: 'e', | ||||
|         ě: 'e', | ||||
|         ĝ: 'g', | ||||
|         ğ: 'g', | ||||
|         ġ: 'g', | ||||
|         ģ: 'g', | ||||
|         ĥ: 'h', | ||||
|         ħ: 'h', | ||||
|         ĩ: 'i', | ||||
|         ī: 'i', | ||||
|         ĭ: 'i', | ||||
|         į: 'i', | ||||
|         ı: 'i', | ||||
|         ij: 'ij', | ||||
|         ĵ: 'j', | ||||
|         ķ: 'k', | ||||
|         ĺ: 'l', | ||||
|         ļ: 'l', | ||||
|         ľ: 'l', | ||||
|         ŀ: 'l', | ||||
|         ł: 'l', | ||||
|         ń: 'n', | ||||
|         ņ: 'n', | ||||
|         ň: 'n', | ||||
|         ʼn: 'n', | ||||
|         ō: 'o', | ||||
|         ŏ: 'o', | ||||
|         ő: 'o', | ||||
|         ŕ: 'r', | ||||
|         ŗ: 'r', | ||||
|         ř: 'r', | ||||
|         ś: 's', | ||||
|         ŝ: 's', | ||||
|         ş: 's', | ||||
|         š: 's', | ||||
|         ţ: 't', | ||||
|         ť: 't', | ||||
|         ŧ: 't', | ||||
|         ũ: 'u', | ||||
|         ū: 'u', | ||||
|         ŭ: 'u', | ||||
|         ů: 'u', | ||||
|         ű: 'u', | ||||
|         ų: 'u', | ||||
|         ŵ: 'w', | ||||
|         ŷ: 'y', | ||||
|         ź: 'z', | ||||
|         ż: 'z', | ||||
|         ž: 'z', | ||||
|         ſ: 's', | ||||
|         ƒ: 'f', | ||||
|         ơ: 'o', | ||||
|         ư: 'u', | ||||
|         ǎ: 'a', | ||||
|         ǐ: 'i', | ||||
|         ǒ: 'o', | ||||
|         ǔ: 'u', | ||||
|         ǖ: 'u', | ||||
|         ǘ: 'u', | ||||
|         ǚ: 'u', | ||||
|         ǜ: 'u', | ||||
|         ǻ: 'a', | ||||
|         ǽ: 'ae', | ||||
|         ǿ: 'oe' | ||||
|     }; | ||||
| 
 | ||||
|     constructor(private translate: TranslateService, | ||||
|             private textUtils: CoreTextUtilsProvider) { } | ||||
| 
 | ||||
|     /** | ||||
|      * The metadataSettings field in libraryJson uses 1 for true and 0 for false. | ||||
|      * Here we are converting these to booleans, and also doing JSON encoding. | ||||
|      * | ||||
|      * @param metadataSettings Settings. | ||||
|      * @return Stringified settings. | ||||
|      */ | ||||
|     boolifyAndEncodeMetadataSettings(metadataSettings: any): string { | ||||
|         // Convert metadataSettings values to boolean.
 | ||||
|         if (typeof metadataSettings.disable != 'undefined') { | ||||
|             metadataSettings.disable = metadataSettings.disable === 1; | ||||
|         } | ||||
|         if (typeof metadataSettings.disableExtraTitleField != 'undefined') { | ||||
|             metadataSettings.disableExtraTitleField = metadataSettings.disableExtraTitleField === 1; | ||||
|         } | ||||
| 
 | ||||
|         return JSON.stringify(metadataSettings); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determine the correct embed type to use. | ||||
|      * | ||||
|      * @param Embed type of the content. | ||||
|      * @param Embed type of the main library. | ||||
|      * @return Either 'div' or 'iframe'. | ||||
|      */ | ||||
|     determineEmbedType(contentEmbedType: string, libraryEmbedTypes: string): string { | ||||
|         // Detect content embed type.
 | ||||
|         let embedType = contentEmbedType.toLowerCase().indexOf('div') != -1 ? 'div' : 'iframe'; | ||||
| 
 | ||||
|         if (libraryEmbedTypes) { | ||||
|             // Check that embed type is available for library
 | ||||
|             const embedTypes = libraryEmbedTypes.toLowerCase(); | ||||
| 
 | ||||
|             if (embedTypes.indexOf(embedType) == -1) { | ||||
|                 // Not available, pick default.
 | ||||
|                 embedType = embedTypes.indexOf('div') != -1 ? 'div' : 'iframe'; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return embedType; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Combines path with version. | ||||
|      * | ||||
|      * @param assets List of assets to get their URLs. | ||||
|      * @param assetsFolderPath The path of the folder where the assets are. | ||||
|      * @return List of urls. | ||||
|      */ | ||||
|     getAssetsUrls(assets: CoreH5PDependencyAsset[], assetsFolderPath: string = ''): string[] { | ||||
|         const urls = []; | ||||
| 
 | ||||
|         assets.forEach((asset) => { | ||||
|             let url = asset.path; | ||||
| 
 | ||||
|             // Add URL prefix if not external.
 | ||||
|             if (asset.path.indexOf('://') == -1 && assetsFolderPath) { | ||||
|                 url = this.textUtils.concatenatePaths(assetsFolderPath, url); | ||||
|             } | ||||
| 
 | ||||
|             // Add version if set.
 | ||||
|             if (asset.version) { | ||||
|                 url += asset.version; | ||||
|             } | ||||
| 
 | ||||
|             urls.push(url); | ||||
|         }); | ||||
| 
 | ||||
|         return urls; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the hash of a list of dependencies. | ||||
|      * | ||||
|      * @param dependencies Dependencies. | ||||
|      * @return Hash. | ||||
|      */ | ||||
|     getDependenciesHash(dependencies: {[machineName: string]: CoreH5PContentDependencyData}): string { | ||||
|         // Build hash of dependencies.
 | ||||
|         const toHash = []; | ||||
| 
 | ||||
|         // Use unique identifier for each library version.
 | ||||
|         for (const name in dependencies) { | ||||
|             const dep = dependencies[name]; | ||||
|             toHash.push(dep.machineName + '-' + dep.majorVersion + '.' + dep.minorVersion + '.' + dep.patchVersion); | ||||
|         } | ||||
| 
 | ||||
|         // Sort in case the same dependencies comes in a different order.
 | ||||
|         toHash.sort((a, b) => { | ||||
|             return a.localeCompare(b); | ||||
|         }); | ||||
| 
 | ||||
|         // Calculate hash.
 | ||||
|         return <string> Md5.hashAsciiStr(toHash.join('')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Provide localization for the Core JS. | ||||
|      * | ||||
|      * @return Object with the translations. | ||||
|      */ | ||||
|     getLocalization(): any { | ||||
|         return { | ||||
|             fullscreen: this.translate.instant('core.h5p.fullscreen'), | ||||
|             disableFullscreen: this.translate.instant('core.h5p.disablefullscreen'), | ||||
|             download: this.translate.instant('core.h5p.download'), | ||||
|             copyrights: this.translate.instant('core.h5p.copyright'), | ||||
|             embed: this.translate.instant('core.h5p.embed'), | ||||
|             size: this.translate.instant('core.h5p.size'), | ||||
|             showAdvanced: this.translate.instant('core.h5p.showadvanced'), | ||||
|             hideAdvanced: this.translate.instant('core.h5p.hideadvanced'), | ||||
|             advancedHelp: this.translate.instant('core.h5p.resizescript'), | ||||
|             copyrightInformation: this.translate.instant('core.h5p.copyright'), | ||||
|             close: this.translate.instant('core.h5p.close'), | ||||
|             title: this.translate.instant('core.h5p.title'), | ||||
|             author: this.translate.instant('core.h5p.author'), | ||||
|             year: this.translate.instant('core.h5p.year'), | ||||
|             source: this.translate.instant('core.h5p.source'), | ||||
|             license: this.translate.instant('core.h5p.license'), | ||||
|             thumbnail: this.translate.instant('core.h5p.thumbnail'), | ||||
|             noCopyrights: this.translate.instant('core.h5p.nocopyright'), | ||||
|             reuse: this.translate.instant('core.h5p.reuse'), | ||||
|             reuseContent: this.translate.instant('core.h5p.reuseContent'), | ||||
|             reuseDescription: this.translate.instant('core.h5p.reuseDescription'), | ||||
|             downloadDescription: this.translate.instant('core.h5p.downloadtitle'), | ||||
|             copyrightsDescription: this.translate.instant('core.h5p.copyrighttitle'), | ||||
|             embedDescription: this.translate.instant('core.h5p.embedtitle'), | ||||
|             h5pDescription: this.translate.instant('core.h5p.h5ptitle'), | ||||
|             contentChanged: this.translate.instant('core.h5p.contentchanged'), | ||||
|             startingOver: this.translate.instant('core.h5p.startingover'), | ||||
|             by: this.translate.instant('core.h5p.by'), | ||||
|             showMore: this.translate.instant('core.h5p.showmore'), | ||||
|             showLess: this.translate.instant('core.h5p.showless'), | ||||
|             subLevel: this.translate.instant('core.h5p.sublevel'), | ||||
|             confirmDialogHeader: this.translate.instant('core.h5p.confirmdialogheader'), | ||||
|             confirmDialogBody: this.translate.instant('core.h5p.confirmdialogbody'), | ||||
|             cancelLabel: this.translate.instant('core.h5p.cancellabel'), | ||||
|             confirmLabel: this.translate.instant('core.h5p.confirmlabel'), | ||||
|             licenseU: this.translate.instant('core.h5p.undisclosed'), | ||||
|             licenseCCBY: this.translate.instant('core.h5p.ccattribution'), | ||||
|             licenseCCBYSA: this.translate.instant('core.h5p.ccattributionsa'), | ||||
|             licenseCCBYND: this.translate.instant('core.h5p.ccattributionnd'), | ||||
|             licenseCCBYNC: this.translate.instant('core.h5p.ccattributionnc'), | ||||
|             licenseCCBYNCSA: this.translate.instant('core.h5p.ccattributionncsa'), | ||||
|             licenseCCBYNCND: this.translate.instant('core.h5p.ccattributionncnd'), | ||||
|             licenseCC40: this.translate.instant('core.h5p.licenseCC40'), | ||||
|             licenseCC30: this.translate.instant('core.h5p.licenseCC30'), | ||||
|             licenseCC25: this.translate.instant('core.h5p.licenseCC25'), | ||||
|             licenseCC20: this.translate.instant('core.h5p.licenseCC20'), | ||||
|             licenseCC10: this.translate.instant('core.h5p.licenseCC10'), | ||||
|             licenseGPL: this.translate.instant('core.h5p.licenseGPL'), | ||||
|             licenseV3: this.translate.instant('core.h5p.licenseV3'), | ||||
|             licenseV2: this.translate.instant('core.h5p.licenseV2'), | ||||
|             licenseV1: this.translate.instant('core.h5p.licenseV1'), | ||||
|             licensePD: this.translate.instant('core.h5p.pd'), | ||||
|             licenseCC010: this.translate.instant('core.h5p.licenseCC010'), | ||||
|             licensePDM: this.translate.instant('core.h5p.pdm'), | ||||
|             licenseC: this.translate.instant('core.h5p.copyrightstring'), | ||||
|             contentType: this.translate.instant('core.h5p.contenttype'), | ||||
|             licenseExtras: this.translate.instant('core.h5p.licenseextras'), | ||||
|             changes: this.translate.instant('core.h5p.changelog'), | ||||
|             contentCopied: this.translate.instant('core.h5p.contentCopied'), | ||||
|             connectionLost: this.translate.instant('core.h5p.connectionLost'), | ||||
|             connectionReestablished: this.translate.instant('core.h5p.connectionReestablished'), | ||||
|             resubmitScores: this.translate.instant('core.h5p.resubmitScores'), | ||||
|             offlineDialogHeader: this.translate.instant('core.h5p.offlineDialogHeader'), | ||||
|             offlineDialogBody: this.translate.instant('core.h5p.offlineDialogBody'), | ||||
|             offlineDialogRetryMessage: this.translate.instant('core.h5p.offlineDialogRetryMessage'), | ||||
|             offlineDialogRetryButtonLabel: this.translate.instant('core.h5p.offlineDialogRetryButtonLabel'), | ||||
|             offlineSuccessfulSubmit: this.translate.instant('core.h5p.offlineSuccessfulSubmit'), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}. | ||||
|      * | ||||
|      * @param libraryString On the form {machineName} {majorVersion}.{minorVersion} | ||||
|      * @return Object with keys machineName, majorVersion and minorVersion. Null if string is not parsable. | ||||
|      */ | ||||
|     libraryFromString(libraryString: string): {machineName: string, majorVersion: number, minorVersion: number} { | ||||
| 
 | ||||
|         const matches = libraryString.match(/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i); | ||||
| 
 | ||||
|         if (matches && matches.length >= 4) { | ||||
|             return { | ||||
|                 machineName: matches[1], | ||||
|                 majorVersion: Number(matches[2]), | ||||
|                 minorVersion: Number(matches[3]) | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert list of library parameter values to csv. | ||||
|      * | ||||
|      * @param libraryData Library data as found in library.json files. | ||||
|      * @param key Key that should be found in libraryData. | ||||
|      * @param searchParam The library parameter (Default: 'path'). | ||||
|      * @return Library parameter values separated by ', ' | ||||
|      */ | ||||
|     libraryParameterValuesToCsv(libraryData: any, key: string, searchParam: string = 'path'): string { | ||||
|         if (typeof libraryData[key] != 'undefined') { | ||||
|             const parameterValues = []; | ||||
| 
 | ||||
|             libraryData[key].forEach((file) => { | ||||
|                 for (const index in file) { | ||||
|                     if (index === searchParam) { | ||||
|                         parameterValues.push(file[index]); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             return parameterValues.join(','); | ||||
|         } | ||||
| 
 | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert strings of text into simple kebab case slugs. Based on H5PCore::slugify. | ||||
|      * | ||||
|      * @param input The string to slugify. | ||||
|      * @return Slugified text. | ||||
|      */ | ||||
|     slugify(input: string): string { | ||||
|         input = input || ''; | ||||
| 
 | ||||
|         input = input.toLowerCase(); | ||||
| 
 | ||||
|         // Replace common chars.
 | ||||
|         let newInput = ''; | ||||
|         for (let i = 0; i < input.length; i++) { | ||||
|             const char = input[i]; | ||||
| 
 | ||||
|             newInput += this.SLUGIFY_MAP[char] || char; | ||||
|         } | ||||
| 
 | ||||
|         // Replace everything else.
 | ||||
|         newInput = newInput.replace(/[^a-z0-9]/g, '-'); | ||||
| 
 | ||||
|         // Prevent double hyphen
 | ||||
|         newInput = newInput.replace(/-{2,}/g, '-'); | ||||
| 
 | ||||
|         // Prevent hyphen in beginning or end.
 | ||||
|         newInput = newInput.replace(/(^-+|-+$)/g, ''); | ||||
| 
 | ||||
|         // Prevent too long slug.
 | ||||
|         if (newInput.length > 91) { | ||||
|             newInput = newInput.substr(0, 92); | ||||
|         } | ||||
| 
 | ||||
|         // Prevent empty slug
 | ||||
|         if (newInput === '') { | ||||
|             newInput = 'interactive'; | ||||
|         } | ||||
| 
 | ||||
|         return newInput; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Determine if params contain any match. | ||||
|      * | ||||
|      * @param params Parameters. | ||||
|      * @param pattern Regular expression to identify pattern. | ||||
|      * @return True if params matches pattern. | ||||
|      */ | ||||
|     textAddonMatches(params: any, pattern: string): boolean { | ||||
| 
 | ||||
|         if (typeof params == 'string') { | ||||
|             if (params.match(pattern)) { | ||||
|                 return true; | ||||
|             } | ||||
|         } else if (typeof params == 'object') { | ||||
|             for (const key in params) { | ||||
|                 const value = params[key]; | ||||
| 
 | ||||
|                 if (this.textAddonMatches(value, pattern)) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @ -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(); | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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<Alert> { | ||||
|         if (error && error.coreCanceled) { | ||||
|         if (this.isCanceledError(error)) { | ||||
|             // It's a canceled error, don't display an error.
 | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user