Proposal: userScripts.execute() method
newRoland opened this issue ยท 21 comments
The user-scripts proposal mentions this as a potential future enhancement, nevertheless, I'm creating a formal proposal so that it can be discussed and tracked.
Context
The userScripts API addresses a significant gap that MV3 created for extensions that need to execute user-supplied JavaScript. Nonetheless, a gap persists for userscripts not driven by URLs. Many extensions activate userscripts through varied methods including gestures, keyboard shortcuts, context menus, voice, and more.
Issue was first mentioned by @Robbendebiene.
Proposal
A method to execute user-supplied javascript on the fly.
browser.userScripts.execute({code: "", world, allFrames, etc})
Security Considerations
The current User Scripts proposal already allows this capability. The extension can register the user script to run on all domains, and call a wrapped userscript on demand. For example, when the user triggers a gesture. This is inefficient and it would be harmful to users if developers had to resort to that.
A few extensions that would need this.
CrรMouse Chromeโข Gestures
Vimium [1]
AutoHotkey
modern scroll
Script Runner Pro
Foxy Gestures
Gesturefy
smartUp Gestures
Tampermonkey, Violentmonkey (edge cases like RegExp)
Surfingkeys [1]
Automa [1]
A method to execute user scripts on the fly.
browser.userScripts.execute({code: ""})
Firefox only userScripts in MV2 is replaced by scripting API in MV3 which has scripting.executeScript() method.
It is my understanding that the ability to pass a string
to the scripting
API is already agreed upon. Therefore, all its applicable methods should have the same ability.
Is this confirmed? I was under the impression that they deliberately stripped away that parameter to prevent the execution of remote code.
Although, I don't see executeScript()
mentioned in the proposal, I cant imagine that it would be allowed in register()
method and not in executeScript()
.
Userscript managers also require executeScript()
for their functions.
It would be good to mention it in the user scripts proposal.
Footnote
While the User Scripts API mentions userScripts
namespace, I am under the impression that implementation would be under the original scripting
namespace.
Note: When using Manifest V3 or higher, use scripting.registerContentScripts() to register scripts.
There will be an additional 'USER_SCRIPT' option in the scripting.ExecutionWorld
.
Specifies the execution environment of a script injected with scripting.executeScript() or registered with scripting.registerContentScripts().
While the User Scripts API mentions userScripts namespace, I am under the impression that implementation would be under the original scripting namespace.
For Chromium, the getScripts, register, unregister, and update methods have been implemented under the userScripts namespace.
Preface: I am focusing the discussion to the userScripts API, not the scripting API, given their separate use cases.
What is the request here? The ability to run arbitrary code through the userScripts
API? Or the ability for user script managers to run a new user script in already-loaded documents?
For the latter, rather than a userScripts.execute
method that mimics tabs.executeScript
, I think that it would make more sense to allow a user script to be executed again in a specific context. That ensures that a user script will only run in contexts where a registered user script would usually run. Otherwise issue #8 would be encountered.
Use case is for extensions where users can execute user-supplied code through gestures, shortcut keys, etc. You can never predict when, or where the user will trigger the gesture, or shortcut, so it's not possible to bind it to a specific URL. I think userScripts.execute() is the right approach here.
To avoid issue #8, the userScripts.execute() function could take a documentId, and that way the developer can execute on a specific document.
Since MessageSender includes documentId, the flow is straightforward.
// content script where gesture, shortcut, voice command, etc is triggered.
browser.runtime.sendMessage({action: "ExecuteUserScript", id: "foo"})
// background
function onMessage(msg, sender, sendResponse) {
if (msg.action === "ExecuteUserScript") {
const code = await getUserScriptFromStorage(msg.id)
userScripts.execute({code, documentId: sender.documentId})
}
}
What is the request here? The ability to run arbitrary code through the
userScripts
API? Or the ability for user script managers to run a new user script in already-loaded documents?
What is the difference?
Both are almost same to me. An extension user wants to create his/her own script(arbitrary code, also as a new user script) to be executed in the context of the extension(could be a user script manager) on document loaded.
I'd like to highlight a use case where something like userScripts.execute()
would be useful.
Powerlet is an interface to search and execute scripts from bookmarklets. The user will click the extension button or use a keyboard shortcut to activate the extension popup, search from there and execute a particular script. This is equivalent to clicking on a bookmark button on the browser's bookmarks toolbar.
The project has an issue mentioning migration to MV3.
I've been experimenting with MV3, it might be my lack of experience but I don't see a way to support similar extensions with the current userScripts
API as execution is limited to page load. (Which is what I believe this issue refers to.)
I believe that the scripting
API for MV3 doesn't work either as it requires either a script file to be packaged with the extension or a function to be provided which will then be serialized / deserialized. Any form of converting a bookmarklet URL to a function will be blocked (think eval(code)
or new Function(code)
).
It'd be nice to continue being able to extend the browser like that, I see this no less secure than running scripts on page load, even less intrusive IMO.
@oliverdunk Any chance we get this before Chrome's MV2 deadline in June? Many MV2 Chrome extensions have features that require this API.
Current options for extensions that require this API.
- Wait it out and hope this is implemented before June's deadline.
- Hope the deadline is extended again.
- If it's not a core feature, get rid of it.
- Update to MV3 and use the wrapped userScript approach.
@newRoland, as you mention there is a wrapped userScript approach which should be sufficient as a workaround here. With that in mind, we're not considering this a blocker, and would suggest using the workaround for migration in the short term. There's definitely still a chance a way of doing one-time injection lands in time, since we're planning to do further work on the userScripts API over the next few months, I just can't make any promises :)
FYI: @EmiliaPaz has filed a PR with a proposal at #540
(updated supportive labels - Chrome plans to work on this in 2025Q1 and Firefox tracks the work at https://bugzilla.mozilla.org/show_bug.cgi?id=1930776)
@EmiliaPaz While I was taking a closer look at userScripts.execute
, I noticed that takes only one ScriptSource
in its js
parameter. Was it a conscious decision to specify js: ScriptSource
instead of js: ScriptSource[]
?
For comparison:
userScripts.register
takesjs: ScriptSource[]
scripting.executeScript
takesfiles: string[]
- MV2's
tabs.executeScript
tookcode: string
If the use case is simulating execution of user scripts, then it may make sense to accept an array of ScriptSource[]
. This would also be more consistent with the scripting.executeScript
API.
On the other hand, an extension wishing to execute more than one script could always call userScripts.execute
repeatedly.
I'm about to start the implementation of userScripts.execute
in Firefox, and wonder whether we should support an array of ScriptSource in the API.
IMHO, userScripts.execute
should be considered as one-off version of userScripts.register
. It should be identical in features, without the timed requirements (e.g. id
, matches
, excludeMatches
, includeGlobs
, excludeGlobs
) with the possible addition of documentId
.
On the other hand, an extension wishing to execute more than one script could always call userScripts.execute repeatedly.
Often user-script managers have to inject multiple scripts together (e.g. from @require
) and into the same context or world and in a specific order.
Multiple async operations, especially when scripts have to run together or depend on each other, can have unexpected outcome.
re: https://github.com/w3c/webextensions/blob/main/proposals/user-scripts-execute-api.md
dictionary InjectionTarget {
// Whether the script should inject into all frames within the tab. Defaults
// to false. This must not be true if `frameIds` is specified.
allFrames?: boolean,
// The IDs of specific documentIds to inject into. This must not be set if
// frameIds is set.
documentIds?: string[],
// The IDs of specific frames to inject into.
frameIds?: number[],
// The ID of the tab into which to inject.
tabId: number,
}
I would suggest precedence instead of rejection. e.g.
dictionary InjectionTarget {
// Whether the script should inject into all frames within the tab. Defaults
// to false.This must not be true ifIf true,frameIds
is specified.frameIds
is ignored.
allFrames?: boolean,
// The IDs of specific documentIds to inject into.This must not be set if
//frameIds is set.If set,frameIds
is ignored.
documentIds?: string[],
// The IDs of specific frames to inject into.
frameIds?: number[],
// The ID of the tab into which to inject.
tabId: number,
}
I would suggest precedence instead of rejection. e.g.
dictionary InjectionTarget {
// Whether the script should inject into all frames within the tab. Defaults
// to false.This must not be true ifIf true,frameIds
is specified.frameIds
is ignored.
allFrames?: boolean,
// The IDs of specific documentIds to inject into.This must not be set if
//frameIds is set.If set,frameIds
is ignored.
This suggested behavior does not look appealing to me. I would not replace the "reject invalid input" with "ignore some properties when invalid".
Rejecting allows extension authors to detect invalid input, and allows the API to be expanded to consider not rejecting in the future. For example, a potentially valid use of "allFrames: true, frameIds: [1, 3]` could be to to run in frames 1 and 3 and their sub frames.
It was only a suggestion. However, for better context .....
with "ignore some properties when invalid".
The property is not invalid. One property simply is a sub-set of another.
Property A means all, while property B means some. Property A has a wider implication than property B.
Overriding one property when another property is used, and/or implying precedence is nothing new. It is quite the established M.O. in CSS.
The commonly used Matching URL patterns is an example where exclude_matches
and exclude_globs
properties override and have a higher precedence than matches
and include_globs
properties.
"matches": ["https://example.com/test/*"],
"exclude_matches": ["*://example.com/*"],
Setting excludes that completely override includes is common when setting global excludes in extensions.
It is also found in proxy.settings: passthrough and browser's Network Settings.
Looks like it's launching in Chrome soon (Chrome 134).
https://developer.chrome.com/blog/extension-news-january-2025#userscriptsexecute_in_canary
This is implemented in Chrome Canary as mentioned by @newRoland. We've enabled it by default starting in Chrome 135: https://chromium-review.googlesource.com/c/chromium/src/+/6262015
Copying my comment from last month from #745 (review) :
Can we specify what the behavior should be when a runtime error occurs while executing one of the ScriptSource
s?
Options:
- Interrupt execution and propagate error to the caller.
- Log error and continue executing the next script.
Since executeScript
is specified to propagate the result to the caller, it would make sense to propagate errors.
On the other hand, if registered declaratively, the expected behavior is to run them all (and uncaught errors be reported to the console).
There is something to say for either approach.
For comparison, where I checked Chrome's current scripting.executeScript
implementation will eventually be handled by the generic logic that is shared between declarative scripts and executeScript
(ScriptInjection::InjectJs
. This will ultimately just execute all scripts and swallow errors (in WebScriptExecutor::Execute
). Now that I see this code, I guess that it also explains why scripting.executeScript
does not expose errors in Chrome (https://issues.chromium.org/issues/40205757).