Skip to content

Commit d37a832

Browse files
committed
Merge branch 'main' into release/v1.3
2 parents 9c149ce + e67138e commit d37a832

File tree

18 files changed

+520
-158
lines changed

18 files changed

+520
-158
lines changed

example/gm_download.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,4 @@ GM_download({
1919
}, onload(data) {
2020
console.log("load", data);
2121
},
22-
downloadMethod: "xhr"
2322
});

example/tests/gm_xhr_test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,8 @@ const enableTool = true;
874874
});
875875
assertEq(res.status, 200);
876876
assert(progressEvents >= 4, "received at least 4 progress events");
877-
assert(lastLoaded >= 0, "progress loaded captured");
877+
// `progress` is guaranteed to fire only in the Fetch API.
878+
assert(fetch ? lastLoaded > 0 : lastLoaded >= 0, "progress loaded captured");
878879
assert(!response, "no response");
879880
},
880881
},
@@ -916,7 +917,8 @@ const enableTool = true;
916917
});
917918
assertEq(res.status, 200);
918919
assert(progressEvents >= 4, "received at least 4 progress events");
919-
assert(lastLoaded >= 0, "progress loaded captured");
920+
// `progress` is guaranteed to fire only in the Fetch API.
921+
assert(fetch ? lastLoaded > 0 : lastLoaded >= 0, "progress loaded captured");
920922
assert(response instanceof ReadableStream && typeof response.getReader === "function", "response");
921923
},
922924
},
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
export default class Downloads {
2-
download(_: any, callback: () => void) {
3-
callback && callback();
2+
onChangedCallback: ((downloadDelta: chrome.downloads.DownloadDelta) => void) | null = null;
3+
4+
onChanged = {
5+
addListener: (callback: (downloadDelta: chrome.downloads.DownloadDelta) => void) => {
6+
this.onChangedCallback = callback;
7+
},
8+
removeListener: (_callback: (downloadDelta: chrome.downloads.DownloadDelta) => void) => {
9+
this.onChangedCallback = null;
10+
},
11+
};
12+
13+
download(_: any, callback: (downloadId: number) => void) {
14+
callback && callback(1);
15+
this.onChangedCallback?.({
16+
id: 1,
17+
state: { current: "complete" },
18+
});
419
}
520
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect } from "vitest";
2+
import { shouldFnBind } from "./create_context";
3+
4+
describe.concurrent("shouldFnBind", () => {
5+
it.concurrent("不处理非原生函数", () => {
6+
const o: Record<string, any> = {};
7+
o.targetArrowFn = () => {};
8+
expect(shouldFnBind(o.targetArrowFn)).toBe(false);
9+
o.targetArrowFn = new Proxy(o.targetArrowFn, {});
10+
expect(shouldFnBind(o.targetArrowFn)).toBe(false);
11+
o.targetFn1 = function () {};
12+
expect(shouldFnBind(o.targetFn1)).toBe(false);
13+
o.targetFn1 = new Proxy(o.targetFn1, {});
14+
expect(shouldFnBind(o.targetFn1)).toBe(false);
15+
o.targetFn2 = function targetFn2() {};
16+
expect(shouldFnBind(o.targetFn2)).toBe(false);
17+
o.targetFn2 = new Proxy(o.targetFn2, {});
18+
expect(shouldFnBind(o.targetFn2)).toBe(false);
19+
});
20+
it.concurrent("处理Proxy Function #985", () => {
21+
const o: Record<string, any> = {};
22+
// 例1: valueOf
23+
o.valueOf = global.valueOf;
24+
expect(shouldFnBind(o.valueOf)).toBe(true);
25+
o.valueOf = new Proxy(o.valueOf, {});
26+
expect(shouldFnBind(o.valueOf)).toBe(true);
27+
// 例2: setTimeoutForTest1: 验证一次拦截
28+
// @ts-ignore
29+
o.setTimeoutForTest1 = global.setTimeoutForTest1;
30+
expect(shouldFnBind(o.setTimeoutForTest1)).toBe(true);
31+
o.setTimeoutForTest1 = new Proxy(o.setTimeoutForTest1, {
32+
apply: (target, thisArg, argArray) => {
33+
console.log("proxy call", { target, thisArg, argArray });
34+
},
35+
});
36+
expect(shouldFnBind(o.setTimeoutForTest1)).toBe(true);
37+
// 例2: setTimeoutForTest2: 验证二次拦截
38+
// @ts-ignore
39+
o.setTimeoutForTest2 = global.setTimeoutForTest2;
40+
expect(shouldFnBind(o.setTimeoutForTest2)).toBe(true);
41+
o.setTimeoutForTest2 = new Proxy(o.setTimeoutForTest2, {
42+
apply: (target, thisArg, argArray) => {
43+
console.log("proxy call", { target, thisArg, argArray });
44+
},
45+
});
46+
expect(shouldFnBind(o.setTimeoutForTest2)).toBe(true);
47+
});
48+
});

src/app/service/content/create_context.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,22 +100,22 @@ export const createContext = (
100100
const noEval = false;
101101

102102
// 取得原生函数代码表示
103-
const getNativeCodeSeg = () => {
103+
const getNativeCodeSegs = () => {
104104
const k = "propertyIsEnumerable"; // 选用 Object.propertyIsEnumerable 取得原生函数代码表示
105105
const codeSeg = `${Object[k]}`;
106106
const idx1 = codeSeg.indexOf(k);
107107
const idx2 = codeSeg.indexOf("()");
108108
const idx3 = codeSeg.lastIndexOf("(");
109109
if (idx1 > 0 && idx2 > 0 && idx3 === idx2) {
110-
return codeSeg.substring(idx1 + k.length);
110+
return [codeSeg.substring(0, idx1), codeSeg.substring(idx1 + k.length)];
111111
}
112-
return "";
112+
return null;
113113
};
114114

115-
const nativeCodeSeg = getNativeCodeSeg();
115+
const ncs = getNativeCodeSegs();
116116

117117
// 判断是否应该将函数绑定到global (原生函数)
118-
const shouldFnBind = (f: any) => {
118+
export const shouldFnBind = (f: any) => {
119119
if (typeof f !== "function") return false;
120120
// 函数有 prototype 即为 Class
121121
if ("prototype" in f) return false; // 避免getter, 使用 in operator (注意, nodeJS的测试环境有异)
@@ -125,8 +125,15 @@ const shouldFnBind = (f: any) => {
125125
if (!name) return false;
126126
const e = name.charCodeAt(0);
127127
if (e >= 97 && e <= 122 && !name.includes(" ")) {
128-
// 为避免浏览器插件封装了 原生函数,需要进行 toString 测试
129-
if (nativeCodeSeg.length && `${f}`.endsWith(`${name}${nativeCodeSeg}`)) {
128+
// 为避免浏览器插件封装了 原生函数,需要进行 toString 测试 (Proxy封装例外)
129+
if (ncs?.[1]) {
130+
const s = `${f}`;
131+
// 广告拦截扩展进行Proxy封装后丢失名字 (Chrome:所有经Proxy封装都会变成无名原生函数)
132+
if (s === `${ncs[0]}${name}${ncs[1]}` || s === `${ncs[0]}${ncs[1]}`) {
133+
return true;
134+
}
135+
} else {
136+
// 代码错误,全部 bind
130137
return true;
131138
}
132139
}
@@ -240,6 +247,10 @@ const sharedInitCopy = USE_PSEUDO_WINDOW
240247
...overridedDescs,
241248
});
242249

250+
// 把沙盒的 console 和网页的 console 隔离
251+
const initConsoleDescs = Object.getOwnPropertyDescriptors(console);
252+
const ConsolePrototype = Object.getPrototypeOf(console);
253+
243254
type GMWorldContext = typeof globalThis & Record<PropertyKey, any>;
244255

245256
const isPrimitive = (x: any) => x !== Object(x);
@@ -388,5 +399,8 @@ export const createProxyContext = <const Context extends GMWorldContext>(context
388399
mySandbox.onurlchange = null;
389400
}
390401

402+
// 从网页 console 隔离出来的沙盒 console
403+
mySandbox.console = Object.create(ConsolePrototype, initConsoleDescs);
404+
391405
return mySandbox;
392406
};

src/app/service/content/exec_script.test.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,8 @@ describe("沙盒环境测试", async () => {
303303
// });
304304

305305
// 在模拟环境无法测试:在实际操作中和TM一致
306-
// 在非拦截式沙盒裡删除 沙盒onload 后,会取得页面的真onload
307-
// 在非拦截式沙盒裡删除 真onload 后,会变undefined
306+
// 在非拦截式沙盒里删除 沙盒onload 后,会取得页面的真onload
307+
// 在非拦截式沙盒里删除 真onload 后,会变undefined
308308
// it.concurrent("删除 onload 后应该为 null", () => {
309309
// const mockFn = vi.fn();
310310
// _this["onload"] = function thisOnLoad() {
@@ -376,33 +376,58 @@ describe("沙盒环境测试", async () => {
376376

377377
it.concurrent("[兼容问题] Ensure Illegal invocation can be tested", () => {
378378
expect(global.setTimeout.name).toEqual("setTimeout");
379+
// -----
379380
//@ts-ignore
380-
expect(global.setTimeoutForTest.name).toEqual("setTimeoutForTest");
381-
expect(_this.setTimeoutForTest.name).toEqual("bound setTimeoutForTest");
381+
expect(global.setTimeoutForTest1.name).toEqual("setTimeoutForTest1");
382+
expect(_this.setTimeoutForTest1.name).toEqual("bound setTimeoutForTest1");
382383
//@ts-ignore
383384
expect(() => global.setTimeout.call(global, () => {}, 1)).not.toThrow();
384385
//@ts-ignore
385-
expect(() => global.setTimeoutForTest.call(global, () => {}, 1)).not.toThrow();
386+
expect(() => global.setTimeoutForTest1.call(global, () => {}, 1)).not.toThrow();
386387
//@ts-ignore
387-
expect(() => global.setTimeoutForTest.call({}, () => {}, 1)).toThrow();
388+
expect(() => global.setTimeoutForTest1.call({}, () => {}, 1)).toThrow();
389+
// -----
390+
//@ts-ignore
391+
expect(global.setTimeoutForTest2.name).toEqual("setTimeoutForTest2");
392+
expect(_this.setTimeoutForTest2.name).toEqual("bound setTimeoutForTest2");
393+
//@ts-ignore
394+
expect(() => global.setTimeout.call(global, () => {}, 1)).not.toThrow();
395+
//@ts-ignore
396+
expect(() => global.setTimeoutForTest2.call(global, () => {}, 1)).not.toThrow();
397+
//@ts-ignore
398+
expect(() => global.setTimeoutForTest2.call({}, () => {}, 1)).toThrow();
388399
});
389400
// https://github.com/xcanwin/KeepChatGPT 环境隔离得不够干净导致的
390401
it.concurrent("[兼容问题] Uncaught TypeError: Illegal invocation #189", () => {
391-
// setTimeout 和 setTimeoutForTest 都測試吧
402+
// setTimeout 和 setTimeoutForTest1 都测试吧
392403
const promise1 = new Promise((resolve) => {
393404
console.log(_this.setTimeout.prototype);
394-
_this.setTimeoutForTest(resolve, 1);
405+
_this.setTimeoutForTest1(resolve, 1);
395406
});
396407
const promise2 = new Promise((resolve) => {
397408
console.log(_this.setTimeout.prototype);
398409
_this.setTimeout(resolve, 1);
399410
});
400-
expect(Promise.all([promise1, promise2]).then(() => "ok")).resolves.toBe("ok");
411+
const res = Promise.all([promise1, promise2]);
412+
expect(res.then((res) => (!res[0] && !res[1] ? "ok" : "ng"))).resolves.toBe("ok");
401413
});
402414
// AC-baidu-重定向优化百度搜狗谷歌必应搜索_favicon_双列
403415
it.concurrent("[兼容问题] TypeError: Object.freeze is not a function #116", () => {
404416
expect(() => _this.Object.freeze({})).not.toThrow();
405417
});
418+
it.concurrent("Proxy Function #985", () => {
419+
// setTimeout 和 setTimeoutForTest2 都测试吧
420+
const promise1 = new Promise((resolve) => {
421+
console.log(_this.setTimeout.prototype);
422+
_this.setTimeoutForTest2(resolve, 1);
423+
});
424+
const promise2 = new Promise((resolve) => {
425+
console.log(_this.setTimeout.prototype);
426+
_this.setTimeout(resolve, 1);
427+
});
428+
const res = Promise.all([promise1, promise2]);
429+
expect(res.then((res) => (res[0] === "proxy" && !res[1] ? "ok" : "ng"))).resolves.toBe("ok");
430+
});
406431

407432
const tag = (<any>global)[Symbol.toStringTag]; // 实际环境:'[object Window]' 测试环境:'[object global]'
408433

src/app/service/content/gm_api/gm_api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,7 @@ export default class GMApi extends GM_Base {
887887
retPromiseResolve?.(data.data);
888888
break;
889889
case "onprogress":
890-
details.onprogress?.(makeCallbackParam({ ...data.data }));
890+
details.onprogress?.(makeCallbackParam({ ...data.data, mode: "browser" }));
891891
retPromiseReject?.(new Error("Timeout ERROR"));
892892
break;
893893
case "ontimeout":
@@ -963,7 +963,7 @@ export default class GMApi extends GM_Base {
963963
// details.onload?.(makeCallbackParam({}))
964964
},
965965
onprogress: (e) => {
966-
details.onprogress?.(makeCallbackParam({ ...e }));
966+
details.onprogress?.(makeCallbackParam({ ...e, mode: "native" }));
967967
},
968968
ontimeout: () => {
969969
details.ontimeout?.(makeCallbackParam({}));

src/app/service/content/gm_api/gm_xhr.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ export function GM_xmlhttpRequest(
112112
}
113113
}
114114
const contentContext = details.context;
115+
if (details.method) {
116+
details.method = `${details.method}`.toUpperCase() as typeof details.method;
117+
}
115118

116119
const param: GMSend.XHRDetails = {
117120
method: details.method,
@@ -173,7 +176,18 @@ export function GM_xmlhttpRequest(
173176
const responseType = responseTypeOriginal; // 回传用
174177

175178
// 发送信息
176-
a.connect(isDownload ? "GM_download" : "GM_xmlhttpRequest", [param]).then((con) => {
179+
let connectMessage: Promise<MessageConnect>;
180+
if (isDownload) {
181+
// 如果是下载,带上 downloadMode 参数,呼叫 SW 的 GM_download
182+
// 在 SW 中处理,实际使用 GM_xmlhttpRequest 进行下载
183+
const method = param.method === "POST" ? "POST" : "GET";
184+
const downloadParam: GMTypes.DownloadDetails<string> = { ...param, method, downloadMode: "native", name: "" };
185+
connectMessage = a.connect("GM_download", [downloadParam]);
186+
} else {
187+
// 一般 GM_xmlhttpRequest,呼叫 SW 的 GM_xmlhttpRequest
188+
connectMessage = a.connect("GM_xmlhttpRequest", [param]);
189+
}
190+
connectMessage.then((con) => {
177191
// 注意。在此 callback 里,不应直接存取 param, 否则会影响 GC
178192
connect = con;
179193
const resultTexts = [] as string[]; // 函数参考清掉后,变数会被GC
@@ -336,9 +350,6 @@ export function GM_xmlhttpRequest(
336350
return responseXML as Document | null;
337351
},
338352
get responseText() {
339-
if (responseTypeOriginal === "document") {
340-
// console.log(resultType, resultBuffers.length, resultTexts.length);
341-
}
342353
if (responseText === false) {
343354
if (resultType === 2) {
344355
finalResultBuffers ||= concatUint8(resultBuffers);
@@ -347,7 +358,6 @@ export function GM_xmlhttpRequest(
347358
const text = decoder.decode(buf);
348359
responseText = text;
349360
} else {
350-
// resultType === 3
351361
if (finalResultText === null) finalResultText = `${resultTexts.join("")}`;
352362
responseText = finalResultText;
353363
}

src/app/service/content/script_executor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getStorageName } from "@App/pkg/utils/utils";
33
import type { EmitEventRequest } from "../service_worker/types";
44
import ExecScript from "./exec_script";
55
import type { GMInfoEnv, ScriptFunc, ValueUpdateDataEncoded } from "./types";
6-
import { addStyle, definePropertyListener } from "./utils";
6+
import { addStyleSheet, definePropertyListener } from "./utils";
77
import type { TScriptInfo } from "@App/app/repo/scripts";
88
import { DefinedFlags } from "../service_worker/runtime.consts";
99

@@ -116,7 +116,7 @@ export class ScriptExecutor {
116116
for (const val of metadata["require-css"]) {
117117
const res = resource[val];
118118
if (res) {
119-
addStyle(res.content);
119+
addStyleSheet(res.content);
120120
}
121121
}
122122
}

0 commit comments

Comments
 (0)