haicord/src/webpack/patchWebpack.ts

491 lines
20 KiB
TypeScript
Raw Normal View History

2022-10-22 01:17:06 +02:00
/*
* Vencord, a Discord client mod
2024-05-26 20:38:57 -03:00
* Copyright (c) 2024 Vendicated, Nuckyz, and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
2022-10-22 01:17:06 +02:00
2024-05-23 06:04:21 -03:00
import { Settings } from "@api/Settings";
2023-05-06 01:36:00 +02:00
import { Logger } from "@utils/Logger";
import { interpolateIfDefined } from "@utils/misc";
2024-05-24 05:14:27 -03:00
import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types";
2024-07-05 04:02:34 -03:00
import { traceFunctionWithResults } from "../debug/Tracer";
import { patches } from "../plugins";
import { _initWebpack, AnyModuleFactory, AnyWebpackRequire, factoryListeners, moduleListeners, waitForSubscriptions, WebpackRequire, WrappedModuleFactory, wreq } from ".";
const logger = new Logger("WebpackInterceptor", "#8caaee");
2022-08-29 02:25:27 +02:00
2024-05-28 17:11:17 -03:00
/** A set with all the Webpack instances */
export const allWebpackInstances = new Set<AnyWebpackRequire>();
2024-05-26 20:22:03 -03:00
/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */
let wreqFallbackApplied = false;
2024-07-05 04:02:34 -03:00
export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: string | RegExp, totalTime: number]>;
2024-05-28 17:11:17 -03:00
type Define = typeof Reflect.defineProperty;
const define: Define = (target, p, attributes) => {
if (Object.hasOwn(attributes, "value")) {
attributes.writable = true;
}
return Reflect.defineProperty(target, p, {
configurable: true,
enumerable: true,
...attributes
});
};
2024-06-28 00:42:36 -03:00
// wreq.m is the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push
// We use this setter to intercept when wreq.m is defined and apply the patching in its module factories.
2024-06-01 01:40:33 -03:00
// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions.
// If this is the main Webpack, we also set up the internal references to WebpackRequire.
2024-06-28 00:42:36 -03:00
define(Function.prototype, "m", {
2024-05-28 17:11:17 -03:00
enumerable: false,
2024-05-26 20:22:03 -03:00
2024-06-28 00:42:36 -03:00
set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) {
define(this, "m", { value: originalModules });
2024-06-01 01:40:33 -03:00
// We may also catch Discord bundled libs, React Devtools or other extensions WebpackInstance here.
// This ensures we actually got the right ones
2024-05-26 20:22:03 -03:00
const { stack } = new Error();
2024-07-24 17:55:35 -03:00
if (!stack?.includes("/assets/") || stack?.match(/at \d+? \(/) || !String(this).includes("exports:{}")) {
2024-05-28 17:11:17 -03:00
return;
}
2024-05-23 06:04:21 -03:00
2024-05-28 17:11:17 -03:00
const fileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
2024-05-26 20:22:03 -03:00
2024-05-28 17:11:17 -03:00
allWebpackInstances.add(this);
2024-05-26 20:22:03 -03:00
2024-05-28 17:11:17 -03:00
// Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property.
// So if the setter is called, this means we can initialize the internal references to WebpackRequire.
define(this, "p", {
enumerable: false,
set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) {
2024-05-31 06:16:01 -03:00
define(this, "p", { value: bundlePath });
2024-05-31 06:19:57 -03:00
clearTimeout(setterTimeout);
2024-05-31 06:16:01 -03:00
2024-07-24 17:55:35 -03:00
if (bundlePath !== "/assets/") return;
2024-06-22 02:57:36 -03:00
2024-05-28 17:11:17 -03:00
logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire");
_initWebpack(this);
}
});
// setImmediate to clear this property setter if this is not the main Webpack.
2024-06-01 05:03:47 -03:00
// If this is the main Webpack, wreq.p will always be set before the timeout runs.
2024-05-28 17:11:17 -03:00
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
2024-06-01 01:40:33 -03:00
// Patch the pre-populated factories
2024-06-28 00:42:36 -03:00
for (const id in originalModules) {
if (updateExistingFactory(originalModules, id, originalModules[id], true)) {
continue;
}
2024-06-28 00:42:36 -03:00
notifyFactoryListeners(originalModules[id]);
defineModulesFactoryGetter(id, Settings.eagerPatches ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]);
2024-06-01 01:40:33 -03:00
}
2024-06-28 00:42:36 -03:00
define(originalModules, Symbol.toStringTag, {
2024-05-28 17:11:17 -03:00
value: "ModuleFactories",
enumerable: false
2024-05-26 20:22:03 -03:00
});
2024-05-28 17:11:17 -03:00
// The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions
2024-06-28 00:42:36 -03:00
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);
2024-05-28 17:11:17 -03:00
/*
If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
2024-07-07 15:15:45 -03:00
Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));
2024-05-28 17:11:17 -03:00
*/
2024-05-28 17:14:35 -03:00
define(this, "m", { value: proxiedModuleFactories });
2024-05-26 20:22:03 -03:00
}
});
2024-06-16 19:53:31 -03:00
const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
2024-06-12 17:33:26 -03:00
/*
If Discord ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype
and that requires defining additional traps for keeping the object working
// Proxies on the prototype dont intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined,
// to avoid Reflect.get having no effect and causing a stack overflow
get: (target, p, receiver) => {
return undefined;
},
// Same thing as get
has: (target, p) => {
return false;
2024-07-24 17:55:35 -03:00
},
2024-06-12 17:33:26 -03:00
*/
// The set trap for patching or defining getters for the module factories when new module factories are loaded
set: (target, p, newValue, receiver) => {
// If the property is not a number, we are not dealing with a module factory
if (Number.isNaN(Number(p))) {
return define(target, p, { value: newValue });
}
if (updateExistingFactory(target, p, newValue)) {
return true;
}
notifyFactoryListeners(newValue);
2024-06-16 19:53:31 -03:00
defineModulesFactoryGetter(p, Settings.eagerPatches ? wrapAndPatchFactory(p, newValue) : newValue);
2024-06-12 17:33:26 -03:00
return true;
}
};
/**
* Update a factory that exists in any Webpack instance with a new original factory.
*
* @target The module factories where this new original factory is being set
* @param id The id of the module
* @param newFactory The new original factory
2024-06-01 19:52:17 -03:00
* @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget
* @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance
*/
2024-06-02 18:46:03 -03:00
function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) {
2024-06-16 19:53:31 -03:00
let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined;
for (const wreq of allWebpackInstances) {
2024-06-01 19:52:17 -03:00
if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue;
if (Reflect.getOwnPropertyDescriptor(wreq.m, id) != null) {
existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id);
break;
}
}
if (existingFactory != null) {
2024-07-17 16:07:50 -03:00
// If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required.
// So define the descriptor of it on this current Webpack instance, call Reflect.set with the new original,
// and let the correct logic apply (normal set, or defineModuleFactoryGetter setter)
2024-06-01 19:52:17 -03:00
Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory);
return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget);
}
return false;
}
/**
2024-06-12 17:31:05 -03:00
* Notify all factory listeners.
*
* @param factory The original factory to notify for
*/
function notifyFactoryListeners(factory: AnyModuleFactory) {
for (const factoryListener of factoryListeners) {
try {
factoryListener(factory);
} catch (err) {
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
}
}
}
2024-06-12 17:36:43 -03:00
/**
* Define the getter for returning the patched version of the module factory.
*
* If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original
* and only be patched when accessed for the first time.
*
* @param id The id of the module
* @param factory The original or patched module factory
*/
2024-06-16 19:53:31 -03:00
function defineModulesFactoryGetter(id: PropertyKey, factory: WrappedModuleFactory) {
2024-06-12 17:36:43 -03:00
// Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object
// have the patched version
for (const wreq of allWebpackInstances) {
define(wreq.m, id, {
get() {
// $$vencordOriginal means the factory is already patched
if (factory.$$vencordOriginal != null) {
return factory;
}
2024-06-16 19:53:31 -03:00
return (factory = wrapAndPatchFactory(id, factory));
2024-06-12 17:36:43 -03:00
},
2024-07-24 17:55:35 -03:00
set(newFactory: AnyModuleFactory) {
2024-06-12 17:36:43 -03:00
if (factory.$$vencordOriginal != null) {
2024-07-24 17:55:35 -03:00
factory.toString = newFactory.toString.bind(v);
factory.$$vencordOriginal = newFactory;
2024-06-12 17:36:43 -03:00
} else {
2024-07-24 17:55:35 -03:00
factory = newFactory;
2024-06-12 17:36:43 -03:00
}
}
});
}
}
2024-05-26 20:22:03 -03:00
/**
2024-06-16 19:53:31 -03:00
* Wraps and patches a module factory.
2024-05-26 20:22:03 -03:00
*
* @param id The id of the module
* @param factory The original or patched module factory
* @returns The wrapper for the patched module factory
*/
2024-06-16 19:53:31 -03:00
function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) {
const patchedFactory = patchFactory(id, originalFactory);
2024-05-26 00:27:13 -03:00
2024-07-24 17:55:35 -03:00
const wrappedFactory: WrappedModuleFactory = function (...args) {
// Restore the original factory in all the module factories objects. We want to make sure the original factory is restored properly, no matter what is the Webpack instance
for (const wreq of allWebpackInstances) {
define(wreq.m, id, { value: wrappedFactory.$$vencordOriginal });
}
2024-06-16 19:53:31 -03:00
2024-07-24 17:55:35 -03:00
// eslint-disable-next-line prefer-const
let [module, exports, require] = args;
if (wreq == null) {
if (!wreqFallbackApplied) {
wreqFallbackApplied = true;
// Make sure the require argument is actually the WebpackRequire function
if (typeof require === "function" && require.m != null) {
const { stack } = new Error();
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
logger.warn(
"WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" +
`id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
")"
);
2024-07-24 18:02:02 -03:00
2024-07-24 17:55:35 -03:00
_initWebpack(require as WebpackRequire);
} else if (IS_DEV) {
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
2024-06-21 05:01:35 -03:00
return wrappedFactory.$$vencordOriginal!.apply(this, args);
2024-06-16 19:53:31 -03:00
}
2024-07-24 17:55:35 -03:00
} else if (IS_DEV) {
return wrappedFactory.$$vencordOriginal!.apply(this, args);
2024-06-16 19:53:31 -03:00
}
2024-07-24 17:55:35 -03:00
}
2024-06-16 19:53:31 -03:00
2024-07-24 17:55:35 -03:00
let factoryReturn: unknown;
try {
// Call the patched factory
factoryReturn = patchedFactory.apply(this, args);
} catch (err) {
// Just re-throw Discord errors
if (patchedFactory === originalFactory) {
throw err;
2024-06-16 19:53:31 -03:00
}
2024-07-24 17:55:35 -03:00
logger.error("Error in patched module factory:\n", err);
return wrappedFactory.$$vencordOriginal!.apply(this, args);
}
exports = module.exports;
if (exports == null) return;
2024-06-16 19:53:31 -03:00
2024-07-24 17:55:35 -03:00
// There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance
if (typeof require === "function" && require.c != null) {
let foundWindow = false;
2024-06-21 02:23:04 -03:00
2024-07-24 17:55:35 -03:00
if (exports === window) {
foundWindow = true;
} else if (typeof exports === "object") {
if (exports.default === window) {
2024-06-21 02:23:04 -03:00
foundWindow = true;
2024-07-24 17:55:35 -03:00
} else {
for (const exportKey in exports) if (exportKey.length <= 3) {
if (exports[exportKey] === window) {
foundWindow = true;
2024-06-21 02:23:04 -03:00
}
}
}
2024-07-24 17:55:35 -03:00
}
2024-06-21 02:23:04 -03:00
2024-07-24 17:55:35 -03:00
if (foundWindow) {
Object.defineProperty(require.c, id, {
value: require.c[id],
enumerable: false,
configurable: true,
writable: true
});
2024-06-21 02:23:04 -03:00
2024-07-24 17:55:35 -03:00
return factoryReturn;
2024-06-16 19:53:31 -03:00
}
2024-07-24 17:55:35 -03:00
}
2024-06-16 19:53:31 -03:00
2024-07-24 17:55:35 -03:00
for (const callback of moduleListeners) {
try {
callback(exports, { id, factory: wrappedFactory.$$vencordOriginal! });
} catch (err) {
logger.error("Error in Webpack module listener:\n", err, callback);
2024-06-16 19:53:31 -03:00
}
2024-07-24 17:55:35 -03:00
}
2024-07-24 17:55:35 -03:00
for (const [filter, callback] of waitForSubscriptions) {
try {
if (filter.$$vencordIsFactoryFilter) {
if (filter(wrappedFactory.$$vencordOriginal!)) {
waitForSubscriptions.delete(filter);
callback(exports, { id, exportKey: null, factory: wrappedFactory.$$vencordOriginal! });
2024-06-21 02:23:04 -03:00
}
2024-07-24 17:55:35 -03:00
continue;
}
2024-06-21 02:23:04 -03:00
2024-07-24 17:55:35 -03:00
if (filter(exports)) {
waitForSubscriptions.delete(filter);
callback(exports, { id, exportKey: null, factory: wrappedFactory.$$vencordOriginal! });
continue;
}
2024-06-21 02:23:04 -03:00
2024-07-24 17:55:35 -03:00
if (typeof exports !== "object") {
continue;
}
2024-06-21 02:23:04 -03:00
2024-07-24 17:55:35 -03:00
if (exports.default != null && filter(exports.default)) {
waitForSubscriptions.delete(filter);
callback(exports.default, { id, exportKey: "default", factory: wrappedFactory.$$vencordOriginal! });
continue;
}
for (const exportKey in exports) if (exportKey.length <= 3) {
const exportValue = exports[exportKey];
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, { id, exportKey, factory: wrappedFactory.$$vencordOriginal! });
break;
2024-06-16 19:53:31 -03:00
}
}
2024-07-24 17:55:35 -03:00
} catch (err) {
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
2024-06-16 19:53:31 -03:00
}
}
2024-07-24 17:55:35 -03:00
return factoryReturn;
};
2024-06-16 19:53:31 -03:00
wrappedFactory.toString = originalFactory.toString.bind(originalFactory);
wrappedFactory.$$vencordOriginal = originalFactory;
return wrappedFactory;
}
/**
* Patches a module factory.
*
* @param id The id of the module
* @param factory The original module factory
* @returns The patched module factory
*/
function patchFactory(id: PropertyKey, factory: AnyModuleFactory) {
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + String(factory);
2024-06-16 19:53:31 -03:00
let patchedFactory = factory;
const patchedBy = new Set<string>();
2024-05-19 22:49:58 -03:00
for (let i = 0; i < patches.length; i++) {
const patch = patches[i];
2024-05-19 22:49:58 -03:00
const moduleMatches = typeof patch.find === "string"
2024-05-31 17:34:32 -03:00
? code.includes(patch.find)
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
2024-05-19 22:49:58 -03:00
if (!moduleMatches) continue;
2024-05-19 22:49:58 -03:00
patchedBy.add(patch.plugin);
const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
2024-05-19 22:49:58 -03:00
const previousCode = code;
2024-05-26 00:27:13 -03:00
const previousFactory = factory;
2024-05-19 22:49:58 -03:00
// We change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) {
const lastCode = code;
2024-05-26 00:27:13 -03:00
const lastFactory = factory;
2024-05-19 22:49:58 -03:00
canonicalizeReplacement(replacement, patch.plugin);
2024-05-19 22:49:58 -03:00
try {
const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
if (IS_REPORTER) {
patchTimings.push([patch.plugin, id, replacement.match, totalTime]);
}
2024-05-19 22:49:58 -03:00
if (newCode === code) {
if (!patch.noWarn) {
2024-05-23 03:36:53 -03:00
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(id)}): ${replacement.match}`);
2024-05-19 22:49:58 -03:00
if (IS_DEV) {
logger.debug("Function Source:\n", code);
}
}
2024-05-19 22:49:58 -03:00
if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
code = previousCode;
2024-06-16 19:53:31 -03:00
patchedFactory = previousFactory;
2024-05-19 22:49:58 -03:00
patchedBy.delete(patch.plugin);
break;
}
2024-05-19 22:49:58 -03:00
continue;
}
2024-05-19 22:49:58 -03:00
code = newCode;
2024-06-16 19:53:31 -03:00
patchedFactory = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`);
2024-05-19 22:49:58 -03:00
} catch (err) {
2024-05-23 03:36:53 -03:00
logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err);
2024-05-19 22:49:58 -03:00
if (IS_DEV) {
const changeSize = code.length - lastCode.length;
const match = lastCode.match(replacement.match)!;
// Use 200 surrounding characters of context
const start = Math.max(0, match.index! - 200);
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
// (changeSize may be negative)
const endPatched = end + changeSize;
const context = lastCode.slice(start, end);
const patchedContext = code.slice(start, endPatched);
// inline require to avoid including it in !IS_DEV builds
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
let fmt = "%c %s ";
const elements = [] as string[];
for (const d of diff) {
const color = d.removed
? "red"
: d.added
? "lime"
: "grey";
fmt += "%c%s";
elements.push("color:" + color, d.value);
}
2024-05-19 22:49:58 -03:00
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
}
2024-05-19 22:49:58 -03:00
patchedBy.delete(patch.plugin);
2024-05-19 22:49:58 -03:00
if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
code = previousCode;
2024-06-16 19:53:31 -03:00
patchedFactory = previousFactory;
2024-05-19 22:49:58 -03:00
break;
}
code = lastCode;
2024-06-16 19:53:31 -03:00
patchedFactory = lastFactory;
}
}
2024-05-19 22:49:58 -03:00
if (!patch.all) patches.splice(i--, 1);
}
2024-05-19 22:49:58 -03:00
return patchedFactory;
}