Skip to content

Commit 2acad09

Browse files
authored
Merge pull request #739 from w3c/userScripts-api-clarifications
Clarify behavior of userScripts API
2 parents 233d77a + 04b7ab3 commit 2acad09

File tree

3 files changed

+107
-20
lines changed

3 files changed

+107
-20
lines changed

proposals/multiple_user_script_worlds.md

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Allow developers to configure and use multiple user script worlds in the
1111

1212
**Sponsoring Browser:** Google Chrome
1313

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

1616
**Created:** 2024-03-07
1717

@@ -58,19 +58,24 @@ Relevant methods and types:
5858
export interface WorldProperties {
5959
+ /**
6060
+ * Specifies the ID of the specific user script world to update.
61-
+ * If not provided, updates the properties of the default user script
62-
+ * world.
61+
+ * If not provided, defaults to the empty string (""), which
62+
+ * updates the properties of the default user script world.
6363
+ * Values with leading underscores (`_`) are reserved.
6464
+ */
6565
+ worldId?: string;
6666

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

7275
/**
73-
* Specifies whether messaging APIs are exposed. The default is `false`.
76+
- * Specifies whether messaging APIs are exposed. When not specified, falls
77+
+ * back to the default world's `messaging`. The default is `false` for the
78+
+ * default world.
7479
*/
7580
messaging?: boolean;
7681
}
@@ -115,9 +120,10 @@ Relevant methods and types:
115120

116121
/**
117122
* The list of ScriptSource objects defining sources of scripts to be
118-
* injected into matching pages.
123+
* injected into matching pages. This property must be specified for
124+
* ${ref:register}
119125
*/
120-
js: ScriptSource[];
126+
js?: ScriptSource[];
121127

