MOBILE-2235 h5p: Implement and use content validator

main
Dani Palou 2019-11-28 12:26:20 +01:00
parent 3da7c99fd8
commit 56faa66adc
8 changed files with 1642 additions and 142 deletions

View File

@ -1552,6 +1552,94 @@
"core.group": "moodle", "core.group": "moodle",
"core.groupsseparate": "moodle", "core.groupsseparate": "moodle",
"core.groupsvisible": "moodle", "core.groupsvisible": "moodle",
"core.h5p.additionallicenseinfo": "h5p",
"core.h5p.author": "h5p",
"core.h5p.authorcomments": "h5p",
"core.h5p.authorcommentsdescription": "h5p",
"core.h5p.authorname": "h5p",
"core.h5p.authorrole": "h5p",
"core.h5p.by": "h5p",
"core.h5p.cancellabel": "h5p",
"core.h5p.ccattribution": "h5p",
"core.h5p.ccattributionnc": "h5p",
"core.h5p.ccattributionncnd": "h5p",
"core.h5p.ccattributionncsa": "h5p",
"core.h5p.ccattributionnd": "h5p",
"core.h5p.ccattributionsa": "h5p",
"core.h5p.ccpdd": "h5p",
"core.h5p.changedby": "h5p",
"core.h5p.changedescription": "h5p",
"core.h5p.changelog": "h5p",
"core.h5p.changeplaceholder": "h5p",
"core.h5p.close": "h5p",
"core.h5p.confirmdialogbody": "h5p",
"core.h5p.confirmdialogheader": "h5p",
"core.h5p.confirmlabel": "h5p",
"core.h5p.connectionLost": "h5p",
"core.h5p.connectionReestablished": "h5p",
"core.h5p.contentCopied": "h5p",
"core.h5p.contentchanged": "h5p",
"core.h5p.contenttype": "h5p",
"core.h5p.copyright": "h5p",
"core.h5p.copyrightinfo": "h5p",
"core.h5p.copyrightstring": "h5p",
"core.h5p.copyrighttitle": "h5p",
"core.h5p.creativecommons": "h5p",
"core.h5p.date": "h5p",
"core.h5p.disablefullscreen": "h5p",
"core.h5p.download": "h5p",
"core.h5p.downloadtitle": "h5p",
"core.h5p.editor": "h5p",
"core.h5p.embed": "h5p",
"core.h5p.embedtitle": "h5p",
"core.h5p.fullscreen": "h5p",
"core.h5p.gpl": "h5p",
"core.h5p.h5ptitle": "h5p",
"core.h5p.hideadvanced": "h5p",
"core.h5p.license": "h5p",
"core.h5p.licenseCC010": "h5p",
"core.h5p.licenseCC010U": "h5p",
"core.h5p.licenseCC10": "h5p",
"core.h5p.licenseCC20": "h5p",
"core.h5p.licenseCC25": "h5p",
"core.h5p.licenseCC30": "h5p",
"core.h5p.licenseCC40": "h5p",
"core.h5p.licenseGPL": "h5p",
"core.h5p.licenseV1": "h5p",
"core.h5p.licenseV2": "h5p",
"core.h5p.licenseV3": "h5p",
"core.h5p.licensee": "h5p",
"core.h5p.licenseextras": "h5p",
"core.h5p.licenseversion": "h5p",
"core.h5p.nocopyright": "h5p",
"core.h5p.offlineDialogBody": "h5p",
"core.h5p.offlineDialogHeader": "h5p",
"core.h5p.offlineDialogRetryButtonLabel": "h5p",
"core.h5p.offlineDialogRetryMessage": "h5p",
"core.h5p.offlineSuccessfulSubmit": "h5p",
"core.h5p.originator": "h5p",
"core.h5p.pd": "h5p",
"core.h5p.pddl": "h5p",
"core.h5p.pdm": "h5p",
"core.h5p.resizescript": "h5p",
"core.h5p.resubmitScores": "h5p",
"core.h5p.reuse": "h5p",
"core.h5p.reuseContent": "h5p",
"core.h5p.reuseDescription": "h5p",
"core.h5p.showadvanced": "h5p",
"core.h5p.showless": "h5p",
"core.h5p.showmore": "h5p",
"core.h5p.size": "h5p",
"core.h5p.source": "h5p",
"core.h5p.startingover": "h5p",
"core.h5p.sublevel": "h5p",
"core.h5p.thumbnail": "h5p",
"core.h5p.title": "h5p",
"core.h5p.undisclosed": "h5p",
"core.h5p.year": "h5p",
"core.h5p.years": "h5p",
"core.h5p.yearsfrom": "h5p",
"core.h5p.yearsto": "h5p",
"core.hasdatatosync": "local_moodlemobileapp", "core.hasdatatosync": "local_moodlemobileapp",
"core.help": "moodle", "core.help": "moodle",
"core.hide": "moodle", "core.hide": "moodle",

