openvibers/openvibe.bench Commits
Revert #84: serve each model's output exactly as written (#85)
Changes
@@@ -15,7 +15,6 @@*/const crypto = require('crypto');--const vm = require('vm');const { truncateBytes, MAX_BYTES } = require('./sanitize');const RENDER_ORIGIN = String(process.env.OV_RENDER_ORIGIN || 'https://bench-render.openvibe.services').replace(/\/+$/, '');@@@ -99,185 +98,6 @@ function applyNonce(html, nonce) {.replace(/<script\b/gi, '<script nonce="' + nonce + '"');}--/** The inline scripts a browser would run as classic scripts, in document order. Modules are left alone: their top-- * level is its own scope, and vm.Script cannot parse import/export. */--function inlineScripts(html) {-- const text = String(html == null ? '' : html);-- const scripts = [];-- const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;-- let m;-- while ((m = re.exec(text))) {-- const attrs = m[1] || '';-- if (/\bsrc\s*=/i.test(attrs)) continue;-- const typeMatch = /type\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attrs);-- const type = typeMatch ? String(typeMatch[1] || typeMatch[2] || typeMatch[3] || '').trim().toLowerCase() : '';-- if (type === 'module' || (type && !/^(?:text\/javascript|application\/javascript)$/.test(type))) continue;-- const openEnd = m.index + m[0].indexOf('>') + 1;-- scripts.push({ code: m[2], start: m.index, length: m[0].length, openEnd, originalLength: m[2].length });-- }-- return scripts;--}----/** Which characters of a script are code, not string, template or comment: a declaration in a string is not a-- * declaration. The mask also gives the brace depth at each position, so a shadowing declaration in a nested block is-- * not confused with a duplicate in the same scope. */--function codeMap(code) {-- const isCode = new Uint8Array(code.length);-- const depth = new Int32Array(code.length + 1);-- let state = 'code';-- let d = 0;-- for (let i = 0; i < code.length; i++) {-- const c = code[i], n = code[i + 1];-- depth[i] = d;-- if (state === 'code') {-- if (c === '/' && n === '/') { state = 'line'; i++; continue; }-- if (c === '/' && n === '*') { state = 'block'; i++; continue; }-- isCode[i] = 1;-- if (c === "'") state = 'single';-- else if (c === '"') state = 'double';-- else if (c === '`') state = 'template';-- else if (c === '{') d++;-- else if (c === '}') d--;-- } else if (state === 'line') {-- if (c === '\n') state = 'code';-- } else if (state === 'block') {-- if (c === '*' && n === '/') { state = 'code'; i++; }-- } else if (state === 'single') {-- if (c === '\\') i++; else if (c === "'") state = 'code';-- } else if (state === 'double') {-- if (c === '\\') i++; else if (c === '"') state = 'code';-- } else if (state === 'template') {-- if (c === '\\') i++; else if (c === '`') state = 'code';-- }-- }-- depth[code.length] = d;-- return { isCode, depth };--}----/**-- * A model answer is not trusted to compile. The browser runs a page's classic scripts in one global scope, so two-- * declarations of the same const/let - in one script or across two - throw "Identifier 'x' has already been declared"-- * before either script runs, and every showcase page that frames it reports an uncaught error (11-01: the seeded-- * dataviz entry declares maxVal twice, once per chart scale). Join the duplicate declarations at the same brace depth-- * into one binding (the first becomes let, the later ones assignments) and use the result only when it then compiles.-- * That keeps the model's output running rather than leaving a blank frame. Returns null when there is nothing to-- * repair or the repair does not compile; the caller then leaves the bad script out.-- */--function repairDuplicateDeclarations(html) {-- const text = String(html == null ? '' : html);-- const scripts = inlineScripts(text);-- if (!scripts.length) return null;-- let changed = false;-- for (let attempt = 0; attempt < 6; attempt++) {-- const joined = scripts.map((s) => s.code).join('\n;\n');-- try {-- new vm.Script(joined, { filename: 'model-scripts.js' });-- break;-- } catch (err) {-- const dup = /Identifier '([^']+)' has already been declared/.exec(String((err && err.message) || err));-- if (!dup) return null;-- const name = dup[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');-- const decls = [];-- for (let si = 0; si < scripts.length; si++) {-- const { isCode, depth } = codeMap(scripts[si].code);-- const re = new RegExp('\\b(const|let|var)\\s+' + name + '\\b', 'g');-- let m;-- while ((m = re.exec(scripts[si].code))) {-- if (!isCode[m.index]) continue;-- decls.push({ si, index: m.index, keyword: m[1], keywordEnd: m.index + m[1].length, depth: depth[m.index] });-- }-- }-- const byDepth = new Map();-- for (const d of decls) {-- if (!byDepth.has(d.depth)) byDepth.set(d.depth, []);-- byDepth.get(d.depth).push(d);-- }-- // The shallowest depth with a duplicate is the one the parser named: a legal shadowing declaration in a-- // deeper block cannot be the duplicate, and a conflicting top-level one is shallower.-- const group = [...byDepth.entries()].filter(([, g]) => g.length > 1).sort((a, b) => a[0] - b[0])[0];-- if (!group) return null;-- const declsInGroup = group[1].sort((a, b) => a.si - b.si || a.index - b.index);-- const edits = new Map();-- for (let i = 0; i < declsInGroup.length; i++) {-- const d = declsInGroup[i];-- const code = scripts[d.si].code;-- let end = d.keywordEnd;-- let replacement = '';-- if (i === 0) {-- if (d.keyword !== 'const') continue;-- replacement = 'let';-- } else {-- // Drop the keyword and the whitespace after it: "let maxVal = 1" becomes "maxVal = 1".-- while (end < code.length && /\s/.test(code[end])) end++;-- }-- if (!edits.has(d.si)) edits.set(d.si, []);-- edits.get(d.si).push({ from: d.index, to: end, text: replacement });-- }-- for (const [si, list] of edits) {-- list.sort((a, b) => b.from - a.from);-- let code = scripts[si].code;-- for (const e of list) code = code.slice(0, e.from) + e.text + code.slice(e.to);-- scripts[si].code = code;-- }-- changed = true;-- }-- }-- if (!changed) return null;-- let out = text;-- for (let i = scripts.length - 1; i >= 0; i--) {-- const s = scripts[i];-- out = out.slice(0, s.openEnd) + s.code + out.slice(s.openEnd + s.originalLength);-- }-- return out;--}----/** The scripts that still cannot run, alone or after the ones before them; the repair above handles the duplicate-- * declaration case, this is the honest fallback for every other syntax error. */--function scriptProblems(html) {-- const scripts = inlineScripts(html);-- const bad = new Map();-- let prefix = '';-- for (const script of scripts) {-- try {-- new vm.Script(script.code, { filename: 'model-script.js' });-- } catch (err) {-- bad.set(script, err);-- continue;-- }-- try {-- new vm.Script(prefix + '\n;\n' + script.code, { filename: 'model-scripts.js' });-- prefix += '\n;\n' + script.code;-- } catch (err) {-- bad.set(script, err);-- }-- }-- return { scripts, bad };--}----/** The document without the scripts that cannot run, and a visible line naming the reason (the rest of the output-- * still renders: its HTML and CSS are the model's own work, and often all that can be shown). */--function withoutUnrunnableScripts(html) {-- let text = String(html == null ? '' : html);-- const repaired = repairDuplicateDeclarations(text);-- if (repaired) text = repaired;-- const { scripts, bad } = scriptProblems(text);-- if (!bad.size) return text;-- let out = text;-- for (const script of scripts.slice().reverse()) {-- if (bad.has(script)) out = out.slice(0, script.start) + out.slice(script.start + script.length);-- }-- const messages = [...new Set([...bad.values()].map((err) => String((err && err.message) || err)))];-- const note = '<div class="ov-entry-script-error" data-ov-entry-script-error="1" role="status"'-- + ' style="margin:0;padding:.6rem .8rem;font:600 12px/1.45 system-ui,sans-serif;background:#3a1d1d;color:#ffd7d7;border-bottom:1px solid #6b3030">'-- + 'This output\'s script could not run: <code style="font-weight:500">' + esc(messages.join('; ')) + '</code></div>';-- const body = /<body\b[^>]*>/i.exec(out);-- if (body) {-- const at = body.index + body[0].length;-- return out.slice(0, at) + note + out.slice(at);-- }-- return note + out;--}--/** The namespace names an SVG needs. They look like URLs but are never fetched, so the pass below keeps them: without* them the browser reads the document as plain XML and shows only its text (11-01: every svg_art entry did). */const SVG_NAMESPACES = Object.freeze([@@@ -352,7 +172,6 @@ function modelDocument(kind, payload, opts) {else if (typeof p.gltf_base64 === 'string' && p.gltf_base64.trim()) body = gltfPage(p.gltf_base64);else body = '<p>Nothing was generated for this entry.</p>';body = truncateBytes(body, MAX_ENTRY_BYTES);-- body = withoutUnrunnableScripts(body);// A whole page of the model's own gets the import map too (an addon imports the bare "three"), unless it has one.const own = /<html\b/i.test(body);const withMap = own && !/type\s*=\s*["']?importmap/i.test(body)@@@ -449,5 +268,4 @@ function wrapperDocument(opts) {module.exports = {RENDER_ORIGIN, PINNED_ORIGINS, VENDOR, SANDBOX, WRAPPER_SANDBOX, DEFAULT_LIMIT_MS, MAX_ENTRY_BYTES,FRAME_ANCESTORS, PAYLOAD_FIELD, codeOf, payloadFor, renderablePayload, wrapperCsp, cspFor, applyNonce, sanitizeSvg, fitSvg, SVG_NAMESPACES, limitMsOf, modeOf, modelDocument, wrapperDocument,-- repairDuplicateDeclarations, scriptProblems, withoutUnrunnableScripts,};@@@ -115,39 +115,6 @@ const HOSTILE = [assert.ok(own.includes('<script nonce="N2" type="module">') && !own.includes('guess'), 'a guessed nonce is replaced by ours');});-- await check('a model script that declares a name twice is joined, not thrown into the page console (11-01)', async () => {-- // The seeded dataviz entry: one IIFE declares maxVal for the y scale and again for the highest point. The-- // browser refuses the whole script ("Identifier 'maxVal' has already been declared"), so no chart draws.-- const broken = '<div id="chart">chart</div><script>(function(){'-- + 'const maxVal = Math.max(42000, 77000); const yMax = maxVal;'-- + 'let maxVal = 42000; for (let i = 1; i < 12; i++) maxVal += 1000;'-- + 'document.getElementById("chart").textContent = yMax + "/" + maxVal; })();</script>';-- const doc = renderLib.modelDocument({ id: 'design.dataviz' }, { html: broken }, { nonce: 'N4' });-- assert.strictEqual(renderLib.scriptProblems(doc).bad.size, 0, 'the served document compiles as the browser would');-- assert.ok(doc.includes('let maxVal = Math.max('), 'the first declaration becomes a let');-- assert.ok(doc.includes('maxVal = 42000'), 'the second becomes an assignment to it');-- assert.ok(doc.includes('id="chart"'), 'the rest of the output still renders');-- assert.ok(!doc.includes('data-ov-entry-script-error'), 'no warning when the joined script runs');-- assert.strictEqual(renderLib.repairDuplicateDeclarations('<script>let maxVal = 1; function f(){ let maxVal = 2; return maxVal; }</script>'), null,-- 'a legal shadow in a nested function is not a duplicate');-- });---- await check('two scripts declaring the same const/let are joined too', async () => {-- const two = '<p>two</p><script>const maxVal = 1; console.log(maxVal);</script>'-- + '<script>let maxVal = 2; console.log(maxVal);</script>';-- const doc = renderLib.modelDocument({ id: 'design.dataviz' }, { html: two }, { nonce: 'N5' });-- assert.strictEqual(renderLib.scriptProblems(doc).bad.size, 0, 'scripts compile together');-- assert.ok(doc.includes('let maxVal = 1'), 'the first script keeps its declaration, now a let');-- assert.ok(doc.includes('maxVal = 2'), 'the later script assigns it');-- });---- await check('a script that still does not compile is left out and the frame says why', async () => {-- const doc = renderLib.modelDocument({ id: 'design.dataviz' }, { html: '<p>still here</p><script>function ( {</script>' }, { nonce: 'N6' });-- assert.strictEqual(renderLib.scriptProblems(doc).bad.size, 0, 'no script that throws is served');-- assert.ok(doc.includes('data-ov-entry-script-error'), 'the frame names the failure');-- assert.ok(doc.includes('still here'), 'the rest of the output renders');-- });--await check('an SVG keeps its namespace and fills its frame; external references stay blocked (11-01)', async () => {const art = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1200 700" width="1200" height="700">'+ '<image href="https://evil.example/x.png"/><use xlink:href="http://evil.example/s.svg#a"/><rect fill="url(#g)" width="10" height="10"/></svg>';
@@ -15,7 +15,6 @@
@@ -15,7 +15,6 @@
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
-const vm = require('vm');
const { truncateBytes, MAX_BYTES } = require('./sanitize');
const { truncateBytes, MAX_BYTES } = require('./sanitize');
const RENDER_ORIGIN = String(process.env.OV_RENDER_ORIGIN || 'https://bench-render.openvibe.services').replace(/\/+$/, '');
const RENDER_ORIGIN = String(process.env.OV_RENDER_ORIGIN || 'https://bench-render.openvibe.services').replace(/\/+$/, '');
@@ -99,185 +98,6 @@ function applyNonce(html, nonce) {
@@ -99,185 +98,6 @@ function applyNonce(html, nonce) {
.replace(/<script\b/gi, '<script nonce="' + nonce + '"');
.replace(/<script\b/gi, '<script nonce="' + nonce + '"');
}
}
-/** The inline scripts a browser would run as classic scripts, in document order. Modules are left alone: their top
- * level is its own scope, and vm.Script cannot parse import/export. */
-function inlineScripts(html) {
- const text = String(html == null ? '' : html);
- const scripts = [];
- const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
- let m;
- while ((m = re.exec(text))) {
- const attrs = m[1] || '';
- if (/\bsrc\s*=/i.test(attrs)) continue;
- const typeMatch = /type\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attrs);
- const type = typeMatch ? String(typeMatch[1] || typeMatch[2] || typeMatch[3] || '').trim().toLowerCase() : '';
- if (type === 'module' || (type && !/^(?:text\/javascript|application\/javascript)$/.test(type))) continue;
- const openEnd = m.index + m[0].indexOf('>') + 1;
- scripts.push({ code: m[2], start: m.index, length: m[0].length, openEnd, originalLength: m[2].length });
- }
- return scripts;
-}
-
-/** Which characters of a script are code, not string, template or comment: a declaration in a string is not a
- * declaration. The mask also gives the brace depth at each position, so a shadowing declaration in a nested block is
- * not confused with a duplicate in the same scope. */
-function codeMap(code) {
- const isCode = new Uint8Array(code.length);
- const depth = new Int32Array(code.length + 1);
- let state = 'code';
- let d = 0;
- for (let i = 0; i < code.length; i++) {
- const c = code[i], n = code[i + 1];
- depth[i] = d;
- if (state === 'code') {
- if (c === '/' && n === '/') { state = 'line'; i++; continue; }
- if (c === '/' && n === '*') { state = 'block'; i++; continue; }
- isCode[i] = 1;
- if (c === "'") state = 'single';
- else if (c === '"') state = 'double';
- else if (c === '`') state = 'template';
- else if (c === '{') d++;
- else if (c === '}') d--;
- } else if (state === 'line') {
- if (c === '\n') state = 'code';
- } else if (state === 'block') {
- if (c === '*' && n === '/') { state = 'code'; i++; }
- } else if (state === 'single') {
- if (c === '\\') i++; else if (c === "'") state = 'code';
- } else if (state === 'double') {
- if (c === '\\') i++; else if (c === '"') state = 'code';
- } else if (state === 'template') {
- if (c === '\\') i++; else if (c === '`') state = 'code';
- }
- }
- depth[code.length] = d;
- return { isCode, depth };
-}
-
-/**
- * A model answer is not trusted to compile. The browser runs a page's classic scripts in one global scope, so two
- * declarations of the same const/let - in one script or across two - throw "Identifier 'x' has already been declared"
- * before either script runs, and every showcase page that frames it reports an uncaught error (11-01: the seeded
- * dataviz entry declares maxVal twice, once per chart scale). Join the duplicate declarations at the same brace depth
- * into one binding (the first becomes let, the later ones assignments) and use the result only when it then compiles.
- * That keeps the model's output running rather than leaving a blank frame. Returns null when there is nothing to
- * repair or the repair does not compile; the caller then leaves the bad script out.
- */
-function repairDuplicateDeclarations(html) {
- const text = String(html == null ? '' : html);
- const scripts = inlineScripts(text);
- if (!scripts.length) return null;
- let changed = false;
- for (let attempt = 0; attempt < 6; attempt++) {
- const joined = scripts.map((s) => s.code).join('\n;\n');
- try {
- new vm.Script(joined, { filename: 'model-scripts.js' });
- break;
- } catch (err) {
- const dup = /Identifier '([^']+)' has already been declared/.exec(String((err && err.message) || err));
- if (!dup) return null;
- const name = dup[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const decls = [];
- for (let si = 0; si < scripts.length; si++) {
- const { isCode, depth } = codeMap(scripts[si].code);
- const re = new RegExp('\\b(const|let|var)\\s+' + name + '\\b', 'g');
- let m;
- while ((m = re.exec(scripts[si].code))) {
- if (!isCode[m.index]) continue;
- decls.push({ si, index: m.index, keyword: m[1], keywordEnd: m.index + m[1].length, depth: depth[m.index] });
- }
- }
- const byDepth = new Map();
- for (const d of decls) {
- if (!byDepth.has(d.depth)) byDepth.set(d.depth, []);
- byDepth.get(d.depth).push(d);
- }
- // The shallowest depth with a duplicate is the one the parser named: a legal shadowing declaration in a
- // deeper block cannot be the duplicate, and a conflicting top-level one is shallower.
- const group = [...byDepth.entries()].filter(([, g]) => g.length > 1).sort((a, b) => a[0] - b[0])[0];
- if (!group) return null;
- const declsInGroup = group[1].sort((a, b) => a.si - b.si || a.index - b.index);
- const edits = new Map();
- for (let i = 0; i < declsInGroup.length; i++) {
- const d = declsInGroup[i];
- const code = scripts[d.si].code;
- let end = d.keywordEnd;
- let replacement = '';
- if (i === 0) {
- if (d.keyword !== 'const') continue;
- replacement = 'let';
- } else {
- // Drop the keyword and the whitespace after it: "let maxVal = 1" becomes "maxVal = 1".
- while (end < code.length && /\s/.test(code[end])) end++;
- }
- if (!edits.has(d.si)) edits.set(d.si, []);
- edits.get(d.si).push({ from: d.index, to: end, text: replacement });
- }
- for (const [si, list] of edits) {
- list.sort((a, b) => b.from - a.from);
- let code = scripts[si].code;
- for (const e of list) code = code.slice(0, e.from) + e.text + code.slice(e.to);
- scripts[si].code = code;
- }
- changed = true;
- }
- }
- if (!changed) return null;
- let out = text;
- for (let i = scripts.length - 1; i >= 0; i--) {
- const s = scripts[i];
- out = out.slice(0, s.openEnd) + s.code + out.slice(s.openEnd + s.originalLength);
- }
- return out;
-}
-
-/** The scripts that still cannot run, alone or after the ones before them; the repair above handles the duplicate
- * declaration case, this is the honest fallback for every other syntax error. */
-function scriptProblems(html) {
- const scripts = inlineScripts(html);
- const bad = new Map();
- let prefix = '';
- for (const script of scripts) {
- try {
- new vm.Script(script.code, { filename: 'model-script.js' });
- } catch (err) {
- bad.set(script, err);
- continue;
- }
- try {
- new vm.Script(prefix + '\n;\n' + script.code, { filename: 'model-scripts.js' });
- prefix += '\n;\n' + script.code;
- } catch (err) {
- bad.set(script, err);
- }
- }
- return { scripts, bad };
-}
-
-/** The document without the scripts that cannot run, and a visible line naming the reason (the rest of the output
- * still renders: its HTML and CSS are the model's own work, and often all that can be shown). */
-function withoutUnrunnableScripts(html) {
- let text = String(html == null ? '' : html);
- const repaired = repairDuplicateDeclarations(text);
- if (repaired) text = repaired;
- const { scripts, bad } = scriptProblems(text);
- if (!bad.size) return text;
- let out = text;
- for (const script of scripts.slice().reverse()) {
- if (bad.has(script)) out = out.slice(0, script.start) + out.slice(script.start + script.length);
- }
- const messages = [...new Set([...bad.values()].map((err) => String((err && err.message) || err)))];
- const note = '<div class="ov-entry-script-error" data-ov-entry-script-error="1" role="status"'
- + ' style="margin:0;padding:.6rem .8rem;font:600 12px/1.45 system-ui,sans-serif;background:#3a1d1d;color:#ffd7d7;border-bottom:1px solid #6b3030">'
- + 'This output\'s script could not run: <code style="font-weight:500">' + esc(messages.join('; ')) + '</code></div>';
- const body = /<body\b[^>]*>/i.exec(out);
- if (body) {
- const at = body.index + body[0].length;
- return out.slice(0, at) + note + out.slice(at);
- }
- return note + out;
-}
-
/** The namespace names an SVG needs. They look like URLs but are never fetched, so the pass below keeps them: without
/** The namespace names an SVG needs. They look like URLs but are never fetched, so the pass below keeps them: without
* them the browser reads the document as plain XML and shows only its text (11-01: every svg_art entry did). */
* them the browser reads the document as plain XML and shows only its text (11-01: every svg_art entry did). */
const SVG_NAMESPACES = Object.freeze([
const SVG_NAMESPACES = Object.freeze([
@@ -352,7 +172,6 @@ function modelDocument(kind, payload, opts) {
@@ -352,7 +172,6 @@ function modelDocument(kind, payload, opts) {
else if (typeof p.gltf_base64 === 'string' && p.gltf_base64.trim()) body = gltfPage(p.gltf_base64);
else if (typeof p.gltf_base64 === 'string' && p.gltf_base64.trim()) body = gltfPage(p.gltf_base64);
else body = '<p>Nothing was generated for this entry.</p>';
else body = '<p>Nothing was generated for this entry.</p>';
body = truncateBytes(body, MAX_ENTRY_BYTES);
body = truncateBytes(body, MAX_ENTRY_BYTES);
- body = withoutUnrunnableScripts(body);
// A whole page of the model's own gets the import map too (an addon imports the bare "three"), unless it has one.
// A whole page of the model's own gets the import map too (an addon imports the bare "three"), unless it has one.
const own = /<html\b/i.test(body);
const own = /<html\b/i.test(body);
const withMap = own && !/type\s*=\s*["']?importmap/i.test(body)
const withMap = own && !/type\s*=\s*["']?importmap/i.test(body)
@@ -449,5 +268,4 @@ function wrapperDocument(opts) {
@@ -449,5 +268,4 @@ function wrapperDocument(opts) {
module.exports = {
module.exports = {
RENDER_ORIGIN, PINNED_ORIGINS, VENDOR, SANDBOX, WRAPPER_SANDBOX, DEFAULT_LIMIT_MS, MAX_ENTRY_BYTES,
RENDER_ORIGIN, PINNED_ORIGINS, VENDOR, SANDBOX, WRAPPER_SANDBOX, DEFAULT_LIMIT_MS, MAX_ENTRY_BYTES,
FRAME_ANCESTORS, PAYLOAD_FIELD, codeOf, payloadFor, renderablePayload, wrapperCsp, cspFor, applyNonce, sanitizeSvg, fitSvg, SVG_NAMESPACES, limitMsOf, modeOf, modelDocument, wrapperDocument,
FRAME_ANCESTORS, PAYLOAD_FIELD, codeOf, payloadFor, renderablePayload, wrapperCsp, cspFor, applyNonce, sanitizeSvg, fitSvg, SVG_NAMESPACES, limitMsOf, modeOf, modelDocument, wrapperDocument,
- repairDuplicateDeclarations, scriptProblems, withoutUnrunnableScripts,
};
};
@@ -115,39 +115,6 @@ const HOSTILE = [
@@ -115,39 +115,6 @@ const HOSTILE = [
assert.ok(own.includes('<script nonce="N2" type="module">') && !own.includes('guess'), 'a guessed nonce is replaced by ours');
assert.ok(own.includes('<script nonce="N2" type="module">') && !own.includes('guess'), 'a guessed nonce is replaced by ours');
});
});
- await check('a model script that declares a name twice is joined, not thrown into the page console (11-01)', async () => {
- // The seeded dataviz entry: one IIFE declares maxVal for the y scale and again for the highest point. The
- // browser refuses the whole script ("Identifier 'maxVal' has already been declared"), so no chart draws.
- const broken = '<div id="chart">chart</div><script>(function(){'
- + 'const maxVal = Math.max(42000, 77000); const yMax = maxVal;'
- + 'let maxVal = 42000; for (let i = 1; i < 12; i++) maxVal += 1000;'
- + 'document.getElementById("chart").textContent = yMax + "/" + maxVal; })();</script>';
- const doc = renderLib.modelDocument({ id: 'design.dataviz' }, { html: broken }, { nonce: 'N4' });
- assert.strictEqual(renderLib.scriptProblems(doc).bad.size, 0, 'the served document compiles as the browser would');
- assert.ok(doc.includes('let maxVal = Math.max('), 'the first declaration becomes a let');
- assert.ok(doc.includes('maxVal = 42000'), 'the second becomes an assignment to it');
- assert.ok(doc.includes('id="chart"'), 'the rest of the output still renders');
- assert.ok(!doc.includes('data-ov-entry-script-error'), 'no warning when the joined script runs');
- assert.strictEqual(renderLib.repairDuplicateDeclarations('<script>let maxVal = 1; function f(){ let maxVal = 2; return maxVal; }</script>'), null,
- 'a legal shadow in a nested function is not a duplicate');
- });
-
- await check('two scripts declaring the same const/let are joined too', async () => {
- const two = '<p>two</p><script>const maxVal = 1; console.log(maxVal);</script>'
- + '<script>let maxVal = 2; console.log(maxVal);</script>';
- const doc = renderLib.modelDocument({ id: 'design.dataviz' }, { html: two }, { nonce: 'N5' });
- assert.strictEqual(renderLib.scriptProblems(doc).bad.size, 0, 'scripts compile together');
- assert.ok(doc.includes('let maxVal = 1'), 'the first script keeps its declaration, now a let');
- assert.ok(doc.includes('maxVal = 2'), 'the later script assigns it');
- });
-
- await check('a script that still does not compile is left out and the frame says why', async () => {
- const doc = renderLib.modelDocument({ id: 'design.dataviz' }, { html: '<p>still here</p><script>function ( {</script>' }, { nonce: 'N6' });
- assert.strictEqual(renderLib.scriptProblems(doc).bad.size, 0, 'no script that throws is served');
- assert.ok(doc.includes('data-ov-entry-script-error'), 'the frame names the failure');
- assert.ok(doc.includes('still here'), 'the rest of the output renders');
- });
-
await check('an SVG keeps its namespace and fills its frame; external references stay blocked (11-01)', async () => {
await check('an SVG keeps its namespace and fills its frame; external references stay blocked (11-01)', async () => {
const art = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1200 700" width="1200" height="700">'
const art = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1200 700" width="1200" height="700">'
+ '<image href="https://evil.example/x.png"/><use xlink:href="http://evil.example/s.svg#a"/><rect fill="url(#g)" width="10" height="10"/></svg>';
+ '<image href="https://evil.example/x.png"/><use xlink:href="http://evil.example/s.svg#a"/><rect fill="url(#g)" width="10" height="10"/></svg>';