From 5f08bc2aa0e830b356dda148325109f5e46ed60a Mon Sep 17 00:00:00 2001 From: Jonas Porto Date: Thu, 29 Oct 2026 20:15:35 -0300 Subject: [PATCH] fix: native functions read through window run with the global as receiver (#345) --- cjs/interface/document.js | 18 +++++++++++++++--- esm/interface/document.js | 18 +++++++++++++++--- test/html/document.js | 7 +++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cjs/interface/document.js b/cjs/interface/document.js index 1a92fb28..32f2a7a0 100644 --- a/cjs/interface/document.js +++ b/cjs/interface/document.js @@ -34,6 +34,13 @@ const {Range} = require('./range.js'); const {Text} = require('./text.js'); const {TreeWalker} = require('./tree-walker.js'); +const boundGlobals = new WeakMap; +const boundGlobal = fn => { + if (!boundGlobals.has(fn)) + boundGlobals.set(fn, fn.bind(globalThis)); + return boundGlobals.get(fn); +}; + const query = (method, ownerDocument, selectors) => { let {[NEXT]: next, [END]: end} = ownerDocument; return method.call({ownerDocument, [NEXT]: next, [END]: end}, selectors); @@ -130,9 +137,14 @@ class Document extends NonElementParentNode { this[MUTATION_OBSERVER] = new MutationObserverClass(this); return this[MUTATION_OBSERVER].class; } - return (this[GLOBALS] && this[GLOBALS][name]) || - globalExports[name] || - globalThis[name]; + const value = (this[GLOBALS] && this[GLOBALS][name]) || + globalExports[name] || + globalThis[name]; + // a native function called through the proxy runs with the proxy + // as receiver, which browsers reject ("Illegal invocation"): + // bind it to the global, once, and leave constructors as they are + return typeof value === 'function' && !('prototype' in value) && value === globalThis[name] ? + boundGlobal(value) : value; } })); return window.get(this); diff --git a/esm/interface/document.js b/esm/interface/document.js index d68c9eb5..3cb3777b 100644 --- a/esm/interface/document.js +++ b/esm/interface/document.js @@ -34,6 +34,13 @@ import {Range} from './range.js'; import {Text} from './text.js'; import {TreeWalker} from './tree-walker.js'; +const boundGlobals = new WeakMap; +const boundGlobal = fn => { + if (!boundGlobals.has(fn)) + boundGlobals.set(fn, fn.bind(globalThis)); + return boundGlobals.get(fn); +}; + const query = (method, ownerDocument, selectors) => { let {[NEXT]: next, [END]: end} = ownerDocument; return method.call({ownerDocument, [NEXT]: next, [END]: end}, selectors); @@ -130,9 +137,14 @@ export class Document extends NonElementParentNode { this[MUTATION_OBSERVER] = new MutationObserverClass(this); return this[MUTATION_OBSERVER].class; } - return (this[GLOBALS] && this[GLOBALS][name]) || - globalExports[name] || - globalThis[name]; + const value = (this[GLOBALS] && this[GLOBALS][name]) || + globalExports[name] || + globalThis[name]; + // a native function called through the proxy runs with the proxy + // as receiver, which browsers reject ("Illegal invocation"): + // bind it to the global, once, and leave constructors as they are + return typeof value === 'function' && !('prototype' in value) && value === globalThis[name] ? + boundGlobal(value) : value; } })); return window.get(this); diff --git a/test/html/document.js b/test/html/document.js index b5e15062..066947e9 100644 --- a/test/html/document.js +++ b/test/html/document.js @@ -70,6 +70,13 @@ assert(window.addEventListener, null); assert(typeof window.performance.now(), 'number'); +// https://github.com/WebReflection/linkedom/issues/345 +globalThis.receiver = {receiver() { return this; }}.receiver; +assert(window.receiver() === globalThis, true, 'a native function runs with the global as receiver'); +assert(window.receiver === window.receiver, true, 'bound once'); +assert(window.URL === URL, true, 'constructors keep their identity'); +delete globalThis.receiver; + assert(parseHTML('
asdf
').document.body.outerHTML, '
asdf
'); const location = {href: 'http://ok'};