View File

@ -1550,7 +1550,12 @@
"core.group": "Group", "core.group": "Group",
"core.groupsseparate": "Separate groups", "core.groupsseparate": "Separate groups",
"core.groupsvisible": "Visible groups", "core.groupsvisible": "Visible groups",
"core.h5p.additionallicenseinfo": "Any additional information about the license",
"core.h5p.author": "Author", "core.h5p.author": "Author",
"core.h5p.authorcomments": "Author comments",
"core.h5p.authorcommentsdescription": "Comments for the editor of the content. (This text will not be published as a part of the copyright info.)",
"core.h5p.authorname": "Author's name",
"core.h5p.authorrole": "Author's role",
"core.h5p.by": "by", "core.h5p.by": "by",
"core.h5p.cancellabel": "Cancel", "core.h5p.cancellabel": "Cancel",
"core.h5p.ccattribution": "Attribution (CC BY)", "core.h5p.ccattribution": "Attribution (CC BY)",
@ -1559,7 +1564,11 @@
"core.h5p.ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)", "core.h5p.ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)",
"core.h5p.ccattributionnd": "Attribution-NoDerivs (CC BY-ND)", "core.h5p.ccattributionnd": "Attribution-NoDerivs (CC BY-ND)",
"core.h5p.ccattributionsa": "Attribution-ShareAlike (CC BY-SA)", "core.h5p.ccattributionsa": "Attribution-ShareAlike (CC BY-SA)",
"core.h5p.ccpdd": "Public Domain Dedication (CC0)",
"core.h5p.changedby": "Changed by",
"core.h5p.changedescription": "Description of change",
"core.h5p.changelog": "Changelog", "core.h5p.changelog": "Changelog",
"core.h5p.changeplaceholder": "Photo cropped, text changed, etc.",
"core.h5p.close": "Close", "core.h5p.close": "Close",
"core.h5p.confirmdialogbody": "Please confirm that you wish to proceed. This action is not reversible.", "core.h5p.confirmdialogbody": "Please confirm that you wish to proceed. This action is not reversible.",
"core.h5p.confirmdialogheader": "Confirm action", "core.h5p.confirmdialogheader": "Confirm action",
@ -1570,18 +1579,24 @@
"core.h5p.contentchanged": "This content has changed since you last used it.", "core.h5p.contentchanged": "This content has changed since you last used it.",
"core.h5p.contenttype": "Content Type", "core.h5p.contenttype": "Content Type",
"core.h5p.copyright": "Rights of use", "core.h5p.copyright": "Rights of use",
"core.h5p.copyrightinfo": "Copyright information",
"core.h5p.copyrightstring": "Copyright", "core.h5p.copyrightstring": "Copyright",
"core.h5p.copyrighttitle": "View copyright information for this content.", "core.h5p.copyrighttitle": "View copyright information for this content.",
"core.h5p.creativecommons": "Creative Commons",
"core.h5p.date": "Date",
"core.h5p.disablefullscreen": "Disable fullscreen", "core.h5p.disablefullscreen": "Disable fullscreen",
"core.h5p.download": "Download", "core.h5p.download": "Download",
"core.h5p.downloadtitle": "Download this content as a H5P file.", "core.h5p.downloadtitle": "Download this content as a H5P file.",
"core.h5p.editor": "Editor",
"core.h5p.embed": "Embed", "core.h5p.embed": "Embed",
"core.h5p.embedtitle": "View the embed code for this content.", "core.h5p.embedtitle": "View the embed code for this content.",
"core.h5p.fullscreen": "Fullscreen", "core.h5p.fullscreen": "Fullscreen",
"core.h5p.gpl": "General Public License v3",
"core.h5p.h5ptitle": "Visit H5P.org to check out more cool content.", "core.h5p.h5ptitle": "Visit H5P.org to check out more cool content.",
"core.h5p.hideadvanced": "Hide advanced", "core.h5p.hideadvanced": "Hide advanced",
"core.h5p.license": "License", "core.h5p.license": "License",
"core.h5p.licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", "core.h5p.licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"core.h5p.licenseCC010U": "CC0 1.0 Universal",
"core.h5p.licenseCC10": "1.0 Generic", "core.h5p.licenseCC10": "1.0 Generic",
"core.h5p.licenseCC20": "2.0 Generic", "core.h5p.licenseCC20": "2.0 Generic",
"core.h5p.licenseCC25": "2.5 Generic", "core.h5p.licenseCC25": "2.5 Generic",
@ -1591,14 +1606,18 @@
"core.h5p.licenseV1": "Version 1", "core.h5p.licenseV1": "Version 1",
"core.h5p.licenseV2": "Version 2", "core.h5p.licenseV2": "Version 2",
"core.h5p.licenseV3": "Version 3", "core.h5p.licenseV3": "Version 3",
"core.h5p.licensee": "Licensee",
"core.h5p.licenseextras": "License Extras", "core.h5p.licenseextras": "License Extras",
"core.h5p.licenseversion": "License version",
"core.h5p.nocopyright": "No copyright information available for this content.", "core.h5p.nocopyright": "No copyright information available for this content.",
"core.h5p.offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.", "core.h5p.offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.",
"core.h5p.offlineDialogHeader": "Your connection to the server was lost", "core.h5p.offlineDialogHeader": "Your connection to the server was lost",
"core.h5p.offlineDialogRetryButtonLabel": "Retry now", "core.h5p.offlineDialogRetryButtonLabel": "Retry now",
"core.h5p.offlineDialogRetryMessage": "Retrying in :num....", "core.h5p.offlineDialogRetryMessage": "Retrying in :num....",
"core.h5p.offlineSuccessfulSubmit": "Successfully submitted results.", "core.h5p.offlineSuccessfulSubmit": "Successfully submitted results.",
"core.h5p.originator": "Originator",
"core.h5p.pd": "Public Domain", "core.h5p.pd": "Public Domain",
"core.h5p.pddl": "Public Domain Dedication and Licence",
"core.h5p.pdm": "Public Domain Mark (PDM)", "core.h5p.pdm": "Public Domain Mark (PDM)",
"core.h5p.play": "Play H5P", "core.h5p.play": "Play H5P",
"core.h5p.resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:", "core.h5p.resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:",
@ -1617,6 +1636,9 @@
"core.h5p.title": "Title", "core.h5p.title": "Title",
"core.h5p.undisclosed": "Undisclosed", "core.h5p.undisclosed": "Undisclosed",
"core.h5p.year": "Year", "core.h5p.year": "Year",
"core.h5p.years": "Year(s)",
"core.h5p.yearsfrom": "Years (from)",
"core.h5p.yearsto": "Years (to)",
"core.hasdatatosync": "This {{$a}} has offline data to be synchronised.", "core.hasdatatosync": "This {{$a}} has offline data to be synchronised.",
"core.help": "Help", "core.help": "Help",
"core.hide": "Hide", "core.hide": "Hide",

