Fix/stealth consistency
hasIframeProxy fix contentWindow now returns null when parentNode === null (iframe never inserted into any tree). The previous attempt used isConnected, which broke getBehemothIframe by returning null for iframes inside the phantom iframe's detached document. That caused PHANTOM_DARKNESS to fall back to self (main window), making all DOM measurement ops run on the main document instead of the isolated iframe body, which corrupted the fingerprint element lookup. Worker auto-execution Worker constructor now resolves relative URLs against location.href, fetches the script, and auto-runs it. The worker scope exposes WorkerGlobalScope so IS_WORKER_SCOPE resolves correctly in creepjs. All three worker types now complete within the 3s timeout instead of silently timing out. AudioBuffer + AudioContext AudioBuffer class added to globalThis with getChannelData, copyFromChannel, copyToChannel. AudioContext.createBuffer returns a real instance. AudioContext.addEventListener was a no-op; startRendering now dispatches to stored listeners so the complete event fires. Crypto Crypto class added to globalThis with randomUUID and getRandomValues on its prototype. Fixes ReferenceError in getPlatformEstimate. NetworkInformation + ContentIndex NetworkInformation class with downlinkMax on the prototype clears noDownlinkMax. ContentIndex stub on globalThis clears noContentIndex. navigator.connection is now a real NetworkInformation instance. getElementById / querySelector stale index fix id_index only registers nodes at creation time. When replaceChild(fragment, el) swapped in new nodes their IDs hit .or_insert() and were skipped, leaving the index pointing at the detached original. getElementById now verifies the indexed node is in the live document and falls back to a full attribute-selector scan. query_selector_from falls through to the full scan on both index miss and stale entry instead of returning None early. matchMedia Handles common queries instead of always returning false. prefers-color-scheme: dark returns true (headless Chrome defaults to light mode, so returning true for light is a bot signal). any-pointer: fine, hover: hover, prefers-reduced-motion: no-preference, min/max-width, and (color) return sensible values. Fingerprint consistency jsHeapSizeLimit raised to 4294705152 (4 GB, matching 64-bit Chrome). getHighEntropyValues platform corrected from "Linux" to "Windows", platformVersion from "6.1.0" to "10.0.0" to match the Windows NT 10.0 UA string. Canvas: real PNG encoder, arc fill, multiply blend mode. Screen class, performance.now, chrome runtime errors fixed. Build note deviceMemory and hardwareConcurrency use the high-value pools ([4, 8] GB and [4, 6, 8, 12, 16] cores) only when built with --features stealth.
This commit is contained in:
3 files changed
+311
-87
No files matched your search
@@ -565,11 +565,11 @@ impl DomTree {
|
||||
// querySelector matches strict descendants of root only, so the
|
||||
// indexed element must have root among its ancestors.
|
||||
Some(nid) if self.ancestors(nid).contains(&root) => return Ok(Some(nid)),
|
||||
// No element has this id at all: a bare id selector cannot match.
|
||||
None => return Ok(None),
|
||||
// Indexed (first) element is not under a scoped root; a later
|
||||
// duplicate could still be a descendant, so fall through to scan.
|
||||
Some(_) => {}
|
||||
// Index miss or stale entry (detached node) — fall through to full scan.
|
||||
// The id_index is best-effort: it only registers nodes at creation time
|
||||
// and does not update on reparent, so it can point to a detached clone
|
||||
// while the live node (with the same id) is elsewhere in the tree.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let selector_list = parse_selector(selector)?;
|
||||
|
||||
Vendored
+277
-75
@@ -1628,7 +1628,7 @@ class Element extends Node {
|
||||
el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el);
|
||||
el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl);
|
||||
}
|
||||
_registerIframe(el);
|
||||
|
||||
if (typeof el.onload === 'function') {
|
||||
try { el.onload(); } catch(e) {}
|
||||
} else {
|
||||
@@ -1638,7 +1638,7 @@ class Element extends Node {
|
||||
}).catch(() => {
|
||||
el._iframeDoc = new _IframeDocument('<!DOCTYPE html><html><head></head><body></body></html>', fullUrl, el);
|
||||
el._iframeWin = new _IframeWindow(el._iframeDoc, fullUrl);
|
||||
_registerIframe(el);
|
||||
|
||||
if (typeof el.onload === 'function') try { el.onload(); } catch(e) {}
|
||||
});
|
||||
}
|
||||
@@ -1661,7 +1661,8 @@ class Element extends Node {
|
||||
get contentWindow() {
|
||||
if (this.localName !== 'iframe') return undefined;
|
||||
if (!this._iframeWin) {
|
||||
this.contentDocument; // side effect: creates _iframeDoc + _iframeWin
|
||||
if (this.parentNode === null) return null;
|
||||
this.contentDocument;
|
||||
}
|
||||
return this._iframeWin;
|
||||
}
|
||||
@@ -1802,7 +1803,7 @@ class Element extends Node {
|
||||
toJSON() { return this; },
|
||||
};
|
||||
}
|
||||
getClientRects() { return [this.getBoundingClientRect()]; }
|
||||
getClientRects() { return new DOMRectList([this.getBoundingClientRect()]); }
|
||||
// No layout engine: a stub that always returns true unblocks Playwright's
|
||||
// actionability polling. With a real layout we'd check display, visibility,
|
||||
// opacity and rect dimensions per spec.
|
||||
@@ -2338,6 +2339,9 @@ class Document extends Node {
|
||||
}
|
||||
|
||||
class DocumentFragment extends Node {
|
||||
constructor(nid) {
|
||||
super(nid !== undefined ? nid : +_dom("create_document_fragment"));
|
||||
}
|
||||
get nodeType() { return 11; }
|
||||
get nodeName() { return "#document-fragment"; }
|
||||
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
|
||||
@@ -2539,17 +2543,30 @@ Object.defineProperty(globalThis.Window, Symbol.hasInstance, {
|
||||
});
|
||||
|
||||
|
||||
const _iframeRegistry = [];
|
||||
function _registerIframe(iframeEl) {
|
||||
const idx = _iframeRegistry.length;
|
||||
_iframeRegistry.push(iframeEl);
|
||||
globalThis.length = _iframeRegistry.length;
|
||||
Object.defineProperty(globalThis, idx, {
|
||||
get() { return iframeEl._iframeWin || null; },
|
||||
// Remove the static _iframeRegistry and replace with dynamic getters.
|
||||
Object.defineProperty(globalThis, 'length', {
|
||||
get() {
|
||||
return document.querySelectorAll('iframe').length;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
// Since we cannot define a Proxy on globalThis easily, we'll define a reasonable number of indexed getters.
|
||||
for (let i = 0; i < 50; i++) {
|
||||
Object.defineProperty(globalThis, i, {
|
||||
get() {
|
||||
const iframes = document.querySelectorAll('iframe');
|
||||
if (i < iframes.length) {
|
||||
return iframes[i].contentWindow;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
// Navigator constructor so that typeof Navigator !== 'undefined' and
|
||||
// navigatorPrototype checks don't throw a ReferenceError.
|
||||
function Navigator() {}
|
||||
@@ -2577,6 +2594,23 @@ _markNative(PluginArray.prototype.item);
|
||||
_markNative(PluginArray.prototype.namedItem);
|
||||
_markNative(PluginArray.prototype.refresh);
|
||||
|
||||
class NetworkInformation {
|
||||
get downlink() { return 10; }
|
||||
get downlinkMax() { return Infinity; }
|
||||
get effectiveType() { return '4g'; }
|
||||
get rtt() { return 50; }
|
||||
get saveData() { return false; }
|
||||
get type() { return 'wifi'; }
|
||||
get onchange() { return null; }
|
||||
set onchange(v) {}
|
||||
get ontypechange() { return null; }
|
||||
set ontypechange(v) {}
|
||||
}
|
||||
_markNative(NetworkInformation);
|
||||
globalThis.NetworkInformation = NetworkInformation;
|
||||
|
||||
globalThis.ContentIndex = class ContentIndex {};
|
||||
|
||||
globalThis.navigator = {
|
||||
get userAgent() { return globalThis.__obscura_ua || "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; },
|
||||
get appVersion() { return this.userAgent.replace('Mozilla/', ''); },
|
||||
@@ -2586,7 +2620,7 @@ globalThis.navigator = {
|
||||
vendor: "Google Inc.", product: "Gecko", productSub: "20030107",
|
||||
doNotTrack: null,
|
||||
deviceMemory: 8,
|
||||
connection: { effectiveType: "4g", type: "wifi", rtt: 50, downlink: 10, saveData: false, onchange: null, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} },
|
||||
connection: new NetworkInformation(),
|
||||
pdfViewerEnabled: true,
|
||||
get plugins() {
|
||||
const p = new PluginArray([
|
||||
@@ -3458,7 +3492,29 @@ if (typeof TextDecoder === 'undefined') {
|
||||
};
|
||||
}
|
||||
|
||||
globalThis.matchMedia = _markNative(function matchMedia(q) { return { matches: false, media: q, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} }; });
|
||||
globalThis.matchMedia = _markNative(function matchMedia(q) {
|
||||
var s = (q || '').toLowerCase().replace(/\s+/g, '');
|
||||
var matches = false;
|
||||
if (s.includes('prefers-color-scheme:light')) matches = false;
|
||||
else if (s.includes('prefers-color-scheme:dark')) matches = true;
|
||||
else if (s.includes('prefers-reduced-motion:no-preference')) matches = true;
|
||||
else if (s.includes('prefers-reduced-motion:reduce')) matches = false;
|
||||
else if (s.includes('any-pointer:fine')) matches = true;
|
||||
else if (s.includes('any-pointer:coarse')) matches = false;
|
||||
else if (s.includes('pointer:fine')) matches = true;
|
||||
else if (s.includes('hover:hover')) matches = true;
|
||||
else if (s.includes('any-hover:hover')) matches = true;
|
||||
else if (s.includes('color)') || s === '(color)') matches = true;
|
||||
else if (s.includes('min-width')) {
|
||||
var m = s.match(/min-width:\s*(\d+)px/);
|
||||
matches = m ? (globalThis.innerWidth || 1440) >= parseInt(m[1]) : false;
|
||||
}
|
||||
else if (s.includes('max-width')) {
|
||||
var m2 = s.match(/max-width:\s*(\d+)px/);
|
||||
matches = m2 ? (globalThis.innerWidth || 1440) <= parseInt(m2[1]) : false;
|
||||
}
|
||||
return { matches: matches, media: q, onchange: null, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} };
|
||||
});
|
||||
globalThis.getComputedStyle = (el) => {
|
||||
if (!el) el = document.body || {};
|
||||
const style = el?.style || el?._style || new CSSStyleDeclaration();
|
||||
@@ -4439,7 +4495,7 @@ globalThis.performance = globalThis.performance || {
|
||||
timing: { navigationStart: 0, domContentLoadedEventEnd: 0, loadEventEnd: 0 },
|
||||
navigation: { type: 0, redirectCount: 0 },
|
||||
memory: {
|
||||
jsHeapSizeLimit: 2172649472,
|
||||
jsHeapSizeLimit: 4294705152,
|
||||
totalJSHeapSize: 19321856,
|
||||
usedJSHeapSize: 16781520,
|
||||
},
|
||||
@@ -4492,7 +4548,11 @@ Object.defineProperty(Document.prototype, 'fonts', {
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
globalThis.crypto = globalThis.crypto || { getRandomValues(arr) { for(let i=0;i<arr.length;i++) arr[i]=Math.floor(Math.random()*256); return arr; }, randomUUID(){ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,c=>{const r=Math.random()*16|0;return(c==="x"?r:(r&3|8)).toString(16);}); } };
|
||||
globalThis.Crypto = class Crypto {
|
||||
getRandomValues(arr) { for(let i=0;i<arr.length;i++) arr[i]=Math.floor(Math.random()*256); return arr; }
|
||||
randomUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,c=>{const r=Math.random()*16|0;return(c==="x"?r:(r&3|8)).toString(16);}); }
|
||||
};
|
||||
globalThis.crypto = globalThis.crypto || new globalThis.Crypto();
|
||||
globalThis.structuredClone = globalThis.structuredClone || ((v) => JSON.parse(JSON.stringify(v)));
|
||||
globalThis.reportError = globalThis.reportError || ((e) => console.error(e));
|
||||
|
||||
@@ -4926,8 +4986,20 @@ globalThis.Range = class Range {
|
||||
insertNode(node) { if (node && this._sc && this._sc.insertBefore) { const kids = this._sc.childNodes; this._sc.insertBefore(node, kids[this._so] || null); } }
|
||||
surroundContents(node) { this.insertNode(node); }
|
||||
detach() {}
|
||||
getBoundingClientRect() { return new DOMRect(); }
|
||||
getClientRects() { return []; }
|
||||
getBoundingClientRect() {
|
||||
if (this.collapsed) return new DOMRect();
|
||||
let cac = this.commonAncestorContainer;
|
||||
while (cac && cac.nodeType !== 1 && cac.nodeType !== 9) cac = cac.parentNode;
|
||||
if (cac && cac.getBoundingClientRect) {
|
||||
const r = cac.getBoundingClientRect();
|
||||
return new DOMRect(r.x, r.y, r.width, r.height);
|
||||
}
|
||||
return new DOMRect();
|
||||
}
|
||||
getClientRects() {
|
||||
if (this.collapsed) return new DOMRectList([]);
|
||||
return new DOMRectList([this.getBoundingClientRect()]);
|
||||
}
|
||||
static get START_TO_START() { return 0; }
|
||||
static get START_TO_END() { return 1; }
|
||||
static get END_TO_END() { return 2; }
|
||||
@@ -5017,6 +5089,7 @@ _markNative(globalThis.Selection);
|
||||
Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith,
|
||||
HTMLFormElement.prototype.reset,
|
||||
Element.prototype.getContext, Element.prototype.toDataURL, Element.prototype.toBlob,
|
||||
Element.prototype.getBBox,
|
||||
Node.prototype.appendChild, Node.prototype.removeChild,
|
||||
Node.prototype.replaceChild, Node.prototype.insertBefore,
|
||||
Node.prototype.contains, Node.prototype.hasChildNodes, Node.prototype.cloneNode,
|
||||
@@ -5303,7 +5376,7 @@ class _Canvas2D {
|
||||
this._stateStack = [];
|
||||
}
|
||||
_parseColor(css) {
|
||||
if (!css || css === 'none') return [0,0,0,0];
|
||||
if (!css || typeof css !== 'string' || css === 'none') return [0,0,0,0];
|
||||
if (css.startsWith('#')) {
|
||||
const hex = css.slice(1);
|
||||
if (hex.length === 3) return [parseInt(hex[0]+hex[0],16),parseInt(hex[1]+hex[1],16),parseInt(hex[2]+hex[2],16),255];
|
||||
@@ -5499,6 +5572,13 @@ Element.prototype.getContext = function getContext(type) {
|
||||
if (type === 'webgl' || type === 'experimental-webgl' || type === 'webgl2') {
|
||||
return {
|
||||
canvas: this,
|
||||
MAX_VIEWPORT_DIMS: 0x0D33,
|
||||
MAX_TEXTURE_SIZE: 0x0D33,
|
||||
MAX_RENDERBUFFER_SIZE: 0x84E8,
|
||||
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84EA,
|
||||
MAX_DRAW_BUFFERS_WEBGL: 0x8824,
|
||||
getContextAttributes() { return { alpha: true, antialias: true, depth: true, failIfMajorPerformanceCaveat: false, powerPreference: "default", premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: true, desynchronized: false }; },
|
||||
uniform2f() {},
|
||||
getExtension(name) {
|
||||
if (name === 'WEBGL_debug_renderer_info') return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 };
|
||||
return null;
|
||||
@@ -5510,6 +5590,10 @@ Element.prototype.getContext = function getContext(type) {
|
||||
if (pname === 0x1F00) return 'WebKit'; // GL_VENDOR
|
||||
if (pname === 0x1F02) return 'OpenGL ES 3.0 (ANGLE)'; // GL_VERSION
|
||||
if (pname === 0x8B8C) return 'WebGL GLSL ES 3.00 (ANGLE)'; // GL_SHADING_LANGUAGE_VERSION
|
||||
if (pname === undefined) return [0, 0];
|
||||
// Some properties like MAX_VIEWPORT_DIMS return arrays
|
||||
if (pname === 0x0D33) return [8192, 8192];
|
||||
if (pname === 0x8A2A) return [8192, 8192];
|
||||
return 0;
|
||||
},
|
||||
getSupportedExtensions() { return ['WEBGL_debug_renderer_info','EXT_texture_filter_anisotropic','WEBGL_compressed_texture_s3tc','WEBGL_lose_context']; },
|
||||
@@ -5542,6 +5626,10 @@ Element.prototype.toDataURL = function(type) {
|
||||
return _fp('canvasFingerprint');
|
||||
};
|
||||
Element.prototype.toBlob = function(cb, type, q) { cb(new Blob([''])); };
|
||||
Element.prototype.getBBox = function() { return { x: 0, y: 0, width: 0, height: 0 }; };
|
||||
Element.prototype.getComputedTextLength = function() { return 0; };
|
||||
Element.prototype.getExtentOfChar = function(ch) { return { x: 0, y: 0, width: 0, height: 0 }; };
|
||||
Element.prototype.getSubStringLength = function(ch, len) { return 0; };
|
||||
|
||||
_markNative(Element.prototype.getContext);
|
||||
_markNative(Element.prototype.toDataURL);
|
||||
@@ -5679,24 +5767,37 @@ if (typeof Document !== 'undefined' && typeof Document.parseHTMLUnsafe !== 'func
|
||||
_markNative(Document.parseHTMLUnsafe);
|
||||
}
|
||||
|
||||
globalThis.AudioBuffer = class AudioBuffer {
|
||||
constructor(opts) {
|
||||
var o = (typeof opts === 'object' && opts !== null) ? opts : {};
|
||||
this.numberOfChannels = o.numberOfChannels || 1;
|
||||
this.length = o.length || 0;
|
||||
this.sampleRate = o.sampleRate || 44100;
|
||||
this.duration = this.length / (this.sampleRate || 44100);
|
||||
this._chs = [];
|
||||
for (var c = 0; c < this.numberOfChannels; c++) this._chs.push(new Float32Array(this.length));
|
||||
}
|
||||
getChannelData(c) { return this._chs[c] || this._chs[0] || new Float32Array(0); }
|
||||
copyFromChannel(dst, ch, start) { var s=this._chs[ch]||this._chs[0]; start=start||0; for(var i=0;i<dst.length;i++) dst[i]=(s&&s[start+i])||0; }
|
||||
copyToChannel(src, ch, start) { var d=this._chs[ch]||this._chs[0]; start=start||0; if(d) for(var i=0;i<src.length;i++) d[start+i]=src[i]; }
|
||||
};
|
||||
globalThis.AudioContext = class AudioContext {
|
||||
constructor() { this.sampleRate=_fp('audioSampleRate'); this.state='running'; this.currentTime=0; this.baseLatency=_fp('audioBaseLatency'); this.destination={maxChannelCount:2,numberOfInputs:1,numberOfOutputs:0,channelCount:2}; }
|
||||
createOscillator() { return {type:'sine',frequency:{value:440,setValueAtTime(){}},connect(){},start(){},stop(){},disconnect(){},addEventListener(){}}; }
|
||||
createDynamicsCompressor() { return {threshold:{value:_fp('compThreshold')},knee:{value:_fp('compKnee')},ratio:{value:_fp('compRatio')},attack:{value:0.003},release:{value:0.25},reduction:0,connect(){},disconnect(){}}; }
|
||||
constructor() { this.sampleRate=_fp('audioSampleRate'); this.state='running'; this.currentTime=0; this.baseLatency=_fp('audioBaseLatency'); this.destination={maxChannelCount:2,numberOfInputs:1,numberOfOutputs:0,channelCount:2}; this._listeners={}; }
|
||||
addEventListener(type, fn) { if (!this._listeners[type]) this._listeners[type]=[]; this._listeners[type].push(fn); }
|
||||
removeEventListener(type, fn) { if (this._listeners[type]) this._listeners[type]=this._listeners[type].filter(h=>h!==fn); }
|
||||
_ap(v, min=-3.4028235e38, max=3.4028235e38) { return { value: v, defaultValue: v, minValue: min, maxValue: max, setValueAtTime(){} }; }
|
||||
createOscillator() { return {context:this,type:'sine',frequency:this._ap(440, -22050, 22050),detune:this._ap(0, -153600, 153600),connect(){},start(){},stop(){},disconnect(){},addEventListener(){},removeEventListener(){}}; }
|
||||
createDynamicsCompressor() { return {context:this,threshold:this._ap(_fp('compThreshold'), -100, 0),knee:this._ap(_fp('compKnee'), 0, 40),ratio:this._ap(_fp('compRatio'), 1, 20),attack:this._ap(0.003, 0, 1),release:this._ap(0.25, 0, 1),reduction:0,connect(){},disconnect(){}}; }
|
||||
createAnalyser() {
|
||||
return {fftSize:2048,frequencyBinCount:1024,connect(){},disconnect(){},
|
||||
return {context:this,fftSize:2048,frequencyBinCount:1024,channelCount:2,channelCountMode:'max',channelInterpretation:'speakers',maxDecibels:-30,minDecibels:-100,numberOfInputs:1,numberOfOutputs:1,smoothingTimeConstant:0.8,connect(){},disconnect(){},
|
||||
getByteFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=Math.floor(_fpRand(600+i)*10);},
|
||||
getFloatFrequencyData(a){for(let i=0;i<a.length;i++)a[i]=-100+_fpRand(700+i)*5;}
|
||||
};
|
||||
}
|
||||
createGain() { return {gain:{value:1,setValueAtTime(){}},connect(){},disconnect(){}}; }
|
||||
createBiquadFilter() { return {type:'lowpass',frequency:{value:350},Q:{value:1},connect(){},disconnect(){}}; }
|
||||
createBufferSource() { return {buffer:null,connect(){},start(){},stop(){},disconnect(){},loop:false}; }
|
||||
createBuffer(ch,len,rate) {
|
||||
var chs = [];
|
||||
for (var _c=0; _c<(ch||1); _c++) chs.push(new Float32Array(len||0));
|
||||
return {length:len,sampleRate:rate,numberOfChannels:ch,getChannelData:function(c){return chs[c]||chs[0];},duration:len/rate};
|
||||
}
|
||||
createGain() { return {context:this,gain:this._ap(1),connect(){},disconnect(){}}; }
|
||||
createBiquadFilter() { return {context:this,type:'lowpass',frequency:this._ap(350, 0, 22050),Q:this._ap(1, 0.0001, 1000),gain:this._ap(0, -40, 40),connect(){},disconnect(){}}; }
|
||||
createBufferSource() { return {context:this,buffer:null,connect(){},start(){},stop(){},disconnect(){},loop:false}; }
|
||||
createBuffer(ch,len,rate) { return new globalThis.AudioBuffer({numberOfChannels:ch||1,length:len||0,sampleRate:rate||44100}); }
|
||||
createScriptProcessor() { return {connect(){},disconnect(){},onaudioprocess:null}; }
|
||||
decodeAudioData(buf) { return Promise.resolve(this.createBuffer(2,44100,44100)); }
|
||||
resume() { this.state='running'; return Promise.resolve(); }
|
||||
@@ -5704,7 +5805,17 @@ globalThis.AudioContext = class AudioContext {
|
||||
close() { this.state='closed'; return Promise.resolve(); }
|
||||
};
|
||||
globalThis.OfflineAudioContext = class OfflineAudioContext extends AudioContext {
|
||||
constructor(ch,len,rate) { super(); this.length=len||44100; this.oncomplete=null; }
|
||||
constructor(ch,len,rate) {
|
||||
super();
|
||||
if (typeof ch === 'object' && ch !== null) {
|
||||
this.length = ch.length || 44100;
|
||||
this.sampleRate = ch.sampleRate || 44100;
|
||||
} else {
|
||||
this.length = len || 44100;
|
||||
this.sampleRate = rate || 44100;
|
||||
}
|
||||
this.oncomplete = null;
|
||||
}
|
||||
startRendering() {
|
||||
var self = this;
|
||||
var buf = this.createBuffer(1, self.length, 44100);
|
||||
@@ -5721,11 +5832,16 @@ globalThis.OfflineAudioContext = class OfflineAudioContext extends AudioContext
|
||||
for (var i = 4500; i < 5000; i++) s += Math.abs(data[i]);
|
||||
var scale = s > 0 ? target / s : 0;
|
||||
for (var i = 0; i < self.length; i++) data[i] *= scale;
|
||||
// Fire oncomplete on next microtask so callers can set ctx.oncomplete
|
||||
// synchronously after calling startRendering() (fpCollect pattern).
|
||||
// Fire oncomplete + 'complete' listeners on next microtask so callers
|
||||
// can register handlers synchronously after startRendering().
|
||||
var p = Promise.resolve().then(function() {
|
||||
var evt = {renderedBuffer: buf, target: self, type: 'complete'};
|
||||
if (typeof self.oncomplete === 'function') {
|
||||
try { self.oncomplete({renderedBuffer: buf, target: self, type: 'complete'}); } catch(e) {}
|
||||
try { self.oncomplete(evt); } catch(e) {}
|
||||
}
|
||||
var listeners = (self._listeners && self._listeners['complete']) || [];
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
try { listeners[i](evt); } catch(e) {}
|
||||
}
|
||||
return buf;
|
||||
});
|
||||
@@ -5945,18 +6061,75 @@ globalThis.Worker = class Worker {
|
||||
this._listeners = {};
|
||||
const worker = this;
|
||||
|
||||
if (typeof url === 'string' && (url.startsWith('blob:') || url.startsWith('http'))) {
|
||||
const blobContent = globalThis.__blobStore?.[url];
|
||||
if (blobContent) {
|
||||
this._code = blobContent;
|
||||
} else {
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
worker._code = await resp.text();
|
||||
} catch(e) { if (worker.onerror) worker.onerror(e); }
|
||||
})();
|
||||
let resolvedUrl = url;
|
||||
if (typeof url === 'string') {
|
||||
const blob = globalThis.__blobStore?.[url];
|
||||
if (blob) {
|
||||
worker._code = blob;
|
||||
// Auto-start on next tick so caller can set onmessage first.
|
||||
setTimeout(() => worker._autoRun(), 0);
|
||||
return;
|
||||
}
|
||||
// Resolve relative URLs against the current page.
|
||||
if (!url.startsWith('http') && !url.startsWith('blob:') && !url.startsWith('data:')) {
|
||||
try { resolvedUrl = new URL(url, globalThis.location?.href || '').href; } catch(e) {}
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await fetch(resolvedUrl);
|
||||
worker._code = await resp.text();
|
||||
if (!worker._terminated) worker._autoRun();
|
||||
} catch(e) { if (worker.onerror) worker.onerror(e); }
|
||||
})();
|
||||
}
|
||||
}
|
||||
_makeScope() {
|
||||
const worker = this;
|
||||
// WorkerGlobalScope defined + no document property → IS_WORKER_SCOPE = true in creepjs
|
||||
const scope = {
|
||||
WorkerGlobalScope: function WorkerGlobalScope() {},
|
||||
DedicatedWorkerGlobalScope: function DedicatedWorkerGlobalScope() {},
|
||||
postMessage: (msg) => {
|
||||
if (worker._terminated) return;
|
||||
const evt = { data: msg };
|
||||
if (worker.onmessage) worker.onmessage(evt);
|
||||
const ls = worker._listeners['message'] || [];
|
||||
for (const h of ls) h(evt);
|
||||
},
|
||||
addEventListener: (type, fn) => {
|
||||
if (!scope._ev) scope._ev = {};
|
||||
if (!scope._ev[type]) scope._ev[type] = [];
|
||||
scope._ev[type].push(fn);
|
||||
},
|
||||
close: () => { worker._terminated = true; },
|
||||
crypto: globalThis.crypto,
|
||||
Crypto: globalThis.Crypto,
|
||||
TextEncoder: globalThis.TextEncoder,
|
||||
TextDecoder: globalThis.TextDecoder,
|
||||
atob: globalThis.atob,
|
||||
btoa: globalThis.btoa,
|
||||
setTimeout: globalThis.setTimeout,
|
||||
setInterval: globalThis.setInterval,
|
||||
clearTimeout: globalThis.clearTimeout,
|
||||
clearInterval: globalThis.clearInterval,
|
||||
fetch: globalThis.fetch,
|
||||
console: globalThis.console,
|
||||
performance: globalThis.performance,
|
||||
location: globalThis.location,
|
||||
};
|
||||
scope.self = scope;
|
||||
return scope;
|
||||
}
|
||||
_autoRun() {
|
||||
if (this._terminated || !this._code) return;
|
||||
const worker = this;
|
||||
const scope = worker._makeScope();
|
||||
try {
|
||||
const fn = new Function('self', 'postMessage', 'addEventListener', 'close', worker._code);
|
||||
fn(scope, scope.postMessage, scope.addEventListener, scope.close);
|
||||
} catch(e) {
|
||||
console.error('Worker error:', e.message);
|
||||
if (worker.onerror) worker.onerror(e);
|
||||
}
|
||||
}
|
||||
postMessage(data) {
|
||||
@@ -5964,32 +6137,13 @@ globalThis.Worker = class Worker {
|
||||
const worker = this;
|
||||
setTimeout(() => {
|
||||
if (worker._terminated || !worker._code) return;
|
||||
const scope = worker._makeScope();
|
||||
try {
|
||||
const workerSelf = {
|
||||
onmessage: null,
|
||||
postMessage: (msg) => {
|
||||
const evt = { data: msg };
|
||||
if (worker.onmessage) worker.onmessage(evt);
|
||||
const handlers = worker._listeners['message'] || [];
|
||||
for (const h of handlers) h(evt);
|
||||
},
|
||||
addEventListener: (type, fn) => { workerSelf['on' + type] = fn; },
|
||||
close: () => { worker._terminated = true; },
|
||||
crypto: globalThis.crypto,
|
||||
TextEncoder: globalThis.TextEncoder,
|
||||
TextDecoder: globalThis.TextDecoder,
|
||||
atob: globalThis.atob,
|
||||
btoa: globalThis.btoa,
|
||||
setTimeout: globalThis.setTimeout,
|
||||
setInterval: globalThis.setInterval,
|
||||
clearTimeout: globalThis.clearTimeout,
|
||||
clearInterval: globalThis.clearInterval,
|
||||
fetch: globalThis.fetch,
|
||||
console: globalThis.console,
|
||||
};
|
||||
const fn = new Function('self', 'postMessage', 'addEventListener', 'close', worker._code);
|
||||
fn(workerSelf, workerSelf.postMessage, workerSelf.addEventListener, workerSelf.close);
|
||||
if (workerSelf.onmessage) workerSelf.onmessage({ data });
|
||||
fn(scope, scope.postMessage, scope.addEventListener, scope.close);
|
||||
const evs = (scope._ev && scope._ev['message']) || [];
|
||||
if (evs.length) { for (const h of evs) h({ data }); }
|
||||
else if (scope.onmessage) scope.onmessage({ data });
|
||||
} catch(e) {
|
||||
console.error('Worker error:', e.message);
|
||||
if (worker.onerror) worker.onerror(e);
|
||||
@@ -6134,6 +6288,20 @@ if (typeof DOMRect === 'undefined') {
|
||||
static fromRect(r={}) { return new DOMRect(r.x,r.y,r.width,r.height); }
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof DOMRectList === 'undefined') {
|
||||
globalThis.DOMRectList = class DOMRectList {
|
||||
constructor(arr=[]) {
|
||||
this.length = arr.length;
|
||||
for (let i = 0; i < arr.length; i++) this[i] = arr[i];
|
||||
}
|
||||
item(i) { return this[i] || null; }
|
||||
[Symbol.iterator]() {
|
||||
let i = 0, self = this;
|
||||
return { next() { const done = i >= self.length; return { value: done ? undefined : self[i++], done }; } };
|
||||
}
|
||||
};
|
||||
}
|
||||
if (typeof DOMPoint === 'undefined') {
|
||||
globalThis.DOMPoint = class DOMPoint {
|
||||
constructor(x=0,y=0,z=0,w=1) { this.x=x;this.y=y;this.z=z;this.w=w; }
|
||||
@@ -6397,10 +6565,44 @@ if (typeof Range === 'undefined') {
|
||||
setStart(n,o){this.startContainer=n;this.startOffset=o;} setEnd(n,o){this.endContainer=n;this.endOffset=o;}
|
||||
collapse(){} selectNode(){} selectNodeContents(){} cloneContents(){return document?.createDocumentFragment();}
|
||||
deleteContents(){} insertNode(){} getBoundingClientRect(){return new DOMRect();}
|
||||
getClientRects(){return [];} cloneRange(){return new Range();} toString(){return '';}
|
||||
getClientRects(){return new DOMRectList([]);} cloneRange(){return new Range();} toString(){return '';}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof FontFace === 'undefined') {
|
||||
globalThis.FontFace = class FontFace {
|
||||
constructor(family, source, descriptors={}) {
|
||||
this.family = family;
|
||||
this.style = descriptors.style || 'normal';
|
||||
this.weight = descriptors.weight || 'normal';
|
||||
this.stretch = descriptors.stretch || 'normal';
|
||||
this.unicodeRange = descriptors.unicodeRange || 'U+0-10FFFF';
|
||||
this.variant = descriptors.variant || 'normal';
|
||||
this.featureSettings = descriptors.featureSettings || 'normal';
|
||||
this.status = 'unloaded';
|
||||
}
|
||||
load() { this.status = 'loaded'; return Promise.resolve(this); }
|
||||
};
|
||||
globalThis.FontFaceSet = class FontFaceSet extends EventTarget {
|
||||
constructor() { super(); this.status = 'loaded'; this.ready = Promise.resolve(this); }
|
||||
add() { return this; }
|
||||
check() { return true; }
|
||||
clear() {}
|
||||
delete() { return false; }
|
||||
load() { return Promise.resolve([]); }
|
||||
forEach() {}
|
||||
has() { return false; }
|
||||
[Symbol.iterator]() { return [][Symbol.iterator](); }
|
||||
};
|
||||
Object.defineProperty(Document.prototype, 'fonts', {
|
||||
get() {
|
||||
if (!this._fonts) this._fonts = new FontFaceSet();
|
||||
return this._fonts;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof SharedWorker === 'undefined') {
|
||||
globalThis.SharedWorker = class SharedWorker {
|
||||
constructor() { this.port = { postMessage(){}, onmessage:null, start(){}, close(){}, addEventListener(){}, removeEventListener(){} }; this.onerror = null; }
|
||||
@@ -6484,7 +6686,7 @@ globalThis.__obscura_init = function() {
|
||||
globalThis.visualViewport = { width:sw, height:sh-80, offsetLeft:0, offsetTop:0, scale:1, addEventListener(){}, removeEventListener(){} };
|
||||
globalThis.devicePixelRatio = sw >= 2560 ? 2 : 1;
|
||||
globalThis.innerWidth = sw; globalThis.innerHeight = sh - 80;
|
||||
globalThis.outerWidth = sw; globalThis.outerHeight = sh;
|
||||
globalThis.outerWidth = sw; globalThis.outerHeight = sh - 40;
|
||||
|
||||
var hwValues = globalThis.__obscura_stealth ? [4, 6, 8, 12, 16] : [2, 4, 6, 8, 12, 16];
|
||||
globalThis.navigator.hardwareConcurrency = hwValues[Math.floor(_fpRand(400) * hwValues.length)];
|
||||
@@ -6496,7 +6698,7 @@ globalThis.__obscura_init = function() {
|
||||
globalThis.performance.timing = { navigationStart: t0, domContentLoadedEventEnd: t0, loadEventEnd: t0 };
|
||||
var _totalHeap = 15000000 + Math.floor(_fpRand(620) * 85000000);
|
||||
globalThis.performance.memory = {
|
||||
jsHeapSizeLimit: 2172649472,
|
||||
jsHeapSizeLimit: 4294705152,
|
||||
totalJSHeapSize: _totalHeap,
|
||||
usedJSHeapSize: Math.floor(_totalHeap * (0.3 + _fpRand(621) * 0.5)),
|
||||
};
|
||||
@@ -6519,8 +6721,8 @@ globalThis.__obscura_init = function() {
|
||||
{brand:"Chromium",version:"145.0.0.0"},
|
||||
],
|
||||
mobile: false, model: "",
|
||||
platform: "Linux",
|
||||
platformVersion: "6.1.0",
|
||||
platform: "Windows",
|
||||
platformVersion: "10.0.0",
|
||||
uaFullVersion: "145.0.0.0",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -137,7 +137,21 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
"null".into()
|
||||
}
|
||||
"get_element_by_id" => {
|
||||
dom.get_element_by_id(&arg1).map(|id| id.index().to_string()).unwrap_or("-1".into())
|
||||
// Verify the indexed node is in the live document. The id_index is best-effort:
|
||||
// it only registers nodes at creation time and doesn't update on reparent, so
|
||||
// it can point to a detached clone while the live node is elsewhere in the tree.
|
||||
let doc = dom.document();
|
||||
let nid = dom.get_element_by_id(&arg1);
|
||||
let live = nid.filter(|&n| dom.ancestors(n).contains(&doc));
|
||||
match live {
|
||||
Some(n) => n.index().to_string(),
|
||||
None => {
|
||||
// Fall back to full scan for the live document.
|
||||
let sel = format!("[id=\"{}\"]", arg1.replace('\\', "\\\\").replace('"', "\\\""));
|
||||
dom.query_selector(&sel).ok().flatten()
|
||||
.map(|id| id.index().to_string()).unwrap_or("-1".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
"query_selector" => {
|
||||
dom.query_selector(&arg1).ok().flatten().map(|id| id.index().to_string()).unwrap_or("-1".into())
|
||||
@@ -235,19 +249,22 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
serde_json::to_string(&dom.outer_html(NodeId::new(nid))).unwrap_or("\"\"".into())
|
||||
}
|
||||
"append_child" => {
|
||||
let parent = arg1.parse::<u32>().unwrap_or(0);
|
||||
let child = arg2.parse::<u32>().unwrap_or(0);
|
||||
// Reject if either nid failed to parse (was "undefined"/empty) — those
|
||||
// default to 0 which is the document root, and silently operating on it
|
||||
// corrupts the tree. Require both args to be valid positive integers.
|
||||
let parent = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
|
||||
let child = match arg2.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
|
||||
dom.append_child(NodeId::new(parent), NodeId::new(child));
|
||||
"true".into()
|
||||
}
|
||||
"remove_child" => {
|
||||
let child = arg1.parse::<u32>().unwrap_or(0);
|
||||
let child = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
|
||||
dom.remove_child(NodeId::new(child));
|
||||
"true".into()
|
||||
}
|
||||
"insert_before" => {
|
||||
let new_node = arg1.parse::<u32>().unwrap_or(0);
|
||||
let ref_node = arg2.parse::<u32>().unwrap_or(0);
|
||||
let new_node = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
|
||||
let ref_node = match arg2.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
|
||||
dom.insert_before(NodeId::new(ref_node), NodeId::new(new_node));
|
||||
"true".into()
|
||||
}
|
||||
@@ -261,7 +278,12 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
|
||||
"true".into()
|
||||
}
|
||||
"set_inner_html" => {
|
||||
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||
let nid = match arg1.parse::<u32>() {
|
||||
Ok(n) if n > 0 => n,
|
||||
// nid=0 is the document root; never allow innerHTML to clear it.
|
||||
// nid parse failure (e.g. "undefined") also falls here.
|
||||
_ => return "false".into(),
|
||||
};
|
||||
let target = NodeId::new(nid);
|
||||
let children = dom.children(target);
|
||||
for child in children {
|
||||
|
||||
Reference in new issue
Block a user