@whitesev/utils
Version:
一个常用的工具库
1,028 lines (1,010 loc) • 397 kB
JavaScript
define((function () { 'use strict';
const createCache = (lastNumberWeakMap) => {
return (collection, nextNumber) => {
lastNumberWeakMap.set(collection, nextNumber);
return nextNumber;
};
};
/*
* The value of the constant Number.MAX_SAFE_INTEGER equals (2 ** 53 - 1) but it
* is fairly new.
*/
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER === undefined ? 9007199254740991 : Number.MAX_SAFE_INTEGER;
const TWO_TO_THE_POWER_OF_TWENTY_NINE = 536870912;
const TWO_TO_THE_POWER_OF_THIRTY = TWO_TO_THE_POWER_OF_TWENTY_NINE * 2;
const createGenerateUniqueNumber = (cache, lastNumberWeakMap) => {
return (collection) => {
const lastNumber = lastNumberWeakMap.get(collection);
/*
* Let's try the cheapest algorithm first. It might fail to produce a new
* number, but it is so cheap that it is okay to take the risk. Just
* increase the last number by one or reset it to 0 if we reached the upper
* bound of SMIs (which stands for small integers). When the last number is
* unknown it is assumed that the collection contains zero based consecutive
* numbers.
*/
let nextNumber = lastNumber === undefined ? collection.size : lastNumber < TWO_TO_THE_POWER_OF_THIRTY ? lastNumber + 1 : 0;
if (!collection.has(nextNumber)) {
return cache(collection, nextNumber);
}
/*
* If there are less than half of 2 ** 30 numbers stored in the collection,
* the chance to generate a new random number in the range from 0 to 2 ** 30
* is at least 50%. It's benifitial to use only SMIs because they perform
* much better in any environment based on V8.
*/
if (collection.size < TWO_TO_THE_POWER_OF_TWENTY_NINE) {
while (collection.has(nextNumber)) {
nextNumber = Math.floor(Math.random() * TWO_TO_THE_POWER_OF_THIRTY);
}
return cache(collection, nextNumber);
}
// Quickly check if there is a theoretical chance to generate a new number.
if (collection.size > MAX_SAFE_INTEGER) {
throw new Error('Congratulations, you created a collection of unique numbers which uses all available integers!');
}
// Otherwise use the full scale of safely usable integers.
while (collection.has(nextNumber)) {
nextNumber = Math.floor(Math.random() * MAX_SAFE_INTEGER);
}
return cache(collection, nextNumber);
};
};
const LAST_NUMBER_WEAK_MAP = new WeakMap();
const cache = createCache(LAST_NUMBER_WEAK_MAP);
const generateUniqueNumber = createGenerateUniqueNumber(cache, LAST_NUMBER_WEAK_MAP);
const isMessagePort = (sender) => {
return typeof sender.start === 'function';
};
const PORT_MAP = new WeakMap();
const extendBrokerImplementation = (partialBrokerImplementation) => ({
...partialBrokerImplementation,
connect: ({ call }) => {
return async () => {
const { port1, port2 } = new MessageChannel();
const portId = await call('connect', { port: port1 }, [port1]);
PORT_MAP.set(port2, portId);
return port2;
};
},
disconnect: ({ call }) => {
return async (port) => {
const portId = PORT_MAP.get(port);
if (portId === undefined) {
throw new Error('The given port is not connected.');
}
await call('disconnect', { portId });
};
},
isSupported: ({ call }) => {
return () => call('isSupported');
}
});
const ONGOING_REQUESTS = new WeakMap();
const createOrGetOngoingRequests = (sender) => {
if (ONGOING_REQUESTS.has(sender)) {
// @todo TypeScript needs to be convinced that has() works as expected.
return ONGOING_REQUESTS.get(sender);
}
const ongoingRequests = new Map();
ONGOING_REQUESTS.set(sender, ongoingRequests);
return ongoingRequests;
};
const createBroker = (brokerImplementation) => {
const fullBrokerImplementation = extendBrokerImplementation(brokerImplementation);
return (sender) => {
const ongoingRequests = createOrGetOngoingRequests(sender);
sender.addEventListener('message', (({ data: message }) => {
const { id } = message;
if (id !== null && ongoingRequests.has(id)) {
const { reject, resolve } = ongoingRequests.get(id);
ongoingRequests.delete(id);
if (message.error === undefined) {
resolve(message.result);
}
else {
reject(new Error(message.error.message));
}
}
}));
if (isMessagePort(sender)) {
sender.start();
}
const call = (method, params = null, transferables = []) => {
return new Promise((resolve, reject) => {
const id = generateUniqueNumber(ongoingRequests);
ongoingRequests.set(id, { reject, resolve });
if (params === null) {
sender.postMessage({ id, method }, transferables);
}
else {
sender.postMessage({ id, method, params }, transferables);
}
});
};
const notify = (method, params, transferables = []) => {
sender.postMessage({ id: null, method, params }, transferables);
};
let functions = {};
for (const [key, handler] of Object.entries(fullBrokerImplementation)) {
functions = { ...functions, [key]: handler({ call, notify }) };
}
return { ...functions };
};
};
const createClearIntervalFactory = (scheduledIntervalsState) => (clear) => (timerId) => {
if (typeof scheduledIntervalsState.get(timerId) === 'symbol') {
scheduledIntervalsState.set(timerId, null);
clear(timerId).then(() => {
scheduledIntervalsState.delete(timerId);
});
}
};
const createClearTimeoutFactory = (scheduledTimeoutsState) => (clear) => (timerId) => {
if (typeof scheduledTimeoutsState.get(timerId) === 'symbol') {
scheduledTimeoutsState.set(timerId, null);
clear(timerId).then(() => {
scheduledTimeoutsState.delete(timerId);
});
}
};
const createSetIntervalFactory = (generateUniqueNumber, scheduledIntervalsState) => (set) => (func, delay = 0, ...args) => {
const symbol = Symbol();
const timerId = generateUniqueNumber(scheduledIntervalsState);
scheduledIntervalsState.set(timerId, symbol);
const schedule = () => set(delay, timerId).then(() => {
const state = scheduledIntervalsState.get(timerId);
if (state === undefined) {
throw new Error('The timer is in an undefined state.');
}
if (state === symbol) {
func(...args);
// Doublecheck if the interval should still be rescheduled because it could have been cleared inside of func().
if (scheduledIntervalsState.get(timerId) === symbol) {
schedule();
}
}
});
schedule();
return timerId;
};
const createSetTimeoutFactory = (generateUniqueNumber, scheduledTimeoutsState) => (set) => (func, delay = 0, ...args) => {
const symbol = Symbol();
const timerId = generateUniqueNumber(scheduledTimeoutsState);
scheduledTimeoutsState.set(timerId, symbol);
set(delay, timerId).then(() => {
const state = scheduledTimeoutsState.get(timerId);
if (state === undefined) {
throw new Error('The timer is in an undefined state.');
}
if (state === symbol) {
// A timeout can be savely deleted because it is only called once.
scheduledTimeoutsState.delete(timerId);
func(...args);
}
});
return timerId;
};
// Prefilling the Maps with a function indexed by zero is necessary to be compliant with the specification.
const scheduledIntervalsState = new Map([[0, null]]); // tslint:disable-line no-empty
const scheduledTimeoutsState = new Map([[0, null]]); // tslint:disable-line no-empty
const createClearInterval = createClearIntervalFactory(scheduledIntervalsState);
const createClearTimeout = createClearTimeoutFactory(scheduledTimeoutsState);
const createSetInterval = createSetIntervalFactory(generateUniqueNumber, scheduledIntervalsState);
const createSetTimeout = createSetTimeoutFactory(generateUniqueNumber, scheduledTimeoutsState);
const wrap = createBroker({
clearInterval: ({ call }) => createClearInterval((timerId) => call('clear', { timerId, timerType: 'interval' })),
clearTimeout: ({ call }) => createClearTimeout((timerId) => call('clear', { timerId, timerType: 'timeout' })),
setInterval: ({ call }) => createSetInterval((delay, timerId) => call('set', { delay, now: performance.timeOrigin + performance.now(), timerId, timerType: 'interval' })),
setTimeout: ({ call }) => createSetTimeout((delay, timerId) => call('set', { delay, now: performance.timeOrigin + performance.now(), timerId, timerType: 'timeout' }))
});
const load = (url) => {
const worker = new Worker(url);
return wrap(worker);
};
const createLoadOrReturnBroker = (loadBroker, worker) => {
let broker = null;
return () => {
if (broker !== null) {
return broker;
}
const blob = new Blob([worker], { type: 'application/javascript; charset=utf-8' });
const url = URL.createObjectURL(blob);
broker = loadBroker(url);
// Bug #1: Edge up until v18 didn't like the URL to be revoked directly.
setTimeout(() => URL.revokeObjectURL(url));
return broker;
};
};
// This is the minified and stringified code of the worker-timers-worker package.
const worker = `(()=>{var e={455:function(e,t){!function(e){"use strict";var t=function(e){return function(t){var r=e(t);return t.add(r),r}},r=function(e){return function(t,r){return e.set(t,r),r}},n=void 0===Number.MAX_SAFE_INTEGER?9007199254740991:Number.MAX_SAFE_INTEGER,o=536870912,s=2*o,a=function(e,t){return function(r){var a=t.get(r),i=void 0===a?r.size:a<s?a+1:0;if(!r.has(i))return e(r,i);if(r.size<o){for(;r.has(i);)i=Math.floor(Math.random()*s);return e(r,i)}if(r.size>n)throw new Error("Congratulations, you created a collection of unique numbers which uses all available integers!");for(;r.has(i);)i=Math.floor(Math.random()*n);return e(r,i)}},i=new WeakMap,u=r(i),c=a(u,i),l=t(c);e.addUniqueNumber=l,e.generateUniqueNumber=c}(t)}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var s=t[n]={exports:{}};return e[n].call(s.exports,s,s.exports,r),s.exports}(()=>{"use strict";const e=-32603,t=-32602,n=-32601,o=(e,t)=>Object.assign(new Error(e),{status:t}),s=t=>o('The handler of the method called "'.concat(t,'" returned an unexpected result.'),e),a=(t,r)=>async({data:{id:a,method:i,params:u}})=>{const c=r[i];try{if(void 0===c)throw(e=>o('The requested method called "'.concat(e,'" is not supported.'),n))(i);const r=void 0===u?c():c(u);if(void 0===r)throw(t=>o('The handler of the method called "'.concat(t,'" returned no required result.'),e))(i);const l=r instanceof Promise?await r:r;if(null===a){if(void 0!==l.result)throw s(i)}else{if(void 0===l.result)throw s(i);const{result:e,transferables:r=[]}=l;t.postMessage({id:a,result:e},r)}}catch(e){const{message:r,status:n=-32603}=e;t.postMessage({error:{code:n,message:r},id:a})}};var i=r(455);const u=new Map,c=(e,r,n)=>({...r,connect:({port:t})=>{t.start();const n=e(t,r),o=(0,i.generateUniqueNumber)(u);return u.set(o,()=>{n(),t.close(),u.delete(o)}),{result:o}},disconnect:({portId:e})=>{const r=u.get(e);if(void 0===r)throw(e=>o('The specified parameter called "portId" with the given value "'.concat(e,'" does not identify a port connected to this worker.'),t))(e);return r(),{result:null}},isSupported:async()=>{if(await new Promise(e=>{const t=new ArrayBuffer(0),{port1:r,port2:n}=new MessageChannel;r.onmessage=({data:t})=>e(null!==t),n.postMessage(t,[t])})){const e=n();return{result:e instanceof Promise?await e:e}}return{result:!1}}}),l=(e,t,r=()=>!0)=>{const n=c(l,t,r),o=a(e,n);return e.addEventListener("message",o),()=>e.removeEventListener("message",o)},d=(e,t)=>r=>{const n=t.get(r);if(void 0===n)return Promise.resolve(!1);const[o,s]=n;return e(o),t.delete(r),s(!1),Promise.resolve(!0)},f=(e,t,r,n)=>(o,s,a)=>{const i=o+s-t.timeOrigin,u=i-t.now();return new Promise(t=>{e.set(a,[r(n,u,i,e,t,a),t])})},m=new Map,h=d(globalThis.clearTimeout,m),p=new Map,v=d(globalThis.clearTimeout,p),w=((e,t)=>{const r=(n,o,s,a)=>{const i=n-e.now();i>0?o.set(a,[t(r,i,n,o,s,a),s]):(o.delete(a),s(!0))};return r})(performance,globalThis.setTimeout),g=f(m,performance,globalThis.setTimeout,w),T=f(p,performance,globalThis.setTimeout,w);l(self,{clear:async({timerId:e,timerType:t})=>({result:await("interval"===t?h(e):v(e))}),set:async({delay:e,now:t,timerId:r,timerType:n})=>({result:await("interval"===n?g:T)(e,t,r)})})})()})();`; // tslint:disable-line:max-line-length
const loadOrReturnBroker = createLoadOrReturnBroker(load, worker);
const clearInterval = (timerId) => loadOrReturnBroker().clearInterval(timerId);
const clearTimeout$1 = (timerId) => loadOrReturnBroker().clearTimeout(timerId);
const setInterval = (...args) => loadOrReturnBroker().setInterval(...args);
const setTimeout$1 = (...args) => loadOrReturnBroker().setTimeout(...args);
const version = "2.9.10";
/* eslint-disable */
// ==UserScript==
// @name ajaxHooker
// @author cxxjackie
// @version 1.4.8
// @supportURL https://bbs.tampermonkey.net.cn/thread-3284-1-1.html
// @license GNU LGPL-3.0
// ==/UserScript==
const ajaxHooker = function () {
const version = "1.4.8";
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 xhrExtraProps = ["responseType", "timeout", "withCredentials"];
const fetchExtraProps = [
"cache",
"credentials",
"integrity",
"keepalive",
"mode",
"priority",
"redirect",
"referrer",
"referrerPolicy",
"signal",
];
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 (!value)
continue;
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 };
}
_recoverRequestKey(key) {
if (key in this.requestClone)
this.request[key] = this.requestClone[key];
else
delete this.request[key];
}
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() {
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);
});
for (const key in this.request) {
if (isThenable(this.request[key]))
this._recoverRequestKey(key);
}
});
return new SyncThenable();
}
const promises = [];
const ignoreKeys = new Set(["type", "async", "response"]);
win.__ajaxHooker.hookInsts.forEach(({ hookFns, filters }) => {
if (this.shouldFilter(filters))
return;
promises.push(Promise.all(hookFns.map((fn) => catchError(fn, this.request))).then(() => {
const requestKeys = [];
for (const key in this.request)
!ignoreKeys.has(key) && requestKeys.push(key);
return Promise.all(requestKeys.map((key) => Promise.resolve(this.request[key]).then((val) => (this.request[key] = val), () => this._recoverRequestKey(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);
defineProp(e, "srcElement", () => 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]);
}
for (const prop of xhrExtraProps) {
if (prop in request)
xhr[prop] = request[prop];
}
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]") {
init.method = url.method;
init.headers = url.headers;
if (url.body)
init.body = await url.arrayBuffer();
for (const prop of fetchExtraProps)
init[prop] = url[prop];
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;
for (const prop of fetchExtraProps) {
if (prop in request)
init[prop] = request[prop];
}
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),
};
if (res.ok) {
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));
});
}));
}
else {
catchError(request.response, response);
}
}
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 extends Function {
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;
}
},
};
};
// ==UserScript==
// @name ajaxHooker
// @author cxxjackie
// @version 1.2.4
// @supportURL https://bbs.tampermonkey.net.cn/thread-3284-1-1.html
// ==/UserScript==
const AjaxHooker1_2_4 = function () {
return (function () {
const win = window.unsafeWindow || document.defaultView || window;
const hookFns = [];
const realXhr = win.XMLHttpRequest;
const resProto = win.Response.prototype;
const toString = Object.prototype.toString;
const realFetch = win.fetch;
const xhrResponses = ["response", "responseText", "responseXML"];
const fetchResponses = ["arrayBuffer", "blob", "formData", "json", "text"];
const xhrAsyncEvents = ["readystatechange", "load", "loadend"];
let filter;
function emptyFn() { }
function errorFn(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 toFilterObj(obj) {
return {
type: obj.type,
url: obj.url,
method: obj.method && obj.method.toUpperCase(),
};
}
function shouldFilter(type, url, method) {
return (filter &&
!filter.find((obj) => (!obj.type || obj.type === type) &&
(!obj.url ||
(toString.call(obj.url) === "[object String]"
? url.includes(obj.url)
: obj.url.test(url))) &&
(!obj.method || obj.method === method.toUpperCase())));
}
function lookupGetter(obj, prop) {
let getter;
let proto = obj;
while (proto) {
const descriptor = Object.getOwnPropertyDescriptor(proto, prop);
getter = descriptor && descriptor.get;
if (getter)
break;
proto = Object.getPrototypeOf(proto);
}
return getter ? getter.bind(obj) : emptyFn;
}
function waitForHookFns(request) {
return Promise.all(hookFns.map((fn) => Promise.resolve(fn(request)).then(emptyFn, errorFn)));
}
function waitForRequestKeys(request, requestClone) {
return Promise.all(["url", "method", "abort", "headers", "data"].map((key) => {
return Promise.resolve(request[key]).then((val) => (request[key] = val), () => (request[key] = requestClone[key]));
}));
}
function fakeEventSIP() {
this.ajaxHooker_stopped = true;
}
function xhrDelegateEvent(e) {
const xhr = e.target;
e.stopImmediatePropagation = fakeEventSIP;
xhr.__ajaxHooker.hookedEvents[e.type].forEach((fn) => !e.ajaxHooker_stopped && fn.call(xhr, e));
const onEvent = xhr.__ajaxHooker.hookedEvents["on" + e.type];
typeof onEvent === "function" && onEvent.call(xhr, e);
}
function xhrReadyStateChange(e) {
if (e.target.readyState === 4) {
e.target.dispatchEvent(new CustomEvent("ajaxHooker_responseReady", { detail: e }));
}
else {
e.target.__ajaxHooker.delegateEvent(e);
}
}
function xhrLoadAndLoadend(e) {
e.target.__ajaxHooker.delegateEvent(e);
}
function fakeXhrOpen(method, url, ...args) {
const ah = this.__ajaxHooker;
ah.url = url.toString();
ah.method = method.toUpperCase();
ah.openArgs = args;
ah.headers = {};
return ah.originalMethods.open(method, url, ...args);
}
function fakeXhr() {
const xhr = new realXhr();
let ah = xhr.__ajaxHooker;
if (!ah) {
ah = xhr.__ajaxHooker = {
headers: {},
hookedEvents: {
readystatechange: new Set(),
load: new Set(),
loadend: new Set(),
},
delegateEvent: xhrDelegateEvent,
originalGetters: {},
originalMethods: {},
};
xhr.addEventListener("readystatechange", xhrReadyStateChange);
xhr.addEventListener("load", xhrLoadAndLoadend);
xhr.addEventListener("loadend", xhrLoadAndLoadend);
for (const key of xhrResponses) {
ah.originalGetters[key] = lookupGetter(xhr, key);
}
for (const method of [
"open",
"setRequestHeader",
"addEventListener",
"removeEventListener",
]) {
ah.originalMethods[method] = xhr[method].bind(xhr);
}
xhr.open = fakeXhrOpen;
xhr.setRequestHeader = (header, value) => {
ah.originalMethods.setRequestHeader(header, value);
if (xhr.readyState === 1) {
if (ah.headers[header]) {
ah.headers[header] += ", " + value;
}
else {
ah.headers[header] = value;
}
}
};
xhr.addEventListener = function (...args) {
if (xhrAsyncEvents.includes(args[0])) {
ah.hookedEvents[args[0]].add(args[1]);
}
else {
ah.originalMethods.addEventListener(...args);
}
};
xhr.removeEventListener = function (...args) {
if (xhrAsyncEvents.includes(args[0])) {
ah.hookedEvents[args[0]].delete(args[1]);
}
else {
ah.originalMethods.removeEventListener(...args);
}
};
xhrAsyncEvents.forEach((evt) => {
const onEvt = "on" + evt;
defineProp(xhr, onEvt, () => {
return ah.hookedEvents[onEvt] || null;
}, (val) => {
ah.hookedEvents[onEvt] = typeof val === "function" ? val : null;
});
});
}
const realSend = xhr.send.bind(xhr);
xhr.send = function (data) {
if (xhr.readyState !== 1)
return realSend(data);
ah.delegateEvent = xhrDelegateEvent;
xhrResponses.forEach((prop) => {
delete xhr[prop]; // delete descriptor
});
if (shouldFilter("xhr", ah.url, ah.method)) {
xhr.addEventListener("ajaxHooker_responseReady", (e) => {
ah.delegateEvent(e.detail);
});
return realSend(data);
}
try {
const request = {
type: "xhr",
url: ah.url,
method: ah.method,
abort: false,
headers: ah.headers,
data: data,
response: null,
};
const requestClone = { ...request };
waitForHookFns(request).then(() => {
waitForRequestKeys(request, requestClone).then(() => {
if (request.abort)
return;
ah.originalMethods.open(request.method, request.url, ...ah.openArgs);
for (const header in request.headers) {
ah.originalMethods.setRequestHeader(header, request.headers[header]);
}
data = request.data;
xhr.addEventListener("ajaxHooker_responseReady", (e) => {
try {
if (typeof request.response === "function") {
const arg = {
finalUrl: xhr.responseURL,
status: xhr.status,
responseHeaders: {},
};
for (const line of xhr
.getAllResponseHeaders()
.trim()
.split(/[\r\n]+/)) {
const parts = line.split(/:\s*/);
if (parts.length === 2