From 38fa85946c708d10ec3489ca7594dbfc2d0e92e6 Mon Sep 17 00:00:00 2001 From: Ben Younes Date: Wed, 10 Jun 2026 21:08:46 +0200 Subject: [PATCH] fix(js): add insertAdjacentText/insertAdjacentElement polyfills (#285) (#286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DDoS-Guard's view.js challenge calls Element.prototype.insertAdjacentText and dies with 'TypeError: t.insertAdjacentText is not a function', blocking any DDoS-Guard-protected site (e.g. pikabu.ru). insertAdjacentElement was missing too. insertAdjacentHTML was already implemented, so this just lifts the same insertion pattern for Text-node and Element insertion semantics. Both methods mirror the spec: case-insensitive position keywords, four insertion points (beforebegin/afterbegin/beforeend/afterend), and insertAdjacentElement returns the inserted element (or null when beforebegin/afterend has no parent). Registered with _markNative so Function.prototype.toString keeps reporting [native code] — consistent with the existing Element.prototype.insertAdjacentHTML stealth registration. --- crates/obscura-js/js/bootstrap.js | 48 +++++++++++++++++++- crates/obscura-js/src/runtime.rs | 74 +++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 2904431..da6ed9c 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -1111,6 +1111,51 @@ class Element extends Node { break; } } + // Same insertion semantics as insertAdjacentHTML but inserts a Text node + // instead of parsing markup — that distinction is the whole point of the API + // (DDoS-Guard, reCAPTCHA, and others use it precisely because it does NOT + // parse HTML). Position string is case-insensitive per spec. See issue #285. + insertAdjacentText(position, text) { + const parent = this.parentNode; + const node = document.createTextNode(String(text)); + switch (String(position).toLowerCase()) { + case 'beforebegin': + if (parent) parent.insertBefore(node, this); + break; + case 'afterbegin': + this.insertBefore(node, this.firstChild); + break; + case 'beforeend': + this.appendChild(node); + break; + case 'afterend': + if (parent) parent.insertBefore(node, this.nextSibling); + break; + } + } + // Per spec, returns the inserted element, or null when position is + // beforebegin/afterend and this element has no parent (so the node could + // not be placed). See issue #285. + insertAdjacentElement(position, element) { + const parent = this.parentNode; + switch (String(position).toLowerCase()) { + case 'beforebegin': + if (!parent) return null; + parent.insertBefore(element, this); + return element; + case 'afterbegin': + this.insertBefore(element, this.firstChild); + return element; + case 'beforeend': + this.appendChild(element); + return element; + case 'afterend': + if (!parent) return null; + parent.insertBefore(element, this.nextSibling); + return element; + } + return null; + } addEventListener(type, handler, opts) { const key = this._nid; if (!_eventRegistry[key]) _eventRegistry[key] = {}; @@ -4823,7 +4868,8 @@ _markNative(globalThis.Selection); Element.prototype.focus, Element.prototype.blur, Element.prototype.showPopover, Element.prototype.hidePopover, Element.prototype.togglePopover, Element.prototype.cloneNode, Element.prototype.attachShadow, - Element.prototype.insertAdjacentHTML, Element.prototype.scrollIntoView, + Element.prototype.insertAdjacentHTML, Element.prototype.insertAdjacentText, + Element.prototype.insertAdjacentElement, Element.prototype.scrollIntoView, Element.prototype.append, Element.prototype.prepend, Element.prototype.remove, Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith, HTMLFormElement.prototype.reset, diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index fecde47..22daca0 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -2357,6 +2357,80 @@ mod tests { /// Setting an ARIA reflection property must write through to the /// underlying attribute so frameworks that toggle state via /// `el.ariaExpanded = 'true'` actually update the DOM. + /// Regression test for #285: DDoS-Guard's challenge calls + /// `t.insertAdjacentText(...)` and dies with `TypeError: ... is not a + /// function` because `Element.prototype.insertAdjacentText` was missing. + /// Verify all four positions place a Text node (NOT parsed HTML) at the + /// right spot. Tests `insertAdjacentText` exists, is callable, and that + /// inserted content remains literal text — angle brackets must not be + /// parsed as markup, which is the whole point of the API. + #[test] + fn element_insert_adjacent_text_polyfill() { + let mut rt = setup_runtime(r#"
X
"#); + let result = rt + .evaluate( + r#" + const t = document.getElementById('t'); + t.insertAdjacentText('afterbegin', 'AB'); + t.insertAdjacentText('beforeend', 'BE'); + t.insertAdjacentText('beforebegin', 'BB'); + t.insertAdjacentText('afterend', 'AE'); + t.insertAdjacentText('beforeend', 'raw'); + return [ + typeof Element.prototype.insertAdjacentText, + document.getElementById('p').textContent, + t.getElementsByTagName('b').length, + ]; + "#, + ) + .unwrap(); + assert_eq!( + result, + serde_json::json!(["function", "BBABXBErawAE", 0]) + ); + } + + /// Regression test for #285: `Element.prototype.insertAdjacentElement` + /// was missing alongside `insertAdjacentText`. Verify all four positions + /// place the given element correctly and that the inserted element is + /// returned (per spec — that's the contract callers rely on for chaining). + #[test] + fn element_insert_adjacent_element_polyfill() { + let mut rt = setup_runtime(r#"
X
"#); + let result = rt + .evaluate( + r#" + const t = document.getElementById('t'); + const before = document.createElement('b'); before.id = 'before'; + const after = document.createElement('i'); after.id = 'after'; + const inside = document.createElement('em'); inside.id = 'inside'; + const last = document.createElement('u'); last.id = 'last'; + const r1 = t.insertAdjacentElement('beforebegin', before); + const r2 = t.insertAdjacentElement('afterend', after); + const r3 = t.insertAdjacentElement('afterbegin', inside); + const r4 = t.insertAdjacentElement('beforeend', last); + const siblings = Array.from(document.getElementById('p').children).map(c => c.id); + const inT = Array.from(t.children).map(c => c.id); + return [ + typeof Element.prototype.insertAdjacentElement, + r1 === before && r2 === after && r3 === inside && r4 === last, + siblings, + inT, + ]; + "#, + ) + .unwrap(); + assert_eq!( + result, + serde_json::json!([ + "function", + true, + ["before", "t", "after"], + ["inside", "last"] + ]) + ); + } + #[test] fn element_aria_reflection_setters_write_through() { let mut rt = setup_runtime(r#"
"#);