您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A robust and modular script to hook JavaScript methods and AJAX requests
// ==UserScript== // @name Improved-Everything-Hook // @namespace https://example.com/improved-everything-hook/ // @version 1.0.0 // @description A robust and modular script to hook JavaScript methods and AJAX requests // @author Enhanced by xAI // @match http://*/* // @match https://*/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; /** * Utility module for common operations * @module Utils */ const Utils = { /** * Checks if a value is a function * @param {*} func - Value to check * @returns {boolean} */ isFunction: (func) => typeof func === 'function', /** * Checks if a value is an object and not null * @param {*} obj - Value to check * @returns {boolean} */ isExistObject: (obj) => obj !== null && typeof obj === 'object', /** * Checks if a value is an array * @param {*} arr - Value to check * @returns {boolean} */ isArray: (arr) => Array.isArray(arr), /** * Executes an array of methods with given arguments * @param {Object} context - Execution context * @param {Array<Function>} methods - Methods to execute * @param {Array} args - Arguments to pass * @returns {*} Last non-null result */ invokeMethods: (context, methods, args) => { if (!Utils.isArray(methods)) return null; let result = null; methods.forEach(method => { if (Utils.isFunction(method)) { try { const r = method.apply(context, args); if (r !== null && r !== undefined) result = r; } catch (e) { console.warn(`Error executing method: ${e.message}`); } } }); return result; }, /** * URL utility for parsing and matching URLs */ UrlUtils: { /** * Tests if a URL matches a pattern * @param {string} url - URL to test * @param {string} pattern - Regex pattern * @returns {boolean} */ urlMatching: (url, pattern) => new RegExp(pattern).test(url), /** * Gets URL without query parameters * @param {string} url - Full URL * @returns {string} */ getUrlWithoutParam: (url) => url.split('?')[0], /** * Merges URL with parameters * @param {string} url - Base URL * @param {Array<{key: string, value: string}>} params - Parameters * @returns {string} */ mergeUrlAndParams: (url, params) => { if (!Utils.isArray(params)) return url; const paramStr = params .filter(p => p.key && p.value) .map(p => `${p.key}=${p.value}`) .join('&'); return paramStr ? `${url}?${paramStr}` : url; } } }; /** * Core hooking class * @class EHook */ class EHook { constructor() { this._autoId = 1; this._hookedMap = new Map(); this._hookedContextMap = new Map(); } /** * Generates a unique ID * @returns {string} */ _getAutoStrId() { return `__auto__${this._autoId++}`; } /** * Gets or creates a hook ID for a context * @param {Object} context - Context object * @returns {string} */ _getHookedId(context) { for (const [id, ctx] of this._hookedContextMap) { if (ctx === context) return id; } const id = this._getAutoStrId(); this._hookedContextMap.set(id, context); return id; } /** * Gets or creates a method map for a context * @param {Object} context - Context object * @returns {Map} */ _getHookedMethodMap(context) { const id = this._getHookedId(context); if (!this._hookedMap.has(id)) { this._hookedMap.set(id, new Map()); } return this._hookedMap.get(id); } /** * Gets or creates a hook task for a method * @param {Object} context - Context object * @param {string} methodName - Method name * @returns {Object} */ _getHookedMethodTask(context, methodName) { const methodMap = this._getHookedMethodMap(context); if (!methodMap.has(methodName)) { methodMap.set(methodName, { original: undefined, replace: undefined, task: { before: [], current: undefined, after: [] } }); } return methodMap.get(methodName); } /** * Hooks a method with custom logic * @param {Object} parent - Object containing the method * @param {string} methodName - Method name * @param {Object} config - Hook configuration * @returns {number} Hook ID or -1 on failure */ hook(parent, methodName, config = {}) { const context = config.context || parent; if (!parent[methodName]) { parent[methodName] = () => {}; } if (!Utils.isFunction(parent[methodName])) { return -1; } const methodTask = this._getHookedMethodTask(parent, methodName); const id = this._autoId++; let hooked = false; if (Utils.isFunction(config.replace)) { methodTask.replace = { id, method: config.replace }; hooked = true; } if (Utils.isFunction(config.before)) { methodTask.task.before.push({ id, method: config.before }); hooked = true; } if (Utils.isFunction(config.current)) { methodTask.task.current = { id, method: config.current }; hooked = true; } if (Utils.isFunction(config.after)) { methodTask.task.after.push({ id, method: config.after }); hooked = true; } if (hooked) { this._hook(parent, methodName, context); return id; } return -1; } /** * Replaces a method with a hooked version * @private */ _hook(parent, methodName, context) { const methodTask = this._getHookedMethodTask(parent, methodName); if (!methodTask.original) { methodTask.original = parent[methodName]; } if (methodTask.replace && Utils.isFunction(methodTask.replace.method)) { parent[methodName] = methodTask.replace.method(methodTask.original); return; } const hookedMethod = (...args) => { let result; try { // Execute before hooks Utils.invokeMethods(context, methodTask.task.before, [methodTask.original, args]); // Execute current or original method if (methodTask.task.current && Utils.isFunction(methodTask.task.current.method)) { result = methodTask.task.current.method.call(context, parent, methodTask.original, args); } else { result = methodTask.original.apply(context, args); } // Execute after hooks const afterArgs = [methodTask.original, args, result]; const afterResult = Utils.invokeMethods(context, methodTask.task.after, afterArgs); return afterResult !== null ? afterResult : result; } catch (e) { console.error(`Error in hooked method ${methodName}: ${e.message}`); return methodTask.original.apply(context, args); } }; // Preserve original method properties Object.defineProperties(hookedMethod, { toString: { value: methodTask.original.toString.bind(methodTask.original), configurable: false, enumerable: false } }); hookedMethod.prototype = methodTask.original.prototype; parent[methodName] = hookedMethod; } /** * Hooks AJAX requests * @param {Object} methods - Methods to hook (e.g., open, send, onreadystatechange) * @returns {number} Hook ID */ hookAjax(methods) { if (this.isHooked(window, 'XMLHttpRequest')) { return -1; } return this.hookReplace(window, 'XMLHttpRequest', (OriginalXMLHttpRequest) => { class HookedXMLHttpRequest { constructor() { this.xhr = new OriginalXMLHttpRequest(); this.xhr.xhr = this; // Reference to outer xhr for (const prop in this.xhr) { if (Utils.isFunction(this.xhr[prop])) { this[prop] = (...args) => { if (Utils.isFunction(methods[prop])) { this.hookBefore(this.xhr, prop, methods[prop]); } return this.xhr[prop].apply(this.xhr, args); }; } else { Object.defineProperty(this, prop, { get: () => this[prop + '_'] || this.xhr[prop], set: (value) => { if (prop.startsWith('on') && Utils.isFunction(methods[prop])) { this.hookBefore(this.xhr, prop, methods[prop]); this.xhr[prop] = (...args) => value.apply(this.xhr, args); } else { this[prop + '_'] = value; } } }); } } } } return HookedXMLHttpRequest; }); } /** * Replaces a method entirely * @param {Object} parent - Parent object * @param {string} methodName - Method name * @param {Function} replace - Replacement function * @param {Object} [context] - Execution context * @returns {number} Hook ID */ hookReplace(parent, methodName, replace, context) { return this.hook(parent, methodName, { replace, context }); } /** * Hooks a method to run before the original * @param {Object} parent - Parent object * @param {string} methodName - Method name * @param {Function} before - Function to run before * @param {Object} [context] - Execution context * @returns {number} Hook ID */ hookBefore(parent, methodName, before, context) { return this.hook(parent, methodName, { before, context }); } /** * Checks if a method is hooked * @param {Object} context - Context object * @param {string} methodName - Method name * @returns {boolean} */ isHooked(context, methodName) { const methodMap = this._getHookedMethodMap(context); return methodMap.has(methodName) && !!methodMap.get(methodName).original; } /** * Unhooks a method * @param {Object} context - Context object * @param {string} methodName - Method name * @param {boolean} [isDeeply=false] - Whether to remove all hooks * @param {number} [eqId] - Specific hook ID to remove */ unHook(context, methodName, isDeeply = false, eqId) { if (!context[methodName] || !Utils.isFunction(context[methodName])) return; const methodMap = this._getHookedMethodMap(context); const methodTask = methodMap.get(methodName); if (eqId && this.unHookById(eqId)) return; if (methodTask?.original) { context[methodName] = methodTask.original; if (isDeeply) methodMap.delete(methodName); } } /** * Unhooks by ID * @param {number} eqId - Hook ID * @returns {boolean} Whether a hook was removed */ unHookById(eqId) { let hasEq = false; for (const [_, methodMap] of this._hookedMap) { for (const [_, task] of methodMap) { task.task.before = task.task.before.filter(b => { if (b.id === eqId) hasEq = true; return b.id !== eqId; }); task.task.after = task.task.after.filter(a => { if (a.id === eqId) hasEq = true; return a.id !== eqId; }); if (task.task.current?.id === eqId) { task.task.current = undefined; hasEq = true; } if (task.replace?.id === eqId) { task.replace = undefined; hasEq = true; } } } return hasEq; } } /** * AJAX-specific hooking class * @class AHook */ class AHook { constructor() { this.isHooked = false; this._urlDispatcherList = []; this._autoId = 1; this.eHook = new EHook(); } /** * Registers an AJAX URL interceptor * @param {string} urlPatcher - URL pattern to match * @param {Object|Function} configOrRequest - Configuration or request hook * @param {Function} [response] - Response hook * @returns {number} Registration ID */ register(urlPatcher, configOrRequest, response) { if (!urlPatcher) return -1; const config = {}; if (Utils.isFunction(configOrRequest)) { config.hookRequest = configOrRequest; } else if (Utils.isExistObject(configOrRequest)) { Object.assign(config, configOrRequest); } if (Utils.isFunction(response)) { config.hookResponse = response; } if (!Object.keys(config).length) return -1; const id = this._autoId++; this._urlDispatcherList.push({ id, patcher: urlPatcher, config }); if (!this.isHooked) { this.startHook(); } return id; } /** * Starts AJAX hooking */ startHook() { const methods = { open: (xhr, args) => { const [method, fullUrl, async] = args; const url = Utils.UrlUtils.getUrlWithoutParam(fullUrl); xhr.patcherList = this._urlPatcher(url); xhr.openArgs = { method, url, async }; Utils.invokeMethods(xhr, xhr.patcherList.map(p => p.config.hookRequest), [xhr.openArgs]); args[1] = Utils.UrlUtils.mergeUrlAndParams(xhr.openArgs.url, xhr.openArgs.params); }, onreadystatechange: (xhr, args) => { if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) { this._onResponse(xhr, args); } }, onload: (xhr, args) => this._onResponse(xhr, args) }; this.___hookedId = this.eHook.hookAjax(methods); this.isHooked = true; } /** * Matches URL against registered patchers * @private */ _urlPatcher(url) { return this._urlDispatcherList.filter(p => Utils.UrlUtils.urlMatching(url, p.patcher)); } /** * Handles response hooks * @private */ _onResponse(xhr, args) { const results = Utils.invokeMethods(xhr, xhr.patcherList.map(p => p.config.hookResponse), args); const lastResult = results?.find(r => r !== null && r !== undefined); if (lastResult) { xhr.responseText_ = xhr.response_ = lastResult; } } } // Expose to global scope window.eHook = new EHook(); window.aHook = new AHook(); /** * Example usage: * window.eHook.hook(window, 'alert', { * before: (original, args) => console.log('Before alert:', args), * after: (original, args, result) => console.log('After alert:', result) * }); * * window.aHook.register('https://example.com/api/.*', { * hookRequest: (args) => console.log('Request:', args), * hookResponse: (xhr, args) => console.log('Response:', xhr.responseText) * }); */ })();