File diff suppressed because it is too large Load Diff

View File

@ -129,8 +129,12 @@ export class CoreH5PPlayerComponent implements OnInit, OnChanges, OnDestroy {
// Local package. // Local package.
this.playerSrc = url; this.playerSrc = url;
} else { } else {
// Never allow downloading in the app. This will only work if the user is allowed to change the params.
const src = this.src && this.src.replace(CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=1',
CoreH5PProvider.DISPLAY_OPTION_DOWNLOAD + '=0');
// 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(this.src, false).then((url) => { return this.sitesProvider.getCurrentSite().getAutoLoginUrl(src, false).then((url) => {
// Add the preventredirect param so the user can authenticate. // Add the preventredirect param so the user can authenticate.
this.playerSrc = this.urlUtils.addParamsToUrl(url, {preventredirect: false}); this.playerSrc = this.urlUtils.addParamsToUrl(url, {preventredirect: false});
}); });

View File

@ -1,5 +1,10 @@
{ {
"additionallicenseinfo": "Any additional information about the license",
"author": "Author", "author": "Author",
"authorcomments": "Author comments",
"authorcommentsdescription": "Comments for the editor of the content. (This text will not be published as a part of the copyright info.)",
"authorname": "Author's name",
"authorrole": "Author's role",
"by": "by", "by": "by",
"cancellabel": "Cancel", "cancellabel": "Cancel",
"ccattribution": "Attribution (CC BY)", "ccattribution": "Attribution (CC BY)",
@ -8,7 +13,11 @@
"ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)", "ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)",
"ccattributionnd": "Attribution-NoDerivs (CC BY-ND)", "ccattributionnd": "Attribution-NoDerivs (CC BY-ND)",
"ccattributionsa": "Attribution-ShareAlike (CC BY-SA)", "ccattributionsa": "Attribution-ShareAlike (CC BY-SA)",
"ccpdd": "Public Domain Dedication (CC0)",
"changedby": "Changed by",
"changedescription": "Description of change",
"changelog": "Changelog", "changelog": "Changelog",
"changeplaceholder": "Photo cropped, text changed, etc.",
"close": "Close", "close": "Close",
"confirmdialogbody": "Please confirm that you wish to proceed. This action is not reversible.", "confirmdialogbody": "Please confirm that you wish to proceed. This action is not reversible.",
"confirmdialogheader": "Confirm action", "confirmdialogheader": "Confirm action",
@ -19,18 +28,24 @@
"contentchanged": "This content has changed since you last used it.", "contentchanged": "This content has changed since you last used it.",
"contenttype": "Content Type", "contenttype": "Content Type",
"copyright": "Rights of use", "copyright": "Rights of use",
"copyrightinfo": "Copyright information",
"copyrightstring": "Copyright", "copyrightstring": "Copyright",
"copyrighttitle": "View copyright information for this content.", "copyrighttitle": "View copyright information for this content.",
"creativecommons": "Creative Commons",
"date": "Date",
"disablefullscreen": "Disable fullscreen", "disablefullscreen": "Disable fullscreen",
"download": "Download", "download": "Download",
"downloadtitle": "Download this content as a H5P file.", "downloadtitle": "Download this content as a H5P file.",
"editor": "Editor",
"embed": "Embed", "embed": "Embed",
"embedtitle": "View the embed code for this content.", "embedtitle": "View the embed code for this content.",
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"gpl": "General Public License v3",
"h5ptitle": "Visit H5P.org to check out more cool content.", "h5ptitle": "Visit H5P.org to check out more cool content.",
"hideadvanced": "Hide advanced", "hideadvanced": "Hide advanced",
"license": "License", "license": "License",
"licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", "licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"licenseCC010U": "CC0 1.0 Universal",
"licenseCC10": "1.0 Generic", "licenseCC10": "1.0 Generic",
"licenseCC20": "2.0 Generic", "licenseCC20": "2.0 Generic",
"licenseCC25": "2.5 Generic", "licenseCC25": "2.5 Generic",
@ -40,14 +55,18 @@
"licenseV1": "Version 1", "licenseV1": "Version 1",
"licenseV2": "Version 2", "licenseV2": "Version 2",
"licenseV3": "Version 3", "licenseV3": "Version 3",
"licensee": "Licensee",
"licenseextras": "License Extras", "licenseextras": "License Extras",
"licenseversion": "License version",
"nocopyright": "No copyright information available for this content.", "nocopyright": "No copyright information available for this content.",
"offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.", "offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.",
"offlineDialogHeader": "Your connection to the server was lost", "offlineDialogHeader": "Your connection to the server was lost",
"offlineDialogRetryButtonLabel": "Retry now", "offlineDialogRetryButtonLabel": "Retry now",
"offlineDialogRetryMessage": "Retrying in :num....", "offlineDialogRetryMessage": "Retrying in :num....",
"offlineSuccessfulSubmit": "Successfully submitted results.", "offlineSuccessfulSubmit": "Successfully submitted results.",
"originator": "Originator",
"pd": "Public Domain", "pd": "Public Domain",
"pddl": "Public Domain Dedication and Licence",
"pdm": "Public Domain Mark (PDM)", "pdm": "Public Domain Mark (PDM)",
"play": "Play H5P", "play": "Play H5P",
"resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:", "resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:",
@ -65,5 +84,8 @@
"thumbnail": "Thumbnail", "thumbnail": "Thumbnail",
"title": "Title", "title": "Title",
"undisclosed": "Undisclosed", "undisclosed": "Undisclosed",
"year": "Year" "year": "Year",
"years": "Year(s)",
"yearsfrom": "Years (from)",
"yearsto": "Years (to)"
} }

