From 6b0c02daa63a626698ab58310b024d666c406e1a Mon Sep 17 00:00:00 2001 From: v Date: Mon, 17 Feb 2025 02:10:02 +0100 Subject: [PATCH] fix issues & crashes caused by recent Discord changes (#3231) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- src/debug/runReporter.ts | 4 +- src/plugins/_core/settings.tsx | 3 ++ src/webpack/common/utils.ts | 4 +- src/webpack/patchWebpack.ts | 31 ++++++------ src/webpack/webpack.ts | 86 +++++++++++++++++++++++----------- 5 files changed, 82 insertions(+), 46 deletions(-) diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 8898242e0..2ca83b7fa 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -78,9 +78,9 @@ async function runReporter() { result = await Webpack.extractAndLoadChunks(code, matcher); if (result === false) result = null; } else if (method === "mapMangledModule") { - const [code, mapper] = args; + const [code, mapper, includeBlacklistedExports] = args; - result = Webpack.mapMangledModule(code, mapper); + result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports); if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail"); } else { // @ts-ignore diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index f48f38e03..a9e34f784 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -158,6 +158,9 @@ export default definePlugin({ aboveActivity: getIntlMessage("ACTIVITY_SETTINGS") }; + if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS")) + return firstChild === "PREMIUM"; + return header === names[settingsLocation]; } catch { return firstChild === "PREMIUM"; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index fe0668885..9ed1489c8 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -58,9 +58,9 @@ export const { match, P }: Pick = ma export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep"); export const i18n = mapMangledModuleLazy('defaultLocale:"en-US"', { + t: filters.byProps(runtimeHashMessageKey("DISCORD")), intl: filters.byProps("string", "format"), - t: filters.byProps(runtimeHashMessageKey("DISCORD")) -}); +}, true); export let SnowflakeUtils: t.SnowflakeUtils; waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 9c91eee9a..408d3b708 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -12,7 +12,7 @@ import { canonicalizeReplacement } from "@utils/patches"; import { Patch, PatchReplacement } from "@utils/types"; import { traceFunctionWithResults } from "../debug/Tracer"; -import { _initWebpack, _shouldIgnoreModule, factoryListeners, findModuleId, moduleListeners, waitForSubscriptions, wreq } from "./webpack"; +import { _blacklistBadModules, _initWebpack, factoryListeners, findModuleId, moduleListeners, waitForSubscriptions, wreq } from "./webpack"; import { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, ModuleExports, PatchedModuleFactory, WebpackRequire } from "./wreq.d"; export const patches = [] as Patch[]; @@ -190,6 +190,20 @@ define(Function.prototype, "m", { */ define(this, "m", { value: proxiedModuleFactories }); + + // Overwrite Webpack's defineExports function to define the export descriptors configurable. + // This is needed so we can later blacklist specific exports from Webpack search by making them non-enumerable + this.d = function (exports: object, getters: object) { + for (const key in getters) { + if (Object.hasOwn(getters, key) && !Object.hasOwn(exports, key)) { + Object.defineProperty(exports, key, { + enumerable: true, + configurable: true, + get: getters[key], + }); + } + } + }; }; } }); @@ -407,19 +421,8 @@ function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unkno return factoryReturn; } - if (typeof require === "function") { - const shouldIgnoreModule = _shouldIgnoreModule(exports); - - if (shouldIgnoreModule) { - if (require.c != null) { - Object.defineProperty(require.c, module.id, { - value: require.c[module.id], - enumerable: false, - configurable: true, - writable: true - }); - } - + if (typeof require === "function" && require.c) { + if (_blacklistBadModules(require.c, exports, module.id)) { return factoryReturn; } } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index e129b913d..30180a7e9 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -23,7 +23,7 @@ import { canonicalizeMatch } from "@utils/patches"; import { traceFunction } from "../debug/Tracer"; import { Flux } from "./common"; -import { AnyModuleFactory, ModuleExports, WebpackRequire } from "./wreq"; +import { AnyModuleFactory, AnyWebpackRequire, ModuleExports, WebpackRequire } from "./wreq"; const logger = new Logger("Webpack"); @@ -112,18 +112,38 @@ export function _initWebpack(webpackRequire: WebpackRequire) { // Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too const TypedArray = Object.getPrototypeOf(Int8Array); -function _shouldIgnoreValue(value: any) { +const PROXY_CHECK = "is this a proxy that returns values for any key?"; +function shouldIgnoreValue(value: any) { if (value == null) return true; if (value === window) return true; if (value === document || value === document.documentElement) return true; - if (value[Symbol.toStringTag] === "DOMTokenList") return true; + if (value[Symbol.toStringTag] === "DOMTokenList" || value[Symbol.toStringTag] === "IntlMessagesProxy") return true; + // Discord might export a Proxy that returns non-null values for any property key which would pass all findByProps filters. + // One example of this is their i18n Proxy. However, that is already covered by the IntlMessagesProxy check above. + // As a fallback if they ever change the name or add a new Proxy, use a unique string to detect such proxies and ignore them + if (value[PROXY_CHECK] !== void 0) { + // their i18n Proxy "caches" by setting each accessed property to the return, so try to delete + Reflect.deleteProperty(value, PROXY_CHECK); + return true; + } if (value instanceof TypedArray) return true; return false; } -export function _shouldIgnoreModule(exports: any) { - if (_shouldIgnoreValue(exports)) { +function makePropertyNonEnumerable(target: Object, key: PropertyKey) { + const descriptor = Object.getOwnPropertyDescriptor(target, key); + if (descriptor == null) return; + + Reflect.defineProperty(target, key, { + ...descriptor, + enumerable: false + }); +} + +export function _blacklistBadModules(requireCache: NonNullable, exports: ModuleExports, moduleId: PropertyKey) { + if (shouldIgnoreValue(exports)) { + makePropertyNonEnumerable(requireCache, moduleId); return true; } @@ -131,14 +151,16 @@ export function _shouldIgnoreModule(exports: any) { return false; } - let allNonEnumerable = true; + let hasOnlyBadProperties = true; for (const exportKey in exports) { - if (!_shouldIgnoreValue(exports[exportKey])) { - allNonEnumerable = false; + if (shouldIgnoreValue(exports[exportKey])) { + makePropertyNonEnumerable(exports, exportKey); + } else { + hasOnlyBadProperties = false; } } - return allNonEnumerable; + return hasOnlyBadProperties; } let devToolsOpen = false; @@ -406,7 +428,10 @@ export function findByCodeLazy(...code: CodeFilter) { * Find a store by its displayName */ export function findStore(name: StoreNameFilter) { - const res: any = Flux.Store.getAll().find(filters.byStoreName(name)); + const res = Flux.Store.getAll + ? Flux.Store.getAll().find(filters.byStoreName(name)) + : find(filters.byStoreName(name), { isIndirect: true }); + if (!res) handleModuleNotFound("findStore", name); return res; @@ -474,12 +499,27 @@ export function findExportedComponentLazy(...props: Prop }); } +function getAllPropertyNames(object: Object, includeNonEnumerable: boolean) { + const names = new Set(); + + const getKeys = includeNonEnumerable ? Object.getOwnPropertyNames : Object.keys; + do { + getKeys(object).forEach(name => names.add(name)); + object = Object.getPrototypeOf(object); + } while (object != null); + + return names; +} + /** * Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module) * then maps it into an easily usable module via the specified mappers. * * @param code The code to look for * @param mappers Mappers to create the non mangled exports + * @param includeBlacklistedExports Whether to include blacklisted exports in the search. + * These exports are dangerous. Accessing properties on them may throw errors + * or always return values (so a byProps filter will always return true) * @returns Unmangled exports as specified in mappers * * @example mapMangledModule("headerIdIsManaged:", { @@ -487,7 +527,7 @@ export function findExportedComponentLazy(...props: Prop * closeModal: filters.byCode("key==") * }) */ -export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record): Record { +export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record, includeBlacklistedExports = false): Record { const exports = {} as Record; const id = findModuleId(...Array.isArray(code) ? code : [code]); @@ -495,8 +535,9 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa return exports; const mod = wreq(id as any); + const keys = getAllPropertyNames(mod, includeBlacklistedExports); outer: - for (const key in mod) { + for (const key of keys) { const member = mod[key]; for (const newName in mappers) { // if the current mapper matches this module @@ -510,24 +551,13 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa }); /** - * {@link mapMangledModule}, lazy. - - * Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module) - * then maps it into an easily usable module via the specified mappers. - * - * @param code The code to look for - * @param mappers Mappers to create the non mangled exports - * @returns Unmangled exports as specified in mappers - * - * @example mapMangledModule("headerIdIsManaged:", { - * openModal: filters.byCode("headerIdIsManaged:"), - * closeModal: filters.byCode("key==") - * }) + * lazy mapMangledModule + * @see {@link mapMangledModule} */ -export function mapMangledModuleLazy(code: string | RegExp | CodeFilter, mappers: Record): Record { - if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]); +export function mapMangledModuleLazy(code: string | RegExp | CodeFilter, mappers: Record, includeBlacklistedExports = false): Record { + if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers, includeBlacklistedExports]]); - return proxyLazy(() => mapMangledModule(code, mappers)); + return proxyLazy(() => mapMangledModule(code, mappers, includeBlacklistedExports)); } export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/;