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

Clarify behavior of userScripts API #739

Merged
merged 1 commit into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
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[];
Copy link
Member Author

Choose a reason for hiding this comment

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

There is consensus among the browser vendors that this should be optional. Devlin also confirmed that on behalf of Google.

I filed an issue for Chromium to change that, at https://issues.chromium.org/issues/383712209


/**
* 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.
Copy link
Member Author

Choose a reason for hiding this comment

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

Side note: Chrome currently requires js to be a non-empty array, Firefox currently accepts an 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`.
Copy link
Member Author

Choose a reason for hiding this comment

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

Chrome has currently not implemented this, and this is tracked at https://issues.chromium.org/issues/41483539


### 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.
Copy link
Member Author

Choose a reason for hiding this comment

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

As mentioned before, Chrome currently requires matches, but it should not be. When optional, "obviously" one of the two arrays should be non-empty, as mentioned before at https://issues.chromium.org/issues/41483539#comment16

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree that matches should be optional, and with the other comments. However, it's not as clear when reading it on the updated proposal. "The following fields are required for a valid RegisteredUserScript" are only applicable when registering the script, right? Because updating a script doesn't require to provide at least one of matches or includeGlobs, or js.

What about:

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I have updated the proposal based on your suggestion. This is the difference:

diff --git a/proposals/user-scripts-api.md b/proposals/user-scripts-api.md
index 4dcb1b4..b584136 100644
--- a/proposals/user-scripts-api.md
+++ b/proposals/user-scripts-api.md
@@ -170,19 +170,21 @@ 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.

-The following fields are required for a valid `RegisteredUserScript`:
+#### Requirements per method

-- `js` must be a non-empty array.
+`userScripts.register()`:
+
+- `js` must be present and a non-empty array.
 - At least one of `matches` or `includeGlobs` must be a non-empty array.

-The above requirements must be checked when `userScripts.register()` is called.
+`userScripts.update()`:

-When `userScripts.update()` is invoked, 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.
+- 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.

-Consider the following example:
+#### Example

 ```javascript
 // Valid registration:


`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
4 changes: 4 additions & 0 deletions proposals/user-scripts-execute-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ dictionary UserScriptInjection {
target: InjectionTarget,
// The JavaScript "world" to run the script in. The default is `USER_SCRIPT`.
world?: ExecutionWorld,
// A specific user script world ID to execute in. Only valid if `world` is
// omitted or is `USER_SCRIPT`. If `worldId` is omitted, the default value is
// an empty string ("") and the script will execute in the default world.
worldId?: string,
Copy link
Member Author

Choose a reason for hiding this comment

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

@EmiliaPaz I noticed that userScripts.execute was missing the worldId property, so I decided to include it in the update here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good call! Proposal was made before the introduction of multiple user script worlds. Makes sense to add worldId, thanks!

}
dictionary InjectionTarget {
Expand Down
Loading