зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1509339 - Implement a UserScript API object and remove the userScripts.setScriptAPIs method. r=zombie,robwu
Depends on D12676 Differential Revision: https://phabricator.services.mozilla.com/D12678 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
3a91800a12
Коммит
2074f536c7
|
@ -38,7 +38,6 @@ XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
|
|||
const {
|
||||
DefaultMap,
|
||||
DefaultWeakMap,
|
||||
ExtensionError,
|
||||
getInnerWindowID,
|
||||
getWinUtils,
|
||||
promiseDocumentIdle,
|
||||
|
@ -605,9 +604,10 @@ class UserScript extends Script {
|
|||
},
|
||||
});
|
||||
|
||||
// Inject the custom API registered by the extension API script.
|
||||
// Notify listeners subscribed to the userScripts.onBeforeScript API event,
|
||||
// to allow extension API script to provide its custom APIs to the userScript.
|
||||
if (apiScript) {
|
||||
this.injectUserScriptAPIs(userScriptSandbox, context);
|
||||
context.userScriptsEvents.emit("on-before-script", this.scriptMetadata, userScriptSandbox);
|
||||
}
|
||||
|
||||
for (let script of sandboxScripts) {
|
||||
|
@ -645,88 +645,6 @@ class UserScript extends Script {
|
|||
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
injectUserScriptAPIs(userScriptScope, context) {
|
||||
const {extension, scriptMetadata} = this;
|
||||
const {userScriptAPIs, cloneScope: apiScope} = context;
|
||||
|
||||
if (!userScriptAPIs) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clonedMetadata;
|
||||
|
||||
const UserScriptError = userScriptScope.Error;
|
||||
const UserScriptPromise = userScriptScope.Promise;
|
||||
|
||||
const wrappedFnMap = new WeakMap();
|
||||
|
||||
function safeReturnCloned(res) {
|
||||
try {
|
||||
return Cu.cloneInto(res, userScriptScope);
|
||||
} catch (err) {
|
||||
Cu.reportError(
|
||||
`userScripts API method wrapper for ${extension.policy.debugName}: ${err}`
|
||||
);
|
||||
throw new UserScriptError("Unable to clone object in the userScript sandbox");
|
||||
}
|
||||
}
|
||||
|
||||
function wrapUserScriptAPIMethod(fn, fnName) {
|
||||
return Cu.exportFunction(function(...args) {
|
||||
let fnArgs = Cu.cloneInto([], apiScope);
|
||||
|
||||
try {
|
||||
for (let arg of args) {
|
||||
if (typeof arg === "function") {
|
||||
if (!wrappedFnMap.has(arg)) {
|
||||
wrappedFnMap.set(arg, Cu.exportFunction(arg, apiScope));
|
||||
}
|
||||
fnArgs.push(wrappedFnMap.get(arg));
|
||||
} else {
|
||||
fnArgs.push(Cu.cloneInto(arg, apiScope));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Cu.reportError(`Error cloning userScriptAPIMethod parameters in ${fnName}: ${err}`);
|
||||
throw new UserScriptError("Only serializable parameters are supported");
|
||||
}
|
||||
|
||||
if (clonedMetadata === undefined) {
|
||||
clonedMetadata = Cu.cloneInto(scriptMetadata, apiScope);
|
||||
}
|
||||
|
||||
const res = runSafeSyncWithoutClone(fn, fnArgs, clonedMetadata, userScriptScope);
|
||||
|
||||
if (res instanceof context.Promise) {
|
||||
return UserScriptPromise.resolve().then(async () => {
|
||||
let value;
|
||||
try {
|
||||
value = await res;
|
||||
} catch (err) {
|
||||
if (err instanceof context.Error) {
|
||||
throw new UserScriptError(err.message);
|
||||
} else {
|
||||
throw safeReturnCloned(err);
|
||||
}
|
||||
}
|
||||
return safeReturnCloned(value);
|
||||
});
|
||||
}
|
||||
|
||||
return safeReturnCloned(res);
|
||||
}, userScriptScope);
|
||||
}
|
||||
|
||||
for (let key of Object.keys(userScriptAPIs)) {
|
||||
Schemas.exportLazyGetter(userScriptScope, key, () => {
|
||||
// Wrap the custom API methods exported to the userScript sandbox.
|
||||
return wrapUserScriptAPIMethod(userScriptAPIs[key], key);
|
||||
});
|
||||
}
|
||||
|
||||
context.userScriptsEvents.emit("on-before-script", clonedMetadata, userScriptScope);
|
||||
}
|
||||
}
|
||||
|
||||
var contentScripts = new DefaultWeakMap(matcher => {
|
||||
|
@ -848,9 +766,6 @@ class ContentScriptContextChild extends BaseContext {
|
|||
Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
|
||||
Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
|
||||
|
||||
// A set of exported API methods provided by the extension to the userScripts sandboxes.
|
||||
this.userScriptAPIs = null;
|
||||
|
||||
// Keep track if the userScript API script has been already executed in this context
|
||||
// (e.g. because there are more then one UserScripts that match the related webpage
|
||||
// and so the UserScript apiScript has already been executed).
|
||||
|
@ -878,14 +793,6 @@ class ContentScriptContextChild extends BaseContext {
|
|||
return this.sandbox;
|
||||
}
|
||||
|
||||
setUserScriptAPIs(extCustomAPIs) {
|
||||
if (this.userScriptAPIs) {
|
||||
throw new ExtensionError("userScripts APIs may only be set once");
|
||||
}
|
||||
|
||||
this.userScriptAPIs = extCustomAPIs;
|
||||
}
|
||||
|
||||
async executeAPIScript(apiScript) {
|
||||
// Execute the UserScript apiScript only once per context (e.g. more then one UserScripts
|
||||
// match the same webpage and the apiScript has already been executed).
|
||||
|
|
|
@ -76,7 +76,6 @@ extensions.registerModules({
|
|||
url: "chrome://extensions/content/child/ext-userScripts-content.js",
|
||||
scopes: ["content_child"],
|
||||
paths: [
|
||||
["userScripts", "setScriptAPIs"],
|
||||
["userScripts", "onBeforeScript"],
|
||||
],
|
||||
},
|
||||
|
|
|
@ -5,31 +5,344 @@
|
|||
var USERSCRIPT_PREFNAME = "extensions.webextensions.userScripts.enabled";
|
||||
var USERSCRIPT_DISABLED_ERRORMSG = `userScripts APIs are currently experimental and must be enabled with the ${USERSCRIPT_PREFNAME} preference.`;
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "Schemas", "resource://gre/modules/Schemas.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(this, "userScriptsEnabled", USERSCRIPT_PREFNAME, false);
|
||||
|
||||
var {
|
||||
ExtensionError,
|
||||
} = ExtensionUtils;
|
||||
|
||||
const TYPEOF_PRIMITIVES = ["bigint", "boolean", "number", "string", "symbol"];
|
||||
|
||||
/**
|
||||
* Represents a user script in the child content process.
|
||||
*
|
||||
* This class implements the API object that is passed as a parameter to the
|
||||
* browser.userScripts.onBeforeScript API Event.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {ContentScriptContextChild} params.context
|
||||
* The context which has registered the userScripts.onBeforeScript listener.
|
||||
* @param {PlainJSONValue} params.metadata
|
||||
* An opaque user script metadata value (as set in userScripts.register).
|
||||
* @param {Sandbox} params.scriptSandbox
|
||||
* The Sandbox object of the userScript.
|
||||
*/
|
||||
class UserScript {
|
||||
constructor({context, metadata, scriptSandbox}) {
|
||||
this.context = context;
|
||||
this.extension = context.extension;
|
||||
this.apiSandbox = context.cloneScope;
|
||||
this.metadata = metadata;
|
||||
this.scriptSandbox = scriptSandbox;
|
||||
|
||||
this.ScriptError = scriptSandbox.Error;
|
||||
this.ScriptPromise = scriptSandbox.Promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the API object provided to the userScripts.onBeforeScript listeners.
|
||||
*
|
||||
* @returns {Object}
|
||||
* The API object with the properties and methods to export
|
||||
* to the extension code.
|
||||
*/
|
||||
api() {
|
||||
return {
|
||||
metadata: this.metadata,
|
||||
defineGlobals: (sourceObject) => this.defineGlobals(sourceObject),
|
||||
export: (value) => this.export(value),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Define all the properties of a given plain object as lazy getters of the
|
||||
* userScript global object.
|
||||
*
|
||||
* @param {Object} sourceObject
|
||||
* A set of objects and methods to export into the userScript scope as globals.
|
||||
*
|
||||
* @throws {context.Error}
|
||||
* Throws an apiScript error when sourceObject is not a plain object.
|
||||
*/
|
||||
defineGlobals(sourceObject) {
|
||||
let className;
|
||||
try {
|
||||
className = ChromeUtils.getClassName(sourceObject, true);
|
||||
} catch (e) {
|
||||
// sourceObject is not an object;
|
||||
}
|
||||
|
||||
if (className !== "Object") {
|
||||
throw new this.context.Error("Invalid sourceObject type, plain object expected.");
|
||||
}
|
||||
|
||||
this.exportLazyGetters(sourceObject, this.scriptSandbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a given value to make it accessible to the userScript code.
|
||||
*
|
||||
* - any property value that is already accessible to the userScript code is returned unmodified by
|
||||
* the lazy getter
|
||||
* - any apiScript's Function is wrapped using the `wrapFunction` method
|
||||
* - any apiScript's Object is lazily exported (and the same wrappers are lazily applied to its
|
||||
* properties).
|
||||
*
|
||||
* @param {any} valueToExport
|
||||
* A value to convert into an object accessible to the userScript.
|
||||
*
|
||||
* @param {Object} privateOptions
|
||||
* A set of options used when this method is called internally (not exposed in the
|
||||
* api object exported to the onBeforeScript listeners).
|
||||
* @param {Error} Error
|
||||
* The Error constructor to use to report errors (defaults to the apiScript context's Error
|
||||
* when missing).
|
||||
* @param {Error} errorMessage
|
||||
* A custom error message to report exporting error on values not allowed.
|
||||
*
|
||||
* @returns {any}
|
||||
* The resulting userScript object.
|
||||
*
|
||||
* @throws {context.Error | privateOptions.Error}
|
||||
* Throws an error when the value is not allowed and it can't be exported into an allowed one.
|
||||
*/
|
||||
export(valueToExport, privateOptions = {}) {
|
||||
const ExportError = privateOptions.Error || this.context.Error;
|
||||
|
||||
if (this.canAccess(valueToExport, this.scriptSandbox)) {
|
||||
// Return the value unmodified if the userScript principal is already allowed
|
||||
// to access it.
|
||||
return valueToExport;
|
||||
}
|
||||
|
||||
let className;
|
||||
|
||||
try {
|
||||
className = ChromeUtils.getClassName(valueToExport, true);
|
||||
} catch (e) {
|
||||
// sourceObject is not an object;
|
||||
}
|
||||
|
||||
if (className === "Function") {
|
||||
return this.wrapFunction(valueToExport);
|
||||
}
|
||||
|
||||
if (className === "Object") {
|
||||
return this.exportLazyGetters(valueToExport);
|
||||
}
|
||||
|
||||
let valueType = className || typeof valueToExport;
|
||||
throw new ExportError(privateOptions.errorMessage ||
|
||||
`${valueType} cannot be exported to the userScript`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all the properties of the `src` plain object as lazy getters on the `dest` object,
|
||||
* or in a newly created userScript object if `dest` is `undefined`.
|
||||
*
|
||||
* @param {Object} src
|
||||
* A set of properties to define on a `dest` object as lazy getters.
|
||||
* @param {Object} [dest]
|
||||
* An optional `dest` object (a new userScript object is created by default when not specified).
|
||||
*
|
||||
* @returns {Object}
|
||||
* The resulting userScript object.
|
||||
*/
|
||||
exportLazyGetters(src, dest = undefined) {
|
||||
dest = dest || Cu.createObjectIn(this.scriptSandbox);
|
||||
|
||||
for (let [key, value] of this.shallowCloneEntries(src)) {
|
||||
Schemas.exportLazyGetter(dest, key, () => {
|
||||
return this.export(value, {
|
||||
// Lazy properties will raise an error for properties with not allowed
|
||||
// values to the userScript scope, and so we have to raise an userScript
|
||||
// Error here.
|
||||
Error: this.ScriptError,
|
||||
errorMessage: `Error accessing disallowed property "${key}"`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export and wrap an apiScript function to provide the following behaviors:
|
||||
* - errors throws from an exported function are checked by `handleAPIScriptError`
|
||||
* - returned apiScript's Promises (not accessible to the userScript) are converted into a
|
||||
* userScript's Promise
|
||||
* - check if the returned or resolved value is accessible to the userScript code
|
||||
* (and raise a userScript error if it is not)
|
||||
*
|
||||
* @param {Function} fn
|
||||
* The apiScript function to wrap
|
||||
*
|
||||
* @returns {Object}
|
||||
* The resulting userScript function.
|
||||
*/
|
||||
wrapFunction(fn) {
|
||||
return Cu.exportFunction((...args) => {
|
||||
let res;
|
||||
try {
|
||||
// Checks that all the elements in the `...args` array are allowed to be
|
||||
// received from the apiScript.
|
||||
for (let arg of args) {
|
||||
if (!this.canAccess(arg, this.apiSandbox)) {
|
||||
throw new this.ScriptError(`Parameter not accessible to the userScript API`);
|
||||
}
|
||||
}
|
||||
|
||||
res = fn(...args);
|
||||
} catch (err) {
|
||||
this.handleAPIScriptError(err);
|
||||
}
|
||||
|
||||
// Prevent execution of proxy traps while checking if the return value is a Promise.
|
||||
if (!Cu.isProxy(res) && res instanceof this.context.Promise) {
|
||||
return this.ScriptPromise.resolve().then(async () => {
|
||||
let value;
|
||||
|
||||
try {
|
||||
value = await res;
|
||||
} catch (err) {
|
||||
this.handleAPIScriptError(err);
|
||||
}
|
||||
|
||||
return this.ensureAccessible(value);
|
||||
});
|
||||
}
|
||||
|
||||
return this.ensureAccessible(res);
|
||||
}, this.scriptSandbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow clone the source object and iterate over its Object properties (or Array elements),
|
||||
* which allow us to safely iterate over all its properties (including callable objects that
|
||||
* would be hidden by the xrays vision, but excluding any property that could be tricky, e.g.
|
||||
* getters).
|
||||
*
|
||||
* @param {Object|Array} obj
|
||||
* The Object or Array object to shallow clone and iterate over.
|
||||
*/
|
||||
* shallowCloneEntries(obj) {
|
||||
const clonedObj = ChromeUtils.shallowClone(obj);
|
||||
|
||||
for (let entry of Object.entries(clonedObj)) {
|
||||
yield entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given value is accessible to the targetScope.
|
||||
*
|
||||
* @param {any} val
|
||||
* The value to check.
|
||||
* @param {Sandbox} targetScope
|
||||
* The targetScope that should be able to access the value.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canAccess(val, targetScope) {
|
||||
if (val == null || TYPEOF_PRIMITIVES.includes(typeof val)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Disallow objects that are coming from principals that are not
|
||||
// subsumed by the targetScope's principal.
|
||||
try {
|
||||
const targetPrincipal = Cu.getObjectPrincipal(targetScope);
|
||||
if (!targetPrincipal.subsumes(Cu.getObjectPrincipal(val))) {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
Cu.reportError(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the value returned (or resolved) from an apiScript method is accessible
|
||||
* to the userScript code, and throw a userScript Error if it is not allowed.
|
||||
*
|
||||
* @param {any} res
|
||||
* The value to return/resolve.
|
||||
*
|
||||
* @returns {any}
|
||||
* The exported value.
|
||||
*
|
||||
* @throws {Error}
|
||||
* Throws a userScript error when the value is not accessible to the userScript scope.
|
||||
*/
|
||||
ensureAccessible(res) {
|
||||
if (this.canAccess(res, this.scriptSandbox)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
throw new this.ScriptError("Return value not accessible to the userScript");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the error raised (and rejected promise returned) from apiScript functions exported to the
|
||||
* userScript.
|
||||
*
|
||||
* @param {any} err
|
||||
* The value to return/resolve.
|
||||
*
|
||||
* @throws {any}
|
||||
* This method is expected to throw:
|
||||
* - any value that is already accessible to the userScript code is forwarded unmodified
|
||||
* - any value that is not accessible to the userScript code is logged in the console
|
||||
* (to make it easier to investigate the underlying issue) and converted into a
|
||||
* userScript Error (with the generic "An unexpected apiScript error occurred" error
|
||||
* message accessible to the userScript)
|
||||
*/
|
||||
handleAPIScriptError(err) {
|
||||
if (this.canAccess(err, this.scriptSandbox)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Log the actual error on the console and raise a generic userScript Error
|
||||
// on error objects that can't be accessed by the UserScript principal.
|
||||
try {
|
||||
const debugName = this.extension.policy.debugName;
|
||||
Cu.reportError(
|
||||
`An unexpected apiScript error occurred for '${debugName}': ${err} :: ${err.stack}`);
|
||||
} catch (e) {}
|
||||
|
||||
throw new this.ScriptError(`An unexpected apiScript error occurred`);
|
||||
}
|
||||
}
|
||||
|
||||
this.userScriptsContent = class extends ExtensionAPI {
|
||||
getAPI(context) {
|
||||
return {
|
||||
userScripts: {
|
||||
setScriptAPIs(exportedAPIMethods) {
|
||||
if (!userScriptsEnabled) {
|
||||
throw new ExtensionError(USERSCRIPT_DISABLED_ERRORMSG);
|
||||
}
|
||||
|
||||
context.setUserScriptAPIs(exportedAPIMethods);
|
||||
},
|
||||
onBeforeScript: new EventManager({
|
||||
context,
|
||||
name: "userScripts.onBeforeScript",
|
||||
register: fire => {
|
||||
let handler = (event, userScriptMetadata, userScriptSandbox) => {
|
||||
const apiObj = Cu.createObjectIn(context.cloneScope);
|
||||
apiObj.metadata = userScriptMetadata;
|
||||
apiObj.global = userScriptSandbox;
|
||||
if (!userScriptsEnabled) {
|
||||
throw new ExtensionError(USERSCRIPT_DISABLED_ERRORMSG);
|
||||
}
|
||||
|
||||
let handler = (event, metadata, scriptSandbox, eventResult) => {
|
||||
const us = new UserScript({
|
||||
context, metadata, scriptSandbox,
|
||||
});
|
||||
|
||||
const apiObj = Cu.cloneInto(us.api(), context.cloneScope, {cloneFunctions: true});
|
||||
|
||||
Object.defineProperty(apiObj, "global", {
|
||||
value: scriptSandbox,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
fire.raw(apiObj);
|
||||
};
|
||||
|
|
|
@ -201,7 +201,6 @@
|
|||
"schema": "chrome://extensions/content/schemas/user_scripts_content.json",
|
||||
"scopes": ["content_child"],
|
||||
"paths": [
|
||||
["userScripts", "setScriptAPIs"],
|
||||
["userScripts", "onBeforeScript"]
|
||||
]
|
||||
},
|
||||
|
|
|
@ -7,29 +7,6 @@
|
|||
"namespace": "userScripts",
|
||||
"permissions": ["manifest:user_scripts"],
|
||||
"allowedContexts": ["content"],
|
||||
"types": [
|
||||
{
|
||||
"id": "ExportedAPIMethods",
|
||||
"type": "object",
|
||||
"description": "A set of API methods provided by the extensions to its userScripts",
|
||||
"additionalProperties": { "type": "function" }
|
||||
}
|
||||
],
|
||||
"functions": [
|
||||
{
|
||||
"name": "setScriptAPIs",
|
||||
"permissions": ["manifest:user_scripts.api_script"],
|
||||
"allowedContexts": ["content", "content_only"],
|
||||
"type": "function",
|
||||
"description": "Provides a set of custom API methods available to the registered userScripts",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "exportedAPIMethods",
|
||||
"$ref": "ExportedAPIMethods"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onBeforeScript",
|
||||
|
@ -49,6 +26,31 @@
|
|||
"global": {
|
||||
"type": "any",
|
||||
"description": "The userScript global"
|
||||
},
|
||||
"defineGlobals": {
|
||||
"type": "function",
|
||||
"description": "Exports all the properties of a given plain object as userScript globals",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "object",
|
||||
"name": "sourceObject",
|
||||
"description": "A plain object whose properties are exported as userScript globals"
|
||||
}
|
||||
]
|
||||
},
|
||||
"export": {
|
||||
"type": "function",
|
||||
"description": "Convert a given value to make it accessible to the userScript code",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "any",
|
||||
"name": "value",
|
||||
"description": "A value to convert into an object accessible to the userScript"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -339,11 +339,19 @@ async function test_userScript_APIMethod({
|
|||
browser.test.sendMessage("background-ready");
|
||||
}
|
||||
|
||||
function notifyFinish([failureReason]) {
|
||||
function notifyFinish(failureReason) {
|
||||
browser.test.assertEq(undefined, failureReason, "should be completed without errors");
|
||||
browser.test.sendMessage("test_userScript_APIMethod:done");
|
||||
}
|
||||
|
||||
function assertTrue(val, message) {
|
||||
browser.test.assertTrue(val, message);
|
||||
if (!val) {
|
||||
browser.test.sendMessage("test_userScript_APIMethod:done");
|
||||
throw message;
|
||||
}
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: [
|
||||
|
@ -359,7 +367,10 @@ async function test_userScript_APIMethod({
|
|||
(${backgroundScript})(${userScript}, metadata, ${runtimeMessageListener})
|
||||
`,
|
||||
files: {
|
||||
"api-script.js": `(${apiScript})(${notifyFinish})`,
|
||||
"api-script.js": `(${apiScript})({
|
||||
assertTrue: ${assertTrue},
|
||||
notifyFinish: ${notifyFinish}
|
||||
})`,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -384,10 +395,13 @@ async function test_userScript_APIMethod({
|
|||
}
|
||||
|
||||
add_task(async function test_apiScript_exports_simple_sync_method() {
|
||||
function apiScript(notifyFinish) {
|
||||
browser.userScripts.setScriptAPIs({
|
||||
notifyFinish,
|
||||
testAPIMethod([param1, param2, arrayParam], scriptMetadata) {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
const scriptMetadata = script.metadata;
|
||||
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethod(stringParam, numberParam, boolParam, nullParam, undefinedParam, arrayParam) {
|
||||
browser.test.assertEq("test-user-script-exported-apis", scriptMetadata.name,
|
||||
"Got the expected value for a string scriptMetadata property");
|
||||
browser.test.assertEq(null, scriptMetadata.nullProperty,
|
||||
|
@ -400,8 +414,11 @@ add_task(async function test_apiScript_exports_simple_sync_method() {
|
|||
scriptMetadata.objectProperty.nestedProp === "nestedValue",
|
||||
"Got the expected value for an object scriptMetadata property");
|
||||
|
||||
browser.test.assertEq("param1", param1, "Got the expected parameter value");
|
||||
browser.test.assertEq("param2", param2, "Got the expected parameter value");
|
||||
browser.test.assertEq("param1", stringParam, "Got the expected string parameter value");
|
||||
browser.test.assertEq(123, numberParam, "Got the expected number parameter value");
|
||||
browser.test.assertEq(true, boolParam, "Got the expected boolean parameter value");
|
||||
browser.test.assertEq(null, nullParam, "Got the expected null parameter value");
|
||||
browser.test.assertEq(undefined, undefinedParam, "Got the expected undefined parameter value");
|
||||
|
||||
browser.test.assertEq(3, arrayParam.length, "Got the expected length on the array param");
|
||||
browser.test.assertTrue(arrayParam.includes(1),
|
||||
|
@ -410,10 +427,11 @@ add_task(async function test_apiScript_exports_simple_sync_method() {
|
|||
return "returned_value";
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function userScript() {
|
||||
const {testAPIMethod, notifyFinish} = this;
|
||||
const {assertTrue, notifyFinish, testAPIMethod} = this;
|
||||
|
||||
// Redefine the includes method on the Array prototype, to explicitly verify that the method
|
||||
// redefined in the userScript is not used when accessing arrayParam.includes from the API script.
|
||||
|
@ -421,14 +439,12 @@ add_task(async function test_apiScript_exports_simple_sync_method() {
|
|||
throw new Error("Unexpected prototype leakage");
|
||||
};
|
||||
const arrayParam = new Array(1, 2, 3); // eslint-disable-line no-array-constructor
|
||||
const result = testAPIMethod("param1", "param2", arrayParam);
|
||||
const result = testAPIMethod("param1", 123, true, null, undefined, arrayParam);
|
||||
|
||||
assertTrue(result === "returned_value", `userScript got an unexpected result value: ${result}`);
|
||||
|
||||
if (result !== "returned_value") {
|
||||
notifyFinish(`userScript got an unexpected result value: ${result}`);
|
||||
} else {
|
||||
notifyFinish();
|
||||
}
|
||||
}
|
||||
|
||||
const userScriptMetadata = {
|
||||
name: "test-user-script-exported-apis",
|
||||
|
@ -445,23 +461,23 @@ add_task(async function test_apiScript_exports_simple_sync_method() {
|
|||
});
|
||||
|
||||
add_task(async function test_apiScript_async_method() {
|
||||
function apiScript(notifyFinish) {
|
||||
const {cloneInto} = this;
|
||||
|
||||
browser.userScripts.setScriptAPIs({
|
||||
notifyFinish,
|
||||
async testAPIMethod([param, cb, cb2], scriptMetadata, scriptGlobal) {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethod(param, cb, cb2, objWithCb) {
|
||||
browser.test.assertEq("function", typeof cb, "Got a callback function parameter");
|
||||
browser.test.assertTrue(cb === cb2, "Got the same cloned function for the same function parameter");
|
||||
|
||||
browser.runtime.sendMessage(param).then(bgPageRes => {
|
||||
const cbResult = cb(cloneInto(bgPageRes, scriptGlobal));
|
||||
const cbResult = cb(script.export(bgPageRes));
|
||||
browser.test.sendMessage("user-script-callback-return", cbResult);
|
||||
});
|
||||
|
||||
return "resolved_value";
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
|
@ -475,7 +491,7 @@ add_task(async function test_apiScript_async_method() {
|
|||
throw new Error("Promise constructor poisoning");
|
||||
};
|
||||
|
||||
const {testAPIMethod, notifyFinish} = this;
|
||||
const {assertTrue, notifyFinish, testAPIMethod} = this;
|
||||
|
||||
const cb = (cbParam) => {
|
||||
return `callback param: ${JSON.stringify(cbParam)}`;
|
||||
|
@ -483,12 +499,11 @@ add_task(async function test_apiScript_async_method() {
|
|||
const cb2 = cb;
|
||||
const asyncAPIResult = await testAPIMethod("param3", cb, cb2);
|
||||
|
||||
if (asyncAPIResult !== "resolved_value") {
|
||||
notifyFinish(`userScript got an unexpected resolved value: ${asyncAPIResult}`);
|
||||
} else {
|
||||
assertTrue(asyncAPIResult === "resolved_value",
|
||||
`userScript got an unexpected resolved value: ${asyncAPIResult}`);
|
||||
|
||||
notifyFinish();
|
||||
}
|
||||
}
|
||||
|
||||
async function runtimeMessageListener(param) {
|
||||
if (param !== "param3") {
|
||||
|
@ -510,16 +525,663 @@ add_task(async function test_apiScript_async_method() {
|
|||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_with_webpage_objects_params() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethod(windowParam, documentParam) {
|
||||
browser.test.assertEq(window, windowParam, "Got a reference to the native window as first param");
|
||||
browser.test.assertEq(window.document, documentParam,
|
||||
"Got a reference to the native document as second param");
|
||||
|
||||
// Return an uncloneable webpage object, which checks that if the returned object is from a principal
|
||||
// that is subsumed by the userScript sandbox principal, it is returned without being cloned.
|
||||
return windowParam;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {assertTrue, notifyFinish, testAPIMethod} = this;
|
||||
|
||||
const result = testAPIMethod(window, document);
|
||||
|
||||
// We expect the returned value to be the uncloneable window object.
|
||||
assertTrue(result === window,
|
||||
`userScript got an unexpected returned value: ${result}`);
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_got_param_with_methods() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
const scriptGlobal = script.global;
|
||||
const ScriptFunction = scriptGlobal.Function;
|
||||
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethod(objWithMethods) {
|
||||
browser.test.assertEq("objPropertyValue", objWithMethods && objWithMethods.objProperty,
|
||||
"Got the expected property on the object passed as a parameter");
|
||||
browser.test.assertEq(undefined, typeof objWithMethods && objWithMethods.objMethod,
|
||||
"XrayWrapper should deny access to a callable property");
|
||||
|
||||
browser.test.assertTrue(
|
||||
objWithMethods && objWithMethods.wrappedJSObject &&
|
||||
objWithMethods.wrappedJSObject.objMethod instanceof ScriptFunction.wrappedJSObject,
|
||||
"The callable property is accessible on the wrappedJSObject");
|
||||
|
||||
browser.test.assertEq("objMethodResult: p1", objWithMethods && objWithMethods.wrappedJSObject &&
|
||||
objWithMethods.wrappedJSObject.objMethod("p1"),
|
||||
"Got the expected result when calling the method on the wrappedJSObject");
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {assertTrue, notifyFinish, testAPIMethod} = this;
|
||||
|
||||
let result = testAPIMethod({
|
||||
objProperty: "objPropertyValue",
|
||||
objMethod(param) {
|
||||
return `objMethodResult: ${param}`;
|
||||
},
|
||||
});
|
||||
|
||||
assertTrue(result === true, `userScript got an unexpected returned value: ${result}`);
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_throws_errors() {
|
||||
function apiScript({notifyFinish}) {
|
||||
let proxyTrapsCount = 0;
|
||||
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
const scriptGlobals = {
|
||||
Error: script.global.Error,
|
||||
TypeError: script.global.TypeError,
|
||||
Proxy: script.global.Proxy,
|
||||
};
|
||||
|
||||
script.defineGlobals({
|
||||
notifyFinish,
|
||||
testAPIMethod(errorTestName, returnRejectedPromise) {
|
||||
let err;
|
||||
|
||||
switch (errorTestName) {
|
||||
case "apiScriptError":
|
||||
err = new Error(`${errorTestName} message`);
|
||||
break;
|
||||
case "apiScriptThrowsPlainString":
|
||||
err = `${errorTestName} message`;
|
||||
break;
|
||||
case "apiScriptThrowsNull":
|
||||
err = null;
|
||||
break;
|
||||
case "userScriptError":
|
||||
err = new scriptGlobals.Error(`${errorTestName} message`);
|
||||
break;
|
||||
case "userScriptTypeError":
|
||||
err = new scriptGlobals.TypeError(`${errorTestName} message`);
|
||||
break;
|
||||
case "userScriptProxyObject":
|
||||
let proxyTarget = script.export({
|
||||
name: "ProxyObject", message: "ProxyObject message",
|
||||
});
|
||||
let proxyHandlers = script.export({
|
||||
get(target, prop) {
|
||||
proxyTrapsCount++;
|
||||
switch (prop) {
|
||||
case "name":
|
||||
return "ProxyObjectGetName";
|
||||
case "message":
|
||||
return "ProxyObjectGetMessage";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
getPrototypeOf() {
|
||||
proxyTrapsCount++;
|
||||
return scriptGlobals.TypeError;
|
||||
},
|
||||
});
|
||||
err = new scriptGlobals.Proxy(proxyTarget, proxyHandlers);
|
||||
break;
|
||||
default:
|
||||
browser.test.fail(`Unknown ${errorTestName} error testname`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (returnRejectedPromise) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
throw err;
|
||||
},
|
||||
assertNoProxyTrapTriggered() {
|
||||
browser.test.assertEq(0, proxyTrapsCount, "Proxy traps should not be triggered");
|
||||
},
|
||||
resetProxyTrapCounter() {
|
||||
proxyTrapsCount = 0;
|
||||
},
|
||||
sendResults(results) {
|
||||
browser.test.sendMessage("test-results", results);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {
|
||||
assertNoProxyTrapTriggered,
|
||||
notifyFinish,
|
||||
resetProxyTrapCounter,
|
||||
sendResults,
|
||||
testAPIMethod,
|
||||
} = this;
|
||||
|
||||
let apiThrowResults = {};
|
||||
let apiThrowTestCases = [
|
||||
"apiScriptError",
|
||||
"apiScriptThrowsPlainString",
|
||||
"apiScriptThrowsNull",
|
||||
"userScriptError",
|
||||
"userScriptTypeError",
|
||||
"userScriptProxyObject",
|
||||
];
|
||||
for (let errorTestName of apiThrowTestCases) {
|
||||
try {
|
||||
testAPIMethod(errorTestName);
|
||||
} catch (err) {
|
||||
// We expect that no proxy traps have been triggered by the WebExtensions internals.
|
||||
if (errorTestName === "userScriptProxyObject") {
|
||||
assertNoProxyTrapTriggered();
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
apiThrowResults[errorTestName] = {name: err.name, message: err.message};
|
||||
} else {
|
||||
apiThrowResults[errorTestName] = {
|
||||
name: err && err.name,
|
||||
message: err && err.message,
|
||||
typeOf: typeof err,
|
||||
value: err,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendResults(apiThrowResults);
|
||||
|
||||
resetProxyTrapCounter();
|
||||
|
||||
let apiRejectsResults = {};
|
||||
for (let errorTestName of apiThrowTestCases) {
|
||||
try {
|
||||
await testAPIMethod(errorTestName, true);
|
||||
} catch (err) {
|
||||
// We expect that no proxy traps have been triggered by the WebExtensions internals.
|
||||
if (errorTestName === "userScriptProxyObject") {
|
||||
assertNoProxyTrapTriggered();
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
apiRejectsResults[errorTestName] = {name: err.name, message: err.message};
|
||||
} else {
|
||||
apiRejectsResults[errorTestName] = {
|
||||
name: err && err.name,
|
||||
message: err && err.message,
|
||||
typeOf: typeof err,
|
||||
value: err,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendResults(apiRejectsResults);
|
||||
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
async testFn({extension}) {
|
||||
const expectedResults = {
|
||||
// Any error not explicitly raised as a userScript objects or error instance is
|
||||
// expected to be turned into a generic error message.
|
||||
"apiScriptError": {name: "Error", message: "An unexpected apiScript error occurred"},
|
||||
|
||||
// When the api script throws a primitive value, we expect to receive it unmodified on
|
||||
// the userScript side.
|
||||
"apiScriptThrowsPlainString": {
|
||||
typeOf: "string", value: "apiScriptThrowsPlainString message",
|
||||
name: undefined, message: undefined,
|
||||
},
|
||||
"apiScriptThrowsNull": {
|
||||
typeOf: "object", value: null,
|
||||
name: undefined, message: undefined,
|
||||
},
|
||||
|
||||
// Error messages that the apiScript has explicitly created as userScript's Error
|
||||
// global instances are expected to be passing through unmodified.
|
||||
"userScriptError": {name: "Error", message: "userScriptError message"},
|
||||
"userScriptTypeError": {name: "TypeError", message: "userScriptTypeError message"},
|
||||
|
||||
// Error raised from the apiScript as userScript proxy objects are expected to
|
||||
// be passing through unmodified.
|
||||
"userScriptProxyObject": {
|
||||
typeOf: "object", name: "ProxyObjectGetName", message: "ProxyObjectGetMessage",
|
||||
},
|
||||
};
|
||||
|
||||
info("Checking results from errors raised from an apiScript exported function");
|
||||
|
||||
const apiThrowResults = await extension.awaitMessage("test-results");
|
||||
|
||||
for (let [key, expected] of Object.entries(expectedResults)) {
|
||||
Assert.deepEqual(apiThrowResults[key], expected,
|
||||
`Got the expected error object for test case "${key}"`);
|
||||
}
|
||||
|
||||
Assert.deepEqual(Object.keys(expectedResults).sort(),
|
||||
Object.keys(apiThrowResults).sort(),
|
||||
"the expected and actual test case names matches");
|
||||
|
||||
info("Checking expected results from errors raised from an apiScript exported function");
|
||||
|
||||
// Verify expected results from rejected promises returned from an apiScript exported function.
|
||||
const apiThrowRejections = await extension.awaitMessage("test-results");
|
||||
|
||||
for (let [key, expected] of Object.entries(expectedResults)) {
|
||||
Assert.deepEqual(apiThrowRejections[key], expected,
|
||||
`Got the expected rejected object for test case "${key}"`);
|
||||
}
|
||||
|
||||
Assert.deepEqual(Object.keys(expectedResults).sort(),
|
||||
Object.keys(apiThrowRejections).sort(),
|
||||
"the expected and actual test case names matches");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_ensure_xraywrapped_proxy_in_params() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethod(...args) {
|
||||
// Proxies are opaque when wrapped in Xrays, and the proto of an opaque object
|
||||
// is supposed to be Object.prototype.
|
||||
browser.test.assertEq(
|
||||
script.global.Object.prototype,
|
||||
Object.getPrototypeOf(args[0]),
|
||||
"Calling getPrototypeOf on the XrayWrapped proxy object doesn't run the proxy trap");
|
||||
|
||||
browser.test.assertTrue(Array.isArray(args[0]),
|
||||
"Got an array object for the XrayWrapped proxy object param");
|
||||
browser.test.assertEq(undefined, args[0].length,
|
||||
"XrayWrappers deny access to the length property");
|
||||
browser.test.assertEq(undefined, args[0][0],
|
||||
"Got the expected item in the array object");
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {
|
||||
assertTrue,
|
||||
notifyFinish,
|
||||
testAPIMethod,
|
||||
} = this;
|
||||
|
||||
let proxy = new Proxy(["expectedArrayValue"], {
|
||||
getPrototypeOf() {
|
||||
throw new Error("Proxy's getPrototypeOf trap");
|
||||
},
|
||||
get(target, prop, receiver) {
|
||||
throw new Error("Proxy's get trap");
|
||||
},
|
||||
});
|
||||
|
||||
let result = testAPIMethod(proxy);
|
||||
|
||||
assertTrue(result, `userScript got an unexpected returned value: ${result}`);
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_return_proxy_object() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
const {cloneInto} = this;
|
||||
let proxyTrapsCount = 0;
|
||||
let scriptTrapsCount = 0;
|
||||
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethodError() {
|
||||
return new Proxy(["expectedArrayValue"], {
|
||||
getPrototypeOf(target) {
|
||||
proxyTrapsCount++;
|
||||
return Object.getPrototypeOf(target);
|
||||
},
|
||||
});
|
||||
},
|
||||
testAPIMethodOk() {
|
||||
return new script.global.Proxy(
|
||||
cloneInto(["expectedArrayValue"], script.global),
|
||||
cloneInto({
|
||||
getPrototypeOf(target) {
|
||||
scriptTrapsCount++;
|
||||
return script.global.Object.getPrototypeOf(target);
|
||||
},
|
||||
}, script.global, {cloneFunctions: true}));
|
||||
},
|
||||
assertNoProxyTrapTriggered() {
|
||||
browser.test.assertEq(0, proxyTrapsCount, "Proxy traps should not be triggered");
|
||||
},
|
||||
assertScriptProxyTrapsCount(expected) {
|
||||
browser.test.assertEq(expected, scriptTrapsCount, "Script Proxy traps should have been triggered");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {
|
||||
assertTrue,
|
||||
assertNoProxyTrapTriggered,
|
||||
assertScriptProxyTrapsCount,
|
||||
notifyFinish,
|
||||
testAPIMethodError,
|
||||
testAPIMethodOk,
|
||||
} = this;
|
||||
|
||||
let error;
|
||||
try {
|
||||
let result = testAPIMethodError();
|
||||
notifyFinish(`Unexpected returned value while expecting error: ${result}`);
|
||||
return;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
assertTrue(error && error.message.includes("Return value not accessible to the userScript"),
|
||||
`Got an unexpected error message: ${error}`);
|
||||
|
||||
error = undefined;
|
||||
try {
|
||||
let result = testAPIMethodOk();
|
||||
assertScriptProxyTrapsCount(0);
|
||||
if (!(result instanceof Array)) {
|
||||
notifyFinish(`Got an unexpected result: ${result}`);
|
||||
return;
|
||||
}
|
||||
assertScriptProxyTrapsCount(1);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
assertTrue(!error, `Got an unexpected error: ${error}`);
|
||||
|
||||
assertNoProxyTrapTriggered();
|
||||
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_returns_functions() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIReturnsFunction() {
|
||||
// Return a function with provides the same kind of behavior
|
||||
// of the API methods exported as globals.
|
||||
return script.export(() => window);
|
||||
},
|
||||
testAPIReturnsObjWithMethod() {
|
||||
return script.export({
|
||||
getWindow() {
|
||||
return window;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {
|
||||
assertTrue,
|
||||
notifyFinish,
|
||||
testAPIReturnsFunction,
|
||||
testAPIReturnsObjWithMethod,
|
||||
} = this;
|
||||
|
||||
let resultFn = testAPIReturnsFunction();
|
||||
assertTrue(typeof resultFn === "function",
|
||||
`userScript got an unexpected returned value: ${typeof resultFn}`);
|
||||
|
||||
let fnRes = resultFn();
|
||||
assertTrue(fnRes === window,
|
||||
`Got an unexpected value from the returned function: ${fnRes}`);
|
||||
|
||||
let resultObj = testAPIReturnsObjWithMethod();
|
||||
let actualTypeof = resultObj && typeof resultObj.getWindow;
|
||||
assertTrue(actualTypeof === "function",
|
||||
`Returned object does not have the expected getWindow method: ${actualTypeof}`);
|
||||
|
||||
let methodRes = resultObj.getWindow();
|
||||
assertTrue(methodRes === window,
|
||||
`Got an unexpected value from the returned method: ${methodRes}`);
|
||||
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_clone_non_subsumed_returned_values() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethodReturnOk() {
|
||||
return script.export({
|
||||
objKey1: {
|
||||
nestedProp: "nestedvalue",
|
||||
},
|
||||
window,
|
||||
});
|
||||
},
|
||||
testAPIMethodExplicitlyClonedError() {
|
||||
let result = script.export({apiScopeObject: undefined});
|
||||
|
||||
browser.test.assertThrows(
|
||||
() => {
|
||||
result.apiScopeObject = {disallowedProp: "disallowedValue"};
|
||||
},
|
||||
/Not allowed to define cross-origin object as property on .* XrayWrapper/,
|
||||
"Assigning a property to a xRayWrapper is expected to throw");
|
||||
|
||||
// Let the exception to be raised, so that we check that the actual underlying
|
||||
// error message is not leaking in the userScript (replaced by the generic
|
||||
// "An unexpected apiScript error occurred" error message).
|
||||
result.apiScopeObject = {disallowedProp: "disallowedValue"};
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {
|
||||
assertTrue,
|
||||
notifyFinish,
|
||||
testAPIMethodReturnOk,
|
||||
testAPIMethodExplicitlyClonedError,
|
||||
} = this;
|
||||
|
||||
let result = testAPIMethodReturnOk();
|
||||
|
||||
assertTrue(result && ("objKey1" in result) && result.objKey1.nestedProp === "nestedvalue",
|
||||
`userScript got an unexpected returned value: ${result}`);
|
||||
|
||||
assertTrue(result.window === window,
|
||||
`userScript should have access to the window property: ${result.window}`);
|
||||
|
||||
let error;
|
||||
try {
|
||||
result = testAPIMethodExplicitlyClonedError();
|
||||
notifyFinish(`Unexpected returned value while expecting error: ${result}`);
|
||||
return;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
// We expect the generic "unexpected apiScript error occurred" to be raised to the
|
||||
// userScript code.
|
||||
assertTrue(error && error.message.includes("An unexpected apiScript error occurred"),
|
||||
`Got an unexpected error message: ${error}`);
|
||||
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_export_primitive_types() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethod(typeToExport) {
|
||||
switch (typeToExport) {
|
||||
case "boolean": return script.export(true);
|
||||
case "number": return script.export(123);
|
||||
case "string": return script.export("a string");
|
||||
case "symbol": return script.export(Symbol("a symbol"));
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {assertTrue, notifyFinish, testAPIMethod} = this;
|
||||
|
||||
let v = testAPIMethod("boolean");
|
||||
assertTrue(v === true, `Should export a boolean`);
|
||||
|
||||
v = testAPIMethod("number");
|
||||
assertTrue(v === 123, `Should export a number`);
|
||||
|
||||
v = testAPIMethod("string");
|
||||
assertTrue(v === "a string", `Should export a string`);
|
||||
|
||||
v = testAPIMethod("symbol");
|
||||
assertTrue(typeof v === "symbol", `Should export a symbol`);
|
||||
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_apiScript_method_avoid_unnecessary_params_cloning() {
|
||||
function apiScript(sharedTestAPIMethods) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
...sharedTestAPIMethods,
|
||||
testAPIMethodReturnsParam(param) {
|
||||
return param;
|
||||
},
|
||||
testAPIMethodReturnsUnwrappedParam(param) {
|
||||
return param.wrappedJSObject;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function userScript() {
|
||||
const {
|
||||
assertTrue,
|
||||
notifyFinish,
|
||||
testAPIMethodReturnsParam,
|
||||
testAPIMethodReturnsUnwrappedParam,
|
||||
} = this;
|
||||
|
||||
let obj = {};
|
||||
|
||||
let result = testAPIMethodReturnsParam(obj);
|
||||
|
||||
assertTrue(result === obj,
|
||||
`Expect returned value to be strictly equal to the API method parameter`);
|
||||
|
||||
result = testAPIMethodReturnsUnwrappedParam(obj);
|
||||
|
||||
assertTrue(result === obj,
|
||||
`Expect returned value to be strictly equal to the unwrapped API method parameter`);
|
||||
|
||||
notifyFinish();
|
||||
}
|
||||
|
||||
await test_userScript_APIMethod({
|
||||
userScript,
|
||||
apiScript,
|
||||
});
|
||||
});
|
||||
|
||||
// This test verify that a cached script is still able to catch the document
|
||||
// while it is still loading (when we do not block the document parsing as
|
||||
// we do for a non cached script).
|
||||
add_task(async function test_cached_userScript_on_document_start() {
|
||||
function apiScript() {
|
||||
browser.userScripts.setScriptAPIs({
|
||||
sendTestMessage([name, params]) {
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
sendTestMessage(name, params) {
|
||||
return browser.test.sendMessage(name, params);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function background() {
|
||||
|
@ -610,14 +1272,12 @@ add_task(async function test_userScripts_pref_disabled() {
|
|||
|
||||
async function contentScript() {
|
||||
let promise = (async () => {
|
||||
browser.userScripts.setScriptAPIs({
|
||||
GM_apiMethod() {},
|
||||
});
|
||||
browser.userScripts.onBeforeScript.addListener(() => {});
|
||||
})();
|
||||
await browser.test.assertRejects(
|
||||
promise,
|
||||
/userScripts APIs are currently experimental/,
|
||||
"Got the expected error from userScripts.setScriptAPIs when the userScripts API is disabled");
|
||||
"Got the expected error from userScripts.onBeforeScript when the userScripts API is disabled");
|
||||
|
||||
browser.test.sendMessage("content-script:done");
|
||||
}
|
||||
|
@ -657,7 +1317,7 @@ add_task(async function test_userScripts_pref_disabled() {
|
|||
run_userScript_on_pref_disabled_test);
|
||||
});
|
||||
|
||||
// This test verify that userScripts.setScriptAPIs is not available without
|
||||
// This test verify that userScripts.onBeforeScript API Event is not available without
|
||||
// a "user_scripts.api_script" property in the manifest.
|
||||
add_task(async function test_user_script_api_script_required() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
|
@ -673,9 +1333,9 @@ add_task(async function test_user_script_api_script_required() {
|
|||
},
|
||||
files: {
|
||||
"content_script.js": function() {
|
||||
browser.test.assertEq(undefined, browser.userScripts && browser.userScripts.setScriptAPIs,
|
||||
"Got an undefined setScriptAPIs as expected");
|
||||
browser.test.sendMessage("no-setScriptAPIs:done");
|
||||
browser.test.assertEq(undefined, browser.userScripts && browser.userScripts.onBeforeScript,
|
||||
"Got an undefined onBeforeScript property as expected");
|
||||
browser.test.sendMessage("no-onBeforeScript:done");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -685,7 +1345,7 @@ add_task(async function test_user_script_api_script_required() {
|
|||
let url = `${BASE_URL}/file_sample.html`;
|
||||
let contentPage = await ExtensionTestUtils.loadContentPage(url);
|
||||
|
||||
await extension.awaitMessage("no-setScriptAPIs:done");
|
||||
await extension.awaitMessage("no-onBeforeScript:done");
|
||||
|
||||
await extension.unload();
|
||||
await contentPage.close();
|
||||
|
@ -735,32 +1395,27 @@ add_task(async function test_scriptMetaData() {
|
|||
function apiScript() {
|
||||
let testCases = getTestCases(false);
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
let metadataOnFirstCall = [];
|
||||
browser.userScripts.setScriptAPIs({
|
||||
checkMetadata(params, metadata, scriptGlobal) {
|
||||
// We save the reference to the received metadata object, so that
|
||||
// checkMetadataAgain can verify that the same object is received.
|
||||
metadataOnFirstCall[i] = metadata;
|
||||
|
||||
browser.userScripts.onBeforeScript.addListener(script => {
|
||||
script.defineGlobals({
|
||||
checkMetadata() {
|
||||
let expectation = testCases[i];
|
||||
let metadata = script.metadata;
|
||||
if (typeof expectation === "object" && expectation !== null) {
|
||||
// Non-primitive values cannot be compared with assertEq,
|
||||
// so serialize both and just verify that they are equal.
|
||||
expectation = JSON.stringify(expectation);
|
||||
metadata = JSON.stringify(metadata);
|
||||
metadata = JSON.stringify(script.metadata);
|
||||
}
|
||||
browser.test.assertEq(expectation, metadata, `Expected metadata at call ${i}`);
|
||||
++i;
|
||||
},
|
||||
checkMetadataAgain(params, metadata, scriptGlobal) {
|
||||
browser.test.assertEq(metadataOnFirstCall[j], metadata, `Expected same metadata at call ${j}`);
|
||||
|
||||
if (++j === testCases.length) {
|
||||
browser.test.assertEq(expectation, metadata,
|
||||
`Expected metadata at call ${i}`);
|
||||
if (++i === testCases.length) {
|
||||
browser.test.sendMessage("apiscript:done");
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
|
@ -773,7 +1428,7 @@ add_task(async function test_scriptMetaData() {
|
|||
},
|
||||
files: {
|
||||
"apiscript.js": `${getTestCases};(${apiScript})()`,
|
||||
"userscript.js": "checkMetadata();checkMetadataAgain();",
|
||||
"userscript.js": "checkMetadata();",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -12,11 +12,15 @@ const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
|
|||
|
||||
async function run_userScripts_telemetry_test() {
|
||||
function apiScript() {
|
||||
browser.userScripts.setScriptAPIs({
|
||||
US_test_sendMessage([msg, data], scriptMetadata, scriptGlobal) {
|
||||
browser.userScripts.onBeforeScript.addListener(userScript => {
|
||||
const scriptMetadata = userScript.metadata;
|
||||
|
||||
userScript.defineGlobals({
|
||||
US_test_sendMessage(msg, data) {
|
||||
browser.test.sendMessage(msg, {data, scriptMetadata});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function background() {
|
||||
|
|
Загрузка…
Ссылка в новой задаче