Ajax-hook-userscript

Ajax hook is a lightweight library for intercepting XMLHttpRequest objects.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyfork.org/scripts/450907/1090926/Ajax-hook-userscript.js

// ==UserScript==
// @name        Ajax-hook-userscript
// @namespace   https://github.com/wendux/Ajax-hook
// @version     2.0.3
// @author      wendux
// @description Ajax hook is a lightweight library for intercepting XMLHttpRequest objects.
// @license     MIT
// @grant       unsafeWindow
// @run-at      document-start
// ==/UserScript==

const ah = (W => {
    /*
     * author: wendux
     * email: 824783146@qq.com
     * source code: https://github.com/wendux/Ajax-hook
     * modify by https://github.com/lzghzr
     */

    // Save original XMLHttpRequest as _rxhr
    var realXhr = "_rxhr"

    function configEvent(event, xhrProxy) {
        var e = {};
        for (var attr in event) e[attr] = event[attr];
        // xhrProxy instead
        e.target = e.currentTarget = xhrProxy
        return e;
    }

    function hook(proxy) {
        // Avoid double hookAjax
        W[realXhr] = W[realXhr] || W.XMLHttpRequest

        W.XMLHttpRequest = function () {
            var xhr = new W[realXhr];
            // We shouldn't hookAjax XMLHttpRequest.prototype because we can't
            // guarantee that all attributes are on the prototype。
            // Instead, hooking XMLHttpRequest instance can avoid this problem.
            for (var attr in xhr) {
                var type = "";
                try {
                    type = typeof xhr[attr] // May cause exception on some browser
                } catch (e) {
                }
                if (type === "function") {
                    // hookAjax methods of xhr, such as `open`、`send` ...
                    this[attr] = hookFunction(attr);
                } else {
                    Object.defineProperty(this, attr, {
                        get: getterFactory(attr),
                        set: setterFactory(attr),
                        enumerable: true
                    })
                }
            }
            var that = this;
            xhr.getProxy = function () {
                return that
            }
            this.xhr = xhr;
        }

        // Generate getter for attributes of xhr
        function getterFactory(attr) {
            return function () {
                var v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : this.xhr[attr];
                var attrGetterHook = (proxy[attr] || {})["getter"]
                return attrGetterHook && attrGetterHook(v, this) || v
            }
        }

        // Generate setter for attributes of xhr; by this we have an opportunity
        // to hookAjax event callbacks (eg: `onload`) of xhr;
        function setterFactory(attr) {
            return function (v) {
                var xhr = this.xhr;
                var that = this;
                var hook = proxy[attr];
                // hookAjax  event callbacks such as `onload`、`onreadystatechange`...
                if (attr.substring(0, 2) === 'on') {
                    that[attr + "_"] = v;
                    xhr[attr] = function (e) {
                        e = configEvent(e, that)
                        var ret = proxy[attr] && proxy[attr].call(that, xhr, e)
                        ret || v.call(that, e);
                    }
                } else {
                    //If the attribute isn't writable, generate proxy attribute
                    var attrSetterHook = (hook || {})["setter"];
                    v = attrSetterHook && attrSetterHook(v, that) || v
                    this[attr + "_"] = v;
                    try {
                        // Not all attributes of xhr are writable(setter may undefined).
                        xhr[attr] = v;
                    } catch (e) {
                    }
                }
            }
        }

        // Hook methods of xhr.
        function hookFunction(fun) {
            return function () {
                var args = [].slice.call(arguments)
                if (proxy[fun]) {
                    var ret = proxy[fun].call(this, args, this.xhr)
                    // If the proxy return value exists, return it directly,
                    // otherwise call the function of xhr.
                    if (ret) return ret;
                }
                return this.xhr[fun].apply(this.xhr, args);
            }
        }

        // Return the real XMLHttpRequest
        return W[realXhr];
    }

    function unHook() {
        if (W[realXhr]) W.XMLHttpRequest = W[realXhr];
        W[realXhr] = undefined;
    }

    /*
     * author: wendux
     * email: 824783146@qq.com
     * source code: https://github.com/wendux/Ajax-hook
     */


    var events = ['load', 'loadend', 'timeout', 'error', 'readystatechange', 'abort'];
    var eventLoad = events[0],
        eventLoadEnd = events[1],
        eventTimeout = events[2],
        eventError = events[3],
        eventReadyStateChange = events[4],
        eventAbort = events[5];


    var singleton,
        prototype = 'prototype';


    function proxy(proxy) {
        if (singleton) throw "Proxy already exists";
        return singleton = new Proxy(proxy);
    }

    function unProxy() {
        singleton = null
        unHook()
    }

    function trim(str) {
        return str.replace(/^\s+|\s+$/g, '');
    }

    function getEventTarget(xhr) {
        return xhr.watcher || (xhr.watcher = document.createElement('a'));
    }

    function triggerListener(xhr, name) {
        var xhrProxy = xhr.getProxy();
        var callback = 'on' + name + '_';
        var event = configEvent({ type: name }, xhrProxy);
        xhrProxy[callback] && xhrProxy[callback](event);
        var evt;
        if (typeof (Event) === 'function') {
            evt = new Event(name, { bubbles: false });
        } else {
            // https://stackoverflow.com/questions/27176983/dispatchevent-not-working-in-ie11
            evt = document.createEvent('Event');
            evt.initEvent(name, false, true);
        }
        getEventTarget(xhr).dispatchEvent(evt);
    }


    function Handler(xhr) {
        this.xhr = xhr;
        this.xhrProxy = xhr.getProxy();
    }

    Handler[prototype] = Object.create({
        resolve: function resolve(response) {
            var xhrProxy = this.xhrProxy;
            var xhr = this.xhr;
            xhrProxy.readyState = 4;
            xhr.resHeader = response.headers;
            xhrProxy.response = xhrProxy.responseText = response.response;
            xhrProxy.statusText = response.statusText;
            xhrProxy.status = response.status;
            triggerListener(xhr, eventReadyStateChange);
            triggerListener(xhr, eventLoad);
            triggerListener(xhr, eventLoadEnd);
        },
        reject: function reject(error) {
            this.xhrProxy.status = 0;
            triggerListener(this.xhr, error.type);
            triggerListener(this.xhr, eventLoadEnd);
        }
    });

    function makeHandler(next) {
        function sub(xhr) {
            Handler.call(this, xhr);
        }

        sub[prototype] = Object.create(Handler[prototype]);
        sub[prototype].next = next;
        return sub;
    }

    var RequestHandler = makeHandler(function (rq) {
        var xhr = this.xhr;
        rq = rq || xhr.config;
        xhr.withCredentials = rq.withCredentials;
        xhr.open(rq.method, rq.url, rq.async !== false, rq.user, rq.password);
        for (var key in rq.headers) {
            xhr.setRequestHeader(key, rq.headers[key]);
        }
        xhr.send(rq.body);
    });

    var ResponseHandler = makeHandler(function (response) {
        this.resolve(response);
    });

    var ErrorHandler = makeHandler(function (error) {
        this.reject(error);
    });

    function Proxy(proxy) {
        var onRequest = proxy.onRequest,
            onResponse = proxy.onResponse,
            onError = proxy.onError;

        function handleResponse(xhr, xhrProxy) {
            var handler = new ResponseHandler(xhr);
            if (!onResponse) return handler.resolve();
            var ret = {
                response: xhrProxy.response,
                status: xhrProxy.status,
                statusText: xhrProxy.statusText,
                config: xhr.config,
                headers: xhr.resHeader || xhr.getAllResponseHeaders().split('\r\n').reduce(function (ob, str) {
                    if (str === "") return ob;
                    var m = str.split(":");
                    ob[m.shift()] = trim(m.join(':'));
                    return ob;
                }, {})
            };
            onResponse(ret, handler);
        }

        function onerror(xhr, xhrProxy, e) {
            var handler = new ErrorHandler(xhr);
            var error = { config: xhr.config, error: e };
            if (onError) {
                onError(error, handler);
            } else {
                handler.next(error);
            }
        }

        function preventXhrProxyCallback() {
            return true;
        }

        function errorCallback(xhr, e) {
            onerror(xhr, this, e);
            return true;
        }

        function stateChangeCallback(xhr, xhrProxy) {
            if (xhr.readyState === 4 && xhr.status !== 0) {
                handleResponse(xhr, xhrProxy);
            } else if (xhr.readyState !== 4) {
                triggerListener(xhr, eventReadyStateChange);
            }
            return true;
        }

        return hook({
            onload: preventXhrProxyCallback,
            onloadend: preventXhrProxyCallback,
            onerror: errorCallback,
            ontimeout: errorCallback,
            onabort: errorCallback,
            onreadystatechange: function (xhr) {
                return stateChangeCallback(xhr, this);
            },
            open: function open(args, xhr) {
                var _this = this;
                var config = xhr.config = { headers: {} };
                config.method = args[0];
                config.url = args[1];
                config.async = args[2];
                config.user = args[3];
                config.password = args[4];
                config.xhr = xhr;
                var evName = 'on' + eventReadyStateChange;
                if (!xhr[evName]) {
                    xhr[evName] = function () {
                        return stateChangeCallback(xhr, _this);
                    };
                }

                var defaultErrorHandler = function defaultErrorHandler(e) {
                    onerror(xhr, _this, configEvent(e, _this));
                };
                [eventError, eventTimeout, eventAbort].forEach(function (e) {
                    var event = 'on' + e;
                    if (!xhr[event]) xhr[event] = defaultErrorHandler;
                });

                // 如果有请求拦截器,则在调用onRequest后再打开链接。因为onRequest最佳调用时机是在send前,
                // 所以我们在send拦截函数中再手动调用open,因此返回true阻止xhr.open调用。
                //
                // 如果没有请求拦截器,则不用阻断xhr.open调用
                if (onRequest) return true;
            },
            send: function (args, xhr) {
                var config = xhr.config
                config.withCredentials = xhr.withCredentials
                config.body = args[0];
                if (onRequest) {
                    // In 'onRequest', we may call XHR's event handler, such as `xhr.onload`.
                    // However, XHR's event handler may not be set until xhr.send is called in
                    // the user's code, so we use `setTimeout` to avoid this situation
                    var req = function () {
                        onRequest(config, new RequestHandler(xhr));
                    }
                    config.async === false ? req() : setTimeout(req)
                    return true;
                }
            },
            setRequestHeader: function (args, xhr) {
                // Collect request headers
                xhr.config.headers[args[0].toLowerCase()] = args[1];
                return true;
            },
            addEventListener: function (args, xhr) {
                var _this = this;
                if (events.indexOf(args[0]) !== -1) {
                    var handler = args[1];
                    getEventTarget(xhr).addEventListener(args[0], function (e) {
                        var event = configEvent(e, _this);
                        event.type = args[0];
                        event.isTrusted = true;
                        handler.call(_this, event);
                    });
                    return true;
                }
            },
            getAllResponseHeaders: function (_, xhr) {
                var headers = xhr.resHeader
                if (headers) {
                    var header = "";
                    for (var key in headers) {
                        header += key + ': ' + headers[key] + '\r\n';
                    }
                    return header;
                }
            },
            getResponseHeader: function (args, xhr) {
                var headers = xhr.resHeader
                if (headers) {
                    return headers[(args[0] || '').toLowerCase()];
                }
            }
        });
    }

    return {
        proxy,
        unProxy,
        hook,
        unHook,
    }
})(typeof unsafeWindow === 'undefined' ? window : unsafeWindow);