Skip to content

Commit

Permalink
Support script injections in frames
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JingMatrix committed Jan 21, 2025
1 parent 22edbf9 commit b95a094
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 21 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/assets/GM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions app/src/main/java/org/matrix/chromext/Chrome.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String>, tabId: String, bypassCSP: Boolean) {
wakeUpDevTools()
var client = DevSessions.new(tabId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/org/matrix/chromext/script/Local.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ object GM {

fun bootstrap(
script: Script,
codes: MutableList<String> = mutableListOf<String>()
codes: MutableList<String> = mutableListOf<String>(),
folder: String,
): MutableList<String> {
var code = script.code
var grants = ""
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down
24 changes: 19 additions & 5 deletions app/src/main/java/org/matrix/chromext/script/Manager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
val codes = mutableListOf<String>(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
Expand Down Expand Up @@ -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() {
Expand Down
31 changes: 20 additions & 11 deletions app/src/main/java/org/matrix/chromext/script/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.matrix.chromext.Chrome
private val blocksReg =
Regex(
"""(?<metablock>[\S\s]*?// ==UserScript==\r?\n[\S\s]*?\r?\n// ==/UserScript==\s+)(?<code>[\S\s]*)""")
private val metaReg = Regex("""^//\s+@(?<key>[\w-]+)\s+(?<value>.+)""")
private val metaReg = Regex("""^//\s+@(?<key>[\w-]+)(\s+(?<value>.+))?""")

fun parseScript(input: String, storage: String? = null): Script? {
val blockMatchGroup = blocksReg.matchEntire(input)?.groups
Expand All @@ -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
}
}
}
}
Expand Down Expand Up @@ -76,7 +84,8 @@ fun parseScript(input: String, storage: String? = null): Script? {
script.meta,
script.code,
script.storage,
lib)
lib,
script.noframes)
return parsed
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/org/matrix/chromext/script/SQLite.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ data class Script(
val code: String,
var storage: JSONObject?,
val lib: List<String> = mutableListOf<String>(),
val noframes: Boolean,
)

private const val SQL_CREATE_ENTRIES =
Expand Down

0 comments on commit b95a094

Please sign in to comment.