2022-10-21 23:17:06 +00:00
/ *
2024-05-20 01:49:58 +00:00
* Vencord , a Discord client mod
* Copyright ( c ) 2024 Vendicated and contributors
* SPDX - License - Identifier : GPL - 3.0 - or - later
* /
2023-05-05 23:36:00 +00:00
import { Logger } from "@utils/Logger" ;
2024-05-20 01:49:58 +00:00
import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc" ;
2024-05-02 21:52:41 +00:00
import { canonicalizeMatch , canonicalizeReplacement } from "@utils/patches" ;
2022-12-19 22:59:54 +00:00
import { PatchReplacement } from "@utils/types" ;
2024-05-02 21:52:41 +00:00
import { WebpackInstance } from "discord-types/other" ;
2022-11-28 12:37:55 +00:00
2023-02-08 20:54:11 +00:00
import { traceFunction } from "../debug/Tracer" ;
2024-05-02 21:52:41 +00:00
import { patches } from "../plugins" ;
import { _initWebpack , beforeInitListeners , factoryListeners , module Listeners , subscriptions , wreq } from "." ;
const logger = new Logger ( "WebpackInterceptor" , "#8caaee" ) ;
const initCallbackRegex = canonicalizeMatch ( /{return \i\(".+?"\)}/ ) ;
2022-08-29 00:25:27 +00:00
2024-05-20 01:49:58 +00:00
const module sProxyhandler : ProxyHandler < any > = {
. . . Object . fromEntries ( Object . getOwnPropertyNames ( Reflect ) . map ( propName = >
[ propName , ( target : any , . . . args : any [ ] ) = > Reflect [ propName ] ( target , . . . args ) ]
) ) ,
get : ( target , p : string ) = > {
const mod = Reflect . get ( target , p ) ;
// If the property is not a module id, return the value of it without trying to patch
if ( mod == null || mod . $ $vencordOriginal != null || Number . isNaN ( Number ( p ) ) ) return mod ;
const patchedMod = patchFactory ( p , mod ) ;
Reflect . set ( target , p , patchedMod ) ;
return patchedMod ;
} ,
set : ( target , p , newValue ) = > Reflect . set ( target , p , newValue ) ,
ownKeys : target = > {
const keys = Reflect . ownKeys ( target ) ;
for ( const key of UNCONFIGURABLE_PROPERTIES ) {
if ( ! keys . includes ( key ) ) keys . push ( key ) ;
2024-05-02 21:52:41 +00:00
}
2024-05-20 01:49:58 +00:00
return keys ;
2024-05-02 21:52:41 +00:00
}
2024-05-20 01:49:58 +00:00
} ;
2024-05-02 21:52:41 +00: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 ,
set ( onChunksLoaded : any ) {
// 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 16:56:49 +00:00
const { stack } = new Error ( ) ;
if ( ( stack ? . includes ( "discord.com" ) || stack ? . includes ( "discordapp.com" ) ) && String ( this . e ) . includes ( "Promise.all" ) ) {
2024-05-02 21:52:41 +00:00
logger . info ( "Found main WebpackRequire.onChunksLoaded" ) ;
delete ( Function . prototype as any ) . O ;
const originalOnChunksLoaded = onChunksLoaded ;
onChunksLoaded = function ( this : unknown , result : any , chunkIds : string [ ] , callback : ( ) = > any , priority : number ) {
if ( callback != null && initCallbackRegex . test ( callback . toString ( ) ) ) {
Object . defineProperty ( this , "O" , {
value : originalOnChunksLoaded ,
configurable : true
} ) ;
const wreq = this as WebpackInstance ;
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 12:26:10 +00:00
2024-05-02 21:52:41 +00:00
originalCallback . apply ( this , arguments as any ) ;
} ;
callback . toString = originalCallback . toString . bind ( originalCallback ) ;
arguments [ 2 ] = callback ;
2023-10-25 12:26:10 +00:00
}
2023-10-26 19:21:21 +00:00
2024-05-02 21:52:41 +00:00
originalOnChunksLoaded . apply ( this , arguments as any ) ;
} ;
2023-10-26 19:21:21 +00:00
2024-05-02 21:52:41 +00:00
onChunksLoaded . toString = originalOnChunksLoaded . toString . bind ( originalOnChunksLoaded ) ;
}
Object . defineProperty ( this , "O" , {
value : onChunksLoaded ,
configurable : true
} ) ;
}
} ) ;
2024-05-20 01:49:58 +00:00
// wreq.m is the webpack object containing module factories.
// This is pre-populated with modules, and is also populated via webpackGlobal.push
2024-05-20 02:22:48 +00:00
// The sentry module also has their own webpack with a pre-populated modules object, so this also targets that
2024-05-20 01:49:58 +00:00
// We replace its prototype with our proxy, which is responsible for returning patched module factories containing our patches
2024-05-02 21:52:41 +00:00
Object . defineProperty ( Function . prototype , "m" , {
configurable : true ,
2024-05-20 01:49:58 +00:00
set ( originalModules : any ) {
2024-05-02 21:52:41 +00: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 16:58:23 +00:00
const { stack } = new Error ( ) ;
2024-05-20 02:14:05 +00:00
if ( ( stack ? . includes ( "discord.com" ) || stack ? . includes ( "discordapp.com" ) ) && ! Array . isArray ( originalModules ) ) {
2024-05-05 16:58:23 +00:00
logger . info ( "Found Webpack module factory" , stack . match ( /\/assets\/(.+?\.js)/ ) ? . [ 1 ] ? ? "" ) ;
2024-05-20 01:49:58 +00:00
// The new object which will contain the factories
const module s = Object . assign ( { } , originalModules ) ;
// Clear the original object so pre-populated factories are patched
for ( const propName in originalModules ) {
delete originalModules [ propName ] ;
}
Object . setPrototypeOf ( originalModules , new Proxy ( module s , module sProxyhandler ) ) ;
2024-05-02 21:52:41 +00:00
}
Object . defineProperty ( this , "m" , {
2024-05-20 01:49:58 +00:00
value : originalModules ,
2024-05-02 21:52:41 +00:00
configurable : true
} ) ;
}
} ) ;
2022-08-29 00:25:27 +00:00
2024-05-20 01:49:58 +00:00
let webpackNotInitializedLogged = false ;
function patchFactory ( id : string , mod : ( module : any , exports : any , require : WebpackInstance ) = > void ) {
for ( const factoryListener of factoryListeners ) {
2022-08-29 00:25:27 +00:00
try {
2024-05-20 01:49:58 +00:00
factoryListener ( mod ) ;
2022-08-29 00:25:27 +00:00
} catch ( err ) {
2024-05-20 01:49:58 +00:00
logger . error ( "Error in Webpack factory listener:\n" , err , factoryListener ) ;
2022-08-29 00:25:27 +00:00
}
}
2024-05-20 01:49:58 +00:00
const originalMod = mod ;
2024-05-22 09:08:28 +00:00
const patchedBy = new Set < string > ( ) ;
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00: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,
let code : string = "0," + mod . toString ( ) . replaceAll ( "\n" , "" ) ;
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
for ( let i = 0 ; i < patches . length ; i ++ ) {
const patch = patches [ i ] ;
if ( patch . predicate && ! patch . predicate ( ) ) continue ;
2024-05-02 21:52:41 +00:00
2024-05-20 01:49:58 +00:00
const module Matches = typeof patch . find === "string"
? code . includes ( patch . find )
: patch . find . test ( code ) ;
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
if ( ! module Matches ) continue ;
2024-05-02 21:52:41 +00:00
2024-05-20 01:49:58 +00:00
patchedBy . add ( patch . plugin ) ;
2024-05-02 21:52:41 +00:00
2024-05-20 01:49:58 +00:00
const executePatch = traceFunction ( ` patch by ${ patch . plugin } ` , ( match : string | RegExp , replace : string ) = > code . replace ( match , replace ) ) ;
const previousMod = mod ;
const previousCode = code ;
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00: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 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
const lastMod = mod ;
const lastCode = code ;
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
canonicalizeReplacement ( replacement , patch . plugin ) ;
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
try {
const newCode = executePatch ( replacement . match , replacement . replace as string ) ;
if ( newCode === code ) {
if ( ! patch . noWarn ) {
logger . warn ( ` Patch by ${ patch . plugin } had no effect (Module id is ${ id } ): ${ replacement . match } ` ) ;
if ( IS_DEV ) {
logger . debug ( "Function Source:\n" , code ) ;
}
}
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
if ( patch . group ) {
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } had no effect ` ) ;
mod = previousMod ;
code = previousCode ;
patchedBy . delete ( patch . plugin ) ;
break ;
}
continue ;
2023-10-26 19:21:21 +00:00
}
2024-05-20 01:49:58 +00:00
code = newCode ;
mod = ( 0 , eval ) ( ` // Webpack Module ${ id } - Patched by ${ [ . . . patchedBy ] . join ( ", " ) } \ n ${ newCode } \ n//# sourceURL=WebpackModule ${ id } ` ) ;
} catch ( err ) {
logger . error ( ` Patch by ${ patch . plugin } errored (Module id is ${ id } ): ${ replacement . match } \ n ` , err ) ;
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 19:21:21 +00:00
}
2024-05-20 01:49:58 +00: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 19:21:21 +00:00
}
2024-05-20 01:49:58 +00:00
patchedBy . delete ( patch . plugin ) ;
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
if ( patch . group ) {
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } errored ` ) ;
mod = previousMod ;
code = previousCode ;
break ;
}
mod = lastMod ;
code = lastCode ;
2024-05-02 21:52:41 +00:00
}
}
2024-05-20 01:49:58 +00:00
if ( ! patch . all ) patches . splice ( i -- , 1 ) ;
}
2023-11-22 06:23:21 +00:00
2024-05-20 01:49:58 +00:00
function patchedFactory ( module : any , exports : any , require : WebpackInstance ) {
if ( wreq == null && IS_DEV ) {
if ( ! webpackNotInitializedLogged ) {
webpackNotInitializedLogged = true ;
logger . error ( "WebpackRequire was not initialized, running modules without patches instead." ) ;
}
2023-11-22 06:23:21 +00:00
2024-05-20 01:49:58 +00:00
return void originalMod ( module , exports , require ) ;
}
2023-10-26 19:21:21 +00:00
2024-05-20 01:49:58 +00:00
try {
mod ( module , exports , require ) ;
} catch ( err ) {
// Just rethrow Discord errors
if ( mod === originalMod ) throw err ;
2024-05-02 21:52:41 +00:00
2024-05-20 01:49:58 +00:00
logger . error ( "Error in patched module" , err ) ;
return void originalMod ( module , exports , require ) ;
}
2024-05-02 21:52:41 +00:00
2024-05-20 01:49:58 +00: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 ,
writable : true ,
enumerable : false
} ) ;
return ;
}
2024-05-02 21:52:41 +00:00
2024-05-20 01:49:58 +00:00
for ( const callback of module Listeners ) {
try {
callback ( exports , id ) ;
} catch ( err ) {
logger . error ( "Error in Webpack module listener:\n" , err , callback ) ;
}
}
2024-05-02 21:52:41 +00:00
2024-05-20 01:49:58 +00: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 21:52:41 +00:00
}
2024-05-20 01:49:58 +00:00
} catch ( err ) {
logger . error ( "Error while firing callback for Webpack subscription:\n" , err , filter , callback ) ;
2023-10-26 19:21:21 +00:00
}
}
}
2024-05-20 01:49:58 +00:00
patchedFactory . toString = originalMod . toString . bind ( originalMod ) ;
// @ts-ignore
patchedFactory . $ $vencordOriginal = originalMod ;
return patchedFactory ;
2023-10-26 19:21:21 +00:00
}