w3c / webextensions

Charter and administrivia for the WebExtensions Community Group (WECG)
Other
602 stars 56 forks source link

Proposal: userScripts.execute() method #477

Open newRoland opened 1 year ago

newRoland commented 1 year ago

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]

erosman commented 1 year ago

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.

newRoland commented 1 year ago

@erosman

Is this confirmed? I was under the impression that they deliberately stripped away that parameter to prevent the execution of remote code.

erosman commented 1 year ago

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.

userScripts

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.

scripting.ExecutionWorld

Specifies the execution environment of a script injected with scripting.executeScript() or registered with scripting.registerContentScripts().

newRoland commented 1 year ago

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.

Rob--W commented 12 months ago

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.

newRoland commented 12 months ago

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})
    }
}
brookhong commented 11 months ago

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.

ricobl commented 11 months ago

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.

newRoland commented 10 months ago

@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.

  1. Wait it out and hope this is implemented before June's deadline.
  2. Hope the deadline is extended again.
  3. If it's not a core feature, get rid of it.
  4. Update to MV3 and use the wrapped userScript approach.
oliverdunk commented 10 months ago

@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 :)

Rob--W commented 9 months ago

FYI: @EmiliaPaz has filed a PR with a proposal at https://github.com/w3c/webextensions/pull/540

newRoland commented 8 months ago

PR #540 has been approved.