333 lines
12 KiB
JavaScript
333 lines
12 KiB
JavaScript
const { scaffoldModule } = require("./scaffold-module");
|
||
|
||
function clone(value) {
|
||
return JSON.parse(JSON.stringify(value || {}));
|
||
}
|
||
|
||
function normalizeDocumentSeed(seed = {}) {
|
||
const documentSeed = seed.document && typeof seed.document === "object" ? seed.document : seed;
|
||
return {
|
||
document: {
|
||
URL: documentSeed.URL || "https://example.invalid/",
|
||
referrer: documentSeed.referrer || "",
|
||
documentURI: documentSeed.documentURI || documentSeed.URL || "https://example.invalid/",
|
||
compatMode: documentSeed.compatMode || "CSS1Compat",
|
||
dir: documentSeed.dir || "",
|
||
title: documentSeed.title || "",
|
||
designMode: documentSeed.designMode || "off",
|
||
readyState: documentSeed.readyState || "complete",
|
||
contentType: documentSeed.contentType || "text/html",
|
||
inputEncoding: documentSeed.inputEncoding || "UTF-8",
|
||
domain: documentSeed.domain || "example.invalid",
|
||
characterSet: documentSeed.characterSet || "UTF-8",
|
||
charset: documentSeed.charset || "UTF-8",
|
||
hidden: documentSeed.hidden === true,
|
||
visibilityState: documentSeed.visibilityState || "visible",
|
||
cookie: typeof documentSeed.cookie === "string" ? documentSeed.cookie : (typeof seed.cookie === "string" ? seed.cookie : ""),
|
||
},
|
||
queryMap: clone(seed.queryMap || {}),
|
||
ids: clone(seed.ids || {}),
|
||
tags: clone(seed.tags || {}),
|
||
specialCases: clone(seed.specialCases || {}),
|
||
};
|
||
}
|
||
|
||
function renderDocumentPatch(seed) {
|
||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||
return `(
|
||
function installDocumentModule(globalObject) {
|
||
const seed = ${serializedSeed};
|
||
const protector = globalObject.__envNativeProtector || null;
|
||
|
||
function defineValue(target, key, value) {
|
||
Object.defineProperty(target, key, {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: true,
|
||
value: value
|
||
});
|
||
}
|
||
|
||
function defineGetter(target, key, getter, setter) {
|
||
const descriptor = {
|
||
configurable: true,
|
||
enumerable: false,
|
||
get: getter
|
||
};
|
||
if (typeof setter === "function") {
|
||
descriptor.set = setter;
|
||
}
|
||
Object.defineProperty(target, key, descriptor);
|
||
if (protector && protector.protectDescriptor) {
|
||
protector.protectDescriptor(target, key);
|
||
}
|
||
}
|
||
|
||
function applyEventTarget(target) {
|
||
defineValue(target, "__listeners__", Object.create(null));
|
||
defineValue(target, "addEventListener", function addEventListener(type, listener) {
|
||
if (!this.__listeners__[type]) {
|
||
this.__listeners__[type] = [];
|
||
}
|
||
this.__listeners__[type].push(listener);
|
||
});
|
||
defineValue(target, "removeEventListener", function removeEventListener(type, listener) {
|
||
const list = this.__listeners__[type] || [];
|
||
this.__listeners__[type] = list.filter(function filterItem(item) {
|
||
return item !== listener;
|
||
});
|
||
});
|
||
defineValue(target, "dispatchEvent", function dispatchEvent(event) {
|
||
const list = (this.__listeners__[event.type] || []).slice();
|
||
for (const listener of list) {
|
||
listener.call(this, event);
|
||
}
|
||
const handler = this["on" + event.type];
|
||
if (typeof handler === "function") {
|
||
handler.call(this, event);
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function createElementNode(tagName) {
|
||
const normalized = String(tagName || "div").toUpperCase();
|
||
if (normalized === "CANVAS" && typeof globalObject.__envCreateCanvasElement === "function") {
|
||
return globalObject.__envCreateCanvasElement();
|
||
}
|
||
const element = {
|
||
nodeType: 1,
|
||
nodeName: normalized,
|
||
tagName: normalized,
|
||
ownerDocument: null,
|
||
style: {},
|
||
dataset: {},
|
||
children: [],
|
||
childNodes: []
|
||
};
|
||
applyEventTarget(element);
|
||
defineValue(element, "appendChild", function appendChild(child) {
|
||
this.children.push(child);
|
||
this.childNodes.push(child);
|
||
child.parentNode = this;
|
||
return child;
|
||
});
|
||
defineValue(element, "setAttribute", function setAttribute(name, value) {
|
||
this[name] = String(value);
|
||
});
|
||
defineValue(element, "getAttribute", function getAttribute(name) {
|
||
return Object.prototype.hasOwnProperty.call(this, name) ? this[name] : null;
|
||
});
|
||
return element;
|
||
}
|
||
|
||
function buildNodeMap(source, kind) {
|
||
const out = Object.create(null);
|
||
Object.keys(source || {}).forEach(function eachKey(key) {
|
||
const value = source[key];
|
||
if (Array.isArray(value)) {
|
||
out[key] = value.map(function mapTag(tagName) {
|
||
const node = createElementNode(tagName);
|
||
node.ownerDocument = documentInstance;
|
||
return node;
|
||
});
|
||
} else {
|
||
const node = createElementNode(value || kind || "div");
|
||
node.ownerDocument = documentInstance;
|
||
out[key] = node;
|
||
}
|
||
});
|
||
return out;
|
||
}
|
||
|
||
function Document() {}
|
||
function HTMLDocument() {}
|
||
HTMLDocument.prototype = Object.create(Document.prototype);
|
||
defineValue(HTMLDocument.prototype, "constructor", HTMLDocument);
|
||
|
||
const state = seed.document || {};
|
||
const documentInstance = Object.create(HTMLDocument.prototype);
|
||
let cookieValue = state.cookie || "";
|
||
applyEventTarget(documentInstance);
|
||
|
||
const queryNodes = buildNodeMap(seed.queryMap || {}, "div");
|
||
const idNodes = buildNodeMap(seed.ids || {}, "div");
|
||
const tagNodes = buildNodeMap(seed.tags || {}, "div");
|
||
const htmlNode = createElementNode("html");
|
||
const headNode = createElementNode("head");
|
||
const bodyNode = createElementNode("body");
|
||
htmlNode.ownerDocument = documentInstance;
|
||
headNode.ownerDocument = documentInstance;
|
||
bodyNode.ownerDocument = documentInstance;
|
||
htmlNode.appendChild(headNode);
|
||
htmlNode.appendChild(bodyNode);
|
||
|
||
defineGetter(Document.prototype, "URL", function getURL() {
|
||
return state.URL;
|
||
});
|
||
defineGetter(Document.prototype, "referrer", function getReferrer() {
|
||
return state.referrer;
|
||
});
|
||
defineGetter(Document.prototype, "documentURI", function getDocumentURI() {
|
||
return state.documentURI;
|
||
});
|
||
defineGetter(Document.prototype, "compatMode", function getCompatMode() {
|
||
return state.compatMode;
|
||
});
|
||
defineGetter(Document.prototype, "dir", function getDir() {
|
||
return state.dir;
|
||
});
|
||
defineGetter(Document.prototype, "title", function getTitle() {
|
||
return state.title;
|
||
}, function setTitle(value) {
|
||
state.title = String(value);
|
||
});
|
||
defineGetter(Document.prototype, "designMode", function getDesignMode() {
|
||
return state.designMode;
|
||
}, function setDesignMode(value) {
|
||
state.designMode = String(value);
|
||
});
|
||
defineGetter(Document.prototype, "readyState", function getReadyState() {
|
||
return state.readyState;
|
||
});
|
||
defineGetter(Document.prototype, "contentType", function getContentType() {
|
||
return state.contentType;
|
||
});
|
||
defineGetter(Document.prototype, "inputEncoding", function getInputEncoding() {
|
||
return state.inputEncoding;
|
||
});
|
||
defineGetter(Document.prototype, "domain", function getDomain() {
|
||
return state.domain;
|
||
});
|
||
defineGetter(Document.prototype, "characterSet", function getCharacterSet() {
|
||
return state.characterSet;
|
||
});
|
||
defineGetter(Document.prototype, "charset", function getCharset() {
|
||
return state.charset;
|
||
});
|
||
defineGetter(Document.prototype, "hidden", function getHidden() {
|
||
return !!state.hidden;
|
||
});
|
||
defineGetter(Document.prototype, "visibilityState", function getVisibilityState() {
|
||
return state.visibilityState;
|
||
});
|
||
defineGetter(Document.prototype, "cookie", function getCookie() {
|
||
return cookieValue;
|
||
}, function setCookie(value) {
|
||
cookieValue = cookieValue ? cookieValue + "; " + String(value) : String(value);
|
||
});
|
||
defineGetter(Document.prototype, "head", function getHead() {
|
||
return headNode;
|
||
});
|
||
defineGetter(Document.prototype, "body", function getBody() {
|
||
return bodyNode;
|
||
});
|
||
defineGetter(Document.prototype, "documentElement", function getDocumentElement() {
|
||
return htmlNode;
|
||
});
|
||
defineGetter(Document.prototype, "defaultView", function getDefaultView() {
|
||
return globalObject;
|
||
});
|
||
|
||
defineValue(Document.prototype, "createElement", function createElement(tagName) {
|
||
const element = createElementNode(tagName);
|
||
element.ownerDocument = documentInstance;
|
||
return element;
|
||
});
|
||
defineValue(Document.prototype, "querySelector", function querySelector(selector) {
|
||
return queryNodes[selector] || null;
|
||
});
|
||
defineValue(Document.prototype, "getElementById", function getElementById(id) {
|
||
return idNodes[id] || null;
|
||
});
|
||
defineValue(Document.prototype, "getElementsByTagName", function getElementsByTagName(tagName) {
|
||
const key = String(tagName || "").toLowerCase();
|
||
const list = tagNodes[key] || [];
|
||
return list.slice();
|
||
});
|
||
|
||
const documentAllFallback = {
|
||
length: 0,
|
||
item: function item() {
|
||
return null;
|
||
},
|
||
namedItem: function namedItem() {
|
||
return null;
|
||
}
|
||
};
|
||
defineGetter(Document.prototype, "all", function getAll() {
|
||
return documentAllFallback;
|
||
});
|
||
|
||
if (protector && protector.repairFunctionMeta) {
|
||
protector.repairFunctionMeta(Document, { name: "Document", length: 0 });
|
||
protector.repairFunctionMeta(HTMLDocument, { name: "HTMLDocument", length: 0 });
|
||
protector.repairFunctionMeta(Document.prototype.createElement, { name: "createElement", length: 1 });
|
||
protector.repairFunctionMeta(Document.prototype.querySelector, { name: "querySelector", length: 1 });
|
||
protector.repairFunctionMeta(Document.prototype.getElementById, { name: "getElementById", length: 1 });
|
||
protector.repairFunctionMeta(Document.prototype.getElementsByTagName, { name: "getElementsByTagName", length: 1 });
|
||
}
|
||
if (protector && protector.protectFunction) {
|
||
protector.protectFunction(Document, "Document");
|
||
protector.protectFunction(HTMLDocument, "HTMLDocument");
|
||
protector.protectFunction(Document.prototype.createElement, "createElement");
|
||
protector.protectFunction(Document.prototype.querySelector, "querySelector");
|
||
protector.protectFunction(Document.prototype.getElementById, "getElementById");
|
||
protector.protectFunction(Document.prototype.getElementsByTagName, "getElementsByTagName");
|
||
}
|
||
|
||
Object.defineProperty(globalObject, "Document", {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: true,
|
||
value: Document
|
||
});
|
||
Object.defineProperty(globalObject, "HTMLDocument", {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: true,
|
||
value: HTMLDocument
|
||
});
|
||
Object.defineProperty(globalObject, "document", {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: true,
|
||
value: documentInstance
|
||
});
|
||
|
||
return documentInstance;
|
||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||
`;
|
||
}
|
||
|
||
function buildDocumentModule(seed = {}) {
|
||
const module = scaffoldModule("document-module");
|
||
const normalizedSeed = normalizeDocumentSeed(seed);
|
||
|
||
module.patchPlan = [
|
||
"构建 Document / HTMLDocument 构造器、prototype 和 document 实例。",
|
||
"把 document.cookie、URL、readyState 等高频字段补成访问器描述符。",
|
||
"补出 createElement/querySelector/getElementById/getElementsByTagName,并为 canvas 入口预留到 fingerprint 模块的接线点。",
|
||
];
|
||
module.patchCode = renderDocumentPatch(normalizedSeed);
|
||
module.runtimeState = normalizedSeed;
|
||
module.validation = [
|
||
"检查 document.URL / document.documentURI / document.readyState。",
|
||
"检查 document.cookie getter/setter 是否正常工作。",
|
||
"检查 document.createElement('canvas') 是否能接到 fingerprint 模块。",
|
||
"检查 querySelector / getElementById / getElementsByTagName 的基本返回。",
|
||
"检查 document.all 仍被标注为特殊兼容风险,而不是宣称完全等价。",
|
||
];
|
||
module.residualRisk = [
|
||
"这里只覆盖静态 DOM 壳和常见查询入口,未实现完整 Node/Element/MutationObserver/CSSStyleDeclaration 系统。",
|
||
"document.all 仅提供降级 fallback,无法在纯 JS 中完整等价原生特殊对象语义。",
|
||
];
|
||
|
||
return module;
|
||
}
|
||
|
||
module.exports = {
|
||
buildDocumentModule,
|
||
normalizeDocumentSeed,
|
||
};
|