diff --git a/backend/package.json b/backend/package.json index 29c7ecbc..bacdac6c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,7 +24,6 @@ "express": "^5.0.1", "uuid": "^11.1.0", "js-yaml": "^4.1.0", - "@types/js-yaml": "^4.0.9" }, "devDependencies": { "@mikro-orm/cli": "^6.4.6", diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index aeee268d..4cf3f163 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -11,6 +11,7 @@ import { import { Language } from './language.js'; import { Attachment } from './attachment.entity.js'; import { Teacher } from '../users/teacher.entity.js'; +import {DwengoContentType} from "../../services/learning-objects/processing/content_type"; @Entity() export class LearningObject { @@ -33,7 +34,7 @@ export class LearningObject { description!: string; @Property({ type: 'string' }) - contentType!: string; + contentType!: DwengoContentType; @Property({ type: 'array' }) keywords: string[] = []; @@ -95,12 +96,3 @@ export class ReturnValue { @Property({ type: 'json' }) callbackSchema!: string; } - -export enum ContentType { - Markdown = 'text/markdown', - Image = 'image/image', - Mpeg = 'audio/mpeg', - Pdf = 'application/pdf', - Extern = 'extern', - Blockly = 'Blockly', -} diff --git a/backend/src/services/learning-objects/processing/audio/audio_processor.js b/backend/src/services/learning-objects/processing/audio/audio_processor.js new file mode 100644 index 00000000..d41ed1ce --- /dev/null +++ b/backend/src/services/learning-objects/processing/audio/audio_processor.js @@ -0,0 +1,82 @@ +/** + * Based on + */ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import { findFile } from '../../utils/file_io.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import ProcessingHistory from "../../models/processing_history.js"; +import path from "path" +import fs from "fs" + +class AudioProcessor extends Processor { + + constructor() { + super(); + this.types = ["audio/mpeg"] // TODO add functionality to accept other audio types (ogg, wav) + } + + /** + * + * @param {string} audioUrl + * @param {object} args Optional arguments specific to the render function of the AudioProcessor + * @returns + */ + render(audioUrl, args = { files: [], metadata: {} }) { + + if ((!args.files || args.files.length <= 0 || !findFile(audioUrl, args.files)) && !isValidHttpUrl(audioUrl)) { + if (args.metadata && args.metadata.hruid && args.metadata.version && args.metadata.language){ + ProcessingHistory.error(args.metadata.hruid, args.metadata.version, args.metadata.language, "The audio file cannot be found. Please check if the url is spelled correctly.") + }else{ + ProcessingHistory.error("generalError", "99999999", "en", "The audio file cannot be found. Please check if the url is spelled correctly.") + } + + throw new InvalidArgumentError("The audio file cannot be found. Please check if the url is spelled correctly."); + } + + let type; + if (!args.metadata || !args.metadata.content_type || !this.types.includes(args.metadata.content_type)) { + type = this.types[0]; + } else { + type = args.metadata.content_type; + } + + if (isValidHttpUrl(audioUrl)) { + return DOMPurify.sanitize(``); + } + + if (!args.metadata._id) { + throw new InvalidArgumentError("The metadata for for the object which uses the file '" + audioUrl + "' is not loaded in the processor."); + } + + return DOMPurify.sanitize(``); + + } + + processFiles(files, metadata){ + let args = {} + let inputString = ""; + let file = files.find((f) => { + let ext = path.extname(f.originalname); + if (ext == ".mp3") { + inputString = f["originalname"] + // add files to args to check if file exists + args.files = files; + args.metadata = metadata + return true; + }else{ + return false; + } + }); + return [this.render(inputString, args), files] + } +} + +export default AudioProcessor; diff --git a/backend/src/services/learning-objects/processing/blockly/blockly_processor.js b/backend/src/services/learning-objects/processing/blockly/blockly_processor.js new file mode 100644 index 00000000..45296454 --- /dev/null +++ b/backend/src/services/learning-objects/processing/blockly/blockly_processor.js @@ -0,0 +1,83 @@ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import Logger from "../../logger.js"; +import path from "path" + +let logger = Logger.getLogger() +class BlocklyProcessor extends Processor { + constructor() { + super(); + this.blockly_base_url = process.env.SIMULATOR_READONLY_BASE_PATH; + } + + /** + * + * @param {string} blocklyXml + * @param {object} args Optional arguments specific to the render function of the BlocklyProcessor + * @returns + */ + render(blocklyXml, args = { language: "nl", id: "" }, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) { + if (!args.language || args.language.trim() == "") { + args.language = "nl"; + } + if (!args.id || args.id.trim() == "") { + throw new InvalidArgumentError("The unique object id must be passed to the blockly processor."); + } + let languages = ["aa", "ab", "af", "ak", "sq", "am", "ar", "an", "hy", "as", "av", "ae", "ay", "az", "ba", "bm", "eu", "be", "bn", "bh", "bi", "bs", "br", "bg", "my", "ca", "ch", "ce", "zh", "cu", "cv", "kw", "co", "cr", "cs", "da", "dv", "nl", "dz", "en", "eo", "et", "ee", "fo", "fj", "fi", "fr", "fy", "ff", "ka", "de", "gd", "ga", "gl", "gv", "el", "gn", "gu", "ht", "ha", "he", "hz", "hi", "ho", "hr", "hu", "ig", "is", "io", "ii", "iu", "ie", "ia", "id", "ik", "it", "jv", "ja", "kl", "kn", "ks", "kr", "kk", "km", "ki", "rw", "ky", "kv", "kg", "ko", "kj", "ku", "lo", "la", "lv", "li", "ln", "lt", "lb", "lu", "lg", "mk", "mh", "ml", "mi", "mr", "ms", "mg", "mt", "mn", "na", "nv", "nr", "nd", "ng", "ne", "nn", "nb", "no", "ny", "oc", "oj", "or", "om", "os", "pa", "fa", "pi", "pl", "pt", "ps", "qu", "rm", "ro", "rn", "ru", "sg", "sa", "si", "sk", "sl", "se", "sm", "sn", "sd", "so", "st", "es", "sc", "sr", "ss", "su", "sw", "sv", "ty", "ta", "tt", "te", "tg", "tl", "th", "bo", "ti", "to", "tn", "ts", "tk", "tr", "tw", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "cy", "wa", "wo", "xh", "yi", "yo", "za", "zu"]; + if (!languages.includes(args.language)) { + throw new InvalidArgumentError("The language must be valid. " + args.language + " is not a supported language.") + } + if (typeof blocklyXml == 'undefined') { + throw new InvalidArgumentError("The blockly XML is undefined. Please provide correct XML code.") + } + + let simulatorUrl = `${this.blockly_base_url}` + + let form = ` +
+ +
+ ` + + let iframe = ` +
+ ` + + let code = `(function(){ + var auto = setTimeout(function(){ submitform(); }, 50); + + function submitform(){ + document.forms["blockly_form_${args.id}"].submit(); + } + })() + ` + + let script = `` + + let html = form + iframe // DOMPurify.sanitize(form + iframe, {ALLOW_UNKNOWN_PROTOCOLS: true, ADD_TAGS: ["iframe", "xml"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'target']}); + html = html + script; + + return html; //TODO is not sanitized using DOMPurify.sanitize (problems with script tags) + } + + processFiles(files, metadata){ + let args = {} + let inputString = ""; + let file = files.find((f) => { + let ext = path.extname(f.originalname); + if (ext == ".xml") { + inputString = f.buffer.toString('utf8'); + args.language = metadata.language; + args.id = metadata._id; + return true; + }else{ + return false; + } + }); + return [this.render(inputString, args), files] + } +} + +export default BlocklyProcessor; diff --git a/backend/src/services/learning-objects/processing/content_type.ts b/backend/src/services/learning-objects/processing/content_type.ts new file mode 100644 index 00000000..d71c97b4 --- /dev/null +++ b/backend/src/services/learning-objects/processing/content_type.ts @@ -0,0 +1,18 @@ +/** + * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/content_type.js + */ + +enum DwengoContentType { + TEXT_PLAIN = "text/plain", + TEXT_MARKDOWN = "text/markdown", + IMAGE_BLOCK = "image/image-block", + IMAGE_INLINE = "image/image", + AUDIO_MPEG = "audio/mpeg", + APPLICATION_PDF = "application/pdf", + EXTERN = "extern", + BLOCKLY = "blockly", + GIFT = "text/gift", + CT_SCHEMA = "text/ct-schema" +} + +export { DwengoContentType } diff --git a/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js b/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js new file mode 100644 index 00000000..78c3dbc7 --- /dev/null +++ b/backend/src/services/learning-objects/processing/ct_schema/ct_schema_processor.js @@ -0,0 +1,67 @@ +import Logger from '../../logger.js'; +import InvalidArgumentError from "../../utils/invalid_argument_error.js"; +import { MarkdownProcessor } from '../markdown/markdown_processor.js'; + +class CTSchemaProcessor extends MarkdownProcessor{ + logger = Logger.getLogger(); + constructor(args = { files: [], metadata: {} }) { + super(); + this.staticPath = `${process.env.DOMAIN_URL}${process.env.STATIC_BASE_PATH}/img/ct_schema/`; + } + + /** + * + * @param {string} mdText a string containing the content for the four ct schema in markdown + * @returns The sanitized version of the generated html. + */ + render(text, args = {}) { + let html = ""; + // 1. Split text into markdown parts for each CT aspect + // 2. Call super.render on the individual parts + // 3. Group the parts together with specific html structure + + const regexObject = { + context: /([\s\S]*?)<\/context>/, + decomp: /([\s\S]*?)<\/decomposition>/, + abstr: /([\s\S]*?)<\/abstraction>/, + pattern: /([\s\S]*?)<\/patternRecognition>/, + algo: /([\s\S]*?)<\/algorithms>/, + impl: /([\s\S]*?)<\/implementation>/, + } + + let htmlObject = {} + + let htmlStructure = (valueObject) => ` +
+
${valueObject.context}
+
+
${valueObject.decomp}
+
${valueObject.pattern}
+
+
+
${valueObject.abstr}
+
${valueObject.algo}
+
+
${valueObject.impl}
+
` + + try { + for (let key in regexObject) { + let match = text.match(regexObject[key]); + if (match && match.length >= 1){ + htmlObject[key] = super.render(match[1]); + }else{ + htmlObject[key] = ""; + } + } + } catch (e) { + throw new InvalidArgumentError(e.message); + return "" + } + return htmlStructure(htmlObject); + } + +} + + +export { CTSchemaProcessor }; \ No newline at end of file diff --git a/backend/src/services/learning-objects/processing/extern/extern_processor.js b/backend/src/services/learning-objects/processing/extern/extern_processor.js new file mode 100644 index 00000000..b89ec401 --- /dev/null +++ b/backend/src/services/learning-objects/processing/extern/extern_processor.js @@ -0,0 +1,37 @@ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import Logger from "../../logger.js"; + +let logger = Logger.getLogger() +class ExternProcessor extends Processor { + constructor() { + super(); + } + + /** + * + * @param {string} externURL + * @param {object} args Optional arguments specific to the render function of the ExternProcessor + * @returns + */ + render(externURL, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) { + if (!isValidHttpUrl(externURL)) { + throw new InvalidArgumentError("The url is not valid: " + externURL); + } + + // If a seperate youtube-processor would be added, this code would need to move to that processor + // Converts youtube urls to youtube-embed urls + let match = /(.*youtube.com\/)watch\?v=(.*)/.exec(externURL) + if (match) { + externURL = match[1] + "embed/" + match[2]; + } + + + return DOMPurify.sanitize(`
`, { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']}); + + } +} + +export default ExternProcessor; diff --git a/backend/src/services/learning-objects/processing/gift/gift_processor.js b/backend/src/services/learning-objects/processing/gift/gift_processor.js new file mode 100644 index 00000000..4825eb75 --- /dev/null +++ b/backend/src/services/learning-objects/processing/gift/gift_processor.js @@ -0,0 +1,52 @@ +import Processor from "../processor.ts"; +import { isValidHttpUrl } from '../../utils/utils.js' +import { findFile } from '../../utils/file_io.js' +import InvalidArgumentError from '../../utils/invalid_argument_error.js' +import DOMPurify from 'isomorphic-dompurify'; +import ProcessingHistory from "../../models/processing_history.js"; +import path from "path" +import fs from "fs" +import { parse } from "gift-pegjs" + +class GiftProcessor extends Processor { + + constructor() { + super(); + this.types = ["text/gift"] + } + + /** + * + * @param {string} audioUrl + * @param {object} args Optional arguments specific to the render function of the GiftProcessor + * @returns + */ + render(giftString, args = { }) { + + const quizQuestions = parse(giftString); + console.log(quizQuestions); + + + return DOMPurify.sanitize(`