Skip to content

Instantly share code, notes, and snippets.

@rpl
Last active January 23, 2018 16:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rpl/34cb471b752055d99ed445446613ae75 to your computer and use it in GitHub Desktop.
Save rpl/34cb471b752055d99ed445446613ae75 to your computer and use it in GitHub Desktop.
A fiction example webextension (used to design a new userScripts WE API)
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((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);
});
// 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 (!apiScript) {
// Lazily 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).
});
}
{
"manifest_version": 2,
"name": "example-userscript-manager",
"version": "2.0",
"background": {
"scripts": ["background.js"]
},
"permissions": ["userScripts", "<all_urls>", "..."]
}
@mixedpuppy
Copy link

I'm not convinced that userScript.registerAPI should register a single function. It seems that is much higher overhead than registering the entire API once and giving the api implementation access to the script metadata. Any given extension could choose to implement a grants system on top of that.

I also think that we should do the matching for user scripts, userScripts.register should take matches and run_at. Or is that what apiOptions is?

@rpl
Copy link
Author

rpl commented Jan 22, 2018

I'm not convinced that userScript.registerAPI should register a single function. It seems that is much higher overhead than registering the entire API once and giving the api implementation access to the script metadata. Any given extension could choose to implement a grants system on top of that.

@mixedpuppy 👍 allowing to register a set of function all at once from userScript.registerAPI sound good to me too

I also think that we should do the matching for user scripts, userScripts.register should take matches and run_at. Or is that what apiOptions is?

yeah, exactly, the apiOptions from the example are supposed to include most of the options supported by contentScripts.register (at least matches / excludeMatches, includeGlobs / excludeGlobs, allFrames, matchAboutBlank and runAt)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment