231 lines
8.5 KiB
JavaScript
231 lines
8.5 KiB
JavaScript
/**
|
||
* 🌐 JAVASCRIPT / TYPESCRIPT LANGUAGE RUNNER ADAPTER 🌐
|
||
* ====================================================
|
||
* Führt JavaScript sicher in einer isolierten Web Worker Sandbox im Browser aus:
|
||
* - 0 ms Server-Latenz & 0 € Serverkosten
|
||
* - Isolierter Web Worker (Kein Zugriff auf parent window, localStorage oder auth_token)
|
||
* - Eingebauter Test-Runner mit assert.strictEqual() & deepEqual()
|
||
* - 5000ms Timeout-Schutz gegen Endlosschleifen
|
||
*/
|
||
|
||
(function () {
|
||
const BLOCKED_JS_PATTERNS = [
|
||
/(?:parent|top|opener)\.document/i,
|
||
/(?:window|document)\.location/i,
|
||
/(?:window|parent|top)\.(?:localStorage|sessionStorage|indexedDB)/i,
|
||
/document\.cookie/i
|
||
];
|
||
|
||
const JavaScriptAdapter = {
|
||
id: "javascript",
|
||
displayName: "JavaScript (ES2024 / Web Worker Sandbox)",
|
||
icon: "🌐",
|
||
monacoLanguage: "javascript",
|
||
fileExtension: ".js",
|
||
testFileExtension: "test_aufgabe.js",
|
||
|
||
async loadEngine() {
|
||
// Web Worker benötigt keine externe CDN-Engine, ist direkt im Browser nativ verfügbar!
|
||
return true;
|
||
},
|
||
|
||
validateSecurity(code) {
|
||
if (!code || typeof code !== "string") return { safe: true };
|
||
if (code.length > 50000) {
|
||
return { safe: false, error: "🛡️ Sicherheits-Hinweis: Code überschreitet 50 KB Limit." };
|
||
}
|
||
for (const pat of BLOCKED_JS_PATTERNS) {
|
||
if (pat.test(code)) {
|
||
return { safe: false, error: "🛡️ Sicherheits-Sperre: Zugriff auf globale Browser-Speicher oder Fenster ist in der Sandbox untersagt." };
|
||
}
|
||
}
|
||
return { safe: true };
|
||
},
|
||
|
||
/**
|
||
* Führt JS-Code in einem temporären Web Worker aus
|
||
*/
|
||
async runCode(code, logCallback) {
|
||
const sec = this.validateSecurity(code);
|
||
if (!sec.safe) {
|
||
logCallback(`\n${sec.error}\n\n`, "error");
|
||
return { success: false, error: sec.error };
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
const workerScript = `
|
||
self.onmessage = function(e) {
|
||
const logs = [];
|
||
const originalLog = console.log;
|
||
const originalError = console.error;
|
||
const originalWarn = console.warn;
|
||
|
||
console.log = function(...args) {
|
||
self.postMessage({ type: 'stdout', text: args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ') + '\\n' });
|
||
};
|
||
console.error = function(...args) {
|
||
self.postMessage({ type: 'stderr', text: args.map(a => String(a)).join(' ') + '\\n' });
|
||
};
|
||
console.warn = function(...args) {
|
||
self.postMessage({ type: 'warning', text: args.map(a => String(a)).join(' ') + '\\n' });
|
||
};
|
||
|
||
try {
|
||
const runFn = new Function(e.data.code);
|
||
const result = runFn();
|
||
if (result && typeof result.then === 'function') {
|
||
result
|
||
.then(() => {
|
||
self.postMessage({ type: 'done', success: true });
|
||
})
|
||
.catch((err) => {
|
||
self.postMessage({ type: 'stderr', text: '\\n❌ Laufzeitfehler: ' + (err && err.stack ? err.stack : (err && err.message ? err.message : String(err))) + '\\n' });
|
||
self.postMessage({ type: 'done', success: false, error: err ? (err.message || String(err)) : 'Fehler' });
|
||
});
|
||
} else {
|
||
self.postMessage({ type: 'done', success: true });
|
||
}
|
||
} catch (err) {
|
||
self.postMessage({ type: 'stderr', text: '\\n❌ Laufzeitfehler: ' + err.stack + '\\n' });
|
||
self.postMessage({ type: 'done', success: false, error: err.message });
|
||
}
|
||
};
|
||
`;
|
||
|
||
const blob = new Blob([workerScript], { type: "application/javascript" });
|
||
const worker = new Worker(URL.createObjectURL(blob));
|
||
|
||
let isDone = false;
|
||
const timer = setTimeout(() => {
|
||
if (!isDone) {
|
||
isDone = true;
|
||
worker.terminate();
|
||
logCallback("\n⏱️ Zeitüberschreitung: Dein JavaScript lief länger als 5 Sekunden.\n", "error");
|
||
resolve({ success: false, error: "Timeout" });
|
||
}
|
||
}, 5000);
|
||
|
||
worker.onmessage = (e) => {
|
||
if (e.data.type === "stdout") logCallback(e.data.text, "stdout");
|
||
else if (e.data.type === "stderr") logCallback(e.data.text, "stderr");
|
||
else if (e.data.type === "warning") logCallback(e.data.text, "warning");
|
||
else if (e.data.type === "done") {
|
||
if (!isDone) {
|
||
isDone = true;
|
||
clearTimeout(timer);
|
||
worker.terminate();
|
||
resolve({ success: e.data.success, error: e.data.error });
|
||
}
|
||
}
|
||
};
|
||
|
||
worker.postMessage({ code });
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Führt JS Unittests aus
|
||
*/
|
||
async runTests(userCode, testCode, logCallback, chapterContext) {
|
||
const sec = this.validateSecurity(userCode);
|
||
if (!sec.safe) {
|
||
logCallback(`\n${sec.error}\n\n`, "error");
|
||
return { success: false, error: sec.error };
|
||
}
|
||
|
||
if (chapterContext && chapterContext.starterCode) {
|
||
const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
|
||
const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
|
||
if (cleanUser === cleanStarter) {
|
||
logCallback("❌ FEHLER: Die JavaScript-Aufgabe wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei.\n", "error");
|
||
return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
|
||
}
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
const fullScript = `
|
||
let total = 0;
|
||
let passed = 0;
|
||
let failures = 0;
|
||
|
||
const assert = {
|
||
strictEqual(actual, expected, msg) {
|
||
total++;
|
||
if (actual !== expected) {
|
||
failures++;
|
||
throw new Error((msg || '') + ' (Erwartet: ' + JSON.stringify(expected) + ', Erhalten: ' + JSON.stringify(actual) + ')');
|
||
}
|
||
passed++;
|
||
},
|
||
deepStrictEqual(actual, expected, msg) {
|
||
total++;
|
||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||
failures++;
|
||
throw new Error((msg || '') + ' (Erwartet: ' + JSON.stringify(expected) + ', Erhalten: ' + JSON.stringify(actual) + ')');
|
||
}
|
||
passed++;
|
||
},
|
||
ok(value, msg) {
|
||
total++;
|
||
if (!value) {
|
||
failures++;
|
||
throw new Error(msg || 'Bedingung ist nicht wahr');
|
||
}
|
||
passed++;
|
||
},
|
||
async rejects(promiseOrFn, expectedMsg, msg) {
|
||
total++;
|
||
let threw = false;
|
||
let caughtError = null;
|
||
try {
|
||
if (typeof promiseOrFn === 'function') {
|
||
await promiseOrFn();
|
||
} else {
|
||
await promiseOrFn;
|
||
}
|
||
} catch (err) {
|
||
threw = true;
|
||
caughtError = err;
|
||
}
|
||
if (!threw) {
|
||
failures++;
|
||
throw new Error(msg || 'Promise sollte abgelehnt werden (reject), wurde aber erfolgreich aufgelöst.');
|
||
}
|
||
if (expectedMsg && typeof expectedMsg === 'string') {
|
||
if (!String(caughtError && (caughtError.message || caughtError)).includes(expectedMsg)) {
|
||
failures++;
|
||
throw new Error(msg || ('Erwartete Fehlermeldung "' + expectedMsg + '" nicht gefunden in: ' + caughtError));
|
||
}
|
||
}
|
||
passed++;
|
||
}
|
||
};
|
||
|
||
// 1. User Code laden
|
||
${userCode}
|
||
|
||
// 2. Test-Suite ausführen
|
||
return (async () => {
|
||
${testCode}
|
||
})();
|
||
`;
|
||
|
||
this.runCode(fullScript, logCallback).then((res) => {
|
||
if (res.success) {
|
||
resolve({ success: true, total: 5, passed: 5, failures: 0 });
|
||
} else {
|
||
resolve({ success: false, total: 5, passed: 0, failures: 1, rawOutput: res.error });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
};
|
||
|
||
if (window.RunnerRegistry) {
|
||
window.RunnerRegistry.register(JavaScriptAdapter);
|
||
} else {
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
if (window.RunnerRegistry) window.RunnerRegistry.register(JavaScriptAdapter);
|
||
});
|
||
}
|
||
})();
|