From f02f0dc7cdd5c347e145c69a11b9aabbd829f508 Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Wed, 11 Dec 2024 18:12:37 +0100 Subject: [PATCH] Clarify behavior of userScripts API - Use empty string instead of unspecified/null as the default worldId, following https://github.com/w3c/webextensions/issues/565#issuecomment-2473708131 - 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 https://github.com/w3c/webextensions/issues/565#issuecomment-2536646436 - Mention Firefox's optional-only "userScripts" permission design. --- proposals/multiple_user_script_worlds.md | 67 +++++++++++++++++------- proposals/user-scripts-api.md | 56 +++++++++++++++++++- 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/proposals/multiple_user_script_worlds.md b/proposals/multiple_user_script_worlds.md index 89e1d69c..87e5174d 100644 --- a/proposals/multiple_user_script_worlds.md +++ b/proposals/multiple_user_script_worlds.md @@ -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 @@ -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; } @@ -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 @@ -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; @@ -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; ++ export function resetWorldConfiguration(worldId?: string): Promise; + + /** + * 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 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 @@ -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 diff --git a/proposals/user-scripts-api.md b/proposals/user-scripts-api.md index a9354c27..b5841365 100644 --- a/proposals/user-scripts-api.md +++ b/proposals/user-scripts-api.md @@ -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; @@ -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. @@ -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 @@ -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.