Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Multiple user script worlds #560

Merged
merged 8 commits into from
Apr 26, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 285 additions & 0 deletions proposals/multiple_user_script_worlds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# Proposal: Allow multiple user script worlds

**Summary**

Allow developers to configure and use multiple user script worlds in the
`browser.userScripts` API.

**Document Metadata**

**Author:** rdcronin

**Sponsoring Browser:** Google Chrome

**Contributors:** N/A

**Created:** 2024-03-07

**Related Issues:** TODO
dotproto marked this conversation as resolved.
Show resolved Hide resolved

## Motivation

### Objective

User scripts are (usually small) scripts that enable myriad use cases by
injecting scripts into different web pages. A user may have many different
user scripts that execute on the same page; however, these scripts are
conceptually distinct from one another and should generally not interact with
each other.

Today, user script managers take various steps to isolate user scripts as they
are injected, since all user scripts will run either in the context of the
main world or in a single user script world for the extension. This API would
allow user script managers to instead have multiple user script worlds, each
isolated from one another, to reduce the likelihood of user scripts negatively
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
affecting one another through collisions.

#### Use Cases

The primary use case is user script managers that handle multiple, independent
script injections in a single page.

### Known Consumers

We anticipate user script managers such as Tampermonkey, Violentmonkey, and
others to use this new capability.

## Specification

### Schema

This change would modify existing schemas.

Relevant methods and types:

