diff --git a/proposals/runtime_get_contexts.md b/proposals/runtime_get_contexts.md new file mode 100644 index 00000000..f2a09c55 --- /dev/null +++ b/proposals/runtime_get_contexts.md @@ -0,0 +1,302 @@ +# New API: runtime.getContexts() + +## Background + +Chromium currently has the +[`extension.getViews()`](https://developer.chrome.com/docs/extensions/reference/extension/#method-getViews) +API method that allows an extension to get information about the "views" that +are active for the extension. A "view" in this context is any HTML frame in the +extension's process that commits to the extension origin; this may not be all +the extension owns, such as in the case of incognito-mode frames. The returned +values are a set of `HTMLWindow` objects, which the extension has permission to +reach directly into (as they are same-origin). + +Some common reasons for calling this method are: determining if a toolbar +popup, tab, or options page is open; directly interacting with those pages by +reaching into their `HTMLWindow`; etc. + +Chromium's implementation of Manifest V2 (MV2) allows for +[background pages](https://developer.chrome.com/docs/extensions/mv2/background_pages/) +to call this API. This is possible because these pages are themselves an +extension frame (albeit one that isn't visibly rendered) and run on the main +thread of the renderer; this allows them easy access to the JavaScript +[`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window#instance_properties) +objects provided by `extension.getViews()`. + +## Problem + +With the migration to Manifest V3 (MV3), background pages +[no longer exist](https://developer.chrome.com/docs/extensions/mv3/migrating_to_service_workers/); +instead, an extension's background context is +[service worker](https://developer.chrome.com/docs/workbox/service-worker-overview/)-based. +For technical reasons[1](#footnotes), service workers cannot access +the [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window#instance_properties) +objects that `extension.getViews()` provides and it is not feasible to +implement that with our browser design. Due to this, we cannot provide access +to the JavaScript context for these views, but we can allow an extension to +query for them (determining if they exist) and target them for messaging +purposes. + +## Solution + +Considering the above situation, we'd like to propose a new extension API +method, `runtime.getContexts()`, to asynchronously provide metadata about +associated contexts that is still useful for an extension. A "context" here is +considered an environment running the extension's code; the contexts we will +consider are described in more detail in later sections. This will allow +extension background scripts to identify the active contexts in the extension. +For example, this can be used to target messages to send using +[`runtime.sendMessage()`](https://developer.chrome.com/docs/extensions/reference/runtime/#method-sendMessage), +etc.). Introducing this API will allow an easier migration from MV2. + +This also obviates the need for introducing multiple one-off APIs, such as +separate APIs to query for the extension popup, offscreen documents, etc. + +## API Proposal + +This method will return an array of matching contexts, represented by a new +`ExtensionContext` type. This will be defined as: + +```js +runtime.ExtensionContext = { + // Context type -- tab, popup, etc. + contextType: ContextType, + // A unique identifier for this context. + contextId: string, + // ID of the tab for this context, or -1 if this context is not hosted in a + // tab. + tabId: int, + // ID of the window for this context, or -1 if this context is not hosted in a + // window. + windowId: int, + // ID of the DOM document for this context, or undefined if this context is + // not associated with a document. + documentId?: string, + // ID of the frame for this context, or -1 if this context is not hosted in a + // frame. + frameId: int, + // The current URL of the document, or undefined if this context is not + // hosted in a document. + documentUrl?: string, + // The current origin of the document, or undefined if this context is not + // hosted in a document. + documentOrigin?: string, + // Whether the context is for an incognito profile. + incognito: boolean, +} +``` + +`ContextType` will indicate the type of context retrieved. It is an enum +defined as: + +```js +extension.ContextType = { + // Tabs the extension is running in. + TAB: 'TAB', + // Toolbar popups created by the extension. + // TODO: Should this be `TOOLBAR_POPUP` to avoid ambiguity with web popup + // windows? Or perhaps `ACTION_POPUP` to avoid tying it to a particular UI + // surface? + POPUP: 'POPUP', + // The background context for the extension (in Chromium, the extension + // service worker). + BACKGROUND: 'BACKGROUND', + // Offscreen documents for the extension. + OFFSCREEN_DOCUMENT: 'OFFSCREEN_DOCUMENT', + // A side panel context. Currently under development in Chromium. + SIDE_PANEL, +}; +``` + +This enum will be expanded in the future as more context types are added. + +The method signature will be defined as: + +```js +runtime.getContexts( + filter?: ContextFilter +): Promise; +``` + +A `ContextFilter` will be defined as: + +```js +runtime.ContextFilter = { + contextTypes?: ContextType[], + contextIds?: string[], + tabIds?: int[], + windowIds?: int[], + documentIds?: string[], + frameIds?: int[], + documentUrls?: string[], + documentOrigins?: string[], + incognito?: boolean, +} +``` + +This type will be used to query for specific contexts. For each array property, +a given `ExtensionContext` matches if it matches any properties within the +array. If a property is undefined, all contexts match; thus, a filter of `{}` +matches all available contexts. Note that `incognito`, as a boolean, is not an +array (because to match both `true` and `false`, it is simply omitted). + +As examples: +* `{}` matches all available contexts. +* `{incognito: false}` matches all non-incognito contexts. +* `{contextTypes: ['TAB', 'POPUP']}` matches any `ExtensionContext` that is + either a tab or a popup. +* `{contextTypes: ['TAB'], tabIds: [1, 2, 3]}` matches any `ExtensionContext + that is a tab where that context is in a tab with the ID `1`, `2`, or `3`. +* `{contextTypes: ['OFFSCREEN_DOCUMENT'], tabIds: [1, 2, 3]}` would inherently + match no contexts, since no offscreen documents are not hosted in tabs and + thus cannot match. + +### Additional Considerations + +#### Context ID + +Each extension context will have a unique context ID, represented by a string. +This is necessary to uniquely identify a context, since other fields may be +non-unique (such as URL) or absent (such as documentId). + +The `contextId` is unique and persistent for the lifetime of a context. For +contexts associated with a document, this means `contextId` behaves like +`documentId`. For service worker contexts, the `contextId` is unique for the +duration of the service worker context (but will be different across service +worker restarts). + +#### Document Properties + +Document-related properties (`documentId`, `documentUrl`, `documentOrigin`) +refer to the document associated with this context. For extension documents +(such as tabs, popups, and offscreen documents), these will point to the +extension (e.g., `chrome-extension:///popup.html`). If/when we add support +for content scripts, this will be the document the script is injected within. +For service workers (which have no document), these are undefined. + +#### Frames + +`ContextType` refers to the overall embedder for a context; for instance, +`ContextType.TAB` will be used for any context associated with a tab. This +includes subframes and cached frames. Developers can determine the finer +grained state of these via other properties, such as `frameId`. + +#### Incognito mode + +[Split-mode](https://developer.chrome.com/docs/extensions/mv3/manifest/incognito/#split) +extensions will _not_ have access to the contexts from their corresponding +profile. That is, the incognito extension process will not be able to access +contexts from the non-incognito extension process, and vice versa. + +For spanning-mode extensions, most contexts are accessible. Due to differences +in browser architecture, exact details about whether incognito contexts (such as +iframes in incognito pages) for spanning mode extensions are left as an exercise +to the implementor. + +#### Sandboxed Pages + +Sandboxed pages will not be included in the returned contexts. They are on a +separate origin (`"null"`) and do not have access to extension APIs. + +#### TOCTOU + +Time-of-Check vs Time-of-Use (TOCTOU) issues are an unavoidable aspect of this +API, given its asynchronous nature. An extension may call `getContexts()` and, +by the time it receives the result, the values (such as available contexts or +URLs of those contexts) may be different. + +This is something extension developers already need to worry about with any +asynchronous API. API calls should handle references to non-existent contexts +gracefully, and extension developers can leverage concepts like `documentId` +(which is updated when a frame navigates, allowing extensions to identify if a +given context is the same as when they queried it). + +#### Naming + +* *ContextType*: While there already exist more "Context" references in APIs + (such as `ContextType` on the `contextMenus` API), the namespace (`runtime`) + helps differentiate these. +* *`documentUrl` et al*: We use `documentUrl` on contexts (as opposed to + `url`) to indicate the URL is that of the document associated with the + context. This is relevant for cases where there may not be a document URL + (such as service workers) and allows for potential future expansion where + other URLs (such as script URLs) may exist. + +#### Default / Absent Values + +There is an inconsistency in how we represent a value for a context that doesn't +have the associated trait, such as a `tabId` or `documentId` for an extension's +background service worker context (which has neither an associated tab nor +document). Some of these values -- `tabId`, `windowId`, and `frameId` -- use +constant values to indicate "no state", such as `-1` for `tabId`. Others -- +`documentId`, `documentUrl`, and `documentOrigin` -- use undefined to indicate +this. + +This is an artifact of existing APIs and precedence. Since many existing APIs +use the constant integer values, we want to be consistent with those. However, +for newly-introduced fields, we use the more intuitive `undefined` state. + +## Future Work + +### Messaging APIs Support `ContextId`s as Target + +In practice, many extension messages are meant for a single target, but are +broadcast to all extension contexts. With the ability to uniquely identify a +single extension context, we will modify messaging APIs (such as +`runtime.sendMessage()` and `runtime.connect()`) to allow specifying specific +targets that should receive a message. + +### Multi Page Architecture Fields (`DocumentLifeCycle`, `FrameType`, and `parentDocumentId`, and `parentFrameId`) + +[Multi Page Architecture](https://docs.google.com//1NginQ8k0w3znuwTiJ5qjYmBKgZDekvEPC22q0I4swxQ#heading=h.w1qo2n6sr8wn) +caused +[multiple changes](https://developer.chrome.com/blog/extension-instantnav/) to +Chromium's +[tabs API](https://developer.chrome.com/docs/extensions/reference/tabs/), +[scripting API](https://developer.chrome.com/docs/extensions/reference/scripting/), +and +[web navigation API](https://developer.chrome.com/docs/extensions/reference/webNavigation/). + +Among these changes are the additions of `DocumentLifecycle`, `FrameType`, and +`parentDocumentId`. If there is sufficient demand, we can consider adding these +fields to the `ExtensionContext` type. + +### runtime.getCurrentContext() + +We would like to provide an additional API, `runtime.getCurrentContext()`, to +return the calling context. + +### Content Script Contexts + +[Content scripts](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) +run in a separate +[Renderer](https://developer.chrome.com/blog/inside-browser-part3/) (and +process) from the extension process. In this first version of the API, we will +not include these contexts due to the complexity it entails. However, we would +like to add these contexts in the future. + +With the content script additions, we may add new fields to `ExtensionContext`, +such as `scriptUrl` (to indicate the content script's source). + +### Dev Tools Contexts + +Extensions can use the +[Dev Tools API](https://developer.chrome.com/docs/extensions/mv3/devtools/] to +extend the browser's developer tools. When doing so, these extensions can have +a panel (an extension view) within the developer tools console. These views +are a little different than others, though -- in Chromium, they commit to a +different origin (one with a devtools:-scheme). These are also not currently +returned from `chrome.extension.getViews()`. In the future, we will expand +`runtime.getContexts()` with devtools context types to accommodate these. + +## Footnotes + +1: Non-main threads in a +[Renderer](https://developer.chrome.com/blog/inside-browser-part3/) (where +service workers run in Chromium) cannot access DOM concepts directly (they are +only accessible from the main renderer thread). Service workers thus cannot +synchronously access the JavaScript +[`Window`](http://go/mdn/API/Window#instance_properties) objects provided by +`extension.getViews()`. Supporting this access would take engineering years to +change, and is likely undesirable due to the complexity and considerations it +would introduce (threading and locking, slowing down main thread execution).