Bug 1197346 - Allow browser.storage to be used from content scripts (r=kmag)

This commit is contained in:
Bill McCloskey 2016-04-05 14:44:07 -07:00
Родитель 9dc52ee77a
Коммит 261372f2cb
8 изменённых файлов: 708 добавлений и 110 удалений

Просмотреть файл

@ -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);