```
export namespace userScripts {
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
export interface WorldProperties {
/**
* Specifies the world csp. The default is the `` `ISOLATED` `` world csp.
*/
csp?: string;
xeenon marked this conversation as resolved.
Show resolved Hide resolved

/**
* Specifies whether messaging APIs are exposed. The default is `false`.
*/
messaging?: boolean;
}

/**
* Properties for a registered user script.
*/
export interface RegisteredUserScript {
/**
* If true, it will inject into all frames, even if the frame is not the
* top-most frame in the tab. Each frame is checked independently for URL
* requirements; it will not inject into child frames if the URL
* requirements are not met. Defaults to false, meaning that only the top
* frame is matched.
*/
allFrames?: boolean;
/**
* Specifies wildcard patterns for pages this user script will NOT be
* injected into.
*/
excludeGlobs?: string[];
/**
* Excludes pages that this user script would otherwise be injected into.
* See Match Patterns for more details on the syntax of these strings.
*/
excludeMatches?: string[];
/**
* The ID of the user script specified in the API call. This property
* must not start with a '_' as it's reserved as a prefix for generated
* script IDs.
*/
id: string;
/**
* Specifies wildcard patterns for pages this user script will be
* injected into.
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
*/
includeGlobs?: string[];
/**
* The list of ScriptSource objects defining sources of scripts to be
* injected into matching pages.
*/
js: ScriptSource[];
/**
* Specifies which pages this user script will be injected into. See
* Match Patterns for more details on the syntax of these strings. This
* property must be specified for ${ref:register}.
*/
matches?: string[];
/**
* Specifies when JavaScript files are injected into the web page. The
* preferred and default value is document_idle
*/
runAt?: RunAt;
/**
* The JavaScript execution environment to run the script in. The default
* is `USER_SCRIPT`
*/
world?: ExecutionWorld;
xeenon marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

We will add a new property, `worldId`, to the `WorldProperties` and
`RegisteredUserScript` types, as below.

```
export namespace userScripts {
export interface WorldProperties {
/**
+ * Specifies the ID of the specific user script world to update.
+ * If not provided, updates the properties of the default user script world.
+ */
+ worldId?: string;

/**
* Specifies the world csp. The default is the `ISOLATED` world csp.
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
*/
csp?: string;

/**
* Specifies whether messaging APIs are exposed. The default is `false`.
*/
messaging?: boolean;
}

/**
* Properties for a registered user script.
*/
export interface RegisteredUserScript {
/**
* If true, it will inject into all frames, even if the frame is not the
* top-most frame in the tab. Each frame is checked independently for URL
* requirements; it will not inject into child frames if the URL
* requirements are not met. Defaults to false, meaning that only the top
* frame is matched.
*/
allFrames?: boolean;
/**
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
* Specifies wildcard patterns for pages this user script will NOT be
* injected into.
*/
excludeGlobs?: string[];
/**
* Excludes pages that this user script would otherwise be injected into.
* See Match Patterns for more details on the syntax of these strings.
*/
excludeMatches?: string[];
/**
* The ID of the user script specified in the API call. This property
* must not start with a '_' as it's reserved as a prefix for generated
* script IDs.
*/
id: string;
/**
* Specifies wildcard patterns for pages this user script will be
* injected into.
*/
includeGlobs?: string[];
/**
* The list of ScriptSource objects defining sources of scripts to be
* injected into matching pages.
*/
js: ScriptSource[];
/**
* Specifies which pages this user script will be injected into. See
* Match Patterns for more details on the syntax of these strings. This
* property must be specified for ${ref:register}.
*/
matches?: string[];
/**
* Specifies when JavaScript files are injected into the web page. The
* preferred and default value is document_idle
*/
runAt?: RunAt;
/**
* The JavaScript execution environment to run the script in. The default
* is `USER_SCRIPT`
*/
world?: ExecutionWorld;
xeenon marked this conversation as resolved.
Show resolved Hide resolved

+ /**
+ * If specified, specifies a specific user script world ID to execute in.
+ * Only valid if `world` is omitted or is `USER_SCRIPT`. If omitted, the
+ * script will execute in the default user script world.
+ */
+ worldId?: string;
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

Note that the signatures of the related functions, including `configureWorld()`,
`register()`, and others are left unchanged.

When the developer specifies a `worldId` in either the `WorldProperties` or
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
a `RegisteredUserScript`, the browser will create a separate user script world
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important design question: what are the persistence/lifetime characteristics of worldId + configureWorld? worldId should not persist across extension updates for consistency with the existing userScripts API design (https://developer.chrome.com/docs/extensions/reference/api/userScripts#extension_updates), but there is still an open question of the lifetime of worldId (and consequently, potentially reduce the duration of persistence).

Previously, configureWorld would have a limited scope, exactly one. Now, with the introduction of worldId, there may be an unbounded number. According to the current proposal, the concept of a specific world (identified by worldId) becomes concrete whenever it is used in RegisteredUserScript or WorldProperties. Because the number of worlds is now dynamic, we should clarify when a worldId exists and when it ceases to exist. As currently written, worldIds are instantiated and never discarded.

This section seems to have been written with userScripts.register in mind. The issue that I'm thinking of would have become much more obvious if the proposal explicitly spelled out the expected behavioral change (or non-change) per method (from the proposal process perspective - I think that it would be a good idea for future proposals to exhaustively enumerate all uses of a type to make sure that the behavior is fully specified where needed):

  • userScripts.register() - may introduce a worldId. Does not necessarily have to do anything until the script is used in the renderer (unless there is a desire to broadcast all known worldIds?).
  • userScripts.configureWorld() - may create a persistent configuration for a world. What if the worldId does not exist yet?
  • userScripts.getScripts() - does this have to "create a separate user script world"?
  • userScripts.unregister() - when the last use of worldId disappears, would that delete the configuration?
  • userScripts.update() - when the worldId changes, and the previous one was the last one, does that delete the registered configured worlds?

I think that there are roughly two ways to address this issue (not mutually exclusive):

  1. Explicit world management (userScripts.registerWorld + userScripts.unregisterWorld). Using an unregistered worldId in RegisteredUserScript or WorldProperties is an error.
  2. Automatic world cleanup: a world configuration is guaranteed to persist when there is at least one RegisteredUserScript that references it. What happens when there is no reference to it is more interesting:
    • When there are no references, we could unregister the world as soon as the refcount drops to zero would be obvious (by userScripts.unregister or userScripts.update).
    • Do we want to support calling userScripts.configureWorld before userScripts.register? If so, then configureWorld should register a world implicitly, despite there not being any scripts that reference it.
      • to avoid accumulating junk, unused worlds should eventually be discarded. This is non-trivial.
      • an alternative is to enable the world to be configured along with registering a user script.
      • another alternative is to not support worldId in configureWorld unless the worldId has been allocated explicitly by userScripts.register. Extensions that really care about ensuring setup of the world before execution can register a dummy script with a worldId, then configure the world, then register the actual script for that world, and then unregister the dummy script. Is this an acceptable pattern?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @Rob--W !

I was thinking about most of these already (complete with a few TODOs, e.g. here) and was planning on submitting an update to the proposal as we got nearer, but I hadn't considered the update behavior and allowing developers to clear out old registrations -- both of which I agree are valuable.

The way I'm thinking about worlds is that:

  • A user script is registered with a world. That determines where it injects.
  • The configuration for that world may or may not exist, depending on whether the developer wants to use non-default values (for messaging and CSP, but potentially other APIs in the future).

As such, there isn't really the "creation" of a world when you register a script -- worlds are created in the document in which they run, and are created according to the configuration provided (if any).

I don't like the idea of either reference counting or requiring a user script be registered for every specified worldId -- that sounds very difficult for both us (the browser) and for developers to keep track of, and seems like it would very likely lead to some unpredictable or unexpected edge cases (e.g., a developer updates a user script world via unregister + register, and doesn't realize that unregistered the user script world). It also leads to a (very small) race where it'd be impossible to ensure a user script injected with given world properties, since you'd have to register the script before the configuring the world, but then the script may inject before that configuration took hold. I'd much rather lean on the developers handling the registration.

I've expanded the proposal to include:

  • Two new functions, removeWorld() and getWorldConfigurations().
  • Notes about how the worlds behave when injecting.
  • Notes on world persistence and world limits.

Please take a look and let me know what you think.

for those cases. If `worldId` is omitted, a "default" user script world will
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
be used.

### New Permissions

No new permissions are necessary. This is inline with the `userScripts` API's
current functionality and purpose.

### Manifest File Changes

No manifest file changes are necessary.

## Security and Privacy

### Exposed Sensitive Data

This does not expose any additional sensitive data.

### Abuse Mitigations

This API does not provide any additional avenue for abuse beyond what the
`userScripts` API is already capable of.

Extension developers may abuse this API by requiring too many user script
worlds, which would have a heavy performance cost. However, this is not a
security consideration. Additionally, browsers can mitigate this by enforcing
a limit of the maximum number of user script worlds an extension may register
or have active at a given time.

These enforcements will be left up to the browser to implement, if they feel
necessary.

### Additional Security Considerations

No additional security considerations. This API may (mildly) increase security
by (somewhat) isolating individual user scripts from interacting with one
another.

## Alternatives

### Existing Workarounds

Today, user script managers go to various lengths to try to isolate behavior
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
between user scripts. This includes having various frozen types, making heavy
use of function closures, ensuring injection before untrusted code, and more.

These workarounds are fragile and require significantly more complex code in
user script managers, while providing a lesser guarantee of isolation than a
separate user script world would provide.

### Open Web API

The concept of user scripts should not be exposed to the web; this should not
be an open web API.

## Implementation Notes

N/A

## Future Work
Rob--W marked this conversation as resolved.
Show resolved Hide resolved

With the addition of a `userScripts.execute()` function (as described in
this [PR](https://github.com/w3c/webextensions/pull/540) and
[issue](https://github.com/w3c/webextensions/issues/477), we should also
rdcronin marked this conversation as resolved.
Show resolved Hide resolved
allow developers to specify the `worldId` when injecting dynamically. This
will behave similarly to its behavior for a `RegisteredUserScript`.
Loading