fix: share HTML escaping for invalid wikilink atoms
This commit is contained in:
1 parent
21b71d1dad
commit
2ec3f5b5dd
5 files changed
+49
-12
No files matched your search
@@ -121,3 +121,18 @@ MOSAIC_REQUIRE_EDITOR_BUILD=1 for provenance and the later whole webui
|
||||
gate. Capture all skip names, raw logs, short scratch TMPDIR and final
|
||||
Node/Chromium process sweep. Two failures stop a gate; no rerun after an
|
||||
out-of-row failure without reporting to Sage.
|
||||
|
||||
## Filbert R2 static B1 fixed before execution
|
||||
|
||||
Review at2026-10-24T17:14:55Z confirmed a defect in the authored invalid
|
||||
atom path: state.text alone lacks the text-node override's HTML/entity
|
||||
escaping. Factored that existing policy into escapedText and reused it for
|
||||
invalid atoms. Extended the test payload/assertions to cover both <img and
|
||||
©. Valid atom behavior is unchanged. Excluded backticks from atom
|
||||
recognition and added a text/code precedence fixture for N1.
|
||||
|
||||
Requested focused-grant addition: a second mutant that swaps only the
|
||||
invalid-atom escapedText call for vendored state.text, dropping the <&
|
||||
escape for this path. The existing malformed-attrs browser test must kill
|
||||
it. The original serializer-removal mutant is still required. No runs,
|
||||
checks, installation or rebuild have occurred; all remain held for CLEAR.
|
||||
@@ -150,3 +150,19 @@ work, not a green candidate. No Node, npm, rebuild or browser starts until
|
||||
Sage sends CLEAR. HASH-PINS.json records the test-tree audit and row266
|
||||
intersection. The gate still requires MOSAIC_REQUIRE_EDITOR_BUILD=1, a
|
||||
named skip list, whole webui and every scripts/test-*.sh.
|
||||
|
||||
## R2 static follow-up, Filbert B1/N1 (2026-10-24T17:14:55Z)
|
||||
|
||||
B1: state.text(..., true) escapes Markdown punctuation, not HTML/entities.
|
||||
The invalid-atom writer now calls the same escapedText helper as ordinary
|
||||
text nodes, including the <& step. Valid atoms still preserve their source
|
||||
verbatim. The malformed-attribute regression now asserts both an escaped
|
||||
<img and an escaped ©, plus flattened line breaks. A second mutant
|
||||
will replace only the invalid atom's escapedText call with state.text,
|
||||
leaving normal text escaping intact; this browser regression must kill it.
|
||||
This additional mutant needs Sage's grant alongside the serializer-removal
|
||||
mutant. No runtime result is claimed for either.
|
||||
|
||||
N1: backticks are excluded from the atom body, preserving code-span
|
||||
precedence. The text-only fixture includes [[a`]]` and must produce no
|
||||
wikilink atom. All other malformed/HTML/entity boundaries remain as before.
|
||||
@@ -5,12 +5,12 @@
|
||||
{
|
||||
"path": "packages/webui/editor/model.mjs",
|
||||
"mode": "100644",
|
||||
"sha256": "d56b75264c2c099fa0fc8c9f7a48d8ee701d8657ca64b9bd9492ec27c9446901"
|
||||
"sha256": "81ac44d852cb54213a9644ebbde26630ec49614e468f376d71b471395e3dd9f7"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/editor-wikilink.test.mjs",
|
||||
"mode": "100644",
|
||||
"sha256": "73360cfc64b2f4d2f7888b576743baea59ea9a53557934507744775fafdc8ab5"
|
||||
"sha256": "e144e7dbf23c68c136eef8f0fb2e8979c27e908c7cc14ee4ab4f722220e67d77"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/helpers/wikilink-cases.mjs",
|
||||
|
||||
@@ -26,8 +26,8 @@ export const linkTarget = raw => {
|
||||
// C2-11a: an inert single-line atom keeps its exact source, including labels
|
||||
// and heading fragments. HTML/entity-like targets stay ordinary text so the
|
||||
// existing escaped-text serialiser still protects rewritten blocks.
|
||||
const wikilinkSource = /^!?\[\[[^\[\]\r\n<>&]+\]\]$/;
|
||||
const wikilinkAt = /!?\[\[[^\[\]\r\n<>&]+\]\]/y;
|
||||
const wikilinkSource = /^!?\[\[[^\[\]\r\n<>&`]+\]\]$/;
|
||||
const wikilinkAt = /!?\[\[[^\[\]\r\n<>&`]+\]\]/y;
|
||||
const isWikilink = value => typeof value === 'string' && wikilinkSource.test(value);
|
||||
|
||||
const kept = { raw: { default: null }, sig: { default: null }, lead: { default: '' } };
|
||||
@@ -202,6 +202,13 @@ const fenceInfo = value => {
|
||||
const info = typeof value === 'string' ? value.split(/[\r\n]/, 1)[0] : '';
|
||||
return info.includes('`') ? '' : info;
|
||||
};
|
||||
// Share the complete text policy with invalid wikilink attributes. The
|
||||
// vendored state.text(..., true) alone does not escape HTML or entities.
|
||||
function escapedText(state, text, escape = true) {
|
||||
const esc = state.esc;
|
||||
state.esc = (str, start) => esc.call(state, str, start).replace(/[<&]/g, '\\$&');
|
||||
try { state.text(text, escape); } finally { delete state.esc; }
|
||||
}
|
||||
const link = defaultMarkdownSerializer.marks.link;
|
||||
const serializer = new MarkdownSerializer({
|
||||
...defaultMarkdownSerializer.nodes,
|
||||
@@ -209,11 +216,7 @@ const serializer = new MarkdownSerializer({
|
||||
const params = fenceInfo(node.attrs.params);
|
||||
defaultMarkdownSerializer.nodes.code_block(state, params === node.attrs.params ? node : node.type.create({ ...node.attrs, params }, node.content));
|
||||
},
|
||||
text(state, node) {
|
||||
const esc = state.esc;
|
||||
state.esc = (str, start) => esc.call(state, str, start).replace(/[<&]/g, '\\$&');
|
||||
try { state.text(node.text, !state.inAutolink); } finally { delete state.esc; }
|
||||
},
|
||||
text(state, node) { escapedText(state, node.text, !state.inAutolink); },
|
||||
bullet_list(state, node) { state.renderList(node, ' ', () => '- '); },
|
||||
image(state, node) {
|
||||
const a = node.attrs;
|
||||
@@ -224,7 +227,8 @@ const serializer = new MarkdownSerializer({
|
||||
const source = node.attrs.source, valid = isWikilink(source);
|
||||
// A programmatic invalid atom must not bypass the same text escaping that
|
||||
// protects pasted HTML and one-line attributes elsewhere in this model.
|
||||
state.text(valid ? source : oneLine(String(source ?? '')), !valid);
|
||||
if (valid) state.text(source, false);
|
||||
else escapedText(state, oneLine(String(source ?? '')));
|
||||
},
|
||||
raw_block(state, node) { state.text(node.textContent, false); state.closeBlock(node); },
|
||||
}, {
|
||||
|
||||
@@ -55,7 +55,7 @@ test('wikilinks are inline atoms and survive a neighbouring edit in every requir
|
||||
|
||||
test('code, escaped openers, malformed syntax and HTML-like targets stay outside wikilink atoms', { timeout: 120000 }, async t => {
|
||||
const b = await page(t);
|
||||
const text = '\\[[escaped]] `[[inline-code]]`\n\n```\n[[fenced-code]]\n```\n\n[[open\n\n[[]]\n\n[[Q&A]]\n\n[[<img src=x onerror=alert(1)>]]\n';
|
||||
const text = '\\[[escaped]] `[[inline-code]]`\n\n```\n[[fenced-code]]\n```\n\n[[open\n\n[[]]\n\n[[Q&A]]\n\n[[a`]]`\n\n[[<img src=x onerror=alert(1)>]]\n';
|
||||
assert.equal(editor.roundTrip(text), text);
|
||||
await b.evaluate(mount(text));
|
||||
assert.deepEqual(await b.evaluate(atoms), []);
|
||||
@@ -68,7 +68,7 @@ test('wikilink DOM paste accepts only bounded single-line inert syntax; malforme
|
||||
const e = window.__wikiEditor, spec = e.view.state.schema.nodes.wikilink.spec;
|
||||
const attrs = source => spec.parseDOM[0].getAttrs({getAttribute: () => source});
|
||||
const invalid = ['not a link', '[[a]]\\n\\n<script>x</script>', '[[<img src=x>]]', '[[a©]]', null].map(source => attrs(source));
|
||||
const bad = e.view.state.schema.nodes.wikilink.create({source:'[[a]]\\n\\n<img src=x onerror=alert(1)>'});
|
||||
const bad = e.view.state.schema.nodes.wikilink.create({source:'[[a]]\\n\\n<img src=x onerror=alert(1)>©'});
|
||||
e.view.dispatch(e.view.state.tr.replaceWith(1, e.view.state.doc.content.size - 1, bad));
|
||||
return {valid:attrs('[[a#h|label]]'), invalid, output:e.text()};
|
||||
})()`);
|
||||
@@ -77,4 +77,6 @@ test('wikilink DOM paste accepts only bounded single-line inert syntax; malforme
|
||||
assert.ok(!result.output.includes('\n\n<img'));
|
||||
assert.doesNotMatch(result.output, /(?<!\\)<img/);
|
||||
assert.match(result.output, /\\<img/);
|
||||
assert.doesNotMatch(result.output, /(?<!\\)©/);
|
||||
assert.match(result.output, /\\©/);
|
||||
});
|
||||
Reference in new issue
Block a user