2022-10-22 01:17:06 +02:00
/ *
2024-05-19 22:49:58 -03:00
* Vencord , a Discord client mod
* Copyright ( c ) 2024 Vendicated and contributors
* 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-19 22:49:58 -03:00
import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc" ;
2024-05-02 18:52:41 -03:00
import { canonicalizeMatch , 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-23 03:38:06 -03:00
import { _initWebpack , beforeInitListeners , factoryListeners , ModuleFactory , moduleListeners , OnChunksLoaded , subscriptions , WebpackRequire , wreq } from "." ;
2024-05-02 18:52:41 -03:00
const logger = new Logger ( "WebpackInterceptor" , "#8caaee" ) ;
const initCallbackRegex = canonicalizeMatch ( /{return \i\(".+?"\)}/ ) ;
2022-08-29 02:25:27 +02:00
2024-05-23 06:04:21 -03:00
const allProxiedModules = new Set < WebpackRequire [ "m" ] > ( ) ;
2024-05-23 07:09:03 -03:00
const modulesProxyHandler : ProxyHandler < WebpackRequire [ "m" ] > = {
2024-05-19 22:49:58 -03:00
. . . Object . fromEntries ( Object . getOwnPropertyNames ( Reflect ) . map ( propName = >
2024-05-23 03:42:58 -03:00
[ propName , ( . . . args : any [ ] ) = > Reflect [ propName ] ( . . . args ) ]
2024-05-19 22:49:58 -03:00
) ) ,
2024-05-23 03:10:59 -03:00
get : ( target , p ) = > {
2024-05-23 06:04:21 -03:00
const propValue = Reflect . get ( target , p ) ;
// If the property is not a number, we are not dealing with a module factory
// $$vencordOriginal means the factory is already patched, $$vencordRequired means it has already been required
// and replaced with the original
// @ts-ignore
if ( propValue == null || Number . isNaN ( Number ( p ) ) || propValue . $ $vencordOriginal != null || propValue . $ $vencordRequired === true ) {
return propValue ;
}
// This patches factories if eagerPatches are disabled
const patchedFactory = patchFactory ( p , propValue ) ;
Reflect . set ( target , p , patchedFactory ) ;
return patchedFactory ;
} ,
set : ( target , p , newValue ) = > {
// $$vencordRequired means we are resetting the factory to its original after being required
// If the property is not a number, we are not dealing with a module factory
if ( ! Settings . eagerPatches || newValue ? . $ $vencordRequired === true || Number . isNaN ( Number ( p ) ) ) {
return Reflect . set ( target , p , newValue ) ;
}
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
const existingFactory = Reflect . get ( target , p ) ;
// Check if this factory is already patched
2024-05-23 03:36:53 -03:00
// @ts-ignore
2024-05-23 06:04:21 -03:00
if ( existingFactory ? . $ $vencordOriginal === newValue ) {
return true ;
}
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
const patchedFactory = patchFactory ( p , newValue ) ;
// Modules are only patched once, so we need to set the patched factory on all the modules
for ( const proxiedModules of allProxiedModules ) {
Reflect . set ( proxiedModules , p , patchedFactory ) ;
}
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
return true ;
2024-05-19 22:49:58 -03:00
} ,
ownKeys : target = > {
const keys = Reflect . ownKeys ( target ) ;
for ( const key of UNCONFIGURABLE_PROPERTIES ) {
if ( ! keys . includes ( key ) ) keys . push ( key ) ;
2024-05-02 18:52:41 -03:00
}
2024-05-19 22:49:58 -03:00
return keys ;
2024-05-02 18:52:41 -03:00
}
2024-05-19 22:49:58 -03:00
} ;
2024-05-02 18:52:41 -03:00
// wreq.O is the webpack onChunksLoaded function
// Discord uses it to await for all the chunks to be loaded before initializing the app
// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it
Object . defineProperty ( Function . prototype , "O" , {
configurable : true ,
2024-05-23 03:39:59 -03:00
set ( this : WebpackRequire , onChunksLoaded : WebpackRequire [ "O" ] ) {
2024-05-02 18:52:41 -03:00
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
// This ensures we actually got the right one
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
2024-05-05 18:56:49 +02:00
const { stack } = new Error ( ) ;
if ( ( stack ? . includes ( "discord.com" ) || stack ? . includes ( "discordapp.com" ) ) && String ( this . e ) . includes ( "Promise.all" ) ) {
2024-05-02 18:52:41 -03:00
logger . info ( "Found main WebpackRequire.onChunksLoaded" ) ;
delete ( Function . prototype as any ) . O ;
const originalOnChunksLoaded = onChunksLoaded ;
2024-05-23 03:10:59 -03:00
onChunksLoaded = function ( result , chunkIds , callback , priority ) {
2024-05-02 18:52:41 -03:00
if ( callback != null && initCallbackRegex . test ( callback . toString ( ) ) ) {
Object . defineProperty ( this , "O" , {
value : originalOnChunksLoaded ,
2024-05-23 06:04:21 -03:00
configurable : true ,
enumerable : true ,
writable : true
2024-05-02 18:52:41 -03:00
} ) ;
2024-05-23 03:10:59 -03:00
const wreq = this ;
2024-05-02 18:52:41 -03:00
const originalCallback = callback ;
callback = function ( this : unknown ) {
logger . info ( "Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners" ) ;
_initWebpack ( wreq ) ;
for ( const beforeInitListener of beforeInitListeners ) {
beforeInitListener ( wreq ) ;
}
2023-10-25 14:26:10 +02:00
2024-05-02 18:52:41 -03:00
originalCallback . apply ( this , arguments as any ) ;
} ;
callback . toString = originalCallback . toString . bind ( originalCallback ) ;
arguments [ 2 ] = callback ;
2023-10-25 14:26:10 +02:00
}
2023-10-26 21:21:21 +02:00
2024-05-02 18:52:41 -03:00
originalOnChunksLoaded . apply ( this , arguments as any ) ;
2024-05-23 03:10:59 -03:00
} as WebpackRequire [ "O" ] ;
2023-10-26 21:21:21 +02:00
2024-05-02 18:52:41 -03:00
onChunksLoaded . toString = originalOnChunksLoaded . toString . bind ( originalOnChunksLoaded ) ;
2024-05-22 05:11:09 -03:00
// Returns whether a chunk has been loaded
Object . defineProperty ( onChunksLoaded , "j" , {
2024-05-23 03:36:53 -03:00
configurable : true ,
2024-05-23 03:38:06 -03:00
set ( v : OnChunksLoaded [ "j" ] ) {
2024-05-23 19:53:19 -03:00
function setValue ( target : any ) {
Object . defineProperty ( target , "j" , {
value : v ,
configurable : true ,
enumerable : true ,
writable : true
} ) ;
}
setValue ( onChunksLoaded ) ;
setValue ( originalOnChunksLoaded ) ;
2024-05-23 03:36:53 -03:00
}
2024-05-22 05:11:09 -03:00
} ) ;
2024-05-02 18:52:41 -03:00
}
Object . defineProperty ( this , "O" , {
value : onChunksLoaded ,
2024-05-23 06:04:21 -03:00
configurable : true ,
enumerable : true ,
writable : true
2024-05-02 18:52:41 -03:00
} ) ;
}
} ) ;
2024-05-19 22:49:58 -03:00
// wreq.m is the webpack object containing module factories.
2024-05-23 06:04:21 -03:00
// This 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 module factories object, so this also targets that
// We replace its prototype with our proxy, which is responsible for patching the module factories
2024-05-02 18:52:41 -03:00
Object . defineProperty ( Function . prototype , "m" , {
configurable : true ,
2024-05-23 03:39:59 -03:00
set ( this : WebpackRequire , originalModules : WebpackRequire [ "m" ] ) {
2024-05-02 18:52:41 -03:00
// When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one
2024-05-05 18:58:23 +02:00
const { stack } = new Error ( ) ;
2024-05-19 23:14:05 -03:00
if ( ( stack ? . includes ( "discord.com" ) || stack ? . includes ( "discordapp.com" ) ) && ! Array . isArray ( originalModules ) ) {
2024-05-05 18:58:23 +02:00
logger . info ( "Found Webpack module factory" , stack . match ( /\/assets\/(.+?\.js)/ ) ? . [ 1 ] ? ? "" ) ;
2024-05-19 22:49:58 -03:00
// The new object which will contain the factories
2024-05-23 06:04:21 -03:00
const proxiedModules : WebpackRequire [ "m" ] = { } ;
for ( const id in originalModules ) {
// If we have eagerPatches enabled we have to patch the pre-populated factories
if ( Settings . eagerPatches ) {
proxiedModules [ id ] = patchFactory ( id , originalModules [ id ] ) ;
} else {
proxiedModules [ id ] = originalModules [ id ] ;
}
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
// Clear the original object so pre-populated factories are patched if eagerPatches are disabled
delete originalModules [ id ] ;
2024-05-19 22:49:58 -03:00
}
2024-05-23 06:04:21 -03:00
// @ts-ignore
originalModules . $ $proxiedModules = proxiedModules ;
allProxiedModules . add ( proxiedModules ) ;
2024-05-23 07:09:03 -03:00
Object . setPrototypeOf ( originalModules , new Proxy ( proxiedModules , modulesProxyHandler ) ) ;
2024-05-02 18:52:41 -03:00
}
Object . defineProperty ( this , "m" , {
2024-05-19 22:49:58 -03:00
value : originalModules ,
2024-05-23 06:04:21 -03:00
configurable : true ,
enumerable : true ,
writable : true
2024-05-02 18:52:41 -03:00
} ) ;
}
} ) ;
2022-08-29 02:25:27 +02:00
2024-05-19 22:49:58 -03:00
let webpackNotInitializedLogged = false ;
2024-05-23 06:04:21 -03:00
function patchFactory ( id : PropertyKey , factory : ModuleFactory ) {
2024-05-19 22:49:58 -03:00
for ( const factoryListener of factoryListeners ) {
2022-08-29 02:25:27 +02:00
try {
2024-05-23 06:04:21 -03:00
factoryListener ( factory ) ;
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-23 06:04:21 -03:00
const originalFactory = factory ;
2024-05-22 06:08:28 -03:00
const patchedBy = new Set < string > ( ) ;
2023-10-26 21:21:21 +02:00
2024-05-19 22:49:58 -03:00
// Discords Webpack chunks for some ungodly reason contain random
// newlines. Cyn recommended this workaround and it seems to work fine,
// however this could potentially break code, so if anything goes weird,
// this is probably why.
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
// ever targets newer browsers, the minifier could potentially use this trick and
// cause issues.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
2024-05-23 06:04:21 -03:00
let code : string = "0," + factory . toString ( ) . replaceAll ( "\n" , "" ) ;
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-19 22:49:58 -03:00
const moduleMatches = typeof patch . find === "string"
? code . includes ( patch . find )
: 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 ) ) ;
2024-05-23 06:04:21 -03:00
const previousFactory = factory ;
2024-05-19 22:49:58 -03:00
const previousCode = code ;
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-23 06:04:21 -03:00
const lastFactory = factory ;
2024-05-19 22:49:58 -03:00
const lastCode = code ;
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 ` ) ;
2024-05-23 06:04:21 -03:00
factory = previousFactory ;
2024-05-19 22:49:58 -03:00
code = previousCode ;
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 ` ) ;
2024-05-23 06:04:21 -03:00
factory = previousFactory ;
2024-05-19 22:49:58 -03:00
code = previousCode ;
break ;
}
2024-05-23 06:04:21 -03:00
factory = lastFactory ;
2024-05-19 22:49:58 -03:00
code = lastCode ;
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-23 03:10:59 -03:00
const patchedFactory : ModuleFactory = ( module , exports , require ) = > {
2024-05-23 06:04:21 -03:00
// @ts-ignore
originalFactory . $ $vencordRequired = true ;
for ( const proxiedModules of allProxiedModules ) {
2024-05-23 19:53:19 -03:00
Reflect . set ( proxiedModules , id , originalFactory ) ;
2024-05-23 06:04:21 -03:00
}
2024-05-19 22:49:58 -03:00
if ( wreq == null && IS_DEV ) {
if ( ! webpackNotInitializedLogged ) {
webpackNotInitializedLogged = true ;
logger . error ( "WebpackRequire was not initialized, running modules without patches instead." ) ;
}
2023-11-22 01:23:21 -05:00
2024-05-23 06:04:21 -03:00
return void originalFactory ( module , exports , require ) ;
2024-05-19 22:49:58 -03:00
}
2023-10-26 21:21:21 +02:00
2024-05-19 22:49:58 -03:00
try {
2024-05-23 06:04:21 -03:00
factory ( module , exports , require ) ;
2024-05-19 22:49:58 -03:00
} catch ( err ) {
// Just rethrow Discord errors
2024-05-23 06:04:21 -03:00
if ( factory === originalFactory ) throw err ;
2024-05-02 18:52:41 -03:00
2024-05-19 22:49:58 -03:00
logger . error ( "Error in patched module" , err ) ;
2024-05-23 06:04:21 -03:00
return void originalFactory ( module , exports , require ) ;
2024-05-19 22:49:58 -03:00
}
2024-05-02 18:52:41 -03:00
2024-05-19 22:49:58 -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 ;
// There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance
if ( exports === window && require . c ) {
Object . defineProperty ( require . c , id , {
value : require.c [ id ] ,
configurable : true ,
2024-05-23 06:04:21 -03:00
enumerable : false ,
writable : true
2024-05-19 22:49:58 -03:00
} ) ;
return ;
}
2024-05-02 18:52:41 -03:00
2024-05-19 22:49:58 -03:00
for ( const callback of moduleListeners ) {
try {
callback ( exports , id ) ;
} catch ( err ) {
logger . error ( "Error in Webpack module listener:\n" , err , callback ) ;
}
}
2024-05-02 18:52:41 -03:00
2024-05-19 22:49:58 -03:00
for ( const [ filter , callback ] of subscriptions ) {
try {
if ( filter ( exports ) ) {
subscriptions . delete ( filter ) ;
callback ( exports , id ) ;
} else if ( exports . default && filter ( exports . default ) ) {
subscriptions . delete ( filter ) ;
callback ( exports . default , id ) ;
2024-05-02 18:52:41 -03:00
}
2024-05-19 22:49:58 -03:00
} catch ( err ) {
logger . error ( "Error while firing callback for Webpack subscription:\n" , err , filter , callback ) ;
2023-10-26 21:21:21 +02:00
}
}
2024-05-23 03:10:59 -03:00
} ;
2024-05-19 22:49:58 -03:00
2024-05-23 06:04:21 -03:00
patchedFactory . toString = originalFactory . toString . bind ( originalFactory ) ;
2024-05-19 22:49:58 -03:00
// @ts-ignore
2024-05-23 06:04:21 -03:00
patchedFactory . $ $vencordOriginal = originalFactory ;
2024-05-19 22:49:58 -03:00
return patchedFactory ;
2023-10-26 21:21:21 +02:00
}