ajaxHooker_2

ajax劫持库,支持xhr和fetch劫持。

Ten skrypt nie powinien być instalowany bezpośrednio. Jest to biblioteka dla innych skyptów do włączenia dyrektywą meta // @require https://update.greasyfork.org/scripts/476730/1259979/ajaxHooker_2.js

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         ajaxHooker_2
// @namespace    http://tampermonkey.net/
// @description  ajax劫持库,支持xhr和fetch劫持。
// @author       cxxjackie
// @version      1.3.3
// @license      MIT
// @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);
        }
    };
}();