Skip to content

Commit eedf0c1

Browse files
committed
✨ GM_xhr支持document和stream
1 parent 9445dbc commit eedf0c1

File tree

6 files changed

+214
-13
lines changed

6 files changed

+214
-13
lines changed

src/runtime/background/gm_api.ts

+56-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@ import PermissionVerify, {
1717
ConfirmParam,
1818
IPermissionVerify,
1919
} from "./permission_verify";
20-
import { dealXhr, getIcon, listenerWebRequest, setXhrHeader } from "./utils";
20+
import {
21+
dealFetch,
22+
dealXhr,
23+
getFetchHeader,
24+
getIcon,
25+
listenerWebRequest,
26+
setXhrHeader,
27+
uint8ToArray,
28+
} from "./utils";
2129

2230
// GMApi,处理脚本的GM API调用请求
2331

@@ -150,6 +158,49 @@ export default class GMApi {
150158
return this.valueManager.setValue(request.script, key, value, sender);
151159
}
152160

161+
// 处理GM_xmlhttpRequest fetch的情况,先只处理ReadableStream的情况
162+
// 且不考虑复杂的情况
163+
CAT_fetch(request: Request, channel: Channel): Promise<any> {
164+
const config = <GMSend.XHRDetails>request.params[0];
165+
const { url } = config;
166+
return fetch(url, {
167+
method: config.method || "GET",
168+
body: <any>config.data,
169+
headers: getFetchHeader(this.systemConfig.scriptCatFlag, config),
170+
})
171+
.then((resp) => {
172+
const send = dealFetch(
173+
this.systemConfig.scriptCatFlag,
174+
config,
175+
resp,
176+
1
177+
);
178+
const reader = resp.body?.getReader();
179+
if (!reader) {
180+
throw new Error("read is not found");
181+
}
182+
const { scriptCatFlag } = this.systemConfig;
183+
reader.read().then(function read({ done, value }) {
184+
if (done) {
185+
const data = dealFetch(scriptCatFlag, config, resp, 4);
186+
channel.send({ event: "onreadystatechange", data });
187+
channel.send({ event: "onload", data });
188+
channel.send({ event: "onloadend", data });
189+
channel.disChannel();
190+
} else {
191+
channel.send({ event: "onstream", data: Array.from(value) });
192+
reader.read().then(read);
193+
}
194+
});
195+
channel.send({ event: "onloadstart", data: send });
196+
send.readyState = 2;
197+
channel.send({ event: "onreadystatechange", data: send });
198+
})
199+
.catch((e) => {
200+
channel.throw(e);
201+
});
202+
}
203+
153204
@PermissionVerify.API({
154205
confirm: (request: Request) => {
155206
const config = <GMSend.XHRDetails>request.params[0];
@@ -181,7 +232,10 @@ export default class GMApi {
181232
})
182233
async GM_xmlhttpRequest(request: Request, channel: Channel): Promise<any> {
183234
const config = <GMSend.XHRDetails>request.params[0];
184-
235+
if (config.responseType === "stream") {
236+
// 只有fetch支持ReadableStream
237+
return this.CAT_fetch(request, channel);
238+
}
185239
const xhr = new XMLHttpRequest();
186240
xhr.open(
187241
config.method || "GET",

src/runtime/background/utils.ts

+56
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,39 @@ export function setXhrHeader(
262262
}
263263
}
264264

265+
export function getFetchHeader(
266+
headerFlag: string,
267+
config: GMSend.XHRDetails
268+
): any {
269+
const headers: { [key: string]: string } = {};
270+
headers[`${headerFlag}-gm-xhr`] = "true";
271+
if (config.headers) {
272+
Object.keys(config.headers).forEach((key) => {
273+
const lowKey = key.toLowerCase();
274+
if (
275+
unsafeHeaders[lowKey] ||
276+
lowKey.startsWith("sec-") ||
277+
lowKey.startsWith("proxy-")
278+
) {
279+
headers[`${headerFlag}-${lowKey}`] = config.headers![key]!;
280+
} else {
281+
// 直接设置header
282+
headers[key] = config.headers![key]!;
283+
}
284+
});
285+
}
286+
if (config.maxRedirects !== undefined) {
287+
headers[`${headerFlag}-max-redirects`] = config.maxRedirects.toString();
288+
}
289+
if (config.cookie) {
290+
headers[`${headerFlag}-cookie`] = config.cookie;
291+
}
292+
if (config.anonymous) {
293+
headers[`${headerFlag}-anonymous`] = "true";
294+
}
295+
return headers;
296+
}
297+
265298
export async function dealXhr(
266299
headerFlag: string,
267300
config: GMSend.XHRDetails,
@@ -329,6 +362,29 @@ export async function dealXhr(
329362
return Promise.resolve(respond);
330363
}
331364

365+
export function dealFetch(
366+
headerFlag: string,
367+
config: GMSend.XHRDetails,
368+
response: Response,
369+
readyState: 0 | 1 | 2 | 3 | 4
370+
) {
371+
const removeXCat = new RegExp(`${headerFlag}-`, "g");
372+
let respHeader = "";
373+
response.headers &&
374+
response.headers.forEach((value, key) => {
375+
respHeader += `${key.replace(removeXCat, "")}: ${value}\n`;
376+
});
377+
const respond: GMTypes.XHRResponse = {
378+
finalUrl: response.url || config.url,
379+
readyState,
380+
status: response.status,
381+
statusText: response.statusText,
382+
responseHeaders: respHeader,
383+
responseType: config.responseType,
384+
};
385+
return respond;
386+
}
387+
332388
export function getIcon(script: Script): string {
333389
return (
334390
(script.metadata.icon && script.metadata.icon[0]) ||

src/runtime/content/content.ts

+14
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,19 @@ export default class ContentRuntime {
127127
return Promise.resolve(url);
128128
}
129129
);
130+
// 处理CAT_fetchDocument
131+
this.contentMessage.setHandler("CAT_fetchDocument", (_action, data) => {
132+
return new Promise((resolve) => {
133+
const xhr = new XMLHttpRequest();
134+
xhr.responseType = "document";
135+
xhr.open("GET", data);
136+
xhr.onload = () => {
137+
resolve({
138+
relatedTarget: xhr.response,
139+
});
140+
};
141+
xhr.send();
142+
});
143+
});
130144
}
131145
}

src/runtime/content/gm_api.ts

+66-7
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,37 @@ export default class GMApi {
213213
return this.message.syncSend("CAT_fetchBlob", url);
214214
}
215215

216+
@GMContext.API()
217+
public CAT_fetchDocument(url: string): Promise<Document | undefined> {
218+
return new Promise((resolve) => {
219+
let el: Document | undefined;
220+
(<MessageContent>this.message).sendCallback(
221+
"CAT_fetchDocument",
222+
url,
223+
(resp) => {
224+
el = <Document>(
225+
(<unknown>(
226+
(<MessageContent>this.message).getAndDelRelatedTarget(
227+
resp.relatedTarget
228+
)
229+
))
230+
);
231+
resolve(el);
232+
}
233+
);
234+
});
235+
}
236+
216237
// 辅助GM_xml发送blob数据
217238
@GMContext.API()
218239
public CAT_createBlobUrl(blob: Blob): Promise<string> {
219240
return this.message.syncSend("CAT_createBlobUrl", blob);
220241
}
221242

222243
// 用于脚本跨域请求,需要@connect domain指定允许的域名
223-
@GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl"] })
244+
@GMContext.API({
245+
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
246+
})
224247
public GM_xmlhttpRequest(details: GMTypes.XHRDetails) {
225248
let connect: Channel;
226249

@@ -300,24 +323,45 @@ export default class GMApi {
300323
}
301324
}
302325

326+
let readerStream: ReadableStream<Uint8Array> | undefined;
327+
let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
303328
// 如果返回类型是arraybuffer或者blob的情况下,需要将返回的数据转化为blob
304329
// 在background通过URL.createObjectURL转化为url,然后在content页读取url获取blob对象
330+
const responseType = details.responseType?.toLocaleLowerCase();
305331
const warpResponse = (old: Function) => {
332+
if (responseType === "stream") {
333+
readerStream = new ReadableStream<Uint8Array>({
334+
start(ctrl) {
335+
controller = ctrl;
336+
},
337+
});
338+
}
306339
return async (xhr: GMTypes.XHRResponse) => {
307340
if (xhr.response) {
308-
const resp = await this.CAT_fetchBlob(<string>xhr.response);
309-
if (details.responseType === "arraybuffer") {
310-
xhr.response = await resp.arrayBuffer();
341+
if (responseType === "document") {
342+
xhr.response = await this.CAT_fetchDocument(<string>xhr.response);
343+
xhr.responseXML = xhr.response;
344+
xhr.responseType = "document";
311345
} else {
312-
xhr.response = resp;
346+
const resp = await this.CAT_fetchBlob(<string>xhr.response);
347+
if (responseType === "arraybuffer") {
348+
xhr.response = await resp.arrayBuffer();
349+
} else {
350+
xhr.response = resp;
351+
}
313352
}
314353
}
354+
if (responseType === "stream") {
355+
xhr.response = readerStream;
356+
}
315357
old(xhr);
316358
};
317359
};
318360
if (
319-
details.responseType?.toLowerCase() === "arraybuffer" ||
320-
details.responseType?.toLocaleLowerCase() === "blob"
361+
responseType === "arraybuffer" ||
362+
responseType === "blob" ||
363+
responseType === "document" ||
364+
responseType === "stream"
321365
) {
322366
if (details.onload) {
323367
details.onload = warpResponse(details.onload);
@@ -328,6 +372,15 @@ export default class GMApi {
328372
if (details.onloadend) {
329373
details.onloadend = warpResponse(details.onloadend);
330374
}
375+
// document类型读取blob,然后在content页转化为document对象
376+
if (responseType === "document") {
377+
param.responseType = "blob";
378+
}
379+
if (responseType === "stream") {
380+
if (details.onloadstart) {
381+
details.onloadstart = warpResponse(details.onloadstart);
382+
}
383+
}
331384
}
332385

333386
connect = this.connect("GM_xmlhttpRequest", [param], (resp: any) => {
@@ -338,6 +391,9 @@ export default class GMApi {
338391
break;
339392
case "onloadend":
340393
details.onloadend && details.onloadend(data);
394+
if (readerStream) {
395+
controller?.close();
396+
}
341397
break;
342398
case "onloadstart":
343399
details.onloadstart && details.onloadstart(data);
@@ -357,6 +413,9 @@ export default class GMApi {
357413
case "onabort":
358414
details.onabort && details.onabort();
359415
break;
416+
case "onstream":
417+
controller?.enqueue(new Uint8Array(resp.data));
418+
break;
360419
default:
361420
LoggerCore.getLogger().warn("GM_xmlhttpRequest resp is error", {
362421
resp,

src/types/main.d.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ declare namespace GMSend {
1717
binary?: boolean;
1818
timeout?: number;
1919
context?: CONTEXT_TYPE;
20-
responseType?: "text" | "arraybuffer" | "blob" | "json";
20+
responseType?:
21+
| "text"
22+
| "arraybuffer"
23+
| "blob"
24+
| "json"
25+
| "document"
26+
| "stream";
2127
overrideMimeType?: string;
2228
anonymous?: boolean;
2329
fetch?: boolean;

src/types/scriptcat.d.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,16 @@ declare namespace GMTypes {
253253
responseHeaders?: string;
254254
status?: number;
255255
statusText?: string;
256-
response?: any;
256+
response?: string | Blob | ArrayBuffer | Document | ReadableStream | null;
257257
responseText?: string;
258258
responseXML?: Document | null;
259-
responseType?: "text" | "arraybuffer" | "blob" | "json";
259+
responseType?:
260+
| "text"
261+
| "arraybuffer"
262+
| "blob"
263+
| "json"
264+
| "document"
265+
| "stream";
260266
}
261267

262268
interface XHRProgress extends XHRResponse {
@@ -280,7 +286,13 @@ declare namespace GMTypes {
280286
binary?: boolean;
281287
timeout?: number;
282288
context?: ContextType;
283-
responseType?: "text" | "arraybuffer" | "blob" | "json";
289+
responseType?:
290+
| "text"
291+
| "arraybuffer"
292+
| "blob"
293+
| "json"
294+
| "document"
295+
| "stream"; // stream 在当前版本是一个较为简陋的实现
284296
overrideMimeType?: string;
285297
anonymous?: boolean;
286298
fetch?: boolean;

0 commit comments

Comments
 (0)