From b95a094b99370d6b2ae2e98f84d9bff70b150203 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 21 Jan 2025 05:51:22 +0100 Subject: [PATCH] Support script injections in frames We use `Page.navigate` API to implement a simple frames injections, which might not work as expected when the script size is nearly 2M. `@noframes` is also implemented, which is by default effectively true for most scripts because developers must `@grant frames` in the main page to inject into its frames. Close #136 as completed. --- README.md | 1 + app/src/main/assets/GM.js | 6 ++-- .../main/java/org/matrix/chromext/Chrome.kt | 31 +++++++++++++++++++ .../chromext/devtools/WebSocketClient.kt | 6 ++++ .../java/org/matrix/chromext/script/Local.kt | 6 ++-- .../org/matrix/chromext/script/Manager.kt | 24 +++++++++++--- .../java/org/matrix/chromext/script/Parser.kt | 31 ++++++++++++------- .../java/org/matrix/chromext/script/SQLite.kt | 1 + 8 files changed, 85 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ed3ac016..3b3f950d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Currently, ChromeXt supports almost all [Tampermonkey APIs](https://www.tampermo 6. @grant: GM_setValue, GM_getValue (less powerful than GM.getValue), GM_listValues, GM_addValueChangeListener, GM_removeValueChangeListener, GM_setClipboard, GM_cookie, GM_notification, window.close 7. @require, @resource (without [Subresource Integrity](https://www.tampermonkey.net/documentation.php#api:Subresource_Integrity)) 8. @inject-into, @sandox: by default, imported scripts using `@require` can define/overwrite global JavaScript objects; one can set `@inject-into content` or `@sandbox DOM` to disable this behavior +9. @noframes: note that, to load UserScripts for frames, there must be a script with `@grant frames` on the main page These APIs are implemented differently from the official ones, please refer to the source files [Local.kt](app/src/main/java/org/matrix/chromext/script/Local.kt) and diff --git a/app/src/main/assets/GM.js b/app/src/main/assets/GM.js index 152e25cd..ca0e6836 100644 --- a/app/src/main/assets/GM.js +++ b/app/src/main/assets/GM.js @@ -1038,7 +1038,7 @@ GM.bootstrap = () => { if (ChromeXt.scripts.findIndex((e) => e.script.id == GM_info.script.id) != -1) return; - const row = /\/\/\s+@(\S+)\s+(.+)/g; + const row = /\/\/\s+@(\S+)([ \t]+(.+))?/g; const meta = GM_info.script; if (typeof meta.code != "function" && typeof ChromeXt != "undefined") { return; @@ -1047,8 +1047,8 @@ GM.bootstrap = () => { while ((match = row.exec(GM_info.scriptMetaStr.trim())) !== null) { if (meta[match[1]]) { if (typeof meta[match[1]] == "string") meta[match[1]] = [meta[match[1]]]; - meta[match[1]].push(match[2]); - } else meta[match[1]] = match[2]; + meta[match[1]].push(match[3]); + } else meta[match[1]] = match[3] ? match[3] : true; } for (const it of [ "include", diff --git a/app/src/main/java/org/matrix/chromext/Chrome.kt b/app/src/main/java/org/matrix/chromext/Chrome.kt index 60b31407..bd70ac0c 100644 --- a/app/src/main/java/org/matrix/chromext/Chrome.kt +++ b/app/src/main/java/org/matrix/chromext/Chrome.kt @@ -4,6 +4,7 @@ import android.app.NotificationChannel import android.app.NotificationChannelGroup import android.app.NotificationManager import android.content.Context +import android.net.Uri import android.net.http.HttpResponseCache import android.os.Build import android.os.Handler @@ -22,6 +23,7 @@ import org.matrix.chromext.hook.UserScriptHook import org.matrix.chromext.hook.WebViewHook import org.matrix.chromext.proxy.UserScriptProxy import org.matrix.chromext.script.Local +import org.matrix.chromext.script.ScriptDbManager import org.matrix.chromext.utils.Log import org.matrix.chromext.utils.XMLHttpRequest import org.matrix.chromext.utils.findField @@ -222,6 +224,35 @@ object Chrome { } } + fun injectFrames(tab: Any? = null) { + val url = getUrl(tab)!! + IO.submit { + val tabId = getTabId(tab, url) + wakeUpDevTools() + var client = DevSessions.new(tabId) + DevSessions.add(client) + client.command(null, "Page.enable", JSONObject()) + var frameId: String? = null + client.listen { + if (it.has("method")) { + val method = it.getString("method") + val params = it.getJSONObject("params") + if (method == "Page.frameScheduledNavigation" && params.getString("url") != url) { + frameId = params.getString("frameId") + } else if (method == "Page.frameDetached" && params.getString("frameId") == frameId) { + ScriptDbManager.invokeScript(url, tab, false).forEach { + client.command( + null, + "Page.navigate", + JSONObject().put("url", "javascript: ${Uri.encode(it)}").put("frameId", frameId)) + } + frameId = null + } + } + } + } + } + private fun evaluateJavascriptDevTools(codes: List, tabId: String, bypassCSP: Boolean) { wakeUpDevTools() var client = DevSessions.new(tabId) diff --git a/app/src/main/java/org/matrix/chromext/devtools/WebSocketClient.kt b/app/src/main/java/org/matrix/chromext/devtools/WebSocketClient.kt index 3a81e258..9bb98318 100644 --- a/app/src/main/java/org/matrix/chromext/devtools/WebSocketClient.kt +++ b/app/src/main/java/org/matrix/chromext/devtools/WebSocketClient.kt @@ -20,6 +20,7 @@ class DevToolClient(tabId: String) : LocalSocket() { private var cspBypassed = false private var id = 1 private var mClosed = false + private var listening = false init { connectDevTools(this) @@ -84,6 +85,11 @@ class DevToolClient(tabId: String) : LocalSocket() { } fun listen(callback: (JSONObject) -> Unit = { msg -> Log.d(msg.toString()) }) { + if (listening) { + Log.d("skip duplicated listen calls") + return + } + listening = true runCatching { while (!isClosed()) { val type = inputStream.read() diff --git a/app/src/main/java/org/matrix/chromext/script/Local.kt b/app/src/main/java/org/matrix/chromext/script/Local.kt index 2b4b76dc..6656fb75 100644 --- a/app/src/main/java/org/matrix/chromext/script/Local.kt +++ b/app/src/main/java/org/matrix/chromext/script/Local.kt @@ -35,7 +35,8 @@ object GM { fun bootstrap( script: Script, - codes: MutableList = mutableListOf() + codes: MutableList = mutableListOf(), + folder: String, ): MutableList { var code = script.code var grants = "" @@ -47,6 +48,7 @@ object GM { script.grant.forEach { when (it) { "none" -> return@forEach + "frames" -> return@forEach "GM_info" -> return@forEach "GM.ChromeXt" -> return@forEach "window.close" -> return@forEach @@ -81,7 +83,7 @@ object GM { code = "\ndelete window.__loading__;\n${code};" code = localScript.get("globalThis")!! + script.lib.joinToString("\n") + code codes.add( - "(()=>{ const GM = {key:${Local.key}, name:'${Local.name}'}; const GM_info = ${GM_info}; GM_info.script.code = (key=null) => {${code}};\n${grants}GM.bootstrap();})();\n//# sourceURL=local://ChromeXt/${Uri.encode(script.id)}") + "(()=>{ const GM = {key:${Local.key}, name:'${Local.name}'}; const GM_info = ${GM_info}; GM_info.script.code = (key=null) => {${code}};\n${grants}GM.bootstrap();})();\n//# sourceURL=local://${folder}/${Uri.encode(script.id)}") return codes } } diff --git a/app/src/main/java/org/matrix/chromext/script/Manager.kt b/app/src/main/java/org/matrix/chromext/script/Manager.kt index c2980657..0f74638c 100644 --- a/app/src/main/java/org/matrix/chromext/script/Manager.kt +++ b/app/src/main/java/org/matrix/chromext/script/Manager.kt @@ -94,10 +94,15 @@ object ScriptDbManager { codes.add("fixEncoding();") } - fun invokeScript(url: String, webView: Any? = null) { + fun invokeScript( + url: String, + webView: Any? = null, + evaluate: Boolean = true + ): MutableList { val codes = mutableListOf(Local.initChromeXt) val path = resolveContentUrl(url) val webSettings = webView?.invokeMethod { name == "getSettings" } + val localFolder = if (evaluate) "ChromeXt" else "ChromeXt_DryRun" var trustedPage = true // Whether ChromeXt is accessible in the global context @@ -144,15 +149,24 @@ object ScriptDbManager { } else if (runScripts) { codes.add("Symbol.ChromeXt.lock(${Local.key}, '${Local.name}');") } - codes.add("//# sourceURL=local://ChromeXt/init") + codes.add("//# sourceURL=local://${localFolder}/init") val code = codes.joinToString("\n") webSettings?.invokeMethod(true) { name == "setJavaScriptEnabled" } - Chrome.evaluateJavascript(listOf(code), webView, bypassSandbox, bypassSandbox) + if (evaluate) Chrome.evaluateJavascript(listOf(code), webView, bypassSandbox, bypassSandbox) if (runScripts) { codes.clear() - scripts.filter { matching(it, url) }.forEach { GM.bootstrap(it, codes) } - Chrome.evaluateJavascript(codes) + if (!evaluate) codes.add(code) + var framesGranted = false + scripts + .filter { matching(it, url) && !(!evaluate && it.noframes) } + .forEach { + if (it.grant.contains("frames")) framesGranted = true + GM.bootstrap(it, codes, localFolder) + } + if (evaluate) Chrome.evaluateJavascript(codes) + if (framesGranted && evaluate) Chrome.injectFrames(webView) } + return codes } fun updateScriptStorage() { diff --git a/app/src/main/java/org/matrix/chromext/script/Parser.kt b/app/src/main/java/org/matrix/chromext/script/Parser.kt index 4b4fa1b4..794b1d53 100644 --- a/app/src/main/java/org/matrix/chromext/script/Parser.kt +++ b/app/src/main/java/org/matrix/chromext/script/Parser.kt @@ -11,7 +11,7 @@ import org.matrix.chromext.Chrome private val blocksReg = Regex( """(?[\S\s]*?// ==UserScript==\r?\n[\S\s]*?\r?\n// ==/UserScript==\s+)(?[\S\s]*)""") -private val metaReg = Regex("""^//\s+@(?[\w-]+)\s+(?.+)""") +private val metaReg = Regex("""^//\s+@(?[\w-]+)(\s+(?.+))?""") fun parseScript(input: String, storage: String? = null): Script? { val blockMatchGroup = blocksReg.matchEntire(input)?.groups @@ -30,20 +30,28 @@ fun parseScript(input: String, storage: String? = null): Script? { val meta = (blockMatchGroup[1]?.value as String) val code = blockMatchGroup[2]?.value as String var storage: JSONObject? = null + var noframes = false } script.meta.split("\n").forEach { val metaMatchGroup = metaReg.matchEntire(it)?.groups if (metaMatchGroup != null) { val key = metaMatchGroup[1]?.value as String - val value = metaMatchGroup[2]?.value as String - when (key) { - "name" -> script.name = value.replace(":", "") - "namespace" -> script.namespace = value - "match" -> script.match.add(value) - "include" -> script.match.add(value) - "grant" -> script.grant.add(value) - "exclude" -> script.exclude.add(value) - "require" -> script.require.add(value) + if (metaMatchGroup[2] != null) { + val value = metaMatchGroup[3]?.value as String + when (key) { + "name" -> script.name = value.replace(":", "") + "namespace" -> script.namespace = value + "match" -> script.match.add(value) + "include" -> script.match.add(value) + "grant" -> script.grant.add(value) + "exclude" -> script.exclude.add(value) + "require" -> script.require.add(value) + "noframes" -> script.noframes = true + } + } else { + when (key) { + "noframes" -> script.noframes = true + } } } } @@ -76,7 +84,8 @@ fun parseScript(input: String, storage: String? = null): Script? { script.meta, script.code, script.storage, - lib) + lib, + script.noframes) return parsed } } diff --git a/app/src/main/java/org/matrix/chromext/script/SQLite.kt b/app/src/main/java/org/matrix/chromext/script/SQLite.kt index c9e0d63e..e32b7d1d 100644 --- a/app/src/main/java/org/matrix/chromext/script/SQLite.kt +++ b/app/src/main/java/org/matrix/chromext/script/SQLite.kt @@ -14,6 +14,7 @@ data class Script( val code: String, var storage: JSONObject?, val lib: List = mutableListOf(), + val noframes: Boolean, ) private const val SQL_CREATE_ENTRIES =