Vmeda.Online/src/core/h5p/classes/content-validator.ts

1325 lines
46 KiB
TypeScript

// (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 { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreH5PProvider, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from '../providers/h5p';
import { CoreH5PUtilsProvider } from '../providers/utils';
import { TranslateService } from '@ngx-translate/core';
/**
* Equivalent to Moodle's H5PContentValidator, but without some of the validations.
* It's also used to build the dependency list.
*/
export class CoreH5PContentValidator {
protected static ALLOWED_STYLEABLE_TAGS = ['span', 'p', 'div', 'h1', 'h2', 'h3', 'td'];
protected typeMap = {
text: 'validateText',
number: 'validateNumber',
boolean: 'validateBoolean',
list: 'validateList',
group: 'validateGroup',
file: 'validateFile',
image: 'validateImage',
video: 'validateVideo',
audio: 'validateAudio',
select: 'validateSelect',
library: 'validateLibrary',
};
protected nextWeight = 1;
protected libraries: {[libString: string]: CoreH5PLibraryData} = {};
protected dependencies: {[key: string]: CoreH5PContentDepsTreeDependency} = {};
protected relativePathRegExp = /^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/;
protected allowedHtml: {[tag: string]: any} = {};
protected allowedStyles: RegExp[];
protected metadataSemantics: any[];
protected copyrightSemantics: any;
constructor(protected h5pProvider: CoreH5PProvider,
protected h5pUtils: CoreH5PUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected utils: CoreUtilsProvider,
protected translate: TranslateService,
protected siteId: string) { }
/**
* Add Addon library.
*
* @param library The addon library to add.
* @return Promise resolved when done.
*/
addon(library: CoreH5PLibraryAddonData): Promise<void> {
const depKey = 'preloaded-' + library.machineName;
this.dependencies[depKey] = {
library: library,
type: 'preloaded'
};
return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => {
this.nextWeight = weight;
this.dependencies[depKey].weight = this.nextWeight++;
});
}
/**
* Get the flat dependency tree.
*
* @return array
*/
getDependencies(): {[key: string]: CoreH5PContentDepsTreeDependency} {
return this.dependencies;
}
/**
* Validate metadata
*
* @param metadata Metadata.
* @return Promise resolved with metadata validated & filtered.
*/
validateMetadata(metadata: any): Promise<any> {
const semantics = this.getMetadataSemantics();
const group = this.utils.clone(metadata || {});
// Stop complaining about "invalid selected option in select" for old content without license chosen.
if (typeof group.license == 'undefined') {
group.license = 'U';
}
return this.validateGroup(group, {type: 'group', fields: semantics}, false);
}
/**
* Validate given text value against text semantics.
*
* @param text Text to validate.
* @param semantics Semantics.
* @return Validated text.
*/
validateText(text: string, semantics: any): string {
if (typeof text != 'string') {
text = '';
}
if (semantics.tags) {
// Not testing for empty array allows us to use the 4 defaults without specifying them in semantics.
let tags = ['div', 'span', 'p', 'br'].concat(semantics.tags);
// Add related tags for table etc.
if (tags.indexOf('table') != -1) {
tags = tags.concat(['tr', 'td', 'th', 'colgroup', 'thead', 'tbody', 'tfoot']);
}
if (tags.indexOf('b') != -1) {
tags.push('strong');
}
if (tags.indexOf('i') != -1) {
tags.push('em');
}
if (tags.indexOf('ul') != -1 || tags.indexOf('ol') != -1) {
tags.push('li');
}
if (tags.indexOf('del') != -1 || tags.indexOf('strike') != -1) {
tags.push('s');
}
tags = this.utils.uniqueArray(tags);
// Determine allowed style tags
const stylePatterns: RegExp[] = [];
// All styles must be start to end patterns (^...$)
if (semantics.font) {
if (semantics.font.size) {
stylePatterns.push(/^font-size: *[0-9.]+(em|px|%) *;?$/i);
}
if (semantics.font.family) {
stylePatterns.push(/^font-family: *[-a-z0-9," ]+;?$/i);
}
if (semantics.font.color) {
stylePatterns.push(/^color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i);
}
if (semantics.font.background) {
stylePatterns.push(/^background-color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i);
}
if (semantics.font.spacing) {
stylePatterns.push(/^letter-spacing: *[0-9.]+(em|px|%) *;?$/i);
}
if (semantics.font.height) {
stylePatterns.push(/^line-height: *[0-9.]+(em|px|%|) *;?$/i);
}
}
// Alignment is allowed for all wysiwyg texts
stylePatterns.push(/^text-align: *(center|left|right);?$/i);
// Strip invalid HTML tags.
text = this.filterXss(text, tags, stylePatterns);
} else {
// Filter text to plain text.
text = this.textUtils.escapeHTML(text);
}
// Check if string is within allowed length.
if (typeof semantics.maxLength != 'undefined') {
text = text.substr(0, semantics.maxLength);
}
return text;
}
/**
* Validates content files
*
* @param contentPath The path containing content files to validate.
* @param isLibrary Whether it's a library.
* @return True if all files are valid.
*/
validateContentFiles(contentPath: string, isLibrary: boolean = false): boolean {
// Nothing to do, already checked by Moodle.
return true;
}
/**
* Validate given value against number semantics.
*
* @param num Number to validate.
* @param semantics Semantics.
* @return Validated number.
*/
validateNumber(num: any, semantics: any): number {
// Validate that num is indeed a number.
num = Number(num);
if (isNaN(num)) {
num = 0;
}
// Check if number is within valid bounds. Move within bounds if not.
if (typeof semantics.min != 'undefined' && num < semantics.min) {
num = semantics.min;
}
if (typeof semantics.max != 'undefined' && num > semantics.max) {
num = semantics.max;
}
// Check if number is within allowed bounds even if step value is set.
if (typeof semantics.step != 'undefined') {
const testNumber = num - (typeof semantics.min != 'undefined' ? semantics.min : 0),
rest = testNumber % semantics.step;
if (rest !== 0) {
num -= rest;
}
}
// Check if number has proper number of decimals.
if (typeof semantics.decimals != 'undefined') {
num = num.toFixed(semantics.decimals);
}
return num;
}
/**
* Validate given value against boolean semantics.
*
* @param bool Boolean to check.
* @return Validated bool.
*/
validateBoolean(bool: boolean): boolean {
return !!bool;
}
/**
* Validate select values.
*
* @param select Values to validate.
* @param semantics Semantics.
* @return Validated select.
*/
validateSelect(select: any, semantics: any): any {
const optional = semantics.optional,
options = {};
let strict = false;
if (semantics.options && semantics.options.length) {
// We have a strict set of options to choose from.
strict = true;
semantics.options.forEach((option) => {
// Support optgroup - just flatten options into one.
if (option.type == 'optgroup') {
option.options.forEach((subOption) => {
options[subOption.value] = true;
});
} else if (option.value) {
options[option.value] = true;
}
});
}
if (semantics.multiple) {
// Multi-choice generates array of values. Test each one against valid options, if we are strict.
for (const key in select) {
const value = select[key];
if (strict && !optional && !options[value]) {
delete select[key];
} else {
select[key] = this.textUtils.escapeHTML(value);
}
}
} else {
// Single mode. If we get an array in here, we chop off the first element and use that instead.
if (Array.isArray(select)) {
select = select[0];
}
if (strict && !optional && !options[select]) {
select = semantics.options[0].value;
}
select = this.textUtils.escapeHTML(select);
}
return select;
}
/**
* Validate given list value against list semantics.
* Will recurse into validating each item in the list according to the type.
*
* @param list List to validate.
* @param semantics Semantics.
* @return Validated list.
*/
validateList(list: any, semantics: any): Promise<any[]> {
const field = semantics.field,
fn = this[this.typeMap[field.type]].bind(this);
let promise = Promise.resolve(), // Use a chain of promises so the order is kept.
keys = Object.keys(list);
// Check that list is not longer than allowed length.
if (typeof semantics.max != 'undefined') {
keys = keys.slice(0, semantics.max);
}
// Validate each element in list.
keys.forEach((key) => {
if (isNaN(parseInt(key, 10))) {
// It's an object and the key isn't an integer. Delete it.
delete list[key];
} else {
promise = promise.then(() => {
return Promise.resolve(fn(list[key], field)).then((val) => {
if (val === null) {
list.splice(key, 1);
} else {
list[key] = val;
}
});
});
}
});
return promise.then(() => {
if (!Array.isArray(list)) {
list = this.utils.objectToArray(list);
}
if (!list.length) {
return null;
}
return list;
});
}
/**
* Validate a file like object, such as video, image, audio and file.
*
* @param file File to validate.
* @param semantics Semantics.
* @param typeValidKeys List of valid keys.
* @return Promise resolved with the validated file.
*/
protected validateFilelike(file: any, semantics: any, typeValidKeys: string[] = []): Promise<any> {
// Do not allow to use files from other content folders.
const matches = file.path.match(this.relativePathRegExp);
if (matches && matches.length) {
file.path = matches[5];
}
// Remove temporary files suffix.
if (file.path.substr(-4, 4) === '#tmp') {
file.path = file.path.substr(0, file.path.length - 4);
}
// Make sure path and mime does not have any special chars
file.path = this.textUtils.escapeHTML(file.path);
if (file.mime) {
file.mime = this.textUtils.escapeHTML(file.mime);
}
// Remove attributes that should not exist, they may contain JSON escape code.
let validKeys = ['path', 'mime', 'copyright'].concat(typeValidKeys);
if (semantics.extraAttributes) {
validKeys = validKeys.concat(semantics.extraAttributes);
}
validKeys = this.utils.uniqueArray(validKeys);
this.filterParams(file, validKeys);
if (typeof file.width != 'undefined') {
file.width = parseInt(file.width, 10);
}
if (typeof file.height != 'undefined') {
file.height = parseInt(file.height, 10);
}
if (file.codecs) {
file.codecs = this.textUtils.escapeHTML(file.codecs);
}
if (typeof file.bitrate != 'undefined') {
file.bitrate = parseInt(file.bitrate, 10);
}
if (typeof file.quality != 'undefined') {
if (file.quality === null || typeof file.quality.level == 'undefined' || typeof file.quality.label == 'undefined') {
delete file.quality;
} else {
this.filterParams(file.quality, ['level', 'label']);
file.quality.level = parseInt(file.quality.level);
file.quality.label = this.textUtils.escapeHTML(file.quality.label);
}
}
if (typeof file.copyright != 'undefined') {
return this.validateGroup(file.copyright, this.getCopyrightSemantics()).then(() => {
return file;
});
}
return Promise.resolve(file);
}
/**
* Validate given file data.
*
* @param file File.
* @param semantics Semantics.
* @return Promise resolved with the validated file.
*/
validateFile(file: any, semantics: any): Promise<any> {
return this.validateFilelike(file, semantics);
}
/**
* Validate given image data.
*
* @param image Image.
* @param semantics Semantics.
* @return Promise resolved with the validated file.
*/
validateImage(image: any, semantics: any): Promise<any> {
return this.validateFilelike(image, semantics, ['width', 'height', 'originalImage']);
}
/**
* Validate given video data.
*
* @param video Video.
* @param semantics Semantics.
* @return Promise resolved with the validated file.
*/
validateVideo(video: any, semantics: any): Promise<any> {
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
for (const key in video) {
promise = promise.then(() => {
return this.validateFilelike(video[key], semantics, ['width', 'height', 'codecs', 'quality', 'bitrate']);
});
}
return promise.then(() => {
return video;
});
}
/**
* Validate given audio data.
*
* @param audio Audio.
* @param semantics Semantics.
* @return Promise resolved with the validated file.
*/
validateAudio(audio: any, semantics: any): Promise<any> {
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
for (const key in audio) {
promise = promise.then(() => {
return this.validateFilelike(audio[key], semantics);
});
}
return promise.then(() => {
return audio;
});
}
/**
* Validate given group value against group semantics.
* Will recurse into validating each group member.
*
* @param group Group.
* @param semantics Semantics.
* @param flatten Whether to flatten.
*/
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.
const isSubContent = semantics.isSubContent === true;
if (semantics.fields.length == 1 && flatten && !isSubContent) {
const field = semantics.fields[0],
fn = this[this.typeMap[field.type]].bind(this);
return Promise.resolve(fn(group, field));
} else {
let promise = Promise.resolve(); // Use a chain of promises so the order is kept.
for (const key in group) {
// If subContentId is set, keep value
if (isSubContent && key == 'subContentId') {
continue;
}
// Find semantics for name=key.
let found = false,
fn = null,
field = null;
for (let i = 0; i < semantics.fields.length; i++) {
field = semantics.fields[i];
if (field.name == key) {
if (semantics.optional) {
field.optional = true;
}
fn = this[this.typeMap[field.type]].bind(this);
found = true;
break;
}
}
if (found && fn) {
promise = promise.then(() => {
return Promise.resolve(fn(group[key], field)).then((val) => {
group[key] = val;
if (val === null) {
delete group[key];
}
});
});
} else {
// Something exists in content that does not have a corresponding semantics field. Remove it.
delete group.key;
}
}
return promise.then(() => {
return group;
});
}
}
/**
* Validate given library value against library semantics.
* Check if provided library is within allowed options.
* Will recurse into validating the library's semantics too.
*
* @param value Value.
* @param semantics Semantics.
* @return Promise resolved when done.
*/
validateLibrary(value: any, semantics: any): Promise<any> {
if (!value.library) {
return Promise.resolve();
}
let promise;
if (!this.libraries[value.library]) {
const libSpec = this.h5pUtils.libraryFromString(value.library);
promise = this.h5pProvider.loadLibrary(libSpec.machineName, libSpec.majorVersion, libSpec.minorVersion, this.siteId)
.then((library) => {
this.libraries[value.library] = library;
return library;
});
} else {
promise = Promise.resolve(this.libraries[value.library]);
}
return promise.then((library) => {
// Validate parameters.
return this.validateGroup(value.params, {type: 'group', fields: library.semantics}, false).then((validated) => {
value.params = validated;
// Validate subcontent's metadata
if (value.metadata) {
return this.validateMetadata(value.metadata).then((res) => {
value.metadata = res;
});
}
}).then(() => {
let validKeys = ['library', 'params', 'subContentId', 'metadata'];
if (semantics.extraAttributes) {
validKeys = this.utils.uniqueArray(validKeys.concat(semantics.extraAttributes));
}
this.filterParams(value, validKeys);
if (value.subContentId &&
!value.subContentId.match(/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/)) {
delete value.subContentId;
}
// Find all dependencies for this library.
const depKey = 'preloaded-' + library.machineName;
if (!this.dependencies[depKey]) {
this.dependencies[depKey] = {
library: library,
type: 'preloaded'
};
return this.h5pProvider.findLibraryDependencies(this.dependencies, library, this.nextWeight).then((weight) => {
this.nextWeight = weight;
this.dependencies[depKey].weight = this.nextWeight++;
return value;
});
} else {
return value;
}
});
});
}
/**
* Check params for a whitelist of allowed properties.
*
* @param params Object to filter.
* @param whitelist List of keys to keep.
*/
filterParams(params: any, whitelist: string[]): void {
for (const key in params) {
if (whitelist.indexOf(key) == -1) {
delete params[key];
}
}
}
/**
* Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
* Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
*
* @param str The string with raw HTML in it.
* @param allowedTags An array of allowed tags.
* @param allowedStyles Allowed styles.
* @return An XSS safe version of the string.
*/
protected filterXss(str: string, allowedTags?: string[], allowedStyles?: RegExp[]): string {
if (!str || typeof str != 'string') {
return str;
}
allowedTags = allowedTags || ['a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd'];
this.allowedStyles = allowedStyles;
// Store the text format.
this.filterXssSplit(allowedTags, true);
// Remove Netscape 4 JS entities.
str = str.replace(/&\s*\{[^}]*(\}\s*;?|$)/g, '');
// Defuse all HTML entities.
str = str.replace(/&/g, '&amp;');
// Change back only well-formed entities in our whitelist:
// Decimal numeric entities.
str = str.replace(/&amp;#([0-9]+;)/g, '&#$1');
// Hexadecimal numeric entities.
str = str.replace(/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/g, '&#x$1');
// Named entities.
str = str.replace(/&amp;([A-Za-z][A-Za-z0-9]*;)/g, '&$1');
const matches = str.match(/(<(?=[^a-zA-Z!\/])|<!--.*?-->|<[^>]*(>|$)|>)/g);
if (matches && matches.length) {
matches.forEach((match) => {
str = str.replace(match, this.filterXssSplit([match]));
});
}
return str;
}
/**
* Processes an HTML tag.
*
* @param m An array with various meaning depending on the value of store.
* If store is TRUE then the array contains the allowed tags.
* If store is FALSE then the array has one element, the HTML tag to process.
* @param store Whether to store m.
* @return string If the element isn't allowed, an empty string. Otherwise, the cleaned up version of the HTML element.
*/
protected filterXssSplit(m: string[], store: boolean = false): string {
if (store) {
this.allowedHtml = this.utils.arrayToObject(m);
return '';
}
const str = m[0];
if (str.substr(0, 1) != '<') {
// We matched a lone ">" character.
return '&gt;';
} else if (str.length == 1) {
// We matched a lone "<" character.
return '&lt;';
}
const matches = str.match(/^<\s*(\/\s*)?([a-zA-Z0-9\-]+)([^>]*)>?|(<!--.*?-->)$/);
if (!matches) {
// Seriously malformed.
return '';
}
const slash = matches[1] ? matches[1].trim() : '',
attrList = matches[3] || '',
comment = matches[4] || '';
let elem = matches[2] || '';
if (comment) {
elem = '!--';
}
if (!this.allowedHtml[elem.toLowerCase()]) {
// Disallowed HTML element.
return '';
}
if (comment) {
return comment;
}
if (slash != '') {
return '</' + elem + '>';
}
// Is there a closing XHTML slash at the end of the attributes?
const newAttrList = attrList.replace(/(\s?)\/\s*$/g, '$1'),
xhtmlSlash = attrList != newAttrList ? ' /' : '';
// Clean up attributes.
let attr2 = this.filterXssAttributes(newAttrList,
(CoreH5PContentValidator.ALLOWED_STYLEABLE_TAGS.indexOf(elem) != -1 ? this.allowedStyles : null)).join(' ');
attr2 = attr2.replace(/[<>]/g, '');
attr2 = attr2.length ? ' ' + attr2 : '';
return '<' + elem + attr2 + xhtmlSlash + '>';
}
/**
* Processes a string of HTML attributes.
*
* @param attr HTML attributes.
* @param allowedStyles Allowed styles.
* @return Cleaned up version of the HTML attributes.
*/
protected filterXssAttributes(attr: string, allowedStyles?: RegExp[]): string[] {
const attrArr = [];
let mode = 0,
attrName = '',
skip = false;
while (attr.length != 0) {
// Was the last operation successful?
let working = 0,
matches,
thisVal;
switch (mode) {
case 0:
// Attribute name, href for instance.
matches = attr.match(/^([-a-zA-Z]+)/);
if (matches && matches.length > 1) {
attrName = matches[1].toLowerCase();
skip = (attrName == 'style' || attrName.substr(0, 2) == 'on');
working = mode = 1;
attr = attr.replace(/^[-a-zA-Z]+/, '');
}
break;
case 1:
// Equals sign or valueless ("selected").
if (attr.match(/^\s*=\s*/)) {
working = 1;
mode = 2;
attr = attr.replace(/^\s*=\s*/, '');
break;
}
if (attr.match(/^\s+/)) {
working = 1;
mode = 0;
if (!skip) {
attrArr.push(attrName);
}
attr = attr.replace(/^\s+/, '');
}
break;
case 2:
// Attribute value, a URL after href= for instance.
matches = attr.match(/^"([^"]*)"(\s+|$)/);
if (matches && matches.length > 1) {
if (allowedStyles && attrName === 'style') {
// Allow certain styles.
for (let i = 0; i < allowedStyles.length; i++) {
const pattern = allowedStyles[i];
if (matches[1].match(pattern)) {
// All patterns are start to end patterns, and CKEditor adds one span per style.
attrArr.push('style="' + matches[1] + '"');
break;
}
}
break;
}
thisVal = this.filterXssBadProtocol(matches[1]);
if (!skip) {
attrArr.push(attrName + '="' + thisVal + '"');
}
working = 1;
mode = 0;
attr = attr.replace(/^"[^"]*"(\s+|$)/, '');
break;
}
matches = attr.match(/^'([^']*)'(\s+|$)/);
if (matches && matches.length > 1) {
thisVal = this.filterXssBadProtocol(matches[1]);
if (!skip) {
attrArr.push(attrName + '="' + thisVal + '"');
}
working = 1;
mode = 0;
attr = attr.replace(/^'[^']*'(\s+|$)/, '');
break;
}
matches = attr.match(/^([^\s\"']+)(\s+|$)/);
if (matches && matches.length > 1) {
thisVal = this.filterXssBadProtocol(matches[1]);
if (!skip) {
attrArr.push(attrName + '="' + thisVal + '"');
}
working = 1;
mode = 0;
attr = attr.replace(/^([^\s\"']+)(\s+|$)/, '');
}
break;
default:
}
if (working == 0) {
// Not well formed; remove and try again.
attr = attr.replace(/^("[^"]*("|$)|\'[^\']*(\'|$)||\S)*\s*/, '');
mode = 0;
}
}
// The attribute list ends with a valueless attribute like "selected".
if (mode == 1 && !skip) {
attrArr.push(attrName);
}
return attrArr;
}
/**
* Processes an HTML attribute value and strips dangerous protocols from URLs.
*
* @param str The string with the attribute value.
* @param decode Whether to decode entities in the str.
* @return Cleaned up and HTML-escaped version of str.
*/
filterXssBadProtocol(str: string, decode: boolean = true): string {
// Get the plain text representation of the attribute value (i.e. its meaning).
if (decode) {
str = this.textUtils.decodeHTMLEntities(str);
}
return this.textUtils.escapeHTML(this.stripDangerousProtocols(str));
}
/**
* Strips dangerous protocols (e.g. 'javascript:') from a URI.
*
* @param uri A plain-text URI that might contain dangerous protocols.
* @return A plain-text URI stripped of dangerous protocols.
*/
protected stripDangerousProtocols(uri: string): string {
const allowedProtocols = {
ftp: true,
http: true,
https: true,
mailto: true
};
let before;
// Iteratively remove any invalid protocol found.
do {
before = uri;
const colonPos = uri.indexOf(':');
if (colonPos > 0) {
// We found a colon, possibly a protocol. Verify.
const protocol = uri.substr(0, colonPos);
// If a colon is preceded by a slash, question mark or hash, it cannot possibly be part of the URL scheme.
// This must be a relative URL, which inherits the (safe) protocol of the base document.
if (protocol.match(/[/?#]/)) {
break;
}
// Check if this is a disallowed protocol.
if (!allowedProtocols[protocol.toLowerCase()]) {
uri = uri.substr(colonPos + 1);
}
}
} while (before != uri);
return uri;
}
/**
* Get metadata semantics.
*
* @return Semantics.
*/
getMetadataSemantics(): any[] {
if (this.metadataSemantics) {
return this.metadataSemantics;
}
const ccVersions = this.getCCVersions();
this.metadataSemantics = [
{
name: 'title',
type: 'text',
label: this.translate.instant('core.h5p.title'),
placeholder: 'La Gioconda'
},
{
name: 'license',
type: 'select',
label: this.translate.instant('core.h5p.license'),
default: 'U',
options: [
{
value: 'U',
label: this.translate.instant('core.h5p.undisclosed')
},
{
type: 'optgroup',
label: this.translate.instant('core.h5p.creativecommons'),
options: [
{
value: 'CC BY',
label: this.translate.instant('core.h5p.ccattribution'),
versions: ccVersions
},
{
value: 'CC BY-SA',
label: this.translate.instant('core.h5p.ccattributionsa'),
versions: ccVersions
},
{
value: 'CC BY-ND',
label: this.translate.instant('core.h5p.ccattributionnd'),
versions: ccVersions
},
{
value: 'CC BY-NC',
label: this.translate.instant('core.h5p.ccattributionnc'),
versions: ccVersions
},
{
value: 'CC BY-NC-SA',
label: this.translate.instant('core.h5p.ccattributionncsa'),
versions: ccVersions
},
{
value: 'CC BY-NC-ND',
label: this.translate.instant('core.h5p.ccattributionncnd'),
versions: ccVersions
},
{
value: 'CC0 1.0',
label: this.translate.instant('core.h5p.ccpdd')
},
{
value: 'CC PDM',
label: this.translate.instant('core.h5p.pdm')
},
]
},
{
value: 'GNU GPL',
label: this.translate.instant('core.h5p.gpl')
},
{
value: 'PD',
label: this.translate.instant('core.h5p.pd')
},
{
value: 'ODC PDDL',
label: this.translate.instant('core.h5p.pddl')
},
{
value: 'C',
label: this.translate.instant('core.h5p.copyrightstring')
}
]
},
{
name: 'licenseVersion',
type: 'select',
label: this.translate.instant('core.h5p.licenseversion'),
options: ccVersions,
optional: true
},
{
name: 'yearFrom',
type: 'number',
label: this.translate.instant('core.h5p.yearsfrom'),
placeholder: '1991',
min: '-9999',
max: '9999',
optional: true
},
{
name: 'yearTo',
type: 'number',
label: this.translate.instant('core.h5p.yearsto'),
placeholder: '1992',
min: '-9999',
max: '9999',
optional: true
},
{
name: 'source',
type: 'text',
label: this.translate.instant('core.h5p.source'),
placeholder: 'https://',
optional: true
},
{
name: 'authors',
type: 'list',
field: {
name: 'author',
type: 'group',
fields: [
{
label: this.translate.instant('core.h5p.authorname'),
name: 'name',
optional: true,
type: 'text'
},
{
name: 'role',
type: 'select',
label: this.translate.instant('core.h5p.authorrole'),
default: 'Author',
options: [
{
value: 'Author',
label: this.translate.instant('core.h5p.author')
},
{
value: 'Editor',
label: this.translate.instant('core.h5p.editor')
},
{
value: 'Licensee',
label: this.translate.instant('core.h5p.licensee')
},
{
value: 'Originator',
label: this.translate.instant('core.h5p.originator')
}
]
}
]
}
},
{
name: 'licenseExtras',
type: 'text',
widget: 'textarea',
label: this.translate.instant('core.h5p.licenseextras'),
optional: true,
description: this.translate.instant('core.h5p.additionallicenseinfo')
},
{
name: 'changes',
type: 'list',
field: {
name: 'change',
type: 'group',
label: this.translate.instant('core.h5p.changelog'),
fields: [
{
name: 'date',
type: 'text',
label: this.translate.instant('core.h5p.date'),
optional: true
},
{
name: 'author',
type: 'text',
label: this.translate.instant('core.h5p.changedby'),
optional: true
},
{
name: 'log',
type: 'text',
widget: 'textarea',
label: this.translate.instant('core.h5p.changedescription'),
placeholder: this.translate.instant('core.h5p.changeplaceholder'),
optional: true
}
]
}
},
{
name: 'authorComments',
type: 'text',
widget: 'textarea',
label: this.translate.instant('core.h5p.authorcomments'),
description: this.translate.instant('core.h5p.authorcommentsdescription'),
optional: true
},
{
name: 'contentType',
type: 'text',
widget: 'none'
},
{
name: 'defaultLanguage',
type: 'text',
widget: 'none'
}
];
return this.metadataSemantics;
}
/**
* Get copyright semantics.
*
* @return Semantics.
*/
getCopyrightSemantics(): any {
if (this.copyrightSemantics) {
return this.copyrightSemantics;
}
const ccVersions = this.getCCVersions();
this.copyrightSemantics = {
name: 'copyright',
type: 'group',
label: this.translate.instant('core.h5p.copyrightinfo'),
fields: [
{
name: 'title',
type: 'text',
label: this.translate.instant('core.h5p.title'),
placeholder: 'La Gioconda',
optional: true
},
{
name: 'author',
type: 'text',
label: this.translate.instant('core.h5p.author'),
placeholder: 'Leonardo da Vinci',
optional: true
},
{
name: 'year',
type: 'text',
label: this.translate.instant('core.h5p.years'),
placeholder: '1503 - 1517',
optional: true
},
{
name: 'source',
type: 'text',
label: this.translate.instant('core.h5p.source'),
placeholder: 'http://en.wikipedia.org/wiki/Mona_Lisa',
optional: true,
regexp: {
pattern: '^http[s]?://.+',
modifiers: 'i'
}
},
{
name: 'license',
type: 'select',
label: this.translate.instant('core.h5p.license'),
default: 'U',
options: [
{
value: 'U',
label: this.translate.instant('core.h5p.undisclosed')
},
{
value: 'CC BY',
label: this.translate.instant('core.h5p.ccattribution'),
versions: ccVersions
},
{
value: 'CC BY-SA',
label: this.translate.instant('core.h5p.ccattributionsa'),
versions: ccVersions
},
{
value: 'CC BY-ND',
label: this.translate.instant('core.h5p.ccattributionnd'),
versions: ccVersions
},
{
value: 'CC BY-NC',
label: this.translate.instant('core.h5p.ccattributionnc'),
versions: ccVersions
},
{
value: 'CC BY-NC-SA',
label: this.translate.instant('core.h5p.ccattributionncsa'),
versions: ccVersions
},
{
value: 'CC BY-NC-ND',
label: this.translate.instant('core.h5p.ccattributionncnd'),
versions: ccVersions
},
{
value: 'GNU GPL',
label: this.translate.instant('core.h5p.licenseGPL'),
versions: [
{
value: 'v3',
label: this.translate.instant('core.h5p.licenseV3')
},
{
value: 'v2',
label: this.translate.instant('core.h5p.licenseV2')
},
{
value: 'v1',
label: this.translate.instant('core.h5p.licenseV1')
}
]
},
{
value: 'PD',
label: this.translate.instant('core.h5p.pd'),
versions: [
{
value: '-',
label: '-'
},
{
value: 'CC0 1.0',
label: this.translate.instant('core.h5p.licenseCC010U')
},
{
value: 'CC PDM',
label: this.translate.instant('core.h5p.pdm')
}
]
},
{
value: 'C',
label: this.translate.instant('core.h5p.copyrightstring')
}
]
},
{
name: 'version',
type: 'select',
label: this.translate.instant('core.h5p.licenseversion'),
options: []
}
]
};
return this.copyrightSemantics;
}
/**
* Get CC versions for semantics.
*
* @return CC versions.
*/
protected getCCVersions(): any[] {
return [
{
value: '4.0',
label: this.translate.instant('core.h5p.licenseCC40')
},
{
value: '3.0',
label: this.translate.instant('core.h5p.licenseCC30')
},
{
value: '2.5',
label: this.translate.instant('core.h5p.licenseCC25')
},
{
value: '2.0',
label: this.translate.instant('core.h5p.licenseCC20')
},
{
value: '1.0',
label: this.translate.instant('core.h5p.licenseCC10')
}
];
}
}