View File

@ -25,6 +25,8 @@ import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreH5PUtilsProvider } from './utils'; import { CoreH5PUtilsProvider } from './utils';
import { CoreH5PContentValidator } from '../classes/content-validator';
import { TranslateService } from '@ngx-translate/core';
import { FileEntry } from '@ionic-native/file'; import { FileEntry } from '@ionic-native/file';
/** /**
@ -304,7 +306,8 @@ export class CoreH5PProvider {
private h5pUtils: CoreH5PUtilsProvider, private h5pUtils: CoreH5PUtilsProvider,
private filepoolProvider: CoreFilepoolProvider, private filepoolProvider: CoreFilepoolProvider,
private utils: CoreUtilsProvider, private utils: CoreUtilsProvider,
private urlUtils: CoreUrlUtilsProvider) { private urlUtils: CoreUrlUtilsProvider,
private translate: TranslateService) {
this.logger = logger.getInstance('CoreH5PProvider'); this.logger = logger.getInstance('CoreH5PProvider');
@ -415,6 +418,7 @@ export class CoreH5PProvider {
* @return Promise resolved with all of the files content in one string. * @return Promise resolved with all of the files content in one string.
*/ */
protected concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> { protected concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> {
const basePath = this.fileProvider.getBasePathInstant();
let content = '', let content = '',
promise = Promise.resolve(); // Use a chain of promises so the order is kept. promise = Promise.resolve(); // Use a chain of promises so the order is kept.
@ -433,39 +437,42 @@ export class CoreH5PProvider {
if (matches && matches.length) { if (matches && matches.length) {
matches.forEach((match) => { matches.forEach((match) => {
let url = match.replace(/(url\([\'"]?|[\'"]?\)$)/i, ''); let url = match.replace(/(url\(['"]?|['"]?\)$)/ig, '');
if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) { if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) {
return; // Not relative or already treated, skip. return; // Not relative or already treated, skip.
} }
const pathSplit = assetPath.split('/');
treated[url] = url; treated[url] = url;
/* Find "../" in the URL. If it exists, we have to remove "../" and switch the last folder in the /* 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. filepath for the first folder in the url. */
For instance:
Path: /H5P.Question-1.4/styles/
Url: ../images/plus-one.svg
We want this: H5P.Question-1.4/images/ITEMID/minus-one.svg. */
if (url.match(/^\.\.\//)) { if (url.match(/^\.\.\//)) {
const pathSplit = assetPath.split('/'), const urlSplit = url.split('/').filter((i) => {
urlSplit = url.split('/').filter((i) => {
return i; // Remove empty values. return i; // Remove empty values.
}); });
// Remove the first element: ../. // Remove the file name from the asset path.
urlSplit.unshift(); pathSplit.pop();
// Remove the first element from the file URL: ../ .
urlSplit.shift();
// Put the url's first folder into the asset path. // Put the url's first folder into the asset path.
pathSplit[pathSplit.length - 1] = urlSplit[0]; pathSplit[pathSplit.length - 1] = urlSplit[0];
urlSplit.shift(); urlSplit.shift();
// Create the new URL and replace it in the file contents. // Create the new URL and replace it in the file contents.
url = '/' + pathSplit.join('/') + '/' + urlSplit.join('/'); url = pathSplit.join('/') + '/' + urlSplit.join('/');
fileContent = fileContent.replace(new RegExp(this.textUtils.escapeForRegex(match), 'g'), } else {
'url("' + url + '")'); pathSplit[pathSplit.length - 1] = url; // Put the whole path to the end of the asset path.
url = pathSplit.join('/');
} }
fileContent = fileContent.replace(new RegExp(this.textUtils.escapeForRegex(match), 'g'),
'url("' + this.textUtils.concatenatePaths(basePath, url) + '")');
}); });
} }
@ -510,11 +517,11 @@ export class CoreH5PProvider {
url: this.getEmbedUrl(site.getURL(), h5pUrl), url: this.getEmbedUrl(site.getURL(), h5pUrl),
contentUrl: contentUrl, contentUrl: contentUrl,
metadata: content.metadata, metadata: content.metadata,
contentUserData: { contentUserData: [
0: { {
state: '{}' state: '{}'
} }
} ]
}; };
// Get the core H5P assets, needed by the H5P classes to render the H5P content. // Get the core H5P assets, needed by the H5P classes to render the H5P content.
@ -859,8 +866,7 @@ export class CoreH5PProvider {
return Promise.resolve(null); return Promise.resolve(null);
} }
const dependencies = {}, // In web, dependencies are built by the validator. const params = {
params = {
library: this.libraryToString(content.library), library: this.libraryToString(content.library),
params: this.textUtils.parseJSON(content.params, false) params: this.textUtils.parseJSON(content.params, false)
}; };
@ -869,90 +875,65 @@ export class CoreH5PProvider {
return null; return null;
} }
// Get the main library data. const validator = new CoreH5PContentValidator(this, this.h5pUtils, this.textUtils, this.utils, this.translate, siteId);
return this.loadLibrary(content.library.name, content.library.majorVersion, content.library.minorVersion, siteId)
.then((library) => {
library.semantics = this.textUtils.parseJSON(library.semantics, ''); // Validate the main library and its dependencies.
return validator.validateLibrary(params, {options: [params.library]}).then(() => {
const depKey = 'preloaded-' + library.machineName; // Handle addons.
let nextWeight; return this.loadAddons(siteId);
}).then((addons) => {
// Validate addons. Use a chain of promises to calculate the weight properly.
let promise = Promise.resolve();
if (!dependencies[depKey]) { addons.forEach((addon) => {
dependencies[depKey] = { const addTo = addon.addTo;
library: library,
type: 'preloaded'
};
}
// Get the whole library dependency tree. if (addTo && addTo.content && addTo.content.types && addTo.content.types.length) {
return this.findLibraryDependencies(dependencies, library, 1, false, siteId).then((weight) => { for (let i = 0; i < addTo.content.types.length; i++) {
nextWeight = weight; const type = addTo.content.types[i];
dependencies[depKey].weight = nextWeight++;
// Handle addons. if (type && type.text && type.text.regex &&
return this.loadAddons(siteId); this.h5pUtils.textAddonMatches(params.params, type.text.regex)) {
}).then((addons) => {
// Get the dependencies of all the addons. Use a chain of promises to calculate the weight properly.
let promise = Promise.resolve();
addons.forEach((addon) => { promise = promise.then(() => {
const addTo = this.textUtils.parseJSON(addon.addTo, null); return validator.addon(addon);
});
if (addTo && addTo.content && addTo.content.types && addTo.content.types.length) { // An addon shall only be added once.
for (let i = 0; i < addTo.content.types.length; i++) { break;
const type = addTo.content.types[i];
if (type && type.text && type.text.regex &&
this.h5pUtils.textAddonMatches(params.params, type.text.regex)) {
const addonDepKey = 'preloaded-' + addon.machineName;
dependencies[addonDepKey] = {
library: addon,
type: 'preloaded'
};
promise = promise.then(() => {
return this.findLibraryDependencies(dependencies, addon, nextWeight).then((weight) => {
nextWeight = weight;
dependencies[addonDepKey].weight = nextWeight++;
});
});
break;
}
} }
} }
});
return promise;
}).then(() => {
// Update content dependencies.
content.dependencies = dependencies;
const paramsStr = JSON.stringify(params.params);
// Sometimes the parameters are filtered before content has been created
if (content.id) {
// Update library usage.
return this.deleteLibraryUsage(content.id, siteId).catch(() => {
// Ignore errors.
}).then(() => {
return this.saveLibraryUsage(content.id, content.dependencies, siteId);
}).then(() => {
if (!content.slug) {
content.slug = this.h5pUtils.slugify(content.title);
}
// Cache.
return this.updateContentFields(content.id, {filtered: paramsStr}, siteId).then(() => {
return paramsStr;
});
});
} }
return paramsStr;
}); });
return promise;
}).then(() => {
// Update content dependencies.
content.dependencies = validator.getDependencies();
const paramsStr = JSON.stringify(params.params);
// Sometimes the parameters are filtered before content has been created
if (content.id) {
// Update library usage.
return this.deleteLibraryUsage(content.id, siteId).catch(() => {
// Ignore errors.
}).then(() => {
return this.saveLibraryUsage(content.id, content.dependencies, siteId);
}).then(() => {
if (!content.slug) {
content.slug = this.h5pUtils.slugify(content.title);
}
// Cache.
return this.updateContentFields(content.id, {filtered: paramsStr}, siteId).then(() => {
return paramsStr;
});
});
}
return paramsStr;
}).catch(() => { }).catch(() => {
return null; return null;
}); });
@ -990,12 +971,13 @@ export class CoreH5PProvider {
} }
library[property].forEach((dependency: CoreH5PLibraryBasicData) => { library[property].forEach((dependency: CoreH5PLibraryBasicData) => {
const dependencyKey = type + '-' + dependency.machineName;
if (dependencies[dependencyKey]) {
return; // Skip, already have this.
}
promise = promise.then(() => { promise = promise.then(() => {
const dependencyKey = type + '-' + dependency.machineName;
if (dependencies[dependencyKey]) {
return; // Skip, already have this.
}
// Get the dependency library data and its subdependencies. // Get the dependency library data and its subdependencies.
return this.loadLibrary(dependency.machineName, dependency.majorVersion, dependency.minorVersion, siteId) return this.loadLibrary(dependency.machineName, dependency.majorVersion, dependency.minorVersion, siteId)
.then((dependencyLibrary) => { .then((dependencyLibrary) => {
@ -1424,7 +1406,7 @@ export class CoreH5PProvider {
// Aggregate and store assets. // Aggregate and store assets.
return this.cacheAssets(files, cachedAssetsHash, folderName, siteId).then(() => { return this.cacheAssets(files, cachedAssetsHash, folderName, siteId).then(() => {
// Keep track of which libraries have been cached in case they are updated. // Keep track of which libraries have been cached in case they are updated.
return this.saveCachedAssets(cachedAssetsHash, dependencies, siteId); return this.saveCachedAssets(cachedAssetsHash, dependencies, folderName, siteId);
}).then(() => { }).then(() => {
return files; return files;
}); });
@ -1652,12 +1634,12 @@ export class CoreH5PProvider {
} }
return db.getRecords(this.LIBRARIES_TABLE, conditions); return db.getRecords(this.LIBRARIES_TABLE, conditions);
}).then((libraries) => { }).then((libraries): any => {
if (!libraries.length) { if (!libraries.length) {
return Promise.reject(null); return Promise.reject(null);
} }
return libraries[0]; return this.parseLibDBData(libraries[0]);
}); });
} }
@ -1681,7 +1663,9 @@ export class CoreH5PProvider {
*/ */
protected getLibraryById(id: number, siteId?: string): Promise<CoreH5PLibraryDBData> { protected getLibraryById(id: number, siteId?: string): Promise<CoreH5PLibraryDBData> {
return this.sitesProvider.getSiteDb(siteId).then((db) => { return this.sitesProvider.getSiteDb(siteId).then((db) => {
return db.getRecord(this.LIBRARIES_TABLE, {id: id}); return db.getRecord(this.LIBRARIES_TABLE, {id: id}).then((library) => {
return this.parseLibDBData(library);
});
}); });
} }
@ -1946,7 +1930,7 @@ export class CoreH5PProvider {
const addons = []; const addons = [];
for (let i = 0; i < result.rows.length; i++) { for (let i = 0; i < result.rows.length; i++) {
addons.push(result.rows.item(i)); addons.push(this.parseLibAddonData(result.rows.item(i)));
} }
return addons; return addons;
@ -1963,6 +1947,8 @@ export class CoreH5PProvider {
* @return Promise resolved with the content data. * @return Promise resolved with the content data.
*/ */
protected loadContentData(id?: number, fileUrl?: string, siteId?: string): Promise<CoreH5PContentData> { protected loadContentData(id?: number, fileUrl?: string, siteId?: string): Promise<CoreH5PContentData> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let promise: Promise<CoreH5PContentDBData>; let promise: Promise<CoreH5PContentDBData>;
if (id) { if (id) {
@ -1978,31 +1964,35 @@ export class CoreH5PProvider {
// Load the main library data. // Load the main library data.
return this.getLibraryById(contentData.mainlibraryid, siteId).then((libData) => { return this.getLibraryById(contentData.mainlibraryid, siteId).then((libData) => {
// Map the values to the names used by the H5P core (it's the same Moodle web does). // Validate metadata.
return { const validator = new CoreH5PContentValidator(this, this.h5pUtils, this.textUtils, this.utils, this.translate,
id: contentData.id, siteId);
params: contentData.jsoncontent,
// The embedtype will be always set to 'iframe' to prevent conflicts with JS and CSS. // Validate empty metadata, like Moodle web does.
embedType: 'iframe', return validator.validateMetadata({}).then((metadata) => {
disable: null, // Map the values to the names used by the H5P core (it's the same Moodle web does).
folderName: contentData.foldername, return {
title: libData.title, id: contentData.id,
slug: this.h5pUtils.slugify(libData.title) + '-' + contentData.id, params: contentData.jsoncontent,
filtered: contentData.filtered, embedType: 'iframe', // Always use iframe.
libraryMajorVersion: libData.majorversion, disable: null,
libraryMinorVersion: libData.minorversion, folderName: contentData.foldername,
metadata: { title: libData.title,
license: 'U' // Stop "invalid selected option in select" for old content without license chosen. slug: this.h5pUtils.slugify(libData.title) + '-' + contentData.id,
}, filtered: contentData.filtered,
library: { libraryMajorVersion: libData.majorversion,
id: libData.id, libraryMinorVersion: libData.minorversion,
name: libData.machinename, metadata: metadata,
majorVersion: libData.majorversion, library: {
minorVersion: libData.minorversion, id: libData.id,
embedTypes: libData.embedtypes, name: libData.machinename,
fullscreen: libData.fullscreen majorVersion: libData.majorversion,
} minorVersion: libData.minorversion,
}; embedTypes: libData.embedtypes,
fullscreen: libData.fullscreen
}
};
});
}); });
}); });
} }
@ -2113,6 +2103,31 @@ export class CoreH5PProvider {
}); });
} }
/**
* Parse library addon data.
*
* @param library Library addon data.
* @return Parsed library.
*/
parseLibAddonData(library: any): CoreH5PLibraryAddonData {
library.addto = this.textUtils.parseJSON(library.addto, null);
return library;
}
/**
* Parse library DB data.
*
* @param library Library DB data.
* @return Parsed library.
*/
parseLibDBData(library: any): CoreH5PLibraryDBData {
library.semantics = this.textUtils.parseJSON(library.semantics, null);
library.addto = this.textUtils.parseJSON(library.addto, null);
return library;
}
/** /**
* Process libraries from an H5P library, getting the required data to save them. * Process libraries from an H5P library, getting the required data to save them.
* This code was copied from the isValidPackage function in Moodle's H5PValidator. * This code was copied from the isValidPackage function in Moodle's H5PValidator.
@ -2172,12 +2187,13 @@ export class CoreH5PProvider {
* know which cache file to delete when a library is updated. * know which cache file to delete when a library is updated.
* *
* @param key Hash key for the given libraries. * @param key Hash key for the given libraries.
* @param libraries List of dependencies used to create the key * @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. * @param siteId The site ID.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected saveCachedAssets(hash: string, dependencies: {[machineName: string]: CoreH5PContentDependencyData}, protected saveCachedAssets(hash: string, dependencies: {[machineName: string]: CoreH5PContentDependencyData},
siteId?: string): Promise<any> { folderName: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSiteDb(siteId).then((db) => { return this.sitesProvider.getSiteDb(siteId).then((db) => {
const promises = []; const promises = [];
@ -2185,7 +2201,8 @@ export class CoreH5PProvider {
for (const key in dependencies) { for (const key in dependencies) {
const data = { const data = {
hash: key, hash: key,
libraryid: dependencies[key].libraryId libraryid: dependencies[key].libraryId,
foldername: folderName
}; };
promises.push(db.insertRecord(this.LIBRARIES_CACHEDASSETS_TABLE, data)); promises.push(db.insertRecord(this.LIBRARIES_CACHEDASSETS_TABLE, data));
@ -2279,7 +2296,7 @@ export class CoreH5PProvider {
for (const libString in librariesJsonData) { for (const libString in librariesJsonData) {
const libraryData = librariesJsonData[libString]; const libraryData = librariesJsonData[libString];
// Find local library identifier // Find local library identifier.
promises.push(this.getLibraryByData(libraryData).catch(() => { promises.push(this.getLibraryByData(libraryData).catch(() => {
// Not found. // Not found.
}).then((dbData) => { }).then((dbData) => {
@ -2422,7 +2439,7 @@ export class CoreH5PProvider {
preloadedjs: preloadedJS, preloadedjs: preloadedJS,
preloadedcss: preloadedCSS, preloadedcss: preloadedCSS,
droplibrarycss: dropLibraryCSS, droplibrarycss: dropLibraryCSS,
semantics: libraryData.semantics, semantics: typeof libraryData.semantics != 'undefined' ? JSON.stringify(libraryData.semantics) : null,
addto: typeof libraryData.addTo != 'undefined' ? JSON.stringify(libraryData.addTo) : null, addto: typeof libraryData.addTo != 'undefined' ? JSON.stringify(libraryData.addTo) : null,
}; };
@ -2497,8 +2514,8 @@ export class CoreH5PProvider {
for (const key in librariesInUse) { for (const key in librariesInUse) {
const dependency = librariesInUse[key]; const dependency = librariesInUse[key];
if (dependency.library.dropLibraryCss) { if ((<CoreH5PLibraryData> dependency.library).dropLibraryCss) {
const split = dependency.library.dropLibraryCss.split(', '); const split = (<CoreH5PLibraryData> dependency.library).dropLibraryCss.split(', ');
split.forEach((css) => { split.forEach((css) => {
dropLibraryCssList[css] = css; dropLibraryCssList[css] = css;
@ -2739,7 +2756,7 @@ export type CoreH5PContentDependencyData = {
* Data for each content dependency in the dependency tree. * Data for each content dependency in the dependency tree.
*/ */
export type CoreH5PContentDepsTreeDependency = { export type CoreH5PContentDepsTreeDependency = {
library: CoreH5PLibraryData; // Library data. library: CoreH5PLibraryData | CoreH5PLibraryAddonData; // Library data.
type: string; // Dependency type. type: string; // Dependency type.
weight?: number; // An integer determining the order of the libraries when they are loaded. weight?: number; // An integer determining the order of the libraries when they are loaded.
}; };
@ -2786,7 +2803,7 @@ export type CoreH5PLibraryAddonData = {
patchVersion: number; // Patch version. patchVersion: number; // Patch version.
preloadedJs?: string; // Comma separated list of scripts to load. preloadedJs?: string; // Comma separated list of scripts to load.
preloadedCss?: string; // Comma separated list of stylesheets to load. preloadedCss?: string; // Comma separated list of stylesheets to load.
addTo?: string; // Plugin configuration data. addTo?: any; // Plugin configuration data.
}; };
/** /**
@ -2805,8 +2822,8 @@ export type CoreH5PLibraryDBData = {
preloadedjs?: string; // Comma separated list of scripts to load. preloadedjs?: string; // Comma separated list of scripts to load.
preloadedcss?: string; // Comma separated list of stylesheets 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. droplibrarycss?: string; // List of libraries that should not have CSS included if this library is used. Comma separated list.
semantics?: string; // The semantics definition in json format. semantics?: any; // The semantics definition.
addto?: string; // Plugin configuration data. addto?: any; // Plugin configuration data.
}; };
/** /**

View File

@ -313,6 +313,27 @@ export class CoreH5PUtilsProvider {
}; };
} }
/**
* 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. * Convert list of library parameter values to csv.
* *

View File

@ -135,17 +135,19 @@ export class CoreUtilsProvider {
/** /**
* Converts an array of objects to an object, using a property of each entry as the key. * Converts an array of objects to an object, using a property of each entry as the key.
* It can also be used to convert an array of strings to an object where the keys are the elements of the array.
* E.g. [{id: 10, name: 'A'}, {id: 11, name: 'B'}] => {10: {id: 10, name: 'A'}, 11: {id: 11, name: 'B'}} * E.g. [{id: 10, name: 'A'}, {id: 11, name: 'B'}] => {10: {id: 10, name: 'A'}, 11: {id: 11, name: 'B'}}
* *
* @param array The array to convert. * @param array The array to convert.
* @param propertyName The name of the property to use as the key. * @param propertyName The name of the property to use as the key. If not provided, the whole item will be used.
* @param result Object where to put the properties. If not defined, a new object will be created. * @param result Object where to put the properties. If not defined, a new object will be created.
* @return The object. * @return The object.
*/ */
arrayToObject(array: any[], propertyName: string, result?: any): any { arrayToObject(array: any[], propertyName?: string, result?: any): any {
result = result || {}; result = result || {};
array.forEach((entry) => { array.forEach((entry) => {
result[entry[propertyName]] = entry; const key = propertyName ? entry[propertyName] : entry;
result[key] = entry;
}); });
return result; return result;