2022-10-22 01:17:06 +02:00
/ *
2024-05-19 22:49:58 -03:00
* Vencord , a Discord client mod
2024-05-26 20:38:57 -03:00
* Copyright ( c ) 2024 Vendicated , Nuckyz , and contributors
2024-05-19 22:49:58 -03:00
* SPDX - License - Identifier : GPL - 3.0 - or - later
* /
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" ;
2024-05-26 05:19:52 -03:00
import { interpolateIfDefined } from "@utils/misc" ;
2024-05-24 05:14:27 -03:00
import { canonicalizeReplacement } from "@utils/patches" ;
2022-12-19 17:59:54 -05:00
import { PatchReplacement } from "@utils/types" ;
2022-11-28 13:37:55 +01:00
2023-02-08 17:54:11 -03:00
import { traceFunction } from "../debug/Tracer" ;
2024-05-02 18:52:41 -03:00
import { patches } from "../plugins" ;
2024-05-24 05:14:27 -03:00
import { _initWebpack , factoryListeners , ModuleFactory , moduleListeners , subscriptions , WebpackRequire , wreq } from "." ;
2024-05-28 17:11:17 -03:00
type AnyWebpackRequire = Partial < WebpackRequire > & Pick < WebpackRequire , "m" > ;
2024-05-24 05:14:27 -03:00
type PatchedModuleFactory = ModuleFactory & {
$$vencordOriginal? : ModuleFactory ;
} ;
2024-05-24 07:59:56 -03:00
type PatchedModuleFactories = Record < PropertyKey , PatchedModuleFactory > ;
2024-05-02 18:52:41 -03:00
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-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-05-26 20:22:03 -03:00
// wreq.m is the Webpack object containing module factories.
// We wrap it 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.
// wreq.m is pre-populated with module factories, and is also populated via webpackGlobal.push
// The sentry module also has their own Webpack with a pre-populated wreq.m, so this also patches the sentry module factories.
2024-05-28 17:11:17 -03:00
define ( Function . prototype , "m" , {
enumerable : false ,
2024-05-26 20:22:03 -03:00
set ( this : WebpackRequire , moduleFactories : PatchedModuleFactories ) {
// When using React DevTools or other extensions, we may also catch their Webpack here.
// This ensures we actually got the right ones.
const { stack } = new Error ( ) ;
2024-05-28 17:12:36 -03:00
if ( ! ( stack ? . includes ( "discord.com" ) || stack ? . includes ( "discordapp.com" ) ) || Array . isArray ( moduleFactories ) ) {
2024-05-28 17:11:17 -03:00
define ( this , "m" , { value : moduleFactories } ) ;
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-28 17:11:17 -03:00
if ( bundlePath !== "/assets/" ) return ;
logger . info ( "Main Webpack found" + interpolateIfDefined ` in ${ fileName } ` + ", initializing internal references to WebpackRequire" ) ;
_initWebpack ( this ) ;
clearTimeout ( setterTimeout ) ;
}
} ) ;
// setImmediate to clear this property setter if this is not the main Webpack.
// If this is the main Webpack, wreq.m will always be set before the timeout runs.
const setterTimeout = setTimeout ( ( ) = > Reflect . deleteProperty ( this , "p" ) , 0 ) ;
define ( moduleFactories , Symbol . toStringTag , {
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-05-28 17:14:35 -03:00
const proxiedModuleFactories = new Proxy ( moduleFactories , 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
Reflect . setPrototypeOf ( moduleFactories , new Proxy ( moduleFactories , moduleFactoriesHandler ) ) ;
* /
2024-05-28 17:14:35 -03:00
define ( this , "m" , { value : proxiedModuleFactories } ) ;
2024-05-28 17:11:17 -03:00
// Patch the pre-populated factories
for ( const id in moduleFactories ) {
defineModulesFactoryGetter ( id , Settings . eagerPatches ? patchFactory ( id , moduleFactories [ id ] ) : moduleFactories [ id ] ) ;
}
2024-05-26 20:22:03 -03:00
}
} ) ;
/ * *
2024-05-27 18:27:21 -03:00
* Define the getter for returning the patched version of the module factory .
2024-05-26 20:22:03 -03:00
*
2024-05-27 18:27:21 -03:00
* 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 .
2024-05-26 20:22:03 -03:00
*
* @param id The id of the module
* @param factory The original or patched module factory
* /
2024-05-26 00:27:13 -03:00
function defineModulesFactoryGetter ( id : PropertyKey , factory : PatchedModuleFactory ) {
2024-05-26 20:22:03 -03:00
// Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object
2024-05-27 18:27:21 -03:00
// have the patched version
2024-05-28 17:11:17 -03:00
for ( const wreq of allWebpackInstances ) {
define ( wreq . m , id , {
2024-05-26 00:27:13 -03:00
get ( ) {
// $$vencordOriginal means the factory is already patched
if ( factory . $ $vencordOriginal != null ) {
return factory ;
}
2024-05-23 06:04:21 -03:00
2024-05-26 00:27:13 -03:00
return ( factory = patchFactory ( id , factory ) ) ;
} ,
set ( v : ModuleFactory ) {
if ( factory . $ $vencordOriginal != null ) {
factory . $ $vencordOriginal = v ;
} else {
factory = v ;
}
2024-05-24 03:44:40 -03:00
}
2024-05-26 00:27:13 -03:00
} ) ;
}
2024-05-24 03:08:35 -03:00
}
2024-05-24 05:14:27 -03:00
const moduleFactoriesHandler : ProxyHandler < PatchedModuleFactories > = {
2024-05-27 18:27:21 -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 ;
} ,
2024-05-27 23:31:08 -03:00
// Same thing as get
has : ( target , p ) = > {
return false ;
}
2024-05-27 18:27:21 -03:00
* /
2024-05-26 20:22:03 -03:00
// The set trap for patching or defining getters for the module factories when new module factories are loaded
2024-05-24 03:08:35 -03:00
set : ( target , p , newValue , receiver ) = > {
2024-05-23 06:04:21 -03:00
// If the property is not a number, we are not dealing with a module factory
2024-05-24 03:08:35 -03:00
if ( Number . isNaN ( Number ( p ) ) ) {
2024-05-28 17:11:17 -03:00
return define ( target , p , { value : newValue } ) ;
2024-05-23 06:04:21 -03:00
}
2024-05-27 18:27:21 -03:00
const existingFactory = Reflect . get ( target , p , receiver ) ;
2024-05-24 03:44:40 -03:00
2024-05-24 03:08:35 -03:00
if ( ! Settings . eagerPatches ) {
2024-05-24 03:44:40 -03:00
// If existingFactory exists, its either wrapped in defineModuleFactoryGetter, or it has already been required
// so call Reflect.set with the new original and let the correct logic apply (normal set, or defineModuleFactoryGetter setter)
if ( existingFactory != null ) {
return Reflect . set ( target , p , newValue , receiver ) ;
}
2024-05-27 18:27:21 -03:00
// eagerPatches are disabled, so the factory argument should be the original
2024-05-26 00:27:13 -03:00
defineModulesFactoryGetter ( p , newValue ) ;
2024-05-24 03:08:35 -03:00
return true ;
2024-05-23 06:04:21 -03:00
}
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
// Check if this factory is already patched
2024-05-24 03:45:46 -03:00
if ( existingFactory ? . $ $vencordOriginal != null ) {
2024-05-24 03:44:40 -03:00
existingFactory . $ $vencordOriginal = newValue ;
2024-05-23 06:04:21 -03:00
return true ;
}
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
const patchedFactory = patchFactory ( p , newValue ) ;
2024-05-26 20:22:03 -03:00
// If multiple Webpack instances exist, when new a new module is loaded, it will be set in all the module factories objects.
// Because patches are only executed once, we need to set the patched version in all of them, to avoid the Webpack instance
// that uses the factory to contain the original factory instead of the patched, in case it was set first in another instance
2024-05-27 18:27:21 -03:00
defineModulesFactoryGetter ( p , patchedFactory ) ;
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
return true ;
2024-05-02 18:52:41 -03:00
}
2024-05-19 22:49:58 -03:00
} ;
2024-05-26 20:22:03 -03:00
/ * *
* Patches a module factory .
*
* The factory argument will become the patched version of the factory .
* @param id The id of the module
* @param factory The original or patched module factory
* @returns The wrapper for the patched module factory
* /
2024-05-23 06:04:21 -03:00
function patchFactory ( id : PropertyKey , factory : ModuleFactory ) {
2024-05-26 00:27:13 -03:00
const originalFactory = factory ;
2024-05-19 22:49:58 -03:00
for ( const factoryListener of factoryListeners ) {
2022-08-29 02:25:27 +02:00
try {
2024-05-26 00:27:13 -03:00
factoryListener ( originalFactory ) ;
2022-08-29 02:25:27 +02:00
} catch ( err ) {
2024-05-19 22:49:58 -03:00
logger . error ( "Error in Webpack factory listener:\n" , err , factoryListener ) ;
2022-08-29 02:25:27 +02:00
}
}
2024-05-22 06:08:28 -03:00
const patchedBy = new Set < string > ( ) ;
2023-10-26 21:21:21 +02:00
2024-05-31 00:21:15 -03:00
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code : string = "0," + String ( factory ) ;
2023-10-26 21:21:21 +02:00
2024-05-19 22:49:58 -03:00
for ( let i = 0 ; i < patches . length ; i ++ ) {
const patch = patches [ i ] ;
if ( patch . predicate && ! patch . predicate ( ) ) continue ;
2024-05-02 18:52:41 -03:00
2024-05-31 05:19:34 -03:00
// indexOf is faster than includes because it doesn't check if searchString is a RegExp
// https://github.com/moonlight-mod/moonlight/blob/53ae39d4010277f49f3b70bebbd27b9cbcdb1c8b/packages/core/src/patch.ts#L61
2024-05-19 22:49:58 -03:00
const moduleMatches = typeof patch . find === "string"
2024-05-31 05:19:34 -03:00
? code . indexOf ( patch . find ) !== - 1
: ( patch . find . global && ( patch . find . lastIndex = 0 ) , patch . find . test ( code ) ) ;
2023-10-26 21:21:21 +02:00
2024-05-19 22:49:58 -03:00
if ( ! moduleMatches ) continue ;
2024-05-02 18:52:41 -03:00
2024-05-19 22:49:58 -03:00
patchedBy . add ( patch . plugin ) ;
2024-05-02 18:52:41 -03:00
2024-05-19 22:49:58 -03:00
const executePatch = traceFunction ( ` patch by ${ patch . plugin } ` , ( match : string | RegExp , replace : string ) = > code . replace ( match , replace ) ) ;
const previousCode = code ;
2024-05-26 00:27:13 -03:00
const previousFactory = factory ;
2023-10-26 21:21:21 +02:00
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 [ ] ) {
if ( replacement . predicate && ! replacement . predicate ( ) ) continue ;
2023-10-26 21:21:21 +02:00
2024-05-19 22:49:58 -03:00
const lastCode = code ;
2024-05-26 00:27:13 -03:00
const lastFactory = factory ;
2023-10-26 21:21:21 +02:00
2024-05-19 22:49:58 -03:00
canonicalizeReplacement ( replacement , patch . plugin ) ;
2023-10-26 21:21:21 +02:00
2024-05-19 22:49:58 -03:00
try {
const newCode = executePatch ( replacement . match , replacement . replace as string ) ;
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 ) ;
}
}
2023-10-26 21:21:21 +02:00
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-05-26 00:27:13 -03:00
factory = previousFactory ;
2024-05-19 22:49:58 -03:00
patchedBy . delete ( patch . plugin ) ;
break ;
}
continue ;
2023-10-26 21:21:21 +02:00
}
2024-05-19 22:49:58 -03:00
code = newCode ;
2024-05-23 06:04:21 -03:00
factory = ( 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 ) ;
2023-10-26 21:21:21 +02:00
}
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 ) ;
2023-10-26 21:21:21 +02:00
}
2024-05-19 22:49:58 -03:00
patchedBy . delete ( patch . plugin ) ;
2023-10-26 21:21:21 +02:00
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-05-26 00:27:13 -03:00
factory = previousFactory ;
2024-05-19 22:49:58 -03:00
break ;
}
code = lastCode ;
2024-05-26 00:27:13 -03:00
factory = lastFactory ;
2024-05-02 18:52:41 -03:00
}
}
2024-05-19 22:49:58 -03:00
if ( ! patch . all ) patches . splice ( i -- , 1 ) ;
}
2023-11-22 01:23:21 -05:00
2024-05-28 03:57:56 -03:00
// The patched factory wrapper, define it in an object to preserve the name after minification
const patchedFactory : PatchedModuleFactory = {
PatchedFactory ( . . . args : Parameters < ModuleFactory > ) {
// Restore the original factory in all the module factories objects,
// because we want to make sure the original factory is restored properly, no matter what is the Webpack instance
2024-05-28 17:11:17 -03:00
for ( const wreq of allWebpackInstances ) {
define ( wreq . m , id , { value : patchedFactory.$$vencordOriginal } ) ;
2024-05-28 03:57:56 -03:00
}
2024-05-23 06:04:21 -03:00
2024-05-28 03:57:56 -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 } ` +
")"
) ;
_initWebpack ( require ) ;
} else if ( IS_DEV ) {
logger . error ( "WebpackRequire was not initialized, running modules without patches instead." ) ;
}
2024-05-26 05:28:34 -03:00
}
2024-05-28 03:57:56 -03:00
if ( IS_DEV ) {
return originalFactory . apply ( this , args ) ;
}
2024-05-26 05:28:34 -03:00
}
2024-05-02 18:52:41 -03:00
2024-05-28 03:57:56 -03:00
let factoryReturn : unknown ;
2024-05-19 22:49:58 -03:00
try {
2024-05-28 03:57:56 -03:00
// Call the patched factory
factoryReturn = factory . apply ( this , args ) ;
2024-05-19 22:49:58 -03:00
} catch ( err ) {
2024-05-28 03:57:56 -03:00
// Just re-throw Discord errors
if ( factory === originalFactory ) throw err ;
logger . error ( "Error in patched module factory" , err ) ;
return originalFactory . apply ( this , args ) ;
2024-05-19 22:49:58 -03:00
}
2024-05-02 18:52:41 -03:00
2024-05-28 03:57:56 -03:00
// Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it
exports = module ? . exports ;
if ( exports == null ) return factoryReturn ;
// There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance
2024-05-29 05:10:24 -03:00
if ( ( exports === window || exports ? . default === window ) && typeof require === "function" && require . c != null ) {
2024-05-28 17:11:17 -03:00
define ( require . c , id , {
2024-05-28 03:57:56 -03:00
value : require.c [ id ] ,
enumerable : false
} ) ;
return factoryReturn ;
}
for ( const callback of moduleListeners ) {
try {
2024-05-19 22:49:58 -03:00
callback ( exports , id ) ;
2024-05-28 03:57:56 -03:00
} catch ( err ) {
logger . error ( "Error in Webpack module listener:\n" , err , callback ) ;
2024-05-02 18:52:41 -03:00
}
2023-10-26 21:21:21 +02:00
}
2024-05-26 00:27:13 -03:00
2024-05-28 03:57:56 -03:00
for ( const [ filter , callback ] of subscriptions ) {
try {
2024-05-29 06:16:11 +02:00
if ( exports && filter ( exports ) ) {
2024-05-28 03:57:56 -03:00
subscriptions . delete ( filter ) ;
callback ( exports , id ) ;
} else if ( exports . default && filter ( exports . default ) ) {
subscriptions . delete ( filter ) ;
callback ( exports . default , id ) ;
}
} catch ( err ) {
logger . error ( "Error while firing callback for Webpack subscription:\n" , err , filter , callback ) ;
}
}
return factoryReturn ;
}
} . PatchedFactory ;
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
patchedFactory . toString = originalFactory . toString . bind ( originalFactory ) ;
patchedFactory . $ $vencordOriginal = originalFactory ;
2024-05-19 22:49:58 -03:00
return patchedFactory ;
2023-10-26 21:21:21 +02:00
}