Skip to content

Commit

Permalink
Clarify behavior of userScripts API
Browse files Browse the repository at this point in the history
- Use empty string instead of unspecified/null as the default worldId,
  following #565 (comment)

- Declare "js" property as optional with remark that it is required in
  "register", to enable `userScripts.update()` without requiring "js".

- Expand on RegisteredUserScript validation, notably on validating
  matches+includeGlobs after an update.

- Update `resetWorldConfiguration()` signature to match Chrome's and
  Firefox's actual implementation: `worldId` is optional.

- Create a new section "World Configurations" and specify world
  configuration behavior more precisely. In particular, clarify fallback
  behavior, following
  #565 (comment)

- Mention Firefox's optional-only "userScripts" permission design.
  • Loading branch information
Rob--W committed Dec 17, 2024
1 parent 10d6ae6 commit f02f0dc
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 20 deletions.
67 changes: 48 additions & 19 deletions proposals/multiple_user_script_worlds.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Allow developers to configure and use multiple user script worlds in the

**Sponsoring Browser:** Google Chrome

**Contributors:** N/A
**Contributors:** [Rob--W](https://github.com/Rob--W)

**Created:** 2024-03-07

Expand Down Expand Up @@ -58,19 +58,24 @@ Relevant methods and types:
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.
+ * If not provided, defaults to the empty string (""), which
+ * updates the properties of the default user script world.
+ * Values with leading underscores (`_`) are reserved.
+ */
+ worldId?: string;

/**
* Specifies the world's CSP. The default is the `ISOLATED` world CSP.
- * Specifies the world's CSP. The default is the `ISOLATED` world CSP.
+ * Specifies the world's CSP. When not specified, falls back to the
+ * default world's `csp`. The default CSP of the default world is the
+ * `ISOLATED` world's CSP, i.e. `script-src 'self'`.
*/
csp?: string;

/**
* Specifies whether messaging APIs are exposed. The default is `false`.
- * Specifies whether messaging APIs are exposed. When not specified, falls
+ * back to the default world's `messaging`. The default is `false` for the
+ * default world.
*/
messaging?: boolean;
}
Expand Down Expand Up @@ -115,9 +120,10 @@ Relevant methods and types:

/**
* The list of ScriptSource objects defining sources of scripts to be
* injected into matching pages.
* injected into matching pages. This property must be specified for
* ${ref:register}
*/
js: ScriptSource[];
js?: ScriptSource[];