122128
/**
123129
* Specifies which pages this user script will be injected into. See
@@ -141,7 +147,8 @@ Relevant methods and types:
141147
+ /**
142148
+ * If specified, specifies a specific user script world ID to execute in.
143149
+ * Only valid if `world` is omitted or is `USER_SCRIPT`. If `worldId` is
144-
+ * omitted, the script will execute in the default user script world.
150+
+ * omitted, the default value is an empty string ("") and the script will
151+
+ * execute in the default user script world.
145152
+ * Values with leading underscores (`_`) are reserved.
146153
+ */
147154
+ worldId?: string;
@@ -164,8 +171,10 @@ Relevant methods and types:
164171
+ * the world with the specified ID will use the default world configuration.
165172
+ * Does nothing (but does not throw an error) if provided a `worldId` that
166173
+ * does not correspond to a current configuration.
174+
+ * If omitted or the empty string ("") is used, it clears the configuration
175+
+ * of the default world and all worlds without a separate configuration.
167176
+ */
168-
+ export function resetWorldConfiguration(worldId: string): Promise<void>;
177+
+ export function resetWorldConfiguration(worldId?: string): Promise<void>;
169178
+
170179
+ /**
171180
+ * Returns a promise that resolves to an array of the the configurations
@@ -187,15 +196,9 @@ Worlds may be configured via `userScripts.configureWorld()` by indicating the
187196
given `worldId`. User scripts injected into a world with the given `worldId`
188197
will have the associated properties from the world configuration. If a world
189198
does not have a corresponding configuration, it uses the default user script
190-
world properties. Any existing worlds are not directly affected by
191-
`userScripts.configureWorld()` calls; however, the browser may revoke
192-
certain privileges (for instance, message calls from existing user script worlds
193-
may beging to fail if the extension sets `messaging` to false). This is in line
194-
with behavior extensions encounter when e.g. the extension is unloaded and the
195-
content script continues running.
196-
197-
World configurations can be removed via the new
198-
`userScripts.resetWorldConfiguration()` method.
199+
world properties. World configurations can be removed via the new
200+
`userScripts.resetWorldConfiguration()` method. For additional behavioral
201+
notes, see the [World Configurations](#world-configurations) section.
199202

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

235+
### World Configurations
236+
237+
The `userScripts.configureWorld()` method can customize the behavior of
238+
individual worlds as described by `WorldProperties`. Most fields are optional,
239+
and default to the default world when not specified.
240+
241+
When `worldId` is omitted or the empty string, `userScripts.configureWorld()`
242+
updates the default world's properties. This does not only affect the default
243+
world, but also worlds without separate configuration. When properties are
244+
omitted from an update to the default world configuration, the API defaults
245+
as specified in `WorldProperties` are used instead.
246+
247+
The `userScripts.resetWorldConfiguration()` method can clear properties of
248+
individual worlds. When the default world's properties are cleared, this
249+
also applies to worlds without a separate configuration.
250+
251+
Changes to world configurations are only guaranteed to apply to new instances
252+
of the world: if a world is already initialized in a document due to the
253+
execution of a user script, then that document must be reloaded for changes
254+
to apply.
255+
256+
The browser may revoke certain privileges (for instance, message calls from
257+
existing user script worlds may begin to fail if the extension sets `messaging`
258+
to false). This is in line with behavior extensions encounter when e.g. the
259+
extension is unloaded and the content script continues running.
260+
232261
### New Permissions
233262

234-
No new permissions are necessary. This is inline with the `userScripts` API's
263+
No new permissions are necessary. This is in line with the `userScripts` API's
235264
current functionality and purpose.
236265

237266
### Manifest File Changes

proposals/user-scripts-api.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ User scripting related features will be exposed in a new API namespace, tentativ
5252
#### Types
5353

5454
```
55+
// See RegisteredUserScript validation section, below.
5556
dictionary RegisteredUserScript {
5657
boolean? allFrames;
57-
ScriptSource[] js;
58+
// js is required in userScripts.register(), optional in userScripts.update().
59+
// When specified, must be a non-empty array.
60+
ScriptSource[]? js;
5861
string[]? excludeMatches;
5962
string id;
6063
string[]? matches;
@@ -133,6 +136,8 @@ where
133136

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

139+
The proposal at [multiple_user_script_worlds.md](multiple_user_script_worlds.md) expands the behavior of `userScripts.configureWorld`.
140+
136141
##### Messaging
137142

138143
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.
@@ -157,11 +162,56 @@ As mentioned in requirement A, the user script world can communicate with differ
157162
- 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.
158163
- Scripts registered via `userScripts.register()`, following the order they were registered in. Updating a user script doesn’t change its registration order.
159164
- 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.)
165+
- 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`.
166+
167+
### RegisteredUserScript validation
168+
169+
The `RegisteredUserScript` type is shared by `userScripts.register()` and
170+
`userScripts.update()`. All fields except `id` are declared as optional, to
171+
allow `userScripts.update()` to update individual properties.
172+
173+
#### Requirements per method
174+
175+
`userScripts.register()`:
176+
177+
- `js` must be present and a non-empty array.
178+
- At least one of `matches` or `includeGlobs` must be a non-empty array.
179+
180+
`userScripts.update()`:
181+
182+
- Individual properties may be `null` or omitted to leave the value unchanged.
183+
- To clear an array, an empty array can be passed.
184+
- The resulting script must be validated to make sure that the updated
185+
script remains a valid script before it replaces a previous script.
186+
187+
#### Example
188+
189+
```javascript
190+
// Valid registration:
191+
await browser.userScripts.register([
192+
{
193+
worldId: "myScriptId",
194+
js: [{ code: "console.log('Hello world!');" }],
195+
matches: ["*://example.com/*"],
196+
},
197+
]);
198+
199+
// Invalid! Would result in script without matches or includeGlobs!
200+
await browser.userScripts.update([{ matches: [] }]);
201+
202+
// Valid: replaces matches with includeGlobs.
203+
await browser.userScripts.update([{
204+
matches: [],
205+
includeGlobs: ["*example*"],
206+
}]);
207+
```
160208

161209
### Browser level restrictions
162210

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

213+
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)).
214+
165215
## (Potential) Future Enhancements
166216

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

173223
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).
174224

225+
This proposal is at [multiple_user_script_worlds.md](multiple_user_script_worlds.md).
226+
175227
### Execute user scripts one time
176228

177229
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()`).
178230

231+
This proposal is at [user-scripts-execute-api.md](user-scripts-execute-api.md).
232+
179233
### Establish common behaviors for the CSP of scripts injected into the main world by an extension
180234

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

proposals/user-scripts-execute-api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ dictionary UserScriptInjection {
5252
target: InjectionTarget,
5353
// The JavaScript "world" to run the script in. The default is `USER_SCRIPT`.
5454
world?: ExecutionWorld,
55+
// A specific user script world ID to execute in. Only valid if `world` is
56+
// omitted or is `USER_SCRIPT`. If `worldId` is omitted, the default value is
57+
// an empty string ("") and the script will execute in the default world.
58+
worldId?: string,
5559
}
5660
5761
dictionary InjectionTarget {

0 commit comments

Comments
 (0)