MOBILE-3406 h5p: Refactor code into different classes
parent
4f9adba63e
commit
724e0cc292
|
@ -12,14 +12,14 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtils } from '@providers/utils/text';
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
import { CoreUtils } from '@providers/utils/utils';
|
||||||
import { CoreH5PProvider, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from '../providers/h5p';
|
import { CoreH5P } from '../providers/h5p';
|
||||||
import { CoreH5PUtilsProvider } from '../providers/utils';
|
import { Translate } from '@singletons/core.singletons';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { CoreH5PCore, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from './core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Equivalent to Moodle's H5PContentValidator, but without some of the validations.
|
* Equivalent to H5P's H5PContentValidator, but without some of the validations.
|
||||||
* It's also used to build the dependency list.
|
* It's also used to build the dependency list.
|
||||||
*/
|
*/
|
||||||
export class CoreH5PContentValidator {
|
export class CoreH5PContentValidator {
|
||||||
|
@ -43,17 +43,12 @@ export class CoreH5PContentValidator {
|
||||||
protected libraries: {[libString: string]: CoreH5PLibraryData} = {};
|
protected libraries: {[libString: string]: CoreH5PLibraryData} = {};
|
||||||
protected dependencies: {[key: string]: CoreH5PContentDepsTreeDependency} = {};
|
protected dependencies: {[key: string]: CoreH5PContentDepsTreeDependency} = {};
|
||||||
protected relativePathRegExp = /^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/;
|
protected relativePathRegExp = /^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/;
|
||||||
protected allowedHtml: {[tag: string]: any} = {};
|
protected allowedHtml: {[tag: string]: string} = {};
|
||||||
protected allowedStyles: RegExp[];
|
protected allowedStyles: RegExp[];
|
||||||
protected metadataSemantics: any[];
|
protected metadataSemantics: any[];
|
||||||
protected copyrightSemantics: any;
|
protected copyrightSemantics: any;
|
||||||
|
|
||||||
constructor(protected h5pProvider: CoreH5PProvider,
|
constructor(protected siteId: string) { }
|
||||||
protected h5pUtils: CoreH5PUtilsProvider,
|
|
||||||
protected textUtils: CoreTextUtilsProvider,
|
|
||||||
protected utils: CoreUtilsProvider,
|
|
||||||
protected translate: TranslateService,
|
|
||||||
protected siteId: string) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add Addon library.
|
* Add Addon library.
|
||||||
|
@ -61,24 +56,23 @@ export class CoreH5PContentValidator {
|
||||||
* @param library The addon library to add.
|
* @param library The addon library to add.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
addon(library: CoreH5PLibraryAddonData): Promise<void> {
|
async addon(library: CoreH5PLibraryAddonData): Promise<void> {
|
||||||
const depKey = 'preloaded-' + library.machineName;
|
const depKey = 'preloaded-' + library.machineName;
|
||||||
|
|
||||||
this.dependencies[depKey] = {
|
this.dependencies[depKey] = {
|
||||||
library: library,
|
library: library,
|
||||||
type: 'preloaded'
|
type: 'preloaded',
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => {
|
this.nextWeight = await CoreH5P.instance.h5pCore.findLibraryDependencies(this.dependencies, library, this.nextWeight);
|
||||||
this.nextWeight = weight;
|
|
||||||
this.dependencies[depKey].weight = this.nextWeight++;
|
this.dependencies[depKey].weight = this.nextWeight++;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the flat dependency tree.
|
* Get the flat dependency tree.
|
||||||
*
|
*
|
||||||
* @return array
|
* @return Dependencies.
|
||||||
*/
|
*/
|
||||||
getDependencies(): {[key: string]: CoreH5PContentDepsTreeDependency} {
|
getDependencies(): {[key: string]: CoreH5PContentDepsTreeDependency} {
|
||||||
return this.dependencies;
|
return this.dependencies;
|
||||||
|
@ -92,7 +86,7 @@ export class CoreH5PContentValidator {
|
||||||
*/
|
*/
|
||||||
validateMetadata(metadata: any): Promise<any> {
|
validateMetadata(metadata: any): Promise<any> {
|
||||||
const semantics = this.getMetadataSemantics();
|
const semantics = this.getMetadataSemantics();
|
||||||
const group = this.utils.clone(metadata || {});
|
const group = CoreUtils.instance.clone(metadata || {});
|
||||||
|
|
||||||
// Stop complaining about "invalid selected option in select" for old content without license chosen.
|
// Stop complaining about "invalid selected option in select" for old content without license chosen.
|
||||||
if (typeof group.license == 'undefined') {
|
if (typeof group.license == 'undefined') {
|
||||||
|
@ -135,7 +129,7 @@ export class CoreH5PContentValidator {
|
||||||
tags.push('s');
|
tags.push('s');
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = this.utils.uniqueArray(tags);
|
tags = CoreUtils.instance.uniqueArray(tags);
|
||||||
|
|
||||||
// Determine allowed style tags
|
// Determine allowed style tags
|
||||||
const stylePatterns: RegExp[] = [];
|
const stylePatterns: RegExp[] = [];
|
||||||
|
@ -168,7 +162,7 @@ export class CoreH5PContentValidator {
|
||||||
text = this.filterXss(text, tags, stylePatterns);
|
text = this.filterXss(text, tags, stylePatterns);
|
||||||
} else {
|
} else {
|
||||||
// Filter text to plain text.
|
// Filter text to plain text.
|
||||||
text = this.textUtils.escapeHTML(text);
|
text = CoreTextUtils.instance.escapeHTML(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if string is within allowed length.
|
// Check if string is within allowed length.
|
||||||
|
@ -213,8 +207,8 @@ export class CoreH5PContentValidator {
|
||||||
}
|
}
|
||||||
// Check if number is within allowed bounds even if step value is set.
|
// Check if number is within allowed bounds even if step value is set.
|
||||||
if (typeof semantics.step != 'undefined') {
|
if (typeof semantics.step != 'undefined') {
|
||||||
const testNumber = num - (typeof semantics.min != 'undefined' ? semantics.min : 0),
|
const testNumber = num - (typeof semantics.min != 'undefined' ? semantics.min : 0);
|
||||||
rest = testNumber % semantics.step;
|
const rest = testNumber % semantics.step;
|
||||||
if (rest !== 0) {
|
if (rest !== 0) {
|
||||||
num -= rest;
|
num -= rest;
|
||||||
}
|
}
|
||||||
|
@ -245,8 +239,8 @@ export class CoreH5PContentValidator {
|
||||||
* @return Validated select.
|
* @return Validated select.
|
||||||
*/
|
*/
|
||||||
validateSelect(select: any, semantics: any): any {
|
validateSelect(select: any, semantics: any): any {
|
||||||
const optional = semantics.optional,
|
const optional = semantics.optional;
|
||||||
options = {};
|
const options = {};
|
||||||
let strict = false;
|
let strict = false;
|
||||||
|
|
||||||
if (semantics.options && semantics.options.length) {
|
if (semantics.options && semantics.options.length) {
|
||||||
|
@ -273,7 +267,7 @@ export class CoreH5PContentValidator {
|
||||||
if (strict && !optional && !options[value]) {
|
if (strict && !optional && !options[value]) {
|
||||||
delete select[key];
|
delete select[key];
|
||||||
} else {
|
} else {
|
||||||
select[key] = this.textUtils.escapeHTML(value);
|
select[key] = CoreTextUtils.instance.escapeHTML(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -285,7 +279,7 @@ export class CoreH5PContentValidator {
|
||||||
if (strict && !optional && !options[select]) {
|
if (strict && !optional && !options[select]) {
|
||||||
select = semantics.options[0].value;
|
select = semantics.options[0].value;
|
||||||
}
|
}
|
||||||
select = this.textUtils.escapeHTML(select);
|
select = CoreTextUtils.instance.escapeHTML(select);
|
||||||
}
|
}
|
||||||
|
|
||||||
return select;
|
return select;
|
||||||
|
@ -299,11 +293,10 @@ export class CoreH5PContentValidator {
|
||||||
* @param semantics Semantics.
|
* @param semantics Semantics.
|
||||||
* @return Validated list.
|
* @return Validated list.
|
||||||
*/
|
*/
|
||||||
validateList(list: any, semantics: any): Promise<any[]> {
|
async validateList(list: any, semantics: any): Promise<any[]> {
|
||||||
const field = semantics.field,
|
const field = semantics.field;
|
||||||
fn = this[this.typeMap[field.type]].bind(this);
|
const fn = this[this.typeMap[field.type]].bind(this);
|
||||||
let promise = Promise.resolve(), // Use a chain of promises so the order is kept.
|
let keys = Object.keys(list);
|
||||||
keys = Object.keys(list);
|
|
||||||
|
|
||||||
// Check that list is not longer than allowed length.
|
// Check that list is not longer than allowed length.
|
||||||
if (typeof semantics.max != 'undefined') {
|
if (typeof semantics.max != 'undefined') {
|
||||||
|
@ -311,35 +304,32 @@ export class CoreH5PContentValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate each element in list.
|
// Validate each element in list.
|
||||||
keys.forEach((key) => {
|
for (const i in keys) {
|
||||||
|
const key = keys[i];
|
||||||
|
|
||||||
if (isNaN(parseInt(key, 10))) {
|
if (isNaN(parseInt(key, 10))) {
|
||||||
// It's an object and the key isn't an integer. Delete it.
|
// It's an object and the key isn't an integer. Delete it.
|
||||||
delete list[key];
|
delete list[key];
|
||||||
} else {
|
} else {
|
||||||
promise = promise.then(() => {
|
const val = await fn(list[key], field);
|
||||||
return Promise.resolve(fn(list[key], field)).then((val) => {
|
|
||||||
if (val === null) {
|
if (val === null) {
|
||||||
list.splice(key, 1);
|
list.splice(key, 1);
|
||||||
} else {
|
} else {
|
||||||
list[key] = val;
|
list[key] = val;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return promise.then(() => {
|
if (!Array.isArray(list)) {
|
||||||
|
list = CoreUtils.instance.objectToArray(list);
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(list)) {
|
if (!list.length) {
|
||||||
list = this.utils.objectToArray(list);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!list.length) {
|
return list;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -350,7 +340,7 @@ export class CoreH5PContentValidator {
|
||||||
* @param typeValidKeys List of valid keys.
|
* @param typeValidKeys List of valid keys.
|
||||||
* @return Promise resolved with the validated file.
|
* @return Promise resolved with the validated file.
|
||||||
*/
|
*/
|
||||||
protected validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise<any> {
|
protected async validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise<any> {
|
||||||
// Do not allow to use files from other content folders.
|
// Do not allow to use files from other content folders.
|
||||||
const matches = file.path.match(this.relativePathRegExp);
|
const matches = file.path.match(this.relativePathRegExp);
|
||||||
if (matches && matches.length) {
|
if (matches && matches.length) {
|
||||||
|
@ -363,9 +353,9 @@ export class CoreH5PContentValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure path and mime does not have any special chars
|
// Make sure path and mime does not have any special chars
|
||||||
file.path = this.textUtils.escapeHTML(file.path);
|
file.path = CoreTextUtils.instance.escapeHTML(file.path);
|
||||||
if (file.mime) {
|
if (file.mime) {
|
||||||
file.mime = this.textUtils.escapeHTML(file.mime);
|
file.mime = CoreTextUtils.instance.escapeHTML(file.mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove attributes that should not exist, they may contain JSON escape code.
|
// Remove attributes that should not exist, they may contain JSON escape code.
|
||||||
|
@ -373,7 +363,7 @@ export class CoreH5PContentValidator {
|
||||||
if (semantics.extraAttributes) {
|
if (semantics.extraAttributes) {
|
||||||
validKeys = validKeys.concat(semantics.extraAttributes);
|
validKeys = validKeys.concat(semantics.extraAttributes);
|
||||||
}
|
}
|
||||||
validKeys = this.utils.uniqueArray(validKeys);
|
validKeys = CoreUtils.instance.uniqueArray(validKeys);
|
||||||
|
|
||||||
this.filterParams(file, validKeys);
|
this.filterParams(file, validKeys);
|
||||||
|
|
||||||
|
@ -386,7 +376,7 @@ export class CoreH5PContentValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.codecs) {
|
if (file.codecs) {
|
||||||
file.codecs = this.textUtils.escapeHTML(file.codecs);
|
file.codecs = CoreTextUtils.instance.escapeHTML(file.codecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof file.bitrate != 'undefined') {
|
if (typeof file.bitrate != 'undefined') {
|
||||||
|
@ -399,17 +389,15 @@ export class CoreH5PContentValidator {
|
||||||
} else {
|
} else {
|
||||||
this.filterParams(file.quality, ['level', 'label']);
|
this.filterParams(file.quality, ['level', 'label']);
|
||||||
file.quality.level = parseInt(file.quality.level);
|
file.quality.level = parseInt(file.quality.level);
|
||||||
file.quality.label = this.textUtils.escapeHTML(file.quality.label);
|
file.quality.label = CoreTextUtils.instance.escapeHTML(file.quality.label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof file.copyright != 'undefined') {
|
if (typeof file.copyright != 'undefined') {
|
||||||
return this.validateGroup(file.copyright, this.getCopyrightSemantics()).then(() => {
|
await this.validateGroup(file.copyright, this.getCopyrightSemantics());
|
||||||
return file;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(file);
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -441,18 +429,13 @@ export class CoreH5PContentValidator {
|
||||||
* @param semantics Semantics.
|
* @param semantics Semantics.
|
||||||
* @return Promise resolved with the validated file.
|
* @return Promise resolved with the validated file.
|
||||||
*/
|
*/
|
||||||
validateVideo(video: any, semantics: any): Promise<any> {
|
async validateVideo(video: any, semantics: any): Promise<any> {
|
||||||
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
|
|
||||||
|
|
||||||
for (const key in video) {
|
for (const key in video) {
|
||||||
promise = promise.then(() => {
|
await this.validateFilelike(video[key], semantics, ['width', 'height', 'codecs', 'quality', 'bitrate']);
|
||||||
return this.validateFilelike(video[key], semantics, ['width', 'height', 'codecs', 'quality', 'bitrate']);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then(() => {
|
return video;
|
||||||
return video;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -462,18 +445,13 @@ export class CoreH5PContentValidator {
|
||||||
* @param semantics Semantics.
|
* @param semantics Semantics.
|
||||||
* @return Promise resolved with the validated file.
|
* @return Promise resolved with the validated file.
|
||||||
*/
|
*/
|
||||||
validateAudio(audio: any, semantics: any): Promise<any> {
|
async validateAudio(audio: any, semantics: any): Promise<any> {
|
||||||
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
|
|
||||||
|
|
||||||
for (const key in audio) {
|
for (const key in audio) {
|
||||||
promise = promise.then(() => {
|
await this.validateFilelike(audio[key], semantics);
|
||||||
return this.validateFilelike(audio[key], semantics);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then(() => {
|
return audio;
|
||||||
return audio;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -483,19 +461,19 @@ export class CoreH5PContentValidator {
|
||||||
* @param group Group.
|
* @param group Group.
|
||||||
* @param semantics Semantics.
|
* @param semantics Semantics.
|
||||||
* @param flatten Whether to flatten.
|
* @param flatten Whether to flatten.
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
validateGroup(group: any, semantics: any, flatten: boolean = true): Promise<any> {
|
async validateGroup(group: any, semantics: any, flatten: boolean = true): Promise<any> {
|
||||||
// Groups with just one field are compressed in the editor to only output he child content.
|
// Groups with just one field are compressed in the editor to only output the child content.
|
||||||
|
|
||||||
const isSubContent = semantics.isSubContent === true;
|
const isSubContent = semantics.isSubContent === true;
|
||||||
|
|
||||||
if (semantics.fields.length == 1 && flatten && !isSubContent) {
|
if (semantics.fields.length == 1 && flatten && !isSubContent) {
|
||||||
const field = semantics.fields[0],
|
const field = semantics.fields[0];
|
||||||
fn = this[this.typeMap[field.type]].bind(this);
|
const fn = this[this.typeMap[field.type]].bind(this);
|
||||||
|
|
||||||
return Promise.resolve(fn(group, field));
|
return fn(group, field);
|
||||||
} else {
|
} else {
|
||||||
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
|
|
||||||
|
|
||||||
for (const key in group) {
|
for (const key in group) {
|
||||||
// If subContentId is set, keep value
|
// If subContentId is set, keep value
|
||||||
|
@ -504,9 +482,9 @@ export class CoreH5PContentValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find semantics for name=key.
|
// Find semantics for name=key.
|
||||||
let found = false,
|
let found = false;
|
||||||
fn = null,
|
let fn = null;
|
||||||
field = null;
|
let field = null;
|
||||||
|
|
||||||
for (let i = 0; i < semantics.fields.length; i++) {
|
for (let i = 0; i < semantics.fields.length; i++) {
|
||||||
field = semantics.fields[i];
|
field = semantics.fields[i];
|
||||||
|
@ -522,23 +500,19 @@ export class CoreH5PContentValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found && fn) {
|
if (found && fn) {
|
||||||
promise = promise.then(() => {
|
const val = await fn(group[key], field);
|
||||||
return Promise.resolve(fn(group[key], field)).then((val) => {
|
|
||||||
group[key] = val;
|
group[key] = val;
|
||||||
if (val === null) {
|
if (val === null) {
|
||||||
delete group[key];
|
delete group[key];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Something exists in content that does not have a corresponding semantics field. Remove it.
|
// Something exists in content that does not have a corresponding semantics field. Remove it.
|
||||||
delete group.key;
|
delete group.key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then(() => {
|
return group;
|
||||||
return group;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,71 +525,57 @@ export class CoreH5PContentValidator {
|
||||||
* @param semantics Semantics.
|
* @param semantics Semantics.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
validateLibrary(value: any, semantics: any): Promise<any> {
|
async validateLibrary(value: any, semantics: any): Promise<any> {
|
||||||
if (!value.library) {
|
if (!value.library) {
|
||||||
return Promise.resolve();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise;
|
|
||||||
|
|
||||||
if (!this.libraries[value.library]) {
|
if (!this.libraries[value.library]) {
|
||||||
const libSpec = this.h5pUtils.libraryFromString(value.library);
|
// Load the library and store it in the index of libraries.
|
||||||
|
const libSpec = CoreH5PCore.libraryFromString(value.library);
|
||||||
|
|
||||||
promise = this.h5pProvider.loadLibrary(libSpec.machineName, libSpec.majorVersion, libSpec.minorVersion, this.siteId)
|
this.libraries[value.library] = await CoreH5P.instance.h5pCore.loadLibrary(libSpec.machineName, libSpec.majorVersion,
|
||||||
.then((library) => {
|
libSpec.minorVersion, this.siteId);
|
||||||
this.libraries[value.library] = library;
|
|
||||||
|
|
||||||
return library;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
promise = Promise.resolve(this.libraries[value.library]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then((library) => {
|
const library = this.libraries[value.library];
|
||||||
// Validate parameters.
|
|
||||||
return this.validateGroup(value.params, {type: 'group', fields: library.semantics}, false).then((validated) => {
|
|
||||||
|
|
||||||
value.params = validated;
|
// Validate parameters.
|
||||||
|
value.params = await this.validateGroup(value.params, {type: 'group', fields: library.semantics}, false);
|
||||||
|
|
||||||
// Validate subcontent's metadata
|
// Validate subcontent's metadata
|
||||||
if (value.metadata) {
|
if (value.metadata) {
|
||||||
return this.validateMetadata(value.metadata).then((res) => {
|
value.metadata = await this.validateMetadata(value.metadata);
|
||||||
value.metadata = res;
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
|
|
||||||
let validKeys = ['library', 'params', 'subContentId', 'metadata'];
|
let validKeys = ['library', 'params', 'subContentId', 'metadata'];
|
||||||
if (semantics.extraAttributes) {
|
if (semantics.extraAttributes) {
|
||||||
validKeys = this.utils.uniqueArray(validKeys.concat(semantics.extraAttributes));
|
validKeys = CoreUtils.instance.uniqueArray(validKeys.concat(semantics.extraAttributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.filterParams(value, validKeys);
|
this.filterParams(value, validKeys);
|
||||||
|
|
||||||
if (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}\}?$/)) {
|
!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;
|
delete value.subContentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all dependencies for this library.
|
// Find all dependencies for this library.
|
||||||
const depKey = 'preloaded-' + library.machineName;
|
const depKey = 'preloaded-' + library.machineName;
|
||||||
if (!this.dependencies[depKey]) {
|
if (!this.dependencies[depKey]) {
|
||||||
this.dependencies[depKey] = {
|
this.dependencies[depKey] = {
|
||||||
library: library,
|
library: library,
|
||||||
type: 'preloaded'
|
type: 'preloaded'
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => {
|
this.nextWeight = await CoreH5P.instance.h5pCore.findLibraryDependencies(this.dependencies, library, this.nextWeight);
|
||||||
this.nextWeight = weight;
|
|
||||||
this.dependencies[depKey].weight = this.nextWeight++;
|
|
||||||
|
|
||||||
return value;
|
this.dependencies[depKey].weight = this.nextWeight++;
|
||||||
});
|
|
||||||
} else {
|
return value;
|
||||||
return value;
|
} else {
|
||||||
}
|
return value;
|
||||||
});
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -689,7 +649,7 @@ export class CoreH5PContentValidator {
|
||||||
protected filterXssSplit(m: string[], store: boolean = false): string {
|
protected filterXssSplit(m: string[], store: boolean = false): string {
|
||||||
|
|
||||||
if (store) {
|
if (store) {
|
||||||
this.allowedHtml = this.utils.arrayToObject(m);
|
this.allowedHtml = CoreUtils.instance.arrayToObject(m);
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -710,9 +670,9 @@ export class CoreH5PContentValidator {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const slash = matches[1] ? matches[1].trim() : '',
|
const slash = matches[1] ? matches[1].trim() : '';
|
||||||
attrList = matches[3] || '',
|
const attrList = matches[3] || '';
|
||||||
comment = matches[4] || '';
|
const comment = matches[4] || '';
|
||||||
let elem = matches[2] || '';
|
let elem = matches[2] || '';
|
||||||
|
|
||||||
if (comment) {
|
if (comment) {
|
||||||
|
@ -733,8 +693,8 @@ export class CoreH5PContentValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is there a closing XHTML slash at the end of the attributes?
|
// Is there a closing XHTML slash at the end of the attributes?
|
||||||
const newAttrList = attrList.replace(/(\s?)\/\s*$/g, '$1'),
|
const newAttrList = attrList.replace(/(\s?)\/\s*$/g, '$1');
|
||||||
xhtmlSlash = attrList != newAttrList ? ' /' : '';
|
const xhtmlSlash = attrList != newAttrList ? ' /' : '';
|
||||||
|
|
||||||
// Clean up attributes.
|
// Clean up attributes.
|
||||||
let attr2 = this.filterXssAttributes(newAttrList,
|
let attr2 = this.filterXssAttributes(newAttrList,
|
||||||
|
@ -760,9 +720,9 @@ export class CoreH5PContentValidator {
|
||||||
|
|
||||||
while (attr.length != 0) {
|
while (attr.length != 0) {
|
||||||
// Was the last operation successful?
|
// Was the last operation successful?
|
||||||
let working = 0,
|
let working = 0;
|
||||||
matches,
|
let matches;
|
||||||
thisVal;
|
let thisVal;
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -877,10 +837,10 @@ export class CoreH5PContentValidator {
|
||||||
filterXssBadProtocol(str: string, decode: boolean = true): string {
|
filterXssBadProtocol(str: string, decode: boolean = true): string {
|
||||||
// Get the plain text representation of the attribute value (i.e. its meaning).
|
// Get the plain text representation of the attribute value (i.e. its meaning).
|
||||||
if (decode) {
|
if (decode) {
|
||||||
str = this.textUtils.decodeHTMLEntities(str);
|
str = CoreTextUtils.instance.decodeHTMLEntities(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.textUtils.escapeHTML(this.stripDangerousProtocols(str));
|
return CoreTextUtils.instance.escapeHTML(this.stripDangerousProtocols(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -892,11 +852,11 @@ export class CoreH5PContentValidator {
|
||||||
protected stripDangerousProtocols(uri: string): string {
|
protected stripDangerousProtocols(uri: string): string {
|
||||||
|
|
||||||
const allowedProtocols = {
|
const allowedProtocols = {
|
||||||
ftp: true,
|
ftp: true,
|
||||||
http: true,
|
http: true,
|
||||||
https: true,
|
https: true,
|
||||||
mailto: true
|
mailto: true
|
||||||
};
|
};
|
||||||
let before;
|
let before;
|
||||||
|
|
||||||
// Iteratively remove any invalid protocol found.
|
// Iteratively remove any invalid protocol found.
|
||||||
|
@ -939,92 +899,92 @@ export class CoreH5PContentValidator {
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.title'),
|
label: Translate.instance.instant('core.h5p.title'),
|
||||||
placeholder: 'La Gioconda'
|
placeholder: 'La Gioconda'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'license',
|
name: 'license',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: this.translate.instant('core.h5p.license'),
|
label: Translate.instance.instant('core.h5p.license'),
|
||||||
default: 'U',
|
default: 'U',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 'U',
|
value: 'U',
|
||||||
label: this.translate.instant('core.h5p.undisclosed')
|
label: Translate.instance.instant('core.h5p.undisclosed')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'optgroup',
|
type: 'optgroup',
|
||||||
label: this.translate.instant('core.h5p.creativecommons'),
|
label: Translate.instance.instant('core.h5p.creativecommons'),
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 'CC BY',
|
value: 'CC BY',
|
||||||
label: this.translate.instant('core.h5p.ccattribution'),
|
label: Translate.instance.instant('core.h5p.ccattribution'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-SA',
|
value: 'CC BY-SA',
|
||||||
label: this.translate.instant('core.h5p.ccattributionsa'),
|
label: Translate.instance.instant('core.h5p.ccattributionsa'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-ND',
|
value: 'CC BY-ND',
|
||||||
label: this.translate.instant('core.h5p.ccattributionnd'),
|
label: Translate.instance.instant('core.h5p.ccattributionnd'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-NC',
|
value: 'CC BY-NC',
|
||||||
label: this.translate.instant('core.h5p.ccattributionnc'),
|
label: Translate.instance.instant('core.h5p.ccattributionnc'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-NC-SA',
|
value: 'CC BY-NC-SA',
|
||||||
label: this.translate.instant('core.h5p.ccattributionncsa'),
|
label: Translate.instance.instant('core.h5p.ccattributionncsa'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-NC-ND',
|
value: 'CC BY-NC-ND',
|
||||||
label: this.translate.instant('core.h5p.ccattributionncnd'),
|
label: Translate.instance.instant('core.h5p.ccattributionncnd'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC0 1.0',
|
value: 'CC0 1.0',
|
||||||
label: this.translate.instant('core.h5p.ccpdd')
|
label: Translate.instance.instant('core.h5p.ccpdd')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC PDM',
|
value: 'CC PDM',
|
||||||
label: this.translate.instant('core.h5p.pdm')
|
label: Translate.instance.instant('core.h5p.pdm')
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'GNU GPL',
|
value: 'GNU GPL',
|
||||||
label: this.translate.instant('core.h5p.gpl')
|
label: Translate.instance.instant('core.h5p.gpl')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'PD',
|
value: 'PD',
|
||||||
label: this.translate.instant('core.h5p.pd')
|
label: Translate.instance.instant('core.h5p.pd')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'ODC PDDL',
|
value: 'ODC PDDL',
|
||||||
label: this.translate.instant('core.h5p.pddl')
|
label: Translate.instance.instant('core.h5p.pddl')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'C',
|
value: 'C',
|
||||||
label: this.translate.instant('core.h5p.copyrightstring')
|
label: Translate.instance.instant('core.h5p.copyrightstring')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'licenseVersion',
|
name: 'licenseVersion',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: this.translate.instant('core.h5p.licenseversion'),
|
label: Translate.instance.instant('core.h5p.licenseversion'),
|
||||||
options: ccVersions,
|
options: ccVersions,
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'yearFrom',
|
name: 'yearFrom',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
label: this.translate.instant('core.h5p.yearsfrom'),
|
label: Translate.instance.instant('core.h5p.yearsfrom'),
|
||||||
placeholder: '1991',
|
placeholder: '1991',
|
||||||
min: '-9999',
|
min: '-9999',
|
||||||
max: '9999',
|
max: '9999',
|
||||||
|
@ -1033,7 +993,7 @@ export class CoreH5PContentValidator {
|
||||||
{
|
{
|
||||||
name: 'yearTo',
|
name: 'yearTo',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
label: this.translate.instant('core.h5p.yearsto'),
|
label: Translate.instance.instant('core.h5p.yearsto'),
|
||||||
placeholder: '1992',
|
placeholder: '1992',
|
||||||
min: '-9999',
|
min: '-9999',
|
||||||
max: '9999',
|
max: '9999',
|
||||||
|
@ -1042,7 +1002,7 @@ export class CoreH5PContentValidator {
|
||||||
{
|
{
|
||||||
name: 'source',
|
name: 'source',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.source'),
|
label: Translate.instance.instant('core.h5p.source'),
|
||||||
placeholder: 'https://',
|
placeholder: 'https://',
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
|
@ -1054,7 +1014,7 @@ export class CoreH5PContentValidator {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: this.translate.instant('core.h5p.authorname'),
|
label: Translate.instance.instant('core.h5p.authorname'),
|
||||||
name: 'name',
|
name: 'name',
|
||||||
optional: true,
|
optional: true,
|
||||||
type: 'text'
|
type: 'text'
|
||||||
|
@ -1062,24 +1022,24 @@ export class CoreH5PContentValidator {
|
||||||
{
|
{
|
||||||
name: 'role',
|
name: 'role',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: this.translate.instant('core.h5p.authorrole'),
|
label: Translate.instance.instant('core.h5p.authorrole'),
|
||||||
default: 'Author',
|
default: 'Author',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 'Author',
|
value: 'Author',
|
||||||
label: this.translate.instant('core.h5p.author')
|
label: Translate.instance.instant('core.h5p.author')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Editor',
|
value: 'Editor',
|
||||||
label: this.translate.instant('core.h5p.editor')
|
label: Translate.instance.instant('core.h5p.editor')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Licensee',
|
value: 'Licensee',
|
||||||
label: this.translate.instant('core.h5p.licensee')
|
label: Translate.instance.instant('core.h5p.licensee')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Originator',
|
value: 'Originator',
|
||||||
label: this.translate.instant('core.h5p.originator')
|
label: Translate.instance.instant('core.h5p.originator')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1090,9 +1050,9 @@ export class CoreH5PContentValidator {
|
||||||
name: 'licenseExtras',
|
name: 'licenseExtras',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
widget: 'textarea',
|
widget: 'textarea',
|
||||||
label: this.translate.instant('core.h5p.licenseextras'),
|
label: Translate.instance.instant('core.h5p.licenseextras'),
|
||||||
optional: true,
|
optional: true,
|
||||||
description: this.translate.instant('core.h5p.additionallicenseinfo')
|
description: Translate.instance.instant('core.h5p.additionallicenseinfo')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'changes',
|
name: 'changes',
|
||||||
|
@ -1100,26 +1060,26 @@ export class CoreH5PContentValidator {
|
||||||
field: {
|
field: {
|
||||||
name: 'change',
|
name: 'change',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
label: this.translate.instant('core.h5p.changelog'),
|
label: Translate.instance.instant('core.h5p.changelog'),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.date'),
|
label: Translate.instance.instant('core.h5p.date'),
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'author',
|
name: 'author',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.changedby'),
|
label: Translate.instance.instant('core.h5p.changedby'),
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'log',
|
name: 'log',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
widget: 'textarea',
|
widget: 'textarea',
|
||||||
label: this.translate.instant('core.h5p.changedescription'),
|
label: Translate.instance.instant('core.h5p.changedescription'),
|
||||||
placeholder: this.translate.instant('core.h5p.changeplaceholder'),
|
placeholder: Translate.instance.instant('core.h5p.changeplaceholder'),
|
||||||
optional: true
|
optional: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1129,8 +1089,8 @@ export class CoreH5PContentValidator {
|
||||||
name: 'authorComments',
|
name: 'authorComments',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
widget: 'textarea',
|
widget: 'textarea',
|
||||||
label: this.translate.instant('core.h5p.authorcomments'),
|
label: Translate.instance.instant('core.h5p.authorcomments'),
|
||||||
description: this.translate.instant('core.h5p.authorcommentsdescription'),
|
description: Translate.instance.instant('core.h5p.authorcommentsdescription'),
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1164,33 +1124,33 @@ export class CoreH5PContentValidator {
|
||||||
this.copyrightSemantics = {
|
this.copyrightSemantics = {
|
||||||
name: 'copyright',
|
name: 'copyright',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
label: this.translate.instant('core.h5p.copyrightinfo'),
|
label: Translate.instance.instant('core.h5p.copyrightinfo'),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.title'),
|
label: Translate.instance.instant('core.h5p.title'),
|
||||||
placeholder: 'La Gioconda',
|
placeholder: 'La Gioconda',
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'author',
|
name: 'author',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.author'),
|
label: Translate.instance.instant('core.h5p.author'),
|
||||||
placeholder: 'Leonardo da Vinci',
|
placeholder: 'Leonardo da Vinci',
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'year',
|
name: 'year',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.years'),
|
label: Translate.instance.instant('core.h5p.years'),
|
||||||
placeholder: '1503 - 1517',
|
placeholder: '1503 - 1517',
|
||||||
optional: true
|
optional: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'source',
|
name: 'source',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: this.translate.instant('core.h5p.source'),
|
label: Translate.instance.instant('core.h5p.source'),
|
||||||
placeholder: 'http://en.wikipedia.org/wiki/Mona_Lisa',
|
placeholder: 'http://en.wikipedia.org/wiki/Mona_Lisa',
|
||||||
optional: true,
|
optional: true,
|
||||||
regexp: {
|
regexp: {
|
||||||
|
@ -1201,64 +1161,64 @@ export class CoreH5PContentValidator {
|
||||||
{
|
{
|
||||||
name: 'license',
|
name: 'license',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: this.translate.instant('core.h5p.license'),
|
label: Translate.instance.instant('core.h5p.license'),
|
||||||
default: 'U',
|
default: 'U',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 'U',
|
value: 'U',
|
||||||
label: this.translate.instant('core.h5p.undisclosed')
|
label: Translate.instance.instant('core.h5p.undisclosed')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY',
|
value: 'CC BY',
|
||||||
label: this.translate.instant('core.h5p.ccattribution'),
|
label: Translate.instance.instant('core.h5p.ccattribution'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-SA',
|
value: 'CC BY-SA',
|
||||||
label: this.translate.instant('core.h5p.ccattributionsa'),
|
label: Translate.instance.instant('core.h5p.ccattributionsa'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-ND',
|
value: 'CC BY-ND',
|
||||||
label: this.translate.instant('core.h5p.ccattributionnd'),
|
label: Translate.instance.instant('core.h5p.ccattributionnd'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-NC',
|
value: 'CC BY-NC',
|
||||||
label: this.translate.instant('core.h5p.ccattributionnc'),
|
label: Translate.instance.instant('core.h5p.ccattributionnc'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-NC-SA',
|
value: 'CC BY-NC-SA',
|
||||||
label: this.translate.instant('core.h5p.ccattributionncsa'),
|
label: Translate.instance.instant('core.h5p.ccattributionncsa'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC BY-NC-ND',
|
value: 'CC BY-NC-ND',
|
||||||
label: this.translate.instant('core.h5p.ccattributionncnd'),
|
label: Translate.instance.instant('core.h5p.ccattributionncnd'),
|
||||||
versions: ccVersions
|
versions: ccVersions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'GNU GPL',
|
value: 'GNU GPL',
|
||||||
label: this.translate.instant('core.h5p.licenseGPL'),
|
label: Translate.instance.instant('core.h5p.licenseGPL'),
|
||||||
versions: [
|
versions: [
|
||||||
{
|
{
|
||||||
value: 'v3',
|
value: 'v3',
|
||||||
label: this.translate.instant('core.h5p.licenseV3')
|
label: Translate.instance.instant('core.h5p.licenseV3')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'v2',
|
value: 'v2',
|
||||||
label: this.translate.instant('core.h5p.licenseV2')
|
label: Translate.instance.instant('core.h5p.licenseV2')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'v1',
|
value: 'v1',
|
||||||
label: this.translate.instant('core.h5p.licenseV1')
|
label: Translate.instance.instant('core.h5p.licenseV1')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'PD',
|
value: 'PD',
|
||||||
label: this.translate.instant('core.h5p.pd'),
|
label: Translate.instance.instant('core.h5p.pd'),
|
||||||
versions: [
|
versions: [
|
||||||
{
|
{
|
||||||
value: '-',
|
value: '-',
|
||||||
|
@ -1266,24 +1226,24 @@ export class CoreH5PContentValidator {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC0 1.0',
|
value: 'CC0 1.0',
|
||||||
label: this.translate.instant('core.h5p.licenseCC010U')
|
label: Translate.instance.instant('core.h5p.licenseCC010U')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'CC PDM',
|
value: 'CC PDM',
|
||||||
label: this.translate.instant('core.h5p.pdm')
|
label: Translate.instance.instant('core.h5p.pdm')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'C',
|
value: 'C',
|
||||||
label: this.translate.instant('core.h5p.copyrightstring')
|
label: Translate.instance.instant('core.h5p.copyrightstring')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'version',
|
name: 'version',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: this.translate.instant('core.h5p.licenseversion'),
|
label: Translate.instance.instant('core.h5p.licenseversion'),
|
||||||
options: []
|
options: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1301,23 +1261,23 @@ export class CoreH5PContentValidator {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: '4.0',
|
value: '4.0',
|
||||||
label: this.translate.instant('core.h5p.licenseCC40')
|
label: Translate.instance.instant('core.h5p.licenseCC40')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '3.0',
|
value: '3.0',
|
||||||
label: this.translate.instant('core.h5p.licenseCC30')
|
label: Translate.instance.instant('core.h5p.licenseCC30')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '2.5',
|
value: '2.5',
|
||||||
label: this.translate.instant('core.h5p.licenseCC25')
|
label: Translate.instance.instant('core.h5p.licenseCC25')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '2.0',
|
value: '2.0',
|
||||||
label: this.translate.instant('core.h5p.licenseCC20')
|
label: Translate.instance.instant('core.h5p.licenseCC20')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '1.0',
|
value: '1.0',
|
||||||
label: this.translate.instant('core.h5p.licenseCC10')
|
label: Translate.instance.instant('core.h5p.licenseCC10')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
};
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges, SimpleChange } from '@angular/core';
|
import { Component, Input, ElementRef, OnInit, OnDestroy, OnChanges, SimpleChange } from '@angular/core';
|
||||||
import { CoreAppProvider } from '@providers/app';
|
import { CoreApp } from '@providers/app';
|
||||||
import { CoreEventsProvider } from '@providers/events';
|
import { CoreEvents } from '@providers/events';
|
||||||
import { CoreFileProvider } from '@providers/file';
|
import { CoreFile } from '@providers/file';
|
||||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
import { CoreFilepool } from '@providers/filepool';
|
||||||
import { CoreLoggerProvider } from '@providers/logger';
|
import { CoreLogger } from '@providers/logger';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSites } from '@providers/sites';
|
||||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
import { CoreDomUtils } from '@providers/utils/dom';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreUrlUtils } from '@providers/utils/url';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
import { CoreH5P } from '@core/h5p/providers/h5p';
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
|
||||||
import { CoreH5PProvider } from '@core/h5p/providers/h5p';
|
|
||||||
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
|
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
|
||||||
import { CoreFileHelperProvider } from '@providers/file-helper';
|
import { CoreFileHelper } from '@providers/file-helper';
|
||||||
import { CoreConstants } from '@core/constants';
|
import { CoreConstants } from '@core/constants';
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
|
import { CoreH5PCore } from '../../classes/core';
|
||||||
|
import { CoreH5PHelper } from '../../classes/helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render an H5P package.
|
* Component to render an H5P package.
|
||||||
|
@ -55,26 +55,13 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
protected urlParams;
|
protected urlParams;
|
||||||
protected logger;
|
protected logger;
|
||||||
|
|
||||||
constructor(loggerProvider: CoreLoggerProvider,
|
constructor(public elementRef: ElementRef,
|
||||||
public elementRef: ElementRef,
|
protected pluginFileDelegate: CorePluginFileDelegate) {
|
||||||
protected sitesProvider: CoreSitesProvider,
|
|
||||||
protected urlUtils: CoreUrlUtilsProvider,
|
|
||||||
protected utils: CoreUtilsProvider,
|
|
||||||
protected textUtils: CoreTextUtilsProvider,
|
|
||||||
protected h5pProvider: CoreH5PProvider,
|
|
||||||
protected filepoolProvider: CoreFilepoolProvider,
|
|
||||||
protected eventsProvider: CoreEventsProvider,
|
|
||||||
protected appProvider: CoreAppProvider,
|
|
||||||
protected domUtils: CoreDomUtilsProvider,
|
|
||||||
protected pluginFileDelegate: CorePluginFileDelegate,
|
|
||||||
protected fileProvider: CoreFileProvider,
|
|
||||||
protected fileHelper: CoreFileHelperProvider) {
|
|
||||||
|
|
||||||
this.logger = loggerProvider.getInstance('CoreH5PPlayerComponent');
|
this.logger = CoreLogger.instance.getInstance('CoreH5PPlayerComponent');
|
||||||
this.site = sitesProvider.getCurrentSite();
|
this.site = CoreSites.instance.getCurrentSite();
|
||||||
this.siteId = this.site.getId();
|
this.siteId = this.site.getId();
|
||||||
this.siteCanDownload = this.sitesProvider.getCurrentSite().canDownloadFiles() &&
|
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
|
||||||
!this.h5pProvider.isOfflineDisabledInSite();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,90 +86,102 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
*
|
*
|
||||||
* @param e Event.
|
* @param e Event.
|
||||||
*/
|
*/
|
||||||
play(e: MouseEvent): void {
|
async play(e: MouseEvent): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
let promise;
|
let localUrl: string;
|
||||||
|
|
||||||
if (this.canDownload && this.fileHelper.isStateDownloaded(this.state)) {
|
if (this.canDownload && CoreFileHelper.instance.isStateDownloaded(this.state)) {
|
||||||
// Package is downloaded, use the local URL.
|
// Package is downloaded, use the local URL.
|
||||||
promise = this.h5pProvider.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId).catch(() => {
|
try {
|
||||||
|
localUrl = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId);
|
||||||
|
} catch (error) {
|
||||||
// Index file doesn't exist, probably deleted because a lib was updated. Try to create it again.
|
// Index file doesn't exist, probably deleted because a lib was updated. Try to create it again.
|
||||||
return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.urlParams.url).then((path) => {
|
try {
|
||||||
return this.fileProvider.getFile(path);
|
const path = await CoreFilepool.instance.getInternalUrlByUrl(this.siteId, this.urlParams.url);
|
||||||
}).then((file) => {
|
|
||||||
return this.h5pProvider.extractH5PFile(this.urlParams.url, file, this.siteId);
|
const file = await CoreFile.instance.getFile(path);
|
||||||
}).then(() => {
|
|
||||||
|
await CoreH5PHelper.saveH5P(this.urlParams.url, file, this.siteId);
|
||||||
|
|
||||||
// File treated. Try to get the index file URL again.
|
// File treated. Try to get the index file URL again.
|
||||||
return this.h5pProvider.getContentIndexFileUrl(this.urlParams.url, this.urlParams, this.siteId);
|
localUrl = await CoreH5P.instance.h5pPlayer.getContentIndexFileUrl(this.urlParams.url, this.urlParams,
|
||||||
});
|
this.siteId);
|
||||||
}).catch((error) => {
|
} catch (error) {
|
||||||
// Still failing. Delete the H5P package?
|
// Still failing. Delete the H5P package?
|
||||||
this.logger.error('Error loading downloaded index:', error, this.src);
|
this.logger.error('Error loading downloaded index:', error, this.src);
|
||||||
});
|
}
|
||||||
} else {
|
}
|
||||||
promise = Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.then((url) => {
|
try {
|
||||||
if (url) {
|
if (localUrl) {
|
||||||
// Local package.
|
// Local package.
|
||||||
this.playerSrc = url;
|
this.playerSrc = localUrl;
|
||||||
} else {
|
} else {
|
||||||
// Never allow downloading in the app. This will only work if the user is allowed to change the params.
|
// Never allow downloading in the app. This will only work if the user is allowed to change the params.
|
||||||
const src = this.src && this.src.replace(CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=1',
|
const src = this.src && this.src.replace(CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1',
|
||||||
CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=0');
|
CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=0');
|
||||||
|
|
||||||
// Get auto-login URL so the user is automatically authenticated.
|
// Get auto-login URL so the user is automatically authenticated.
|
||||||
return this.sitesProvider.getCurrentSite().getAutoLoginUrl(src, false).then((url) => {
|
const url = await CoreSites.instance.getCurrentSite().getAutoLoginUrl(src, false);
|
||||||
// Add the preventredirect param so the user can authenticate.
|
|
||||||
this.playerSrc = this.urlUtils.addParamsToUrl(url, {preventredirect: false});
|
// Add the preventredirect param so the user can authenticate.
|
||||||
});
|
this.playerSrc = CoreUrlUtils.instance.addParamsToUrl(url, {preventredirect: false});
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
} finally {
|
||||||
|
|
||||||
this.addResizerScript();
|
this.addResizerScript();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.showPackage = true;
|
this.showPackage = true;
|
||||||
|
|
||||||
if (this.canDownload && (this.state == CoreConstants.OUTDATED || this.state == CoreConstants.NOT_DOWNLOADED)) {
|
if (this.canDownload && (this.state == CoreConstants.OUTDATED || this.state == CoreConstants.NOT_DOWNLOADED)) {
|
||||||
// Download the package in background if the size is low.
|
// Download the package in background if the size is low.
|
||||||
this.attemptDownloadInBg().catch((error) => {
|
try {
|
||||||
|
this.attemptDownloadInBg();
|
||||||
|
} catch (error) {
|
||||||
this.logger.error('Error downloading H5P in background', error);
|
this.logger.error('Error downloading H5P in background', error);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the package.
|
* Download the package.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
download(e: Event): void {
|
async download(e: Event): Promise<void> {
|
||||||
e && e.preventDefault();
|
e && e.preventDefault();
|
||||||
e && e.stopPropagation();
|
e && e.stopPropagation();
|
||||||
|
|
||||||
if (!this.appProvider.isOnline()) {
|
if (!CoreApp.instance.isOnline()) {
|
||||||
this.domUtils.showErrorModal('core.networkerrormsg', true);
|
CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the file size and ask the user to confirm.
|
try {
|
||||||
this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId).then((size) => {
|
// Get the file size and ask the user to confirm.
|
||||||
return this.domUtils.confirmDownloadSize({ size: size, total: true }).then(() => {
|
const size = await this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId);
|
||||||
|
|
||||||
// User confirmed, add to the queue.
|
await CoreDomUtils.instance.confirmDownloadSize({ size: size, total: true });
|
||||||
return this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
|
|
||||||
}, () => {
|
// User confirmed, add to the queue.
|
||||||
// User cancelled.
|
await CoreFilepool.instance.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
|
||||||
});
|
|
||||||
}).catch((error) => {
|
} catch (error) {
|
||||||
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
if (CoreDomUtils.instance.isCanceledError(error)) {
|
||||||
|
// User cancelled, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
this.calculateState();
|
this.calculateState();
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -190,21 +189,18 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
*
|
*
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected attemptDownloadInBg(): Promise<any> {
|
protected async attemptDownloadInBg(): Promise<void> {
|
||||||
if (this.urlParams && this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() &&
|
if (this.urlParams && this.src && this.siteCanDownload && CoreH5P.instance.canGetTrustedH5PFileInSite() &&
|
||||||
this.appProvider.isOnline()) {
|
CoreApp.instance.isOnline()) {
|
||||||
|
|
||||||
// Get the file size.
|
// Get the file size.
|
||||||
return this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId).then((size) => {
|
const size = await this.pluginFileDelegate.getFileSize({fileurl: this.urlParams.url}, this.siteId);
|
||||||
|
|
||||||
if (this.filepoolProvider.shouldDownload(size)) {
|
if (CoreFilepool.instance.shouldDownload(size)) {
|
||||||
// Download the file in background.
|
// Download the file in background.
|
||||||
this.filepoolProvider.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
|
CoreFilepool.instance.addToQueueByUrl(this.siteId, this.urlParams.url, this.component, this.componentId);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -219,29 +215,33 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.id = 'core-h5p-resizer-script';
|
script.id = 'core-h5p-resizer-script';
|
||||||
script.type = 'text/javascript';
|
script.type = 'text/javascript';
|
||||||
script.src = this.h5pProvider.getResizerScriptUrl();
|
script.src = CoreH5P.instance.h5pPlayer.getResizerScriptUrl();
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the package can be downloaded.
|
* Check if the package can be downloaded.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected checkCanDownload(): void {
|
protected async checkCanDownload(): Promise<void> {
|
||||||
this.observer && this.observer.off();
|
this.observer && this.observer.off();
|
||||||
this.urlParams = this.urlUtils.extractUrlParams(this.src);
|
this.urlParams = CoreUrlUtils.instance.extractUrlParams(this.src);
|
||||||
|
|
||||||
if (this.src && this.siteCanDownload && this.h5pProvider.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) {
|
if (this.src && this.siteCanDownload && CoreH5P.instance.canGetTrustedH5PFileInSite() && this.site.containsUrl(this.src)) {
|
||||||
|
|
||||||
this.calculateState();
|
this.calculateState();
|
||||||
|
|
||||||
// Listen for changes in the state.
|
// Listen for changes in the state.
|
||||||
this.filepoolProvider.getFileEventNameByUrl(this.siteId, this.urlParams.url).then((eventName) => {
|
try {
|
||||||
this.observer = this.eventsProvider.on(eventName, () => {
|
const eventName = await CoreFilepool.instance.getFileEventNameByUrl(this.siteId, this.urlParams.url);
|
||||||
|
|
||||||
|
this.observer = CoreEvents.instance.on(eventName, () => {
|
||||||
this.calculateState();
|
this.calculateState();
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
} catch (error) {
|
||||||
// An error probably means the file cannot be downloaded or we cannot check it (offline).
|
// An error probably means the file cannot be downloaded or we cannot check it (offline).
|
||||||
});
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.calculating = false;
|
this.calculating = false;
|
||||||
|
@ -254,19 +254,22 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
* Calculate state of the file.
|
* Calculate state of the file.
|
||||||
*
|
*
|
||||||
* @param fileUrl The H5P file URL.
|
* @param fileUrl The H5P file URL.
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected calculateState(): void {
|
protected async calculateState(): Promise<void> {
|
||||||
this.calculating = true;
|
this.calculating = true;
|
||||||
|
|
||||||
// Get the status of the file.
|
// Get the status of the file.
|
||||||
this.filepoolProvider.getFileStateByUrl(this.siteId, this.urlParams.url).then((state) => {
|
try {
|
||||||
|
const state = await CoreFilepool.instance.getFileStateByUrl(this.siteId, this.urlParams.url);
|
||||||
|
|
||||||
this.canDownload = true;
|
this.canDownload = true;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}).catch((error) => {
|
} catch (error) {
|
||||||
this.canDownload = false;
|
this.canDownload = false;
|
||||||
}).finally(() => {
|
} finally {
|
||||||
this.calculating = false;
|
this.calculating = false;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,14 +15,12 @@
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CoreH5PComponentsModule } from './components/components.module';
|
import { CoreH5PComponentsModule } from './components/components.module';
|
||||||
import { CoreH5PProvider } from './providers/h5p';
|
import { CoreH5PProvider } from './providers/h5p';
|
||||||
import { CoreH5PUtilsProvider } from './providers/utils';
|
|
||||||
import { CoreH5PPluginFileHandler } from './providers/pluginfile-handler';
|
import { CoreH5PPluginFileHandler } from './providers/pluginfile-handler';
|
||||||
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
|
import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
|
||||||
|
|
||||||
// List of providers (without handlers).
|
// List of providers (without handlers).
|
||||||
export const CORE_H5P_PROVIDERS: any[] = [
|
export const CORE_H5P_PROVIDERS: any[] = [
|
||||||
CoreH5PProvider,
|
CoreH5PProvider,
|
||||||
CoreH5PUtilsProvider
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -32,7 +30,6 @@ export const CORE_H5P_PROVIDERS: any[] = [
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CoreH5PProvider,
|
CoreH5PProvider,
|
||||||
CoreH5PUtilsProvider,
|
|
||||||
CoreH5PPluginFileHandler
|
CoreH5PPluginFileHandler
|
||||||
],
|
],
|
||||||
exports: []
|
exports: []
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,16 +13,15 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreFileProvider } from '@providers/file';
|
|
||||||
import { CorePluginFileHandler } from '@providers/plugin-file-delegate';
|
import { CorePluginFileHandler } from '@providers/plugin-file-delegate';
|
||||||
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
|
import { CoreMimetypeUtils } from '@providers/utils/mimetype';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreUrlUtils } from '@providers/utils/url';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
import { CoreUtils } from '@providers/utils/utils';
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
import { CoreH5P } from './h5p';
|
||||||
import { CoreH5PProvider } from './h5p';
|
|
||||||
import { CoreWSExternalFile } from '@providers/ws';
|
import { CoreWSExternalFile } from '@providers/ws';
|
||||||
import { FileEntry } from '@ionic-native/file';
|
import { FileEntry } from '@ionic-native/file';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { Translate } from '@singletons/core.singletons';
|
||||||
|
import { CoreH5PHelper } from '../classes/helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to treat H5P files.
|
* Handler to treat H5P files.
|
||||||
|
@ -31,14 +30,6 @@ import { TranslateService } from '@ngx-translate/core';
|
||||||
export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
name = 'CoreH5PPluginFileHandler';
|
name = 'CoreH5PPluginFileHandler';
|
||||||
|
|
||||||
constructor(protected urlUtils: CoreUrlUtilsProvider,
|
|
||||||
protected mimeUtils: CoreMimetypeUtilsProvider,
|
|
||||||
protected textUtils: CoreTextUtilsProvider,
|
|
||||||
protected utils: CoreUtilsProvider,
|
|
||||||
protected fileProvider: CoreFileProvider,
|
|
||||||
protected h5pProvider: CoreH5PProvider,
|
|
||||||
protected translate: TranslateService) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React to a file being deleted.
|
* React to a file being deleted.
|
||||||
*
|
*
|
||||||
|
@ -49,7 +40,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
*/
|
*/
|
||||||
fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> {
|
fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> {
|
||||||
// If an h5p file is deleted, remove the contents folder.
|
// If an h5p file is deleted, remove the contents folder.
|
||||||
return this.h5pProvider.deleteContentByUrl(fileUrl, siteId);
|
return CoreH5P.instance.h5pPlayer.deleteContentByUrl(fileUrl, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +51,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
* @return Promise resolved with the file to use. Rejected if cannot download.
|
* @return Promise resolved with the file to use. Rejected if cannot download.
|
||||||
*/
|
*/
|
||||||
getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile> {
|
getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise<CoreWSExternalFile> {
|
||||||
return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId);
|
return CoreH5P.instance.getTrustedH5PFile(file.fileurl, {}, false, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,7 +66,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
const urls = [];
|
const urls = [];
|
||||||
|
|
||||||
for (let i = 0; i < iframes.length; i++) {
|
for (let i = 0; i < iframes.length; i++) {
|
||||||
const params = this.urlUtils.extractUrlParams(iframes[i].src);
|
const params = CoreUrlUtils.instance.extractUrlParams(iframes[i].src);
|
||||||
|
|
||||||
if (params.url) {
|
if (params.url) {
|
||||||
urls.push(params.url);
|
urls.push(params.url);
|
||||||
|
@ -92,17 +83,19 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with the size.
|
* @return Promise resolved with the size.
|
||||||
*/
|
*/
|
||||||
getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
|
async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise<number> {
|
||||||
return this.h5pProvider.getTrustedH5PFile(file.fileurl, {}, false, siteId).then((file) => {
|
try {
|
||||||
return file.filesize;
|
const trustedFile = await CoreH5P.instance.getTrustedH5PFile(file.fileurl, {}, false, siteId);
|
||||||
}).catch((error): any => {
|
|
||||||
if (this.utils.isWebServiceError(error)) {
|
return trustedFile.filesize;
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.instance.isWebServiceError(error)) {
|
||||||
// WS returned an error, it means it cannot be downloaded.
|
// WS returned an error, it means it cannot be downloaded.
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
throw error;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,7 +104,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
* @return Whether or not the handler is enabled on a site level.
|
* @return Whether or not the handler is enabled on a site level.
|
||||||
*/
|
*/
|
||||||
isEnabled(): boolean | Promise<boolean> {
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
return this.h5pProvider.canGetTrustedH5PFileInSite();
|
return CoreH5P.instance.canGetTrustedH5PFileInSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,12 +115,12 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
* @return Promise resolved with a boolean and a reason why it isn't downloadable if needed.
|
* @return Promise resolved with a boolean and a reason why it isn't downloadable if needed.
|
||||||
*/
|
*/
|
||||||
async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<{downloadable: boolean, reason?: string}> {
|
async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<{downloadable: boolean, reason?: string}> {
|
||||||
const offlineDisabled = await this.h5pProvider.isOfflineDisabled(siteId);
|
const offlineDisabled = await CoreH5P.instance.isOfflineDisabled(siteId);
|
||||||
|
|
||||||
if (offlineDisabled) {
|
if (offlineDisabled) {
|
||||||
return {
|
return {
|
||||||
downloadable: false,
|
downloadable: false,
|
||||||
reason: this.translate.instant('core.h5p.offlinedisabled'),
|
reason: Translate.instance.instant('core.h5p.offlinedisabled'),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
@ -143,7 +136,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
* @return Whether the file should be treated by this handler.
|
* @return Whether the file should be treated by this handler.
|
||||||
*/
|
*/
|
||||||
shouldHandleFile(file: CoreWSExternalFile): boolean {
|
shouldHandleFile(file: CoreWSExternalFile): boolean {
|
||||||
return this.mimeUtils.guessExtensionFromUrl(file.fileurl) == 'h5p';
|
return CoreMimetypeUtils.instance.guessExtensionFromUrl(file.fileurl) == 'h5p';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -154,7 +147,7 @@ export class CoreH5PPluginFileHandler implements CorePluginFileHandler {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<any> {
|
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string): Promise<void> {
|
||||||
return this.h5pProvider.extractH5PFile(fileUrl, file, siteId);
|
return CoreH5PHelper.saveH5P(fileUrl, file, siteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
this.closeModal(text);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (!error.coreCanceled) {
|
if (!this.domUtils.isCanceledError(error)) {
|
||||||
// Show error and stop scanning.
|
// Show error and stop scanning.
|
||||||
this.domUtils.showErrorModalDefault(error, 'An error occurred.');
|
this.domUtils.showErrorModalDefault(error, 'An error occurred.');
|
||||||
this.utils.stopScanQR();
|
this.utils.stopScanQR();
|
||||||
|
|
|
@ -53,7 +53,7 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
|
||||||
const versionApplied: number = await this.configProvider.get(this.VERSION_APPLIED, 0);
|
const versionApplied: number = await this.configProvider.get(this.VERSION_APPLIED, 0);
|
||||||
|
|
||||||
if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
|
if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
|
||||||
promises.push(CoreH5P.instance.deleteAllContentIndexes());
|
promises.push(CoreH5P.instance.h5pPlayer.deleteAllContentIndexes());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -669,7 +669,7 @@ export class CoreDomUtilsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We received an object instead of a string. Search for common properties.
|
// We received an object instead of a string. Search for common properties.
|
||||||
if (error.coreCanceled) {
|
if (this.isCanceledError(error)) {
|
||||||
// It's a canceled error, don't display an error.
|
// It's a canceled error, don't display an error.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -716,6 +716,16 @@ export class CoreDomUtilsProvider {
|
||||||
return this.instances[id];
|
return this.instances[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether an error is an error caused because the user canceled a showConfirm.
|
||||||
|
*
|
||||||
|
* @param error Error to check.
|
||||||
|
* @return Whether it's a canceled error.
|
||||||
|
*/
|
||||||
|
isCanceledError(error: any): boolean {
|
||||||
|
return error && error.coreCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait an element to exists using the findFunction.
|
* Wait an element to exists using the findFunction.
|
||||||
*
|
*
|
||||||
|
@ -1314,7 +1324,7 @@ export class CoreDomUtilsProvider {
|
||||||
* @return Promise resolved with the alert modal.
|
* @return Promise resolved with the alert modal.
|
||||||
*/
|
*/
|
||||||
showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise<Alert> {
|
showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise<Alert> {
|
||||||
if (error && error.coreCanceled) {
|
if (this.isCanceledError(error)) {
|
||||||
// It's a canceled error, don't display an error.
|
// It's a canceled error, don't display an error.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1543,7 +1543,7 @@ export class CoreUtilsProvider {
|
||||||
} else if (typeof data != 'undefined') {
|
} else if (typeof data != 'undefined') {
|
||||||
this.qrScanData.deferred.resolve(data);
|
this.qrScanData.deferred.resolve(data);
|
||||||
} else {
|
} else {
|
||||||
this.qrScanData.deferred.reject({coreCanceled: true});
|
this.qrScanData.deferred.reject(this.domUtils.createCanceledError());
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.qrScanData;
|
delete this.qrScanData;
|
||||||
|
|
Loading…
Reference in New Issue