зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1197346 - Allow browser.storage to be used from content scripts (r=kmag)
This commit is contained in:
Родитель
9dc52ee77a
Коммит
261372f2cb
|
@ -184,7 +184,7 @@ var Management = {
|
|||
|
||||
// Mash together into a single object all the APIs registered by the
|
||||
// functions above. Return the merged object.
|
||||
generateAPIs(extension, context, apis) {
|
||||
generateAPIs(extension, context, apis, namespaces = null) {
|
||||
let obj = {};
|
||||
|
||||
// Recursively copy properties from source to dest.
|
||||
|
@ -203,6 +203,9 @@ var Management = {
|
|||
}
|
||||
|
||||
for (let api of apis) {
|
||||
if (namespaces && !namespaces.includes(api.namespace)) {
|
||||
continue;
|
||||
}
|
||||
if (api.permission) {
|
||||
if (!extension.hasPermission(api.permission)) {
|
||||
continue;
|
||||
|
@ -244,7 +247,7 @@ var Management = {
|
|||
// |incognito| is the content running in a private context (default: false).
|
||||
ExtensionContext = class extends BaseContext {
|
||||
constructor(extension, params) {
|
||||
super();
|
||||
super(extension.id);
|
||||
|
||||
let {type, contentWindow, uri} = params;
|
||||
this.extension = extension;
|
||||
|
@ -269,7 +272,9 @@ ExtensionContext = class extends BaseContext {
|
|||
let filter = {extensionId: extension.id};
|
||||
this.messenger = new Messenger(this, [Services.mm, Services.ppmm], sender, filter, delegate);
|
||||
|
||||
this.extension.views.add(this);
|
||||
if (this.externallyVisible) {
|
||||
this.extension.views.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
|
@ -280,6 +285,10 @@ ExtensionContext = class extends BaseContext {
|
|||
return this.contentWindow.document.nodePrincipal;
|
||||
}
|
||||
|
||||
get externallyVisible() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called when the extension shuts down.
|
||||
shutdown() {
|
||||
Management.emit("page-shutdown", this);
|
||||
|
@ -300,10 +309,172 @@ ExtensionContext = class extends BaseContext {
|
|||
|
||||
Management.emit("page-unload", this);
|
||||
|
||||
this.extension.views.delete(this);
|
||||
if (this.externallyVisible) {
|
||||
this.extension.views.delete(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ProxyContext extends ExtensionContext {
|
||||
constructor(extension, params, messageManager, principal) {
|
||||
params.contentWindow = null;
|
||||
params.uri = NetUtil.newURI(params.url);
|
||||
|
||||
super(extension, params);
|
||||
this.messageManager = messageManager;
|
||||
this.principal_ = principal;
|
||||
|
||||
this.apiObj = {};
|
||||
GlobalManager.injectInObject(extension, this, null, this.apiObj, ["storage", "test"]);
|
||||
|
||||
this.listenerProxies = new Map();
|
||||
|
||||
this.sandbox = Cu.Sandbox(principal, {});
|
||||
}
|
||||
|
||||
get principal() {
|
||||
return this.principal_;
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
return this.sandbox;
|
||||
}
|
||||
|
||||
get externallyVisible() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function findPathInObject(obj, path) {
|
||||
for (let elt of path) {
|
||||
obj = obj[elt];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
let ParentAPIManager = {
|
||||
proxyContexts: new Map(),
|
||||
|
||||
init() {
|
||||
Services.obs.addObserver(this, "message-manager-close", false);
|
||||
|
||||
Services.mm.addMessageListener("API:CreateProxyContext", this);
|
||||
Services.mm.addMessageListener("API:CloseProxyContext", this, true);
|
||||
Services.mm.addMessageListener("API:Call", this);
|
||||
Services.mm.addMessageListener("API:AddListener", this);
|
||||
Services.mm.addMessageListener("API:RemoveListener", this);
|
||||
},
|
||||
|
||||
// "message-manager-close" observer.
|
||||
observe(subject, topic, data) {
|
||||
let mm = subject;
|
||||
for (let [childId, context] of this.proxyContexts) {
|
||||
if (context.messageManager == mm) {
|
||||
this.closeProxyContext(childId);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
receiveMessage({name, data, target}) {
|
||||
switch (name) {
|
||||
case "API:CreateProxyContext":
|
||||
this.createProxyContext(data, target);
|
||||
break;
|
||||
|
||||
case "API:CloseProxyContext":
|
||||
this.closeProxyContext(data.childId);
|
||||
break;
|
||||
|
||||
case "API:Call":
|
||||
this.call(data, target);
|
||||
break;
|
||||
|
||||
case "API:AddListener":
|
||||
this.addListener(data, target);
|
||||
break;
|
||||
|
||||
case "API:RemoveListener":
|
||||
this.removeListener(data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
createProxyContext(data, target) {
|
||||
let {extensionId, childId, principal} = data;
|
||||
let extension = GlobalManager.getExtension(extensionId);
|
||||
|
||||
let context = new ProxyContext(extension, data, target.messageManager, principal);
|
||||
this.proxyContexts.set(childId, context);
|
||||
},
|
||||
|
||||
closeProxyContext(childId) {
|
||||
if (!this.proxyContexts.has(childId)) {
|
||||
return;
|
||||
}
|
||||
let context = this.proxyContexts.get(childId);
|
||||
context.unload();
|
||||
this.proxyContexts.delete(childId);
|
||||
},
|
||||
|
||||
call(data, target) {
|
||||
let context = this.proxyContexts.get(data.childId);
|
||||
function callback(...cbArgs) {
|
||||
let lastError = context.lastError;
|
||||
|
||||
target.messageManager.sendAsyncMessage("API:CallResult", {
|
||||
childId: data.childId,
|
||||
callId: data.callId,
|
||||
args: cbArgs,
|
||||
lastError: lastError ? lastError.message : null,
|
||||
});
|
||||
}
|
||||
|
||||
let args = data.args;
|
||||
args = Cu.cloneInto(args, context.sandbox);
|
||||
if (data.callId) {
|
||||
args = args.concat(callback);
|
||||
}
|
||||
try {
|
||||
findPathInObject(context.apiObj, data.path)[data.name](...args);
|
||||
} catch (e) {
|
||||
let msg = e.message || "API failed";
|
||||
target.messageManager.sendAsyncMessage("API:CallResult", {
|
||||
childId: data.childId,
|
||||
callId: data.callId,
|
||||
lastError: msg,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addListener(data, target) {
|
||||
let context = this.proxyContexts.get(data.childId);
|
||||
|
||||
function listener(...listenerArgs) {
|
||||
target.messageManager.sendAsyncMessage("API:RunListener", {
|
||||
childId: data.childId,
|
||||
path: data.path,
|
||||
name: data.name,
|
||||
args: listenerArgs,
|
||||
});
|
||||
}
|
||||
|
||||
let ref = data.path.concat(data.name).join(".");
|
||||
context.listenerProxies.set(ref, listener);
|
||||
|
||||
let args = Cu.cloneInto(data.args, context.sandbox);
|
||||
findPathInObject(context.apiObj, data.path)[data.name].addListener(listener, ...args);
|
||||
},
|
||||
|
||||
removeListener(data) {
|
||||
let context = this.proxyContexts.get(data.childId);
|
||||
let ref = data.path.concat(data.name).join(".");
|
||||
let listener = context.listenerProxies.get(ref);
|
||||
findPathInObject(context.apiObj, data.path)[data.name].removeListener(listener);
|
||||
},
|
||||
};
|
||||
|
||||
ParentAPIManager.init();
|
||||
|
||||
// For extensions that have called setUninstallURL(), send an event
|
||||
// so the browser can display the URL.
|
||||
let UninstallObserver = {
|
||||
|
@ -356,10 +527,83 @@ GlobalManager = {
|
|||
this.extensionMap.delete(extension.id);
|
||||
},
|
||||
|
||||
getExtension(extensionId) {
|
||||
return this.extensionMap.get(extensionId);
|
||||
},
|
||||
|
||||
injectInDocShell(docShell, extension, context) {
|
||||
this.docShells.set(docShell, {extension, context});
|
||||
},
|
||||
|
||||
injectInObject(extension, context, defaultCallback, dest, namespaces = null) {
|
||||
let api = Management.generateAPIs(extension, context, Management.apis, namespaces);
|
||||
injectAPI(api, dest);
|
||||
|
||||
let schemaApi = Management.generateAPIs(extension, context, Management.schemaApis, namespaces);
|
||||
|
||||
// Add in any extra API namespaces which do not have implementations
|
||||
// outside of their schema file.
|
||||
schemaApi.extensionTypes = {};
|
||||
|
||||
let schemaWrapper = {
|
||||
get cloneScope() {
|
||||
return context.cloneScope;
|
||||
},
|
||||
|
||||
callFunction(path, name, args) {
|
||||
return findPathInObject(schemaApi, path)[name](...args);
|
||||
},
|
||||
|
||||
callFunctionNoReturn(path, name, args) {
|
||||
return findPathInObject(schemaApi, path)[name](...args);
|
||||
},
|
||||
|
||||
callAsyncFunction(path, name, args, callback) {
|
||||
// We pass an empty stub function as a default callback for
|
||||
// the `chrome` API, so promise objects are not returned,
|
||||
// and lastError values are reported immediately.
|
||||
if (callback === null) {
|
||||
callback = defaultCallback;
|
||||
}
|
||||
|
||||
let promise;
|
||||
try {
|
||||
promise = findPathInObject(schemaApi, path)[name](...args);
|
||||
} catch (e) {
|
||||
promise = Promise.reject(e);
|
||||
}
|
||||
|
||||
return context.wrapPromise(promise || Promise.resolve(), callback);
|
||||
},
|
||||
|
||||
shouldInject(namespace, name) {
|
||||
if (namespaces && namespaces.indexOf(namespace) == -1) {
|
||||
return false;
|
||||
}
|
||||
return findPathInObject(schemaApi, [namespace]) != null;
|
||||
},
|
||||
|
||||
getProperty(path, name) {
|
||||
return findPathInObject(schemaApi, path)[name];
|
||||
},
|
||||
|
||||
setProperty(path, name, value) {
|
||||
findPathInObject(schemaApi, path)[name] = value;
|
||||
},
|
||||
|
||||
addListener(path, name, listener, args) {
|
||||
return findPathInObject(schemaApi, path)[name].addListener.call(null, listener, ...args);
|
||||
},
|
||||
removeListener(path, name, listener) {
|
||||
return findPathInObject(schemaApi, path)[name].removeListener.call(null, listener);
|
||||
},
|
||||
hasListener(path, name, listener) {
|
||||
return findPathInObject(schemaApi, path)[name].hasListener.call(null, listener);
|
||||
},
|
||||
};
|
||||
Schemas.inject(dest, schemaWrapper);
|
||||
},
|
||||
|
||||
observe(contentWindow, topic, data) {
|
||||
let inject = (extension, context) => {
|
||||
// We create two separate sets of bindings, one for the `chrome`
|
||||
|
@ -368,80 +612,7 @@ GlobalManager = {
|
|||
// does not.
|
||||
let injectObject = (name, defaultCallback) => {
|
||||
let browserObj = Cu.createObjectIn(contentWindow, {defineAs: name});
|
||||
|
||||
let api = Management.generateAPIs(extension, context, Management.apis);
|
||||
injectAPI(api, browserObj);
|
||||
|
||||
let schemaApi = Management.generateAPIs(extension, context, Management.schemaApis);
|
||||
|
||||
// Add in any extra API namespaces which do not have implementations
|
||||
// outside of their schema file.
|
||||
schemaApi.extensionTypes = {};
|
||||
|
||||
function findPath(path) {
|
||||
let obj = schemaApi;
|
||||
for (let elt of path) {
|
||||
if (!(elt in obj)) {
|
||||
return null;
|
||||
}
|
||||
obj = obj[elt];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
let schemaWrapper = {
|
||||
get cloneScope() {
|
||||
return context.cloneScope;
|
||||
},
|
||||
|
||||
callFunction(path, name, args) {
|
||||
return findPath(path)[name](...args);
|
||||
},
|
||||
|
||||
callFunctionNoReturn(path, name, args) {
|
||||
return findPath(path)[name](...args);
|
||||
},
|
||||
|
||||
callAsyncFunction(path, name, args, callback) {
|
||||
// We pass an empty stub function as a default callback for
|
||||
// the `chrome` API, so promise objects are not returned,
|
||||
// and lastError values are reported immediately.
|
||||
if (callback === null) {
|
||||
callback = defaultCallback;
|
||||
}
|
||||
|
||||
let promise;
|
||||
try {
|
||||
promise = findPath(path)[name](...args);
|
||||
} catch (e) {
|
||||
promise = Promise.reject(e);
|
||||
}
|
||||
|
||||
return context.wrapPromise(promise || Promise.resolve(), callback);
|
||||
},
|
||||
|
||||
shouldInject(path, name) {
|
||||
return findPath(path) != null;
|
||||
},
|
||||
|
||||
getProperty(path, name) {
|
||||
return findPath(path)[name];
|
||||
},
|
||||
|
||||
setProperty(path, name, value) {
|
||||
findPath(path)[name] = value;
|
||||
},
|
||||
|
||||
addListener(path, name, listener, args) {
|
||||
return findPath(path)[name].addListener.call(null, listener, ...args);
|
||||
},
|
||||
removeListener(path, name, listener) {
|
||||
return findPath(path)[name].removeListener.call(null, listener);
|
||||
},
|
||||
hasListener(path, name, listener) {
|
||||
return findPath(path)[name].hasListener.call(null, listener);
|
||||
},
|
||||
};
|
||||
Schemas.inject(browserObj, schemaWrapper);
|
||||
this.injectInObject(extension, context, defaultCallback, browserObj);
|
||||
};
|
||||
|
||||
injectObject("browser", null);
|
||||
|
|
|
@ -31,12 +31,14 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
|||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||
"resource://gre/modules/PromiseUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
||||
"resource://gre/modules/Schemas.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
|
||||
"resource://gre/modules/WebNavigationFrames.jsm");
|
||||
|
||||
|
@ -50,6 +52,7 @@ var {
|
|||
flushJarCache,
|
||||
detectLanguage,
|
||||
promiseDocumentReady,
|
||||
ChildAPIManager,
|
||||
} = ExtensionUtils;
|
||||
|
||||
function isWhenBeforeOrSame(when1, when2) {
|
||||
|
@ -281,7 +284,7 @@ var ExtensionManager;
|
|||
// frame.
|
||||
class ExtensionContext extends BaseContext {
|
||||
constructor(extensionId, contentWindow, contextOptions = {}) {
|
||||
super();
|
||||
super(extensionId);
|
||||
|
||||
let {isExtensionPage} = contextOptions;
|
||||
|
||||
|
@ -368,6 +371,23 @@ class ExtensionContext extends BaseContext {
|
|||
// reason. However, we waive here anyway in case that changes.
|
||||
Cu.waiveXrays(this.sandbox).chrome = this.chromeObj;
|
||||
|
||||
let apis = {
|
||||
"storage": "chrome://extensions/content/schemas/storage.json",
|
||||
"test": "chrome://extensions/content/schemas/test.json",
|
||||
};
|
||||
|
||||
let incognito = PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);
|
||||
this.childManager = new ChildAPIManager(this, mm, Object.keys(apis), {
|
||||
type: "content_script",
|
||||
url,
|
||||
incognito,
|
||||
});
|
||||
|
||||
for (let api in apis) {
|
||||
Schemas.load(apis[api]);
|
||||
}
|
||||
Schemas.inject(this.chromeObj, this.childManager);
|
||||
|
||||
injectAPI(api(this), this.chromeObj);
|
||||
|
||||
// This is an iframe with content script API enabled. (See Bug 1214658 for rationale)
|
||||
|
@ -409,6 +429,8 @@ class ExtensionContext extends BaseContext {
|
|||
close() {
|
||||
super.unload();
|
||||
|
||||
this.childManager.close();
|
||||
|
||||
// Overwrite the content script APIs with an empty object if the APIs objects are still
|
||||
// defined in the content window (See Bug 1214658 for rationale).
|
||||
if (this.isExtensionPage && !Cu.isDeadWrapper(this.contentWindow) &&
|
||||
|
@ -715,6 +737,8 @@ ExtensionManager = {
|
|||
extensions: new Map(),
|
||||
|
||||
init() {
|
||||
Schemas.init();
|
||||
|
||||
Services.cpmm.addMessageListener("Extension:Startup", this);
|
||||
Services.cpmm.addMessageListener("Extension:Shutdown", this);
|
||||
Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
|
||||
|
|
|
@ -71,17 +71,10 @@ this.ExtensionStorage = {
|
|||
*/
|
||||
sanitize(value, global) {
|
||||
// We can't trust that the global has privileges to access this
|
||||
// value enough to clone it using a privileged JSON object. And JSON
|
||||
// objects don't support X-ray wrappers, so we can't use the JSON
|
||||
// object from the unprivileged global directly, either.
|
||||
//
|
||||
// So, instead, we create a new one, which we know is clean,
|
||||
// belonging to the same principal as the unprivileged scope, and
|
||||
// use that instead.
|
||||
let JSON_ = Cu.waiveXrays(Cu.Sandbox(global).JSON);
|
||||
// value enough to clone it using a privileged JSON object.
|
||||
let JSON_ = Cu.waiveXrays(global.JSON);
|
||||
|
||||
let json = JSON_.stringify(value, jsonReplacer);
|
||||
|
||||
return JSON.parse(json);
|
||||
},
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
|||
"resource://gre/modules/MessageChannel.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
|
||||
"resource://gre/modules/Preferences.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||
"resource://gre/modules/PromiseUtils.jsm");
|
||||
|
||||
function filterStack(error) {
|
||||
return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
|
||||
|
@ -139,12 +141,13 @@ class SpreadArgs extends Array {
|
|||
let gContextId = 0;
|
||||
|
||||
class BaseContext {
|
||||
constructor() {
|
||||
constructor(extensionId) {
|
||||
this.onClose = new Set();
|
||||
this.checkedLastError = false;
|
||||
this._lastError = null;
|
||||
this.contextId = ++gContextId;
|
||||
this.unloaded = false;
|
||||
this.extensionId = extensionId;
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
|
@ -347,7 +350,7 @@ class BaseContext {
|
|||
this.unloaded = true;
|
||||
|
||||
MessageChannel.abortResponses({
|
||||
extensionId: this.extension.id,
|
||||
extensionId: this.extensionId,
|
||||
contextId: this.contextId,
|
||||
});
|
||||
|
||||
|
@ -1040,6 +1043,150 @@ function detectLanguage(text) {
|
|||
}));
|
||||
}
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
// We create one instance of this class for every extension context
|
||||
// that needs to use remote APIs. It uses the message manager to
|
||||
// communicate with the ParentAPIManager singleton in
|
||||
// Extension.jsm. It handles asynchronous function calls as well as
|
||||
// event listeners.
|
||||
class ChildAPIManager {
|
||||
constructor(context, messageManager, namespaces, contextData) {
|
||||
this.context = context;
|
||||
this.messageManager = messageManager;
|
||||
this.namespaces = namespaces;
|
||||
|
||||
let id = String(context.extension.id) + "." + String(context.contextId);
|
||||
this.id = id;
|
||||
|
||||
let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
|
||||
Object.assign(data, contextData);
|
||||
messageManager.sendAsyncMessage("API:CreateProxyContext", data);
|
||||
|
||||
messageManager.addMessageListener("API:RunListener", this);
|
||||
messageManager.addMessageListener("API:CallResult", this);
|
||||
|
||||
// Map[path -> Set[listener]]
|
||||
// path is, e.g., "runtime.onMessage".
|
||||
this.listeners = new Map();
|
||||
|
||||
// Map[callId -> Deferred]
|
||||
this.callPromises = new Map();
|
||||
}
|
||||
|
||||
receiveMessage({name, data}) {
|
||||
if (data.childId != this.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "API:RunListener":
|
||||
let ref = data.path.concat(data.name).join(".");
|
||||
let listeners = this.listeners.get(ref);
|
||||
for (let callback of listeners) {
|
||||
runSafe(this.context, callback, ...data.args);
|
||||
}
|
||||
break;
|
||||
|
||||
case "API:CallResult":
|
||||
let deferred = this.callPromises.get(data.callId);
|
||||
if (data.lastError) {
|
||||
deferred.reject({message: data.lastError});
|
||||
} else {
|
||||
deferred.resolve(new SpreadArgs(data.args));
|
||||
}
|
||||
this.callPromises.delete(data.callId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.messageManager.sendAsyncMessage("Extension:CloseProxyContext", {childId: this.id});
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
return this.context.cloneScope;
|
||||
}
|
||||
|
||||
callFunction(path, name, args) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
callFunctionNoReturn(path, name, args) {
|
||||
this.messageManager.sendAsyncMessage("API:Call", {
|
||||
childId: this.id,
|
||||
path, name, args,
|
||||
});
|
||||
}
|
||||
|
||||
callAsyncFunction(path, name, args, callback) {
|
||||
let callId = nextId++;
|
||||
let deferred = PromiseUtils.defer();
|
||||
this.callPromises.set(callId, deferred);
|
||||
|
||||
this.messageManager.sendAsyncMessage("API:Call", {
|
||||
childId: this.id,
|
||||
callId,
|
||||
path, name, args,
|
||||
});
|
||||
|
||||
return this.context.wrapPromise(deferred.promise, callback);
|
||||
}
|
||||
|
||||
shouldInject(namespace, name) {
|
||||
return this.namespaces.includes(namespace);
|
||||
}
|
||||
|
||||
getProperty(path, name) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
setProperty(path, name, value) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
addListener(path, name, listener, args) {
|
||||
let ref = path.concat(name).join(".");
|
||||
let set;
|
||||
if (this.listeners.has(ref)) {
|
||||
set = this.listeners.get(ref);
|
||||
} else {
|
||||
set = new Set();
|
||||
this.listeners.set(ref, set);
|
||||
}
|
||||
|
||||
set.add(listener);
|
||||
|
||||
if (set.size == 1) {
|
||||
args = args.slice(1);
|
||||
|
||||
this.messageManager.sendAsyncMessage("API:AddListener", {
|
||||
childId: this.id,
|
||||
path, name, args,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeListener(path, name, listener) {
|
||||
let ref = path.concat(name).join(".");
|
||||
let set = this.listeners.get(ref) || new Set();
|
||||
set.remove(listener);
|
||||
|
||||
if (set.size == 0) {
|
||||
this.messageManager.sendAsyncMessage("Extension:RemoveListener", {
|
||||
childId: this.id,
|
||||
path, name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hasListener(path, name, listener) {
|
||||
let ref = path.concat(name).join(".");
|
||||
let set = this.listeners.get(ref) || new Set();
|
||||
return set.has(listener);
|
||||
}
|
||||
}
|
||||
|
||||
this.ExtensionUtils = {
|
||||
detectLanguage,
|
||||
extend,
|
||||
|
@ -1060,4 +1207,5 @@ this.ExtensionUtils = {
|
|||
PlatformInfo,
|
||||
SingletonEventManager,
|
||||
SpreadArgs,
|
||||
ChildAPIManager,
|
||||
};
|
||||
|
|
|
@ -24,9 +24,9 @@ Cu.import("resource://gre/modules/NetUtil.jsm");
|
|||
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
function readJSON(uri) {
|
||||
function readJSON(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
||||
NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
||||
if (!Components.isSuccessCode(status)) {
|
||||
reject(new Error(status));
|
||||
return;
|
||||
|
@ -89,12 +89,18 @@ class Context {
|
|||
},
|
||||
};
|
||||
|
||||
let props = ["addListener", "callFunction",
|
||||
"callFunctionNoReturn", "callAsyncFunction",
|
||||
"hasListener", "removeListener",
|
||||
"getProperty", "setProperty",
|
||||
"checkLoadURL", "logError",
|
||||
"preprocessors"];
|
||||
let methods = ["addListener", "callFunction",
|
||||
"callFunctionNoReturn", "callAsyncFunction",
|
||||
"hasListener", "removeListener",
|
||||
"getProperty", "setProperty",
|
||||
"checkLoadURL", "logError"];
|
||||
for (let method of methods) {
|
||||
if (method in params) {
|
||||
this[method] = params[method].bind(params);
|
||||
}
|
||||
}
|
||||
|
||||
let props = ["preprocessors"];
|
||||
for (let prop of props) {
|
||||
if (prop in params) {
|
||||
if (prop in this && typeof this[prop] == "object") {
|
||||
|
@ -1065,6 +1071,15 @@ class Event extends CallEntry {
|
|||
}
|
||||
|
||||
this.Schemas = {
|
||||
initialized: false,
|
||||
|
||||
// Set of URLs that we have loaded via the load() method.
|
||||
loadedUrls: new Set(),
|
||||
|
||||
// Maps a schema URL to the JSON contained in that schema file. This
|
||||
// is useful for sending the JSON across processes.
|
||||
schemaJSON: new Map(),
|
||||
|
||||
// Map[<schema-name> -> Map[<symbol-name> -> Entry]]
|
||||
// This keeps track of all the schemas that have been loaded so far.
|
||||
namespaces: new Map(),
|
||||
|
@ -1339,8 +1354,32 @@ this.Schemas = {
|
|||
this.register(namespaceName, event.name, e);
|
||||
},
|
||||
|
||||
load(uri) {
|
||||
return readJSON(uri).then(json => {
|
||||
init() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
|
||||
let data = Services.cpmm.initialProcessData;
|
||||
let schemas = data["Extension:Schemas"];
|
||||
if (schemas) {
|
||||
this.schemaJSON = schemas;
|
||||
}
|
||||
Services.cpmm.addMessageListener("Schema:Add", this);
|
||||
}
|
||||
},
|
||||
|
||||
receiveMessage(msg) {
|
||||
switch (msg.name) {
|
||||
case "Schema:Add":
|
||||
this.schemaJSON.set(msg.data.url, msg.data.schema);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
load(url) {
|
||||
let loadFromJSON = json => {
|
||||
for (let namespace of json) {
|
||||
let name = namespace.namespace;
|
||||
|
||||
|
@ -1364,14 +1403,35 @@ this.Schemas = {
|
|||
this.loadEvent(name, event);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
|
||||
return readJSON(url).then(json => {
|
||||
this.schemaJSON.set(url, json);
|
||||
|
||||
let data = Services.ppmm.initialProcessData;
|
||||
data["Extension:Schemas"] = this.schemaJSON;
|
||||
|
||||
Services.ppmm.broadcastAsyncMessage("Schema:Add", {url, schema: json});
|
||||
|
||||
loadFromJSON(json);
|
||||
});
|
||||
} else {
|
||||
if (this.loadedUrls.has(url)) {
|
||||
return;
|
||||
}
|
||||
this.loadedUrls.add(url);
|
||||
|
||||
let schema = this.schemaJSON.get(url);
|
||||
loadFromJSON(schema);
|
||||
}
|
||||
},
|
||||
|
||||
inject(dest, wrapperFuncs) {
|
||||
for (let [namespace, ns] of this.namespaces) {
|
||||
let obj = Cu.createObjectIn(dest, {defineAs: namespace});
|
||||
for (let [name, entry] of ns) {
|
||||
if (wrapperFuncs.shouldInject([namespace], name)) {
|
||||
if (wrapperFuncs.shouldInject(namespace, name)) {
|
||||
entry.inject([namespace], name, obj, new Context(wrapperFuncs));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ skip-if = (os == 'android' || buildapp == 'b2g') # sender.tab is undefined on b2
|
|||
[test_ext_sendmessage_doublereply.html]
|
||||
skip-if = (os == 'android' || buildapp == 'b2g') # sender.tab is undefined on b2g. Bug 1258975 on android.
|
||||
[test_ext_storage.html]
|
||||
[test_ext_storage_content.html]
|
||||
[test_ext_storage_tab.html]
|
||||
skip-if = os == 'android' # Android does not currently support tabs.
|
||||
[test_ext_background_runtime_connect_params.html]
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebExtension test</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
|
||||
<script type="text/javascript" src="head.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="application/javascript">
|
||||
"use strict";
|
||||
|
||||
function contentScript() {
|
||||
function set(items) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.set(items, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function get(items) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get(items, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function remove(items) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.remove(items, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(items) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.clear(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function check(prop, value) {
|
||||
return get(null).then(data => {
|
||||
browser.test.assertEq(data[prop], value, "null getter worked for " + prop);
|
||||
return get(prop);
|
||||
}).then(data => {
|
||||
browser.test.assertEq(data[prop], value, "string getter worked for " + prop);
|
||||
return get([prop]);
|
||||
}).then(data => {
|
||||
browser.test.assertEq(data[prop], value, "array getter worked for " + prop);
|
||||
return get({[prop]: undefined});
|
||||
}).then(data => {
|
||||
browser.test.assertEq(data[prop], value, "object getter worked for " + prop);
|
||||
});
|
||||
}
|
||||
|
||||
let globalChanges = {};
|
||||
|
||||
browser.storage.onChanged.addListener((changes, storage) => {
|
||||
browser.test.assertEq(storage, "local", "storage is local");
|
||||
Object.assign(globalChanges, changes);
|
||||
});
|
||||
|
||||
function checkChanges(changes) {
|
||||
function checkSub(obj1, obj2) {
|
||||
for (let prop in obj1) {
|
||||
browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue);
|
||||
browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue);
|
||||
}
|
||||
}
|
||||
|
||||
checkSub(changes, globalChanges);
|
||||
checkSub(globalChanges, changes);
|
||||
globalChanges = {};
|
||||
}
|
||||
|
||||
/* eslint-disable dot-notation */
|
||||
|
||||
// Set some data and then test getters.
|
||||
set({"test-prop1": "value1", "test-prop2": "value2"}).then(() => {
|
||||
checkChanges({"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}});
|
||||
return check("test-prop1", "value1");
|
||||
}).then(() => {
|
||||
return check("test-prop2", "value2");
|
||||
}).then(() => {
|
||||
return get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"});
|
||||
}).then(data => {
|
||||
browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct");
|
||||
browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct");
|
||||
browser.test.assertEq(data["other"], "default", "other correct");
|
||||
return get(["test-prop1", "test-prop2", "other"]);
|
||||
}).then(data => {
|
||||
browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct");
|
||||
browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct");
|
||||
browser.test.assertFalse("other" in data, "other correct");
|
||||
|
||||
// Remove data in various ways.
|
||||
}).then(() => {
|
||||
return remove("test-prop1");
|
||||
}).then(() => {
|
||||
checkChanges({"test-prop1": {oldValue: "value1"}});
|
||||
return get(["test-prop1", "test-prop2"]);
|
||||
}).then(data => {
|
||||
browser.test.assertFalse("test-prop1" in data, "prop1 absent");
|
||||
browser.test.assertTrue("test-prop2" in data, "prop2 present");
|
||||
|
||||
return set({"test-prop1": "value1"});
|
||||
}).then(() => {
|
||||
checkChanges({"test-prop1": {newValue: "value1"}});
|
||||
return get(["test-prop1", "test-prop2"]);
|
||||
}).then(data => {
|
||||
browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct");
|
||||
browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct");
|
||||
}).then(() => {
|
||||
return remove(["test-prop1", "test-prop2"]);
|
||||
}).then(() => {
|
||||
checkChanges({"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}});
|
||||
return get(["test-prop1", "test-prop2"]);
|
||||
}).then(data => {
|
||||
browser.test.assertFalse("test-prop1" in data, "prop1 absent");
|
||||
browser.test.assertFalse("test-prop2" in data, "prop2 absent");
|
||||
|
||||
// test storage.clear
|
||||
}).then(() => {
|
||||
return set({"test-prop1": "value1", "test-prop2": "value2"});
|
||||
}).then(() => {
|
||||
return clear();
|
||||
}).then(() => {
|
||||
checkChanges({"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}});
|
||||
return get(["test-prop1", "test-prop2"]);
|
||||
}).then(data => {
|
||||
browser.test.assertFalse("test-prop1" in data, "prop1 absent");
|
||||
browser.test.assertFalse("test-prop2" in data, "prop2 absent");
|
||||
|
||||
// Test cache invalidation.
|
||||
}).then(() => {
|
||||
return set({"test-prop1": "value1", "test-prop2": "value2"});
|
||||
}).then(() => {
|
||||
globalChanges = {};
|
||||
browser.test.sendMessage("invalidate");
|
||||
return new Promise(resolve => browser.test.onMessage.addListener(resolve));
|
||||
}).then(() => {
|
||||
return check("test-prop1", "value1");
|
||||
}).then(() => {
|
||||
return check("test-prop2", "value2");
|
||||
|
||||
// Make sure we can store complex JSON data.
|
||||
}).then(() => {
|
||||
return set({"test-prop1": {str: "hello", bool: true, undef: undefined, obj: {}, arr: [1, 2]}});
|
||||
}).then(() => {
|
||||
browser.test.assertEq(globalChanges["test-prop1"].oldValue, "value1", "oldValue correct");
|
||||
browser.test.assertEq(typeof(globalChanges["test-prop1"].newValue), "object", "newValue is obj");
|
||||
globalChanges = {};
|
||||
return get({"test-prop1": undefined});
|
||||
}).then(data => {
|
||||
let obj = data["test-prop1"];
|
||||
|
||||
browser.test.assertEq(obj.str, "hello", "string part correct");
|
||||
browser.test.assertEq(obj.bool, true, "bool part correct");
|
||||
browser.test.assertEq(obj.undef, undefined, "undefined part correct");
|
||||
browser.test.assertEq(typeof(obj.obj), "object", "object part correct");
|
||||
browser.test.assertTrue(Array.isArray(obj.arr), "array part present");
|
||||
browser.test.assertEq(obj.arr[0], 1, "arr[0] part correct");
|
||||
browser.test.assertEq(obj.arr[1], 2, "arr[1] part correct");
|
||||
browser.test.assertEq(obj.arr.length, 2, "arr.length part correct");
|
||||
}).then(() => {
|
||||
browser.test.notifyPass("storage");
|
||||
});
|
||||
}
|
||||
|
||||
let extensionData = {
|
||||
manifest: {
|
||||
content_scripts: [{
|
||||
"matches": ["http://mochi.test/*/file_sample.html"],
|
||||
"js": ["content_script.js"],
|
||||
"run_at": "document_idle",
|
||||
}],
|
||||
|
||||
permissions: ["storage"],
|
||||
},
|
||||
|
||||
files: {
|
||||
"content_script.js": "(" + contentScript.toString() + ")()",
|
||||
},
|
||||
};
|
||||
|
||||
add_task(function* test_contentscript() {
|
||||
let win = window.open("file_sample.html");
|
||||
yield waitForLoad(win);
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension(extensionData);
|
||||
yield Promise.all([extension.startup(), extension.awaitMessage("invalidate")]);
|
||||
SpecialPowers.invalidateExtensionStorageCache();
|
||||
extension.sendMessage("invalidated");
|
||||
yield extension.awaitFinish("storage");
|
||||
yield extension.unload();
|
||||
info("extension unloaded");
|
||||
|
||||
win.close();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -375,8 +375,7 @@ let wrapper = {
|
|||
tally("call", ns, name, args);
|
||||
},
|
||||
|
||||
shouldInject(path) {
|
||||
let ns = path.join(".");
|
||||
shouldInject(ns) {
|
||||
return ns != "do-not-inject";
|
||||
},
|
||||
|
||||
|
@ -406,8 +405,7 @@ let wrapper = {
|
|||
|
||||
add_task(function* () {
|
||||
let url = "data:," + JSON.stringify(json);
|
||||
let uri = BrowserUtils.makeURI(url);
|
||||
yield Schemas.load(uri);
|
||||
yield Schemas.load(url);
|
||||
|
||||
let root = {};
|
||||
Schemas.inject(root, wrapper);
|
||||
|
@ -842,8 +840,7 @@ let deprecatedJson = [
|
|||
|
||||
add_task(function* testDeprecation() {
|
||||
let url = "data:," + JSON.stringify(deprecatedJson);
|
||||
let uri = BrowserUtils.makeURI(url);
|
||||
yield Schemas.load(uri);
|
||||
yield Schemas.load(url);
|
||||
|
||||
let root = {};
|
||||
Schemas.inject(root, wrapper);
|
||||
|
|
Загрузка…
Ссылка в новой задаче