Dette scriptet burde ikke installeres direkte. Det er et bibliotek for andre script å inkludere med det nye metadirektivet // @require https://update.greasyfork.org/scripts/455943/1270016/ajaxHooker.js
// ==UserScript==
// @name ajaxHooker
// @author cxxjackie
// @version 1.3.3
// @supportURL https://bbs.tampermonkey.net.cn/thread-3284-1-1.html
// ==/UserScript==
var ajaxHooker = function() {
'use strict';
const win = window.unsafeWindow || document.defaultView || window;
const toString = Object.prototype.toString;
const getDescriptor = Object.getOwnPropertyDescriptor;
const hookFns = [];
const realXhr = win.XMLHttpRequest;
const realFetch = win.fetch;
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'];
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 shouldFilter(type, url, method, async) {
return filter && !filter.find(obj => {
switch (true) {
case obj.type && obj.type !== type:
case toString.call(obj.url) === '[object String]' && !url.includes(obj.url):
case toString.call(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;
});
}
function parseHeaders(obj) {
const headers = {};
switch (toString.call(obj)) {
case '[object String]':
for (const line of obj.trim().split(/[\r\n]+/)) {
const parts = line.split(/\s*:\s*/);
if (parts.length !== 2) continue;
const lheader = parts[0].toLowerCase();
if (lheader in headers) {
headers[lheader] += ', ' + parts[1];
} else {
headers[lheader] = parts[1];
}
}
return headers;
case '[object Headers]':
for (const [key, val] of obj) {
headers[key] = val;
}
return headers;
case '[object Object]':
return {...obj};
default:
return headers;
}
}
class AHRequest {
constructor(request) {
this.request = request;
this.requestClone = {...this.request};
this.response = {};
}
waitForHookFns() {
return Promise.all(hookFns.map(fn => {
try {
return Promise.resolve(fn(this.request)).then(emptyFn, errorFn);
} catch (err) {
console.error(err);
}
}));
}
waitForResponseFn() {
try {
return Promise.resolve(this.request.response(this.response)).then(emptyFn, errorFn);
} catch (err) {
console.error(err);
return Promise.resolve();
}
}
waitForRequestKeys() {
if (this.reqPromise) return this.reqPromise;
const requestKeys = ['url', 'method', 'abort', 'headers', 'data'];
return this.reqPromise = this.waitForHookFns().then(() => Promise.all(
requestKeys.map(key => Promise.resolve(this.request[key]).then(
val => this.request[key] = val,
e => this.request[key] = this.requestClone[key]
))
));
}
waitForResponseKeys() {
if (this.resPromise) return this.resPromise;
const responseKeys = this.request.type === 'xhr' ? xhrResponses : fetchResponses;
return this.resPromise = this.waitForResponseFn().then(() => Promise.all(
responseKeys.map(key => {
const descriptor = getDescriptor(this.response, key);
if (descriptor && 'value' in descriptor) {
return Promise.resolve(descriptor.value).then(
val => this.response[key] = val,
e => delete this.response[key]
);
} else {
delete this.response[key];
}
})
));
}
}
class XhrEvents {
constructor() {
this.events = {};
}
add(type, event) {
if (type.startsWith('on')) {
this.events[type] = typeof event === 'function' ? event : null;
} else {
this.events[type] = this.events[type] || new Set();
this.events[type].add(event);
}
}
remove(type, event) {
if (type.startsWith('on')) {
this.events[type] = null;
} else {
this.events[type] && this.events[type].delete(event);
}
}
_sIP() {
this.ajaxHooker_isStopped = true;
}
trigger(e) {
if (e.ajaxHooker_isTriggered || e.ajaxHooker_isStopped) return;
e.stopImmediatePropagation = this._sIP;
this.events[e.type] && this.events[e.type].forEach(fn => {
!e.ajaxHooker_isStopped && fn.call(e.target, e);
});
this.events['on' + e.type] && this.events['on' + e.type].call(e.target, e);
e.ajaxHooker_isTriggered = true;
}
clone() {
const eventsClone = new XhrEvents();
for (const type in this.events) {
if (type.startsWith('on')) {
eventsClone.events[type] = this.events[type];
} else {
eventsClone.events[type] = new Set([...this.events[type]]);
}
}
return eventsClone;
}
}
const xhrMethods = {
readyStateChange(e) {
if (e.target.readyState === 4) {
e.target.dispatchEvent(new CustomEvent('ajaxHooker_responseReady', {detail: e}));
} else {
e.target.__ajaxHooker.eventTrigger(e);
}
},
asyncListener(e) {
e.target.__ajaxHooker.eventTrigger(e);
},
setRequestHeader(header, value) {
const ah = this.__ajaxHooker;
ah.originalXhr.setRequestHeader(header, value);
if (this.readyState !== 1) return;
if (header in ah.headers) {
ah.headers[header] += ', ' + value;
} else {
ah.headers[header] = value;
}
},
addEventListener(...args) {
const ah = this.__ajaxHooker;
if (xhrAsyncEvents.includes(args[0])) {
ah.proxyEvents.add(args[0], args[1]);
} else {
ah.originalXhr.addEventListener(...args);
}
},
removeEventListener(...args) {
const ah = this.__ajaxHooker;
if (xhrAsyncEvents.includes(args[0])) {
ah.proxyEvents.remove(args[0], args[1]);
} else {
ah.originalXhr.removeEventListener(...args);
}
},
open(method, url, async = true, ...args) {
const ah = this.__ajaxHooker;
ah.url = url.toString();
ah.method = method.toUpperCase();
ah.async = !!async;
ah.openArgs = args;
ah.headers = {};
for (const key of xhrResponses) {
ah.proxyProps[key] = {
get: () => {
const val = ah.originalXhr[key];
ah.originalXhr.dispatchEvent(new CustomEvent('ajaxHooker_readResponse', {
detail: {key, val}
}));
return val;
}
};
}
return ah.originalXhr.open(method, url, ...args);
},
sendFactory(realSend) {
return function(data) {
const ah = this.__ajaxHooker;
const xhr = ah.originalXhr;
if (xhr.readyState !== 1) return realSend.call(xhr, data);
ah.eventTrigger = e => ah.proxyEvents.trigger(e);
if (shouldFilter('xhr', ah.url, ah.method, ah.async)) {
xhr.addEventListener('ajaxHooker_responseReady', e => {
ah.eventTrigger(e.detail);
}, {once: true});
return realSend.call(xhr, data);
}
const request = {
type: 'xhr',
url: ah.url,
method: ah.method,
abort: false,
headers: ah.headers,
data: data,
response: null,
async: ah.async
};
if (!ah.async) {
const requestClone = {...request};
hookFns.forEach(fn => {
try {
toString.call(fn) === '[object Function]' && fn(request);
} catch (err) {
console.error(err);
}
});
for (const key in request) {
if (toString.call(request[key]) === '[object Promise]') {
request[key] = requestClone[key];
}
}
xhr.open(request.method, request.url, ah.async, ...ah.openArgs);
for (const header in request.headers) {
xhr.setRequestHeader(header, request.headers[header]);
}
data = request.data;
xhr.addEventListener('ajaxHooker_responseReady', e => {
ah.eventTrigger(e.detail);
}, {once: true});
realSend.call(xhr, data);
if (toString.call(request.response) === '[object Function]') {
const response = {
finalUrl: xhr.responseURL,
status: xhr.status,
responseHeaders: parseHeaders(xhr.getAllResponseHeaders())
};
for (const key of xhrResponses) {
defineProp(response, key, () => {
return response[key] = ah.originalXhr[key];
}, val => {
if (toString.call(val) !== '[object Promise]') {
delete response[key];
response[key] = val;
}
});
}
try {
request.response(response);
} catch (err) {
console.error(err);
}
for (const key of xhrResponses) {
ah.proxyProps[key] = {get: () => response[key]};
};
}
return;
}
const req = new AHRequest(request);
req.waitForRequestKeys().then(() => {
if (request.abort) return;
xhr.open(request.method, request.url, ...ah.openArgs);
for (const header in request.headers) {
xhr.setRequestHeader(header, request.headers[header]);
}
data = request.data;
xhr.addEventListener('ajaxHooker_responseReady', e => {
if (typeof request.response !== 'function') return ah.eventTrigger(e.detail);
req.response = {
finalUrl: xhr.responseURL,
status: xhr.status,
responseHeaders: parseHeaders(xhr.getAllResponseHeaders())
};
for (const key of xhrResponses) {
defineProp(req.response, key, () => {
return req.response[key] = ah.originalXhr[key];
}, val => {
delete req.response[key];
req.response[key] = val;
});
}
const resPromise = req.waitForResponseKeys().then(() => {
for (const key of xhrResponses) {
if (!(key in req.response)) continue;
ah.proxyProps[key] = {
get: () => {
const val = req.response[key];
xhr.dispatchEvent(new CustomEvent('ajaxHooker_readResponse', {
detail: {key, val}
}));
return val;
}
};
}
});
xhr.addEventListener('ajaxHooker_readResponse', e => {
const descriptor = getDescriptor(req.response, e.detail.key);
if (!descriptor || 'get' in descriptor) {
req.response[e.detail.key] = e.detail.val;
}
});
const eventsClone = ah.proxyEvents.clone();
ah.eventTrigger = event => resPromise.then(() => eventsClone.trigger(event));
ah.eventTrigger(e.detail);
}, {once: true});
realSend.call(xhr, data);
});
};
}
};
function fakeXhr() {
const xhr = new realXhr();
let ah = xhr.__ajaxHooker;
let xhrProxy = xhr;
if (!ah) {
const proxyEvents = new XhrEvents();
ah = xhr.__ajaxHooker = {
headers: {},
originalXhr: xhr,
proxyProps: {},
proxyEvents: proxyEvents,
eventTrigger: e => proxyEvents.trigger(e),
toJSON: emptyFn // Converting circular structure to JSON
};
xhrProxy = new Proxy(xhr, {
get(target, prop) {
try {
if (target === xhr) {
if (prop in ah.proxyProps) {
const descriptor = ah.proxyProps[prop];
return descriptor.get ? descriptor.get() : descriptor.value;
}
if (typeof xhr[prop] === 'function') return xhr[prop].bind(xhr);
}
} catch (err) {
console.error(err);
}
return target[prop];
},
set(target, prop, value) {
try {
if (target === xhr && prop in ah.proxyProps) {
const descriptor = ah.proxyProps[prop];
descriptor.set ? descriptor.set(value) : (descriptor.value = value);
} else {
target[prop] = value;
}
} catch (err) {
console.error(err);
}
return true;
}
});
xhr.addEventListener('readystatechange', xhrMethods.readyStateChange);
xhr.addEventListener('load', xhrMethods.asyncListener);
xhr.addEventListener('loadend', xhrMethods.asyncListener);
for (const evt of xhrAsyncEvents) {
const onEvt = 'on' + evt;
ah.proxyProps[onEvt] = {
get: () => proxyEvents.events[onEvt] || null,
set: val => proxyEvents.add(onEvt, val)
};
}
for (const method of ['setRequestHeader', 'addEventListener', 'removeEventListener', 'open']) {
ah.proxyProps[method] = { value: xhrMethods[method] };
}
}
ah.proxyProps.send = { value: xhrMethods.sendFactory(xhr.send) };
return xhrProxy;
}
function hookFetchResponse(response, req) {
for (const key of fetchResponses) {
response[key] = () => new Promise((resolve, reject) => {
if (key in req.response) return resolve(req.response[key]);
resProto[key].call(response).then(res => {
req.response[key] = res;
req.waitForResponseKeys().then(() => {
resolve(key in req.response ? req.response[key] : res);
});
}, reject);
});
}
}
function fakeFetch(url, options = {}) {
if (!url) return realFetch.call(win, url, options);
let init = {...options};
if (toString.call(url) === '[object Request]') {
init = {};
for (const prop of fetchInitProps) init[prop] = url[prop];
Object.assign(init, options);
url = url.url;
}
url = url.toString();
init.method = init.method || 'GET';
init.headers = init.headers || {};
if (shouldFilter('fetch', url, init.method, true)) return realFetch.call(win, url, init);
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);
return new Promise((resolve, reject) => {
req.waitForRequestKeys().then(() => {
if (request.abort) return reject(new DOMException('aborted', 'AbortError'));
init.method = request.method;
init.headers = request.headers;
init.body = request.data;
realFetch.call(win, request.url, init).then(response => {
if (typeof request.response === 'function') {
req.response = {
finalUrl: response.url,
status: response.status,
responseHeaders: parseHeaders(response.headers)
};
hookFetchResponse(response, req);
response.clone = () => {
const resClone = resProto.clone.call(response);
hookFetchResponse(resClone, req);
return resClone;
};
}
resolve(response);
}, reject);
}).catch(err => {
console.error(err);
resolve(realFetch.call(win, url, init));
});
});
}
win.XMLHttpRequest = fakeXhr;
Object.keys(realXhr).forEach(key => fakeXhr[key] = realXhr[key]);
fakeXhr.prototype = realXhr.prototype;
win.fetch = fakeFetch;
return {
hook: fn => hookFns.push(fn),
filter: arr => {
filter = Array.isArray(arr) && arr;
},
protect: () => {
readonly(win, 'XMLHttpRequest', fakeXhr);
readonly(win, 'fetch', fakeFetch);
},
unhook: () => {
writable(win, 'XMLHttpRequest', realXhr);
writable(win, 'fetch', realFetch);
}
};
}();