@whitesev/utils
Version:
一个常用的工具库
617 lines (615 loc) • 18.1 kB
JavaScript
// @name ajaxHooker
// @author cxxjackie
// @version 1.4.4
// @updateLog 修复头条、抖音部分站点下this引用错误的问题。
// @supportURL https://bbs.tampermonkey.net.cn/thread-3284-1-1.html
export const AjaxHooker = function () {
return (function () {
"use strict";
const version = "1.4.4";
const hookInst = {
hookFns: [],
filters: [],
};
const win = window.unsafeWindow || document.defaultView || window;
let winAh = win.__ajaxHooker;
const resProto = win.Response.prototype;
const xhrResponses = ["response", "responseText", "responseXML"];
const fetchResponses = ["arrayBuffer", "blob", "formData", "json", "text"];
const fetchInitProps = [
"method",
"headers",
"body",
"mode",
"credentials",
"cache",
"redirect",
"referrer",
"referrerPolicy",
"integrity",
"keepalive",
"signal",
"priority",
];
const xhrAsyncEvents = ["readystatechange", "load", "loadend"];
const getType = {}.toString.call.bind({}.toString);
const getDescriptor = Object.getOwnPropertyDescriptor.bind(Object);
const emptyFn = () => {};
const errorFn = (e) => console.error(e);
function isThenable(obj) {
return (
obj &&
["object", "function"].includes(typeof obj) &&
typeof obj.then === "function"
);
}
function catchError(fn, ...args) {
try {
const result = fn(...args);
if (isThenable(result)) return result.then(null, errorFn);
return result;
} catch (err) {
console.error(err);
}
}
function defineProp(obj, prop, getter, setter) {
Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
get: getter,
set: setter,
});
}
function readonly(obj, prop, value = obj[prop]) {
defineProp(obj, prop, () => value, emptyFn);
}
function writable(obj, prop, value = obj[prop]) {
Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
writable: true,
value: value,
});
}
function parseHeaders(obj) {
const headers = {};
switch (getType(obj)) {
case "[object String]":
for (const line of obj.trim().split(/[\r\n]+/)) {
const [header, value] = line.split(/\s*:\s*/);
if (!header) break;
const lheader = header.toLowerCase();
headers[lheader] =
lheader in headers ? `${headers[lheader]}, ${value}` : value;
}
break;
case "[object Headers]":
for (const [key, val] of obj) {
headers[key] = val;
}
break;
case "[object Object]":
return { ...obj };
}
return headers;
}
function stopImmediatePropagation() {
this.ajaxHooker_isStopped = true;
}
class SyncThenable {
then(fn) {
fn && fn();
return new SyncThenable();
}
}
class AHRequest {
constructor(request) {
this.request = request;
this.requestClone = { ...this.request };
}
shouldFilter(filters) {
const { type, url, method, async } = this.request;
return (
filters.length &&
!filters.find((obj) => {
switch (true) {
case obj.type && obj.type !== type:
case getType(obj.url) === "[object String]" &&
!url.includes(obj.url):
case getType(obj.url) === "[object RegExp]" && !obj.url.test(url):
case obj.method &&
obj.method.toUpperCase() !== method.toUpperCase():
case "async" in obj && obj.async !== async:
return false;
}
return true;
})
);
}
waitForRequestKeys() {
const requestKeys = ["url", "method", "abort", "headers", "data"];
if (!this.request.async) {
win.__ajaxHooker.hookInsts.forEach(({ hookFns, filters }) => {
if (this.shouldFilter(filters)) return;
hookFns.forEach((fn) => {
if (getType(fn) === "[object Function]")
catchError(fn, this.request);
});
requestKeys.forEach((key) => {
if (isThenable(this.request[key]))
this.request[key] = this.requestClone[key];
});
});
return new SyncThenable();
}
const promises = [];
win.__ajaxHooker.hookInsts.forEach(({ hookFns, filters }) => {
if (this.shouldFilter(filters)) return;
promises.push(
Promise.all(hookFns.map((fn) => catchError(fn, this.request))).then(
() =>
Promise.all(
requestKeys.map((key) =>
Promise.resolve(this.request[key]).then(
(val) => (this.request[key] = val),
() => (this.request[key] = this.requestClone[key])
)
)
)
)
);
});
return Promise.all(promises);
}
waitForResponseKeys(response) {
const responseKeys =
this.request.type === "xhr" ? xhrResponses : fetchResponses;
if (!this.request.async) {
if (getType(this.request.response) === "[object Function]") {
catchError(this.request.response, response);
responseKeys.forEach((key) => {
if (
"get" in getDescriptor(response, key) ||
isThenable(response[key])
) {
delete response[key];
}
});
}
return new SyncThenable();
}
return Promise.resolve(
catchError(this.request.response, response)
).then(() =>
Promise.all(
responseKeys.map((key) => {
const descriptor = getDescriptor(response, key);
if (descriptor && "value" in descriptor) {
return Promise.resolve(descriptor.value).then(
(val) => (response[key] = val),
() => delete response[key]
);
} else {
delete response[key];
}
})
)
);
}
}
const proxyHandler = {
get(target, prop) {
const descriptor = getDescriptor(target, prop);
if (
descriptor &&
!descriptor.configurable &&
!descriptor.writable &&
!descriptor.get
)
return target[prop];
const ah = target.__ajaxHooker;
if (ah && ah.proxyProps) {
if (prop in ah.proxyProps) {
const pDescriptor = ah.proxyProps[prop];
if ("get" in pDescriptor) return pDescriptor.get();
if (typeof pDescriptor.value === "function")
return pDescriptor.value.bind(ah);
return pDescriptor.value;
}
if (typeof target[prop] === "function")
return target[prop].bind(target);
}
return target[prop];
},
set(target, prop, value) {
const descriptor = getDescriptor(target, prop);
if (
descriptor &&
!descriptor.configurable &&
!descriptor.writable &&
!descriptor.set
)
return true;
const ah = target.__ajaxHooker;
if (ah && ah.proxyProps && prop in ah.proxyProps) {
const pDescriptor = ah.proxyProps[prop];
pDescriptor.set
? pDescriptor.set(value)
: (pDescriptor.value = value);
} else {
target[prop] = value;
}
return true;
},
};
class XhrHooker {
constructor(xhr) {
const ah = this;
Object.assign(ah, {
originalXhr: xhr,
proxyXhr: new Proxy(xhr, proxyHandler),
resThenable: new SyncThenable(),
proxyProps: {},
proxyEvents: {},
});
xhr.addEventListener("readystatechange", (e) => {
if (
ah.proxyXhr.readyState === 4 &&
ah.request &&
typeof ah.request.response === "function"
) {
const response = {
finalUrl: ah.proxyXhr.responseURL,
status: ah.proxyXhr.status,
responseHeaders: parseHeaders(
ah.proxyXhr.getAllResponseHeaders()
),
};
const tempValues = {};
for (const key of xhrResponses) {
try {
tempValues[key] = ah.originalXhr[key];
} catch (err) {}
defineProp(
response,
key,
() => {
return (response[key] = tempValues[key]);
},
(val) => {
delete response[key];
response[key] = val;
}
);
}
ah.resThenable = new AHRequest(ah.request)
.waitForResponseKeys(response)
.then(() => {
for (const key of xhrResponses) {
ah.proxyProps[key] = {
get: () => {
if (!(key in response)) response[key] = tempValues[key];
return response[key];
},
};
}
});
}
ah.dispatchEvent(e);
});
xhr.addEventListener("load", (e) => ah.dispatchEvent(e));
xhr.addEventListener("loadend", (e) => ah.dispatchEvent(e));
for (const evt of xhrAsyncEvents) {
const onEvt = "on" + evt;
ah.proxyProps[onEvt] = {
get: () => ah.proxyEvents[onEvt] || null,
set: (val) => ah.addEvent(onEvt, val),
};
}
for (const method of [
"setRequestHeader",
"addEventListener",
"removeEventListener",
"open",
"send",
]) {
ah.proxyProps[method] = { value: ah[method] };
}
}
toJSON() {} // Converting circular structure to JSON
addEvent(type, event) {
if (type.startsWith("on")) {
this.proxyEvents[type] = typeof event === "function" ? event : null;
} else {
if (typeof event === "object" && event !== null)
event = event.handleEvent;
if (typeof event !== "function") return;
this.proxyEvents[type] = this.proxyEvents[type] || new Set();
this.proxyEvents[type].add(event);
}
}
removeEvent(type, event) {
if (type.startsWith("on")) {
this.proxyEvents[type] = null;
} else {
if (typeof event === "object" && event !== null)
event = event.handleEvent;
this.proxyEvents[type] && this.proxyEvents[type].delete(event);
}
}
dispatchEvent(e) {
e.stopImmediatePropagation = stopImmediatePropagation;
defineProp(e, "target", () => this.proxyXhr);
defineProp(e, "currentTarget", () => this.proxyXhr);
this.proxyEvents[e.type] &&
this.proxyEvents[e.type].forEach((fn) => {
this.resThenable.then(
() => !e.ajaxHooker_isStopped && fn.call(this.proxyXhr, e)
);
});
if (e.ajaxHooker_isStopped) return;
const onEvent = this.proxyEvents["on" + e.type];
onEvent && this.resThenable.then(onEvent.bind(this.proxyXhr, e));
}
setRequestHeader(header, value) {
this.originalXhr.setRequestHeader(header, value);
if (!this.request) return;
const headers = this.request.headers;
headers[header] =
header in headers ? `${headers[header]}, ${value}` : value;
}
addEventListener(...args) {
if (xhrAsyncEvents.includes(args[0])) {
this.addEvent(args[0], args[1]);
} else {
this.originalXhr.addEventListener(...args);
}
}
removeEventListener(...args) {
if (xhrAsyncEvents.includes(args[0])) {
this.removeEvent(args[0], args[1]);
} else {
this.originalXhr.removeEventListener(...args);
}
}
open(method, url, async = true, ...args) {
this.request = {
type: "xhr",
url: url.toString(),
method: method.toUpperCase(),
abort: false,
headers: {},
data: null,
response: null,
async: !!async,
};
this.openArgs = args;
this.resThenable = new SyncThenable();
[
"responseURL",
"readyState",
"status",
"statusText",
...xhrResponses,
].forEach((key) => {
delete this.proxyProps[key];
});
return this.originalXhr.open(method, url, async, ...args);
}
send(data) {
const ah = this;
const xhr = ah.originalXhr;
const request = ah.request;
if (!request) return xhr.send(data);
request.data = data;
new AHRequest(request).waitForRequestKeys().then(() => {
if (request.abort) {
if (typeof request.response === "function") {
Object.assign(ah.proxyProps, {
responseURL: { value: request.url },
readyState: { value: 4 },
status: { value: 200 },
statusText: { value: "OK" },
});
xhrAsyncEvents.forEach((evt) =>
xhr.dispatchEvent(new Event(evt))
);
}
} else {
xhr.open(
request.method,
request.url,
request.async,
...ah.openArgs
);
for (const header in request.headers) {
xhr.setRequestHeader(header, request.headers[header]);
}
xhr.send(request.data);
}
});
}
}
function fakeXHR() {
const xhr = new winAh.realXHR();
if ("__ajaxHooker" in xhr)
console.warn("检测到不同版本的ajaxHooker,可能发生冲突!");
xhr.__ajaxHooker = new XhrHooker(xhr);
return xhr.__ajaxHooker.proxyXhr;
}
fakeXHR.prototype = win.XMLHttpRequest.prototype;
Object.keys(win.XMLHttpRequest).forEach(
(key) => (fakeXHR[key] = win.XMLHttpRequest[key])
);
function fakeFetch(url, options = {}) {
if (!url) return winAh.realFetch.call(win, url, options);
return new Promise(async (resolve, reject) => {
const init = {};
if (getType(url) === "[object Request]") {
for (const prop of fetchInitProps) init[prop] = url[prop];
if (url.body) init.body = await url.arrayBuffer();
url = url.url;
}
url = url.toString();
Object.assign(init, options);
init.method = init.method || "GET";
init.headers = init.headers || {};
const request = {
type: "fetch",
url: url,
method: init.method.toUpperCase(),
abort: false,
headers: parseHeaders(init.headers),
data: init.body,
response: null,
async: true,
};
const req = new AHRequest(request);
await req.waitForRequestKeys();
if (request.abort) {
if (typeof request.response === "function") {
const response = {
finalUrl: request.url,
status: 200,
responseHeaders: {},
};
await req.waitForResponseKeys(response);
const key = fetchResponses.find((k) => k in response);
let val = response[key];
if (key === "json" && typeof val === "object") {
val = catchError(JSON.stringify.bind(JSON), val);
}
const res = new Response(val, {
status: 200,
statusText: "OK",
});
defineProp(res, "type", () => "basic");
defineProp(res, "url", () => request.url);
resolve(res);
} else {
reject(new DOMException("aborted", "AbortError"));
}
return;
}
init.method = request.method;
init.headers = request.headers;
init.body = request.data;
winAh.realFetch.call(win, request.url, init).then((res) => {
if (typeof request.response === "function") {
const response = {
finalUrl: res.url,
status: res.status,
responseHeaders: parseHeaders(res.headers),
};
fetchResponses.forEach(
(key) =>
(res[key] = function () {
if (key in response) return Promise.resolve(response[key]);
return resProto[key].call(this).then((val) => {
response[key] = val;
return req
.waitForResponseKeys(response)
.then(() => (key in response ? response[key] : val));
});
})
);
}
resolve(res);
}, reject);
});
}
function fakeFetchClone() {
const descriptors = Object.getOwnPropertyDescriptors(this);
const res = winAh.realFetchClone.call(this);
Object.defineProperties(res, descriptors);
return res;
}
winAh = win.__ajaxHooker = winAh || {
version,
fakeXHR,
fakeFetch,
fakeFetchClone,
realXHR: win.XMLHttpRequest,
realFetch: win.fetch,
realFetchClone: resProto.clone,
hookInsts: new Set(),
};
if (winAh.version !== version)
console.warn("检测到不同版本的ajaxHooker,可能发生冲突!");
win.XMLHttpRequest = winAh.fakeXHR;
win.fetch = winAh.fakeFetch;
resProto.clone = winAh.fakeFetchClone;
winAh.hookInsts.add(hookInst);
// 针对头条、抖音 secsdk.umd.js 的兼容性处理
class AHFunction {
call(thisArg, ...args) {
if (
thisArg &&
thisArg.__ajaxHooker &&
thisArg.__ajaxHooker.proxyXhr === thisArg
) {
thisArg = thisArg.__ajaxHooker.originalXhr;
}
return Reflect.apply(this, thisArg, args);
}
apply(thisArg, args) {
if (
thisArg &&
thisArg.__ajaxHooker &&
thisArg.__ajaxHooker.proxyXhr === thisArg
) {
thisArg = thisArg.__ajaxHooker.originalXhr;
}
return Reflect.apply(this, thisArg, args || []);
}
}
function hookSecsdk(csrf) {
Object.setPrototypeOf(
csrf.nativeXMLHttpRequestSetRequestHeader,
AHFunction.prototype
);
Object.setPrototypeOf(
csrf.nativeXMLHttpRequestOpen,
AHFunction.prototype
);
Object.setPrototypeOf(
csrf.nativeXMLHttpRequestSend,
AHFunction.prototype
);
}
if (win.secsdk) {
if (win.secsdk.csrf && win.secsdk.csrf.nativeXMLHttpRequestOpen)
hookSecsdk(win.secsdk.csrf);
} else {
defineProp(win, "secsdk", emptyFn, (secsdk) => {
delete win.secsdk;
win.secsdk = secsdk;
defineProp(secsdk, "csrf", emptyFn, (csrf) => {
delete secsdk.csrf;
secsdk.csrf = csrf;
if (csrf.nativeXMLHttpRequestOpen) hookSecsdk(csrf);
});
});
}
return {
hook: (fn) => hookInst.hookFns.push(fn),
filter: (arr) => {
if (Array.isArray(arr)) hookInst.filters = arr;
},
protect: () => {
readonly(win, "XMLHttpRequest", winAh.fakeXHR);
readonly(win, "fetch", winAh.fakeFetch);
readonly(resProto, "clone", winAh.fakeFetchClone);
},
unhook: () => {
winAh.hookInsts.delete(hookInst);
if (!winAh.hookInsts.size) {
writable(win, "XMLHttpRequest", winAh.realXHR);
writable(win, "fetch", winAh.realFetch);
writable(resProto, "clone", winAh.realFetchClone);
delete win.__ajaxHooker;
}
},
};
})();
};