/**
* Specifies which pages this user script will be injected into. See
Expand All @@ -141,7 +147,8 @@ Relevant methods and types:
+ /**
+ * If specified, specifies a specific user script world ID to execute in.
+ * Only valid if `world` is omitted or is `USER_SCRIPT`. If `worldId` is
+ * omitted, the script will execute in the default user script world.
+ * omitted, the default value is an empty string ("") and the script will
+ * execute in the default user script world.
+ * Values with leading underscores (`_`) are reserved.
+ */
+ worldId?: string;
Expand All @@ -164,8 +171,10 @@ Relevant methods and types:
+ * the world with the specified ID will use the default world configuration.
+ * Does nothing (but does not throw an error) if provided a `worldId` that
+ * does not correspond to a current configuration.
+ * If omitted or the empty string ("") is used, it clears the configuration
+ * of the default world and all worlds without a separate configuration.
+ */
+ export function resetWorldConfiguration(worldId: string): Promise<void>;
+ export function resetWorldConfiguration(worldId?: string): Promise<void>;
+
+ /**
+ * Returns a promise that resolves to an array of the the configurations
Expand All @@ -187,15 +196,9 @@ Worlds may be configured via `userScripts.configureWorld()` by indicating the
given `worldId`. User scripts injected into a world with the given `worldId`
will have the associated properties from the world configuration. If a world
does not have a corresponding configuration, it uses the default user script
world properties. Any existing worlds are not directly affected by
`userScripts.configureWorld()` calls; however, the browser may revoke
certain privileges (for instance, message calls from existing user script worlds
may beging to fail if the extension sets `messaging` to false). This is in line
with behavior extensions encounter when e.g. the extension is unloaded and the
content script continues running.

World configurations can be removed via the new
`userScripts.resetWorldConfiguration()` method.
world properties. World configurations can be removed via the new
`userScripts.resetWorldConfiguration()` method. For additional behavioral
notes, see the [World Configurations](#world-configurations) section.

Additionally, `runtime.Port` and `runtime.MessageSender` will each be extended
with a new, optional `userScriptWorldId` property that will be populated in the
Expand Down Expand Up @@ -229,9 +232,35 @@ If an extension tries to inject more scripts into a single document than the
per-document limit, all additional scripts will be injected into the default
world.

### World Configurations

The `userScripts.configureWorld()` method can customize the behavior of
individual worlds as described by `WorldProperties`. Most fields are optional,
and default to the default world when not specified.

When `worldId` is omitted or the empty string, `userScripts.configureWorld()`
updates the default world's properties. This does not only affect the default
world, but also worlds without separate configuration. When properties are
omitted from an update to the default world configuration, the API defaults
as specified in `WorldProperties` are used instead.

The `userScripts.resetWorldConfiguration()` method can clear properties of
individual worlds. When the default world's properties are cleared, this
also applies to worlds without a separate configuration.

Changes to world configurations are only guaranteed to apply to new instances
of the world: if a world is already initialized in a document due to the
execution of a user script, then that document must be reloaded for changes
to apply.

The browser may revoke certain privileges (for instance, message calls from
existing user script worlds may begin to fail if the extension sets `messaging`
to false). This is in line with behavior extensions encounter when e.g. the
extension is unloaded and the content script continues running.

### New Permissions

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

### Manifest File Changes
Expand Down
56 changes: 55 additions & 1 deletion proposals/user-scripts-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ User scripting related features will be exposed in a new API namespace, tentativ
#### Types

```
// See RegisteredUserScript validation section, below.
dictionary RegisteredUserScript {
boolean? allFrames;
ScriptSource[] js;
// js is required in userScripts.register(), optional in userScripts.update().
// When specified, must be a non-empty array.
ScriptSource[]? js;
string[]? excludeMatches;
string id;
string[]? matches;
Expand Down Expand Up @@ -133,6 +136,8 @@ where

In the future, if we allow multiple user script worlds (see section in Future Work below), this method can be expanded to allow for a user script world identifier to customize a single user script world.

The proposal at [multiple_user_script_worlds.md](multiple_user_script_worlds.md) expands the behavior of `userScripts.configureWorld`.

##### Messaging

User scripts can send messages to the extension using extension messaging APIs: `browser.runtime.sendMessage()` and `browser.runtime.connect()`. We leverage the runtime API (instead of introducing new userScripts.onMessage- and userScripts.sendMessage-style values) in order to keep extension messaging in the same API. There is precedent in this (using the same API namespace to send messages from a different (and less trusted) context, as `chrome.runtime` is also the API used to send messages from web pages.
Expand All @@ -157,11 +162,56 @@ As mentioned in requirement A, the user script world can communicate with differ
- Scripts registered via [`scripting.registerContentScripts()`](https://developer.chrome.com/docs/extensions/reference/scripting/#method-registerContentScripts), following the order they were registered in. Updating a content script doesn't change its registration order.
- Scripts registered via `userScripts.register()`, following the order they were registered in. Updating a user script doesn’t change its registration order.
- User scripts are always persisted across sessions, since the opposite behavior would be uncommon. (We may explore providing an option to customize this in the future.)
- Unlike regular content scripts, `matches` is allowed to be optional when `includeGlobs` is specified. A user script matches a document when its URL matches either `matches` or `includeGlobs`.

### RegisteredUserScript validation

The `RegisteredUserScript` type is shared by `userScripts.register()` and
`userScripts.update()`. All fields except `id` are declared as optional, to
allow `userScripts.update()` to update individual properties.

#### Requirements per method

`userScripts.register()`:

- `js` must be present and a non-empty array.
- At least one of `matches` or `includeGlobs` must be a non-empty array.

`userScripts.update()`:

- Individual properties may be `null` or omitted to leave the value unchanged.
- To clear an array, an empty array can be passed.
- The resulting script must be validated to make sure that the updated
script remains a valid script before it replaces a previous script.

#### Example

```javascript
// Valid registration:
await browser.userScripts.register([
{
worldId: "myScriptId",
js: [{ code: "console.log('Hello world!');" }],
matches: ["*://example.com/*"],
},
]);

// Invalid! Would result in script without matches or includeGlobs!
await browser.userScripts.update([{ matches: [] }]);

// Valid: replaces matches with includeGlobs.
await browser.userScripts.update([{
matches: [],
includeGlobs: ["*example*"],
}]);
```

### Browser level restrictions

From here, each browser vendor should be able to implement their own restrictions. Chrome is exploring limiting the access to this API when the user has enabled developer mode (bug), but permission grants are outside of the scope of this API proposal.

Firefox restricts the permission to `optional_permissions` only, which means that the permission is not granted at install time, and has to be requested separately through browser UI or the `permissions.request()` API ([Firefox bug 1917000](https://bugzilla.mozilla.org/show_bug.cgi?id=1917000)).

## (Potential) Future Enhancements

### `USER_SCRIPT`/ `ISOLATED` World Communication
Expand All @@ -172,10 +222,14 @@ In the future, we may want to provide a more straightforward path for communicat

In addition to specifying the execution world of `USER_SCRIPT`, we could allow extensions to inject in unique worlds by providing an identifier. Scripts injected with the same identifier would inject in the same world, while scripts with different world identifiers inject in different worlds. This would allow for greater isolation between user scripts (if, for instance, the user had multiple unrelated user scripts injecting on the same page).

This proposal is at [multiple_user_script_worlds.md](multiple_user_script_worlds.md).

### Execute user scripts one time

Currently, user scripts are registered and executed every time it matches the origin in a persistent way. We may explore a way to execute a user script only one time to provide a new capability to user scripts (e.g `browser.userScripts.execute()`).

This proposal is at [user-scripts-execute-api.md](user-scripts-execute-api.md).

### Establish common behaviors for the CSP of scripts injected into the main world by an extension

Create certain HTML elements even if their src, href or contents violates CSP of the page so that the users don't have to nuke the site's CSP header altogether.
Expand Down

0 comments on commit f02f0dc

Please sign in to comment.