WebExtensions/UserScripts: Difference between revisions

From MozillaWiki
Jump to navigation Jump to search
(minor tweaks to the "Milestone 2: Dinamically registered user scripts" section)
(Fixed typo in the "Milestone 3: Improvements" section)
Line 54: Line 54:
=== Milestone 3: Improvements ===
=== Milestone 3: Improvements ===


Based on the results of the previous manifest, apply further improvements to the
Based on the results of the previous milestones, apply further improvements to the
API and/or its implementation.
API and/or its implementation.



Revision as of 15:01, 24 January 2018

The intent of this document is to clarify the plans for implementing an API which would allow an extension to run third party scripts in isolated sandboxes, and assist in getting it implemented and landed into Firefox.

Some of the reasons for creating this API are:

- Performance: the Greasemonkey-like extensions are currently using inefficient hacks to be able to isolate the userScripts from each other as much as possible.

- Reliability: Greasemonkey-like extensions are currently often using tabs.executeScript API to inject the userScripts, and this implementation strategy is not as reliable as a content script registered to match defined urls and run at a given phase of the page loading, in particular the injected userScripts may easily miss the "document_start" phase because of race conditions between the page loading and the tabs.executeScript API used to inject the script.

- Security: make easier for an extension to reduce the impact that multiple userScrips may have on each other by executing them in separate sandboxes.

Overall

The need for this API arises from extensions like Greasemonkey, which allow its user to register/unregister third party userScripts which should be able to access and change webpages that match the userScript options.

The new userScripts API should:

- allow to run each userScript isolated from each other (on the contrary regular content scripts from the same extensions are all executed in the same sandbox).

- not allow the userScripts to directly use any of the WebExtensions APIs available to the "host" extension.

- allow the "host" extension to optionally provide a set of API functions to the userScripts (e.g. Greasemonkey would provide them the "GM_*" functions that are usually available to a Greasemonkey userScript).

The userScript API aims to provide the API needed to allow these userScript to be executed into isolated sandboxes (and the extension to optionally inject its own API methods into these sandboxes) by abstracting (instead of giving the extension direct access to Cu.Sandbox, as it has been requested in "Bug 1353468 - Allow WebExtensions to construct a Cu.Sandbox") the related internal Gecko implementation details.

Timeline

Main tracking bug: (TODO: link to tracking bug number)

(TODO: bugzilla query table embedded in the wiki page)

This is an ongoing project to provide an API which extensions like Greasemonkey can use to run each of its third party scripts in isolated sandboxes with access to the webpage that their options match.

While this is being built out, developers should understand that this is an experimental API and subject to change.

Milestone 1: Dinamically registered content scripts

In the first milestone we are going to introduce the needed changes to the WebExtensions internals and a new API (contentScripts.register) to allow the WebExtensions content script to be registered programmatically:

- 1332273.

The changes needed to implement the contentScripts.register API are going to be leveraged to implement part of the userScripts API (in particular the userScripts.register API method).

Milestone 2: Dynamically registered user scripts

In this milestone we are going to leverage the work done during milestone 1, which allowed a content script to be registered/unregistered programmatically instead of being declared in the manifest file, to allow a registered script to be executed into an isolated sandbox (instead of a shared sandbox as for the regular extension content scripts).

Besides being able to run a user script into its own isolated sandbox, during this milestone we are going to build the APIs needed to allow the extension to optionally register custom API functions to be provide to the isolated sandboxes (so that the extension can provide its own APIs to the userScripts).

Milestone 3: Improvements

Based on the results of the previous milestones, apply further improvements to the API and/or its implementation.

API

This section provides an idea of the API that the userScripts API namespace may provide to an extension once implemented.

Manifest

Status: not implemented yet (TODO: link to bugzilla issue)

The userScripts API namespace will only be available to the extensions that have a "userScripts" permission:

 {
   "manifest_version": 2,
   "name": "example-userscript-manager",
   "version": "2.0",
   "background": {
     "scripts": ["background.js"]
   },
   "permissions": ["userScripts", "<all_urls>", "..."]
 }


APIs provided in the extension pages

Status: not implemented yet (TODO: link to bugzilla issue))

 browser.userScripts.registerAPI({contentAPI: {file: '...'}, parentAPI: {...}});
 browser.userScripts.register(userScriptOptions);

These APIs are accessible only in the extension pages (any extension page besides the content script and the content script iframes):

- browser.userScripts.registerAPI: used to register the custom APIs that the extension wants to make available to its registered userScripts, the contentAPI parameter specifies the script which register the custom API methods to inject in the userScripts sandbox (and it will be running in the content process as a regulat content script), while the parentAPI parameter is an optional parameter which can be provided when some of the custom API methods have to run some code in a regular extension page (the one that registered the custom API to the userScripts API)

- browser.userScripts.register: used to register a userScript, and optionally specify an object which will provide the API methods implementation that have to be executed in a regular extension page (e.g. to have access to the full WebExtensions APIs, like the tabs API etc.).

example background.js

 // Map<name: string -> {source: string, apiOptions: object, metadata: object, script: RegisteredUserScript}>
 const userScrips: new Map();
 // RegisteredContentScript
 let apiContentScript;
 // This would be a custom function implemented by the extension,
 // which would parse the user script source and extract its
 // metadata (e.g. name, grants etc.)
 // and the apiOptions (url pattern to match, when it should run ext.)
 function parseUserScript(source) {
   ... // parse source header for userScript name and options
   return {metadata, apiOptions};
 }
 const parentAPI = {
   async GM_something([param1], userScript) {
     // May check userScript.metadata to affect the result.
     const result = // ...
     const result = await somethingAsync(args);
     return result;
   }
 };
 async fuction registerUserScript(userScriptSource) {
   if (!apiContentScript) {
     // Register the custom userScripts API methods:
     // - contentAPI is going to run as a regular content scripts injected automatically where on webpages
     //   that matches one of the registered userScripts
     // - parentAPI is an optional parameter which can be used to specify a set of userScripts API methods
     //   that have to be executed in a regular extension page (vs. being executed in the
     //   contentScript context as the apiContentScript)
     apiScript = await browser.userScripts.registerAPI({
       contentAPI: {file: "apiContentScript.js"}
       parentAPI: parentAPI
     });
   }
   // parse the script source and return the userScript
   // name (used as the key in the map) and its options
   // (e.g. matches url pattern, include/exclude pattern, runAt
   // etc.)
   const {metadata, apiOptions} = parseUserScript(userScriptSource);
   userScripts.set(metadata.name, {source: userScriptSource, apiOptions, metadata});
   const userScript = await browser.userScripts.register({
     ...apiOptions, // Used by the API to know which urls to match etc.
     code: source,  // Used by the API to know which source to execute in the userScript sandbox
     metadata: metadata, // A serializable metadata object which is received by the userScripts API method
                         // implementation (provided by the extension from the registered content script).
   });
 }

APIs provided in the regular content scripts

Status: not implemented yet (TODO: link to bugzilla issue)

 browser.userScripts.onUserScript.addListener(userScript => {...});

The following APIs are accessible only in a regular WebExtensions content script:

- browser.userScripts.onUserScript: used in a regular content script (only for an extension which has the userScripts permission), to register a callback which is going to be called right before the user script is executed in its sandbox, which allow the extension to register a set of custom APIs to be injected in a given userScript.

example apiContentScript.js

 const userScriptAPIs = {
   // GM_something -> name of the API method injected in the sandbox
   // args -> arguments of the API call
   // userScriptSandboxAPI -> an API object which provides the metadata of the userScript caller
   //                         and expose other helper methods.
   async GM_something([param1, cb], userScript) {
     if (!validateGMSomethingArgs([param1, cb])) {
       // Throws an error (converted by a wrapper implemented internally
       // into an valid rejection Error instance for the caller sandbox).
     throw new Error("...");
     }
     const result = await userScript.parent.GM_something(param1);
     cb(result);
   },
   async GM_something_else(args, userScriptName) {
     const data = await browser.storage.local.get(userScriptName);
     return doSomethingElseWith(data, args)
   },
   ...
 };
 // This method (only available into the content scripts if the userScripts permission
 // has been asked by the extension) is called when a userScript is going to
 // be executed in its newly created sandbox and allows the extension to register a set of
 // custom API methods into it.
 browser.userScripts.onUserScript.addListener((userScript) => {
   if (!userScript.metadata.grants) {
     // no userScript API to grant to this userScript.
     return;
   }
   // collected the APIs that should be granted into
   // an map "API function name" -> "API function implementation"
   let grantedAPIs = {};
   for (const grant of userScript.metadata.grants) {
     grantedAPIs[grant] = userScriptAPIs[grant];
   }
   // Register all the allowed API on the sandbox.
   userScript.registerAPI(grantedAPIs);
 });

Concerns

TBD: security/performance/implementation concerns

Additional Notes

TBD: additional notes related to the API