您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Library script to patch the code of webpack modules at runtime. Exposes a global register_webpack_patches function.
// ==UserScript== // @name Deezer Webpack Patcher // @description Library script to patch the code of webpack modules at runtime. Exposes a global register_webpack_patches function. // @author bertigert // @version 1.0.0 // @icon https://www.google.com/s2/favicons?sz=64&domain=deezer.com // @namespace Violentmonkey Scripts // @match https://www.deezer.com/* // @grant none // @run-at document-start // ==/UserScript== (function() { "use strict"; /** * Class to patch webpack modules by hooking into the webpack global object array push * * **It is important that the patcher is initialized as early as possible to catch modules as they are loaded** */ class WebpackPatcher { static VERSION = 1; constructor(global_obj_key, logger, benchmark=false) { this.global_obj = window[global_obj_key]; this.patch_targets = new Map(); this.patched_modules = new Set(); this.patch_counts = new Map(); this.hooked = false; this.benchmark = benchmark; this.logger = logger || window.console; this._module_cache = new Map(); this._function_string_cache = new Map(); } /** * Find a module by its ID in the webpack modules array * @param {number} id - The webpack module ID to find * @returns {Array|null} - The module array [ids, exports, ...] or null if not found */ get_module(id) { if (this._module_cache.has(id)) { return this._module_cache.get(id); } for (const e of this.global_obj) { if (e[0].some(mod_id => mod_id == id)) { this._module_cache.set(id, e); return e; } } return null; } /** * Register a module to be patched when it becomes available. Should be called as early as possible. * @param {number} module_id - The webpack module ID to target * @param {Array} methods - Array of method patch configurations from PATCHES structure */ register_patch(module_id, methods) { // Merge with existing patches for this module if (this.patch_targets.has(module_id)) { const existing_methods = this.patch_targets.get(module_id); this.patch_targets.set(module_id, [...existing_methods, ...methods]); } else { this.patch_targets.set(module_id, methods); this.patch_counts.set(module_id, 0); } const existing_module = this.get_module(module_id); if (existing_module && methods.length > 0) { this.patch_module(existing_module, module_id, methods); } if (!this.hooked) { this._hook_webpack_array_push(); } } /** * Register multiple patches at once. Should be called as early as possible. * @param {Object} patches - The PATCHES structure mapping module IDs to patch configurations */ register_patches(patches) { Object.entries(patches).forEach(([id, {methods}]) => { this.register_patch(parseInt(id), methods); }); } /** * Patch a module using the PATCHES structure * @param {*} module * @param {number} module_id * @param {Array} methods - Array of method patch configurations * @returns true if a method was patched, false otherwise */ patch_module(module, module_id, methods) { const start_time = this.benchmark ? performance.now() : 0; try { let patched_count = 0; const module_exports = module[1]; const current_patch_count = this.patch_counts.get(module_id) || 0; for (const [name, method] of Object.entries(module_exports)) { if (typeof method !== 'function') { continue; } if (current_patch_count + patched_count >= this.patch_targets.get(module_id).length) { break; } const matching_method = methods.find(({identifiers}) => identifiers.every(identifier => this.does_method_match(method, identifier, module_id, name)) ); if (matching_method) { const patch_start = this.benchmark ? performance.now() : 0; const patched_method = this.patch_method_with_regexp_or_raw_string( method, matching_method.matches_and_replacements, module_id, name ); const patch_end = this.benchmark ? performance.now() : 0; module_exports[name] = patched_method; if (this.benchmark) { this.logger.debug(`Patched method '${name}' in module ${module_id} (${(patch_end - patch_start).toFixed(2)}ms)`); } else { this.logger.debug(`Patched method '${name}' in module ${module_id}`); } patched_count++; } } if (patched_count > 0) { this.patched_modules.add(module_id); this.patch_counts.set(module_id, current_patch_count + patched_count); const all_methods = this.patch_targets.get(module_id); if (this.patch_counts.get(module_id) >= all_methods.length) { this.logger.debug(`Reached max patches (${all_methods.length}) for module ${module_id}, unregistering`); this.patch_targets.delete(module_id); } if (this.benchmark) { const total_time = performance.now() - start_time; this.logger.debug(`Successfully patched ${patched_count} method(s) in module ${module_id} (total: ${this.patch_counts.get(module_id)}) - ${total_time.toFixed(2)}ms`); } else { this.logger.debug(`Successfully patched ${patched_count} method(s) in module ${module_id} (total: ${this.patch_counts.get(module_id)})`); } return true; } return false; } catch (e) { if (this.benchmark) { const total_time = performance.now() - start_time; this.logger.error(`Error patching method in module ${module_id} after ${total_time.toFixed(2)}ms:`, e); } else { this.logger.error(`Error patching method in module ${module_id}:`, e); } return false; } } _hook_webpack_array_push() { const original_push = this.global_obj.push; const self = this; this.global_obj.push = function(...args) { for (const module of args) { if (Array.isArray(module) && module.length >= 2) { const module_ids = module[0]; for (const module_id of module_ids) { const patch_methods = self.patch_targets.get(module_id); if (patch_methods && !self.patched_modules.has(module_id)) { self.logger.debug(`Target module ${module_id} added, applying patch`); self.patch_module(module, module_id, patch_methods); } } } } return original_push.apply(this, args); } this.hooked = true; this.logger.debug("Webpack array push hooked"); } _get_cached_function_string(func, module_id, method_name) { if (typeof func !== 'function') { return func; } const cache_key = `${module_id}_${method_name}`; if (this._function_string_cache.has(cache_key)) { return this._function_string_cache.get(cache_key); } const func_str = func.toString(); this._function_string_cache.set(cache_key, func_str); return func_str; } _set_cached_function_string(func_str, module_id, method_name) { const cache_key = `${module_id}_${method_name}`; this._function_string_cache.set(cache_key, func_str); } /** * Method for cached method matching * @param {Function} method - The method to check * @param {string|RegExp} string_or_regex - The string or regex to match against * @param {number} module_id - Module in which the method is. Used for the key for caching * @param {string} method_name - Used for the key for caching * @returns {boolean} true if the method matches, false otherwise */ does_method_match(method, string_or_regex, module_id, method_name) { const method_str = this._get_cached_function_string(method, module_id, method_name); if (typeof string_or_regex === 'string') { return method_str.includes(string_or_regex); } else if (string_or_regex instanceof RegExp) { return string_or_regex.test(method_str); } return false; } /** * Replace parts of a method's code using regexes or raw strings with caching * @param {Function} original_method - The method to patch, can also be a string of the method code * @param {Array} matches_and_replacements - Array of {match: RegExp|string, replacement: string|function, global: boolean} objects * @param {number} module_id - Module in which the method is. Used for the key for caching * @param {string} method_name - Used for the key for caching * @returns {Function} Patched method */ patch_method_with_regexp_or_raw_string(original_method, matches_and_replacements, module_id, method_name) { const start_time = this.benchmark ? performance.now() : 0; let patched_code; try { const original_code = this._get_cached_function_string(original_method, module_id, method_name); patched_code = original_code; let total_replacements = 0; for (let i = 0; i < matches_and_replacements.length; i++) { const match_and_replacement = matches_and_replacements[i]; const regex_start = this.benchmark ? performance.now() : 0; let replacement_occurred = false; const { match, replace, global } = match_and_replacement; const func = global || (match instanceof RegExp && match.global) ? "replaceAll" : "replace"; if (typeof replace === 'function') { patched_code = patched_code[func](match, (...args) => { replacement_occurred = true; return replace(...args); }); } else { patched_code = patched_code[func](match, (...args) => { replacement_occurred = true; return replace; }); } if (replacement_occurred) { total_replacements++; } else { this.logger.warn(`Replacement ${i + 1}/${matches_and_replacements.length} skipped (no match) for method ${method_name} in module ${module_id}`); } if (this.benchmark) { const regex_end = performance.now(); this.logger.debug(`Replacement ${i + 1}/${matches_and_replacements.length} ${replacement_occurred ? 'applied' : 'skipped (no match)'} in ${(regex_end - regex_start).toFixed(2)}ms`); } } if (total_replacements === 0) { this.logger.warn(`No replacements occured in method ${method_name} in module ${module_id}, returning original method`); return original_method; } const patched_method = new Function("return " + patched_code)(); this.logger.debug("Patched method:", patched_method); this._set_cached_function_string(patched_code, module_id, method_name); if (this.benchmark) { const total_time = performance.now() - start_time; this.logger.debug(`${total_replacements} replacement(s) applied (cached), method patched in ${total_time.toFixed(2)}ms`); } return patched_method; } catch (e) { if (this.benchmark) { const total_time = performance.now() - start_time; this.logger.error(`Replacement based patching failed after ${total_time.toFixed(2)}ms:`, e, "patched code:", patched_code); } else { this.logger.error(`Replacement based patching failed:`, e, "patched code:", patched_code); } return original_method; } } /** * Static helper to reliably detect and hook webpack global object * @param {string} global_obj_key - Key of the global webpack object array (e.g. "webpackJsonpDeezer") * @param {function} callback - Callback to invoke once webpack is detected */ static detect_and_hook_webpack(global_obj_key, callback) { if (window[global_obj_key]) { callback(); return; } let webpack_found = false; Object.defineProperty(window, global_obj_key, { configurable: true, enumerable: true, set: function(value) { if (!webpack_found) { webpack_found = true; Object.defineProperty(window, global_obj_key, { configurable: true, enumerable: true, writable: true, value: value }); setTimeout(callback, 0); } } }); } /** * Initialize the global webpack patcher asynchronously */ static async initialize(global_obj_key, logger, benchmark = false) { return new Promise((resolve) => { WebpackPatcher.detect_and_hook_webpack(global_obj_key, () => { const patcher = new WebpackPatcher(global_obj_key, logger, benchmark); logger.log("Global WebpackPatcher initialized"); resolve(patcher); }); }); } } /** * Helper class for registering patches with the global WebpackPatcher */ class WebpackPatchRegistrar { constructor(patcher_promise) { this.patcher_promise = patcher_promise; } /** * Register multiple patches from a PATCHES object. Should be called as early as possible. * * @param {Object} patches - PATCHES object with the following structure: * * @structure * ``` * PATCHES = { * [module_id]: { * methods: [ * { * identifiers: [string | RegExp], // Array of strings/regexes that all need to match * matches_and_replacements: [ // Array of replacements to apply * { * match: string | RegExp, // Pattern to match in method code * replace: string | function, // Replacement string or callback function * global: boolean // Whether to replace all occurrences (optional) * } * ] * } * ] * } * } * ``` * * Structure details: * - **module_id**: Webpack module ID (number) * - **methods**: Array of method patch configurations * - **identifiers**: Array of strings or regexes that all need to match the target method. * **Identifiers should be as close to the start of the function as possible for performance** * - **matches_and_replacements**: Array of text replacements to apply to the method * - **match**: Pattern to find in method source code (string or RegExp) * - **replace**: Replacement text (string) or callback function (match, ...groups) => string. * Functions should be used if you need access to capture groups or dynamic replacements. * - **global**: If true, replaces all occurrences. For RegExp, uses regex.global flag if not specified */ async register_patches(patches) { const patcher = await this.patcher_promise; patcher.register_patches(patches); logger.debug("Patches registered with WebpackPatcher"); return patcher; } } class Logger { static PREFIX = "[WebpackPatcher]"; constructor(debug=false) { this.should_debug = debug; } debug(...args) {if (this.should_debug) console.debug(Logger.PREFIX, ...args);} log(...args) {console.log(Logger.PREFIX, ...args);} warn(...args) {console.warn(Logger.PREFIX, ...args);} error(...args) {console.error(Logger.PREFIX, ...args);} } const logger = new Logger(false); const patcher_promise = WebpackPatcher.initialize("webpackJsonpDeezer", logger, false); const webpack_patch_registrar = new WebpackPatchRegistrar(patcher_promise); window.register_webpack_patches = webpack_patch_registrar.register_patches.bind(webpack_patch_registrar); })();