mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 00:05:36 +00:00
Bug 1542035 - Add read-only support for extension storage.local in addon debugger r=miker,rpl
* Add a new extensionStorage actor to enable inspection of data stored by an extension using the WebExtension storage.local API in the Storage panel client. * The actor is only listed when the developer toolbox is targeting an extension process. For multi-process Firefox (e10s), this applies to only the toolbox accessed in about:debugging. * The actor is gated behind a preference: devtools.storage.extensionStorage.enabled. This preference is set to false by default. * The Storage panel displays storage item values as strings. If a storage item value is not JSON-stringifiable, it will be displayed in the table as "Object". * It should be noted that extension storage.local’s storage backend is in the process of migrating from a JSON file to IndexedDB as of Firefox 66 for performance reasons. This actor only works for extensions that have migrated to the IndexedDB storage backend. * In-line comments referencing Bugs 1542038 and 1542039 indicate places where the implementation may differ for local storage versus the other storage areas in the actor. Differential Revision: https://phabricator.services.mozilla.com/D34415 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
f88768528b
commit
780679ebf7
@ -78,6 +78,9 @@ pref("extensions.langpacks.signatures.required", true);
|
||||
pref("xpinstall.signatures.required", true);
|
||||
pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
|
||||
|
||||
// Disable extensionStorage storage actor by default
|
||||
pref("devtools.storage.extensionStorage.enabled", false);
|
||||
|
||||
// Dictionary download preference
|
||||
pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/language-tools/");
|
||||
|
||||
|
@ -31,6 +31,7 @@ tree.labels.localStorage=Local Storage
|
||||
tree.labels.sessionStorage=Session Storage
|
||||
tree.labels.indexedDB=Indexed DB
|
||||
tree.labels.Cache=Cache Storage
|
||||
tree.labels.extensionStorage=Extension Storage
|
||||
|
||||
# LOCALIZATION NOTE (table.headers.*.*):
|
||||
# These strings are the header names of the columns in the Storage Table for
|
||||
@ -62,6 +63,10 @@ table.headers.indexedDB.keyPath2=Key Path
|
||||
table.headers.indexedDB.autoIncrement=Auto Increment
|
||||
table.headers.indexedDB.indexes=Indexes
|
||||
|
||||
table.headers.extensionStorage.area=Storage Area
|
||||
table.headers.extensionStorage.name=Key
|
||||
table.headers.extensionStorage.value=Value
|
||||
|
||||
# LOCALIZATION NOTE (label.expires.session):
|
||||
# This string is displayed in the expires column when the cookie is Session
|
||||
# Cookie
|
||||
|
@ -12,9 +12,24 @@ const Services = require("Services");
|
||||
const defer = require("devtools/shared/defer");
|
||||
const { isWindowIncluded } = require("devtools/shared/layout/utils");
|
||||
const specs = require("devtools/shared/specs/storage");
|
||||
loader.lazyGetter(this, "ExtensionProcessScript", () => {
|
||||
return require("resource://gre/modules/ExtensionProcessScript.jsm")
|
||||
.ExtensionProcessScript;
|
||||
});
|
||||
loader.lazyGetter(this, "ExtensionStorageIDB", () => {
|
||||
return require("resource://gre/modules/ExtensionStorageIDB.jsm")
|
||||
.ExtensionStorageIDB;
|
||||
});
|
||||
loader.lazyGetter(
|
||||
this,
|
||||
"WebExtensionPolicy",
|
||||
() => Cu.getGlobalForObject(ExtensionProcessScript).WebExtensionPolicy
|
||||
);
|
||||
|
||||
const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
|
||||
const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
|
||||
const EXTENSION_STORAGE_ENABLED_PREF =
|
||||
"devtools.storage.extensionStorage.enabled";
|
||||
|
||||
const DEFAULT_VALUE = "value";
|
||||
|
||||
@ -1386,6 +1401,483 @@ StorageActors.createActor(
|
||||
getObjectForLocalOrSessionStorage("sessionStorage")
|
||||
);
|
||||
|
||||
const extensionStorageHelpers = {
|
||||
unresolvedPromises: new Map(),
|
||||
// Map of addonId => onStorageChange listeners in the parent process. Each addon toolbox targets
|
||||
// a single addon, and multiple addon toolboxes could be open at the same time.
|
||||
onChangedParentListeners: new Map(),
|
||||
// Set of onStorageChange listeners in the extension child process. Each addon toolbox will create
|
||||
// a separate extensionStorage actor targeting that addon. The addonId is passed into the listener,
|
||||
// so that changes propagate only if the storage actor has a matching addonId.
|
||||
onChangedChildListeners: new Set(),
|
||||
|
||||
// Sets the parent process message manager
|
||||
setPpmm(ppmm) {
|
||||
this.ppmm = ppmm;
|
||||
},
|
||||
|
||||
// A promise in the main process has resolved, and we need to pass the return value(s)
|
||||
// back to the child process
|
||||
backToChild(...args) {
|
||||
Services.mm.broadcastAsyncMessage(
|
||||
"debug:storage-extensionStorage-request-child",
|
||||
{
|
||||
method: "backToChild",
|
||||
args: args,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Send a message from the main process to a listener in the child process that the
|
||||
// extension has modified storage local data
|
||||
fireStorageOnChanged({ addonId, changes }) {
|
||||
Services.mm.broadcastAsyncMessage(
|
||||
"debug:storage-extensionStorage-request-child",
|
||||
{
|
||||
addonId,
|
||||
changes,
|
||||
method: "storageOnChanged",
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Subscribe a listener for event notifications from the WE storage API when
|
||||
// storage local data has been changed by the extension, and keep track of the
|
||||
// listener to remove it when the debugger is being disconnected.
|
||||
subscribeOnChangedListenerInParent(addonId) {
|
||||
if (!this.onChangedParentListeners.has(addonId)) {
|
||||
const onChangedListener = changes => {
|
||||
this.fireStorageOnChanged({ addonId, changes });
|
||||
};
|
||||
ExtensionStorageIDB.addOnChangedListener(addonId, onChangedListener);
|
||||
this.onChangedParentListeners.set(addonId, onChangedListener);
|
||||
}
|
||||
},
|
||||
|
||||
// The main process does not require an extension context to select the backend
|
||||
// Bug 1542038, 1542039: Each storage area will need its own implementation, as
|
||||
// they use different storage backends.
|
||||
async setupStorageInParent(addonId) {
|
||||
const { extension } = WebExtensionPolicy.getByID(addonId);
|
||||
const parentResult = await ExtensionStorageIDB.selectBackend({ extension });
|
||||
const result = {
|
||||
...parentResult,
|
||||
// Received as a StructuredCloneHolder, so we need to deserialize
|
||||
storagePrincipal: parentResult.storagePrincipal.deserialize(this, true),
|
||||
};
|
||||
|
||||
this.subscribeOnChangedListenerInParent(addonId);
|
||||
return this.backToChild("setupStorageInParent", result);
|
||||
},
|
||||
|
||||
onDisconnected() {
|
||||
for (const [addonId, listener] of this.onChangedParentListeners) {
|
||||
ExtensionStorageIDB.removeOnChangedListener(addonId, listener);
|
||||
}
|
||||
this.onChangedParentListeners.clear();
|
||||
},
|
||||
|
||||
// Runs in the main process. This determines what code to execute based on the message
|
||||
// received from the child process.
|
||||
async handleChildRequest(msg) {
|
||||
switch (msg.json.method) {
|
||||
case "setupStorageInParent":
|
||||
const addonId = msg.data.args[0];
|
||||
const result = await extensionStorageHelpers.setupStorageInParent(
|
||||
addonId
|
||||
);
|
||||
return result;
|
||||
default:
|
||||
console.error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD", msg.json.method);
|
||||
throw new Error("ERR_DIRECTOR_PARENT_UNKNOWN_METHOD");
|
||||
}
|
||||
},
|
||||
|
||||
// Runs in the child process. This determines what code to execute based on the message
|
||||
// received from the parent process.
|
||||
handleParentRequest(msg) {
|
||||
switch (msg.json.method) {
|
||||
case "backToChild": {
|
||||
const [func, rv] = msg.json.args;
|
||||
const deferred = this.unresolvedPromises.get(func);
|
||||
if (deferred) {
|
||||
this.unresolvedPromises.delete(func);
|
||||
deferred.resolve(rv);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "storageOnChanged": {
|
||||
const { addonId, changes } = msg.data;
|
||||
for (const listener of this.onChangedChildListeners) {
|
||||
try {
|
||||
listener({ addonId, changes });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// Ignore errors raised from listeners.
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error("ERR_DIRECTOR_CLIENT_UNKNOWN_METHOD", msg.json.method);
|
||||
throw new Error("ERR_DIRECTOR_CLIENT_UNKNOWN_METHOD");
|
||||
}
|
||||
},
|
||||
|
||||
callParentProcessAsync(methodName, ...args) {
|
||||
const deferred = defer();
|
||||
|
||||
this.unresolvedPromises.set(methodName, deferred);
|
||||
|
||||
this.ppmm.sendAsyncMessage(
|
||||
"debug:storage-extensionStorage-request-parent",
|
||||
{
|
||||
method: methodName,
|
||||
args: args,
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* E10S parent/child setup helpers
|
||||
* Add a message listener in the parent process to receive messages from the child
|
||||
* process.
|
||||
*/
|
||||
exports.setupParentProcessForExtensionStorage = function({ mm, prefix }) {
|
||||
// listen for director-script requests from the child process
|
||||
setMessageManager(mm);
|
||||
|
||||
function setMessageManager(newMM) {
|
||||
if (mm) {
|
||||
mm.removeMessageListener(
|
||||
"debug:storage-extensionStorage-request-parent",
|
||||
extensionStorageHelpers.handleChildRequest
|
||||
);
|
||||
}
|
||||
mm = newMM;
|
||||
if (mm) {
|
||||
mm.addMessageListener(
|
||||
"debug:storage-extensionStorage-request-parent",
|
||||
extensionStorageHelpers.handleChildRequest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onBrowserSwap: setMessageManager,
|
||||
onDisconnected: () => {
|
||||
// Although "disconnected-from-child" implies that the child is already
|
||||
// disconnected this is not the case. The disconnection takes place after
|
||||
// this method has finished. This gives us chance to clean up items within
|
||||
// the parent process e.g. observers.
|
||||
setMessageManager(null);
|
||||
extensionStorageHelpers.onDisconnected();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The Extension Storage actor.
|
||||
*/
|
||||
if (Services.prefs.getBoolPref(EXTENSION_STORAGE_ENABLED_PREF, false)) {
|
||||
StorageActors.createActor(
|
||||
{
|
||||
typeName: "extensionStorage",
|
||||
},
|
||||
{
|
||||
initialize(storageActor) {
|
||||
protocol.Actor.prototype.initialize.call(this, null);
|
||||
|
||||
this.storageActor = storageActor;
|
||||
|
||||
this.addonId = this.storageActor.parentActor.addonId;
|
||||
|
||||
// Retrieve the base moz-extension url for the extension
|
||||
// (and also remove the final '/' from it).
|
||||
this.extensionHostURL = this.getExtensionPolicy()
|
||||
.getURL()
|
||||
.slice(0, -1);
|
||||
|
||||
// Map<host, ExtensionStorageIDB db connection>
|
||||
// Bug 1542038, 1542039: Each storage area will need its own
|
||||
// dbConnectionForHost, as they each have different storage backends.
|
||||
// Anywhere dbConnectionForHost is used, we need to know the storage
|
||||
// area to access the correct database.
|
||||
this.dbConnectionForHost = new Map();
|
||||
|
||||
// Bug 1542038, 1542039: Each storage area will need its own
|
||||
// this.hostVsStores or this actor will need to deviate from how
|
||||
// this.hostVsStores is defined in the framework to associate each
|
||||
// storage item with a storage area. Any methods that use it will also
|
||||
// need to be updated (e.g. getNamesForHost).
|
||||
this.hostVsStores = new Map();
|
||||
|
||||
this.onStorageChange = this.onStorageChange.bind(this);
|
||||
|
||||
this.setupChildProcess();
|
||||
|
||||
this.onWindowReady = this.onWindowReady.bind(this);
|
||||
this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
|
||||
this.storageActor.on("window-ready", this.onWindowReady);
|
||||
this.storageActor.on("window-destroyed", this.onWindowDestroyed);
|
||||
},
|
||||
|
||||
getExtensionPolicy() {
|
||||
return WebExtensionPolicy.getByID(this.addonId);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
extensionStorageHelpers.onChangedChildListeners.delete(
|
||||
this.onStorageChange
|
||||
);
|
||||
|
||||
this.storageActor.off("window-ready", this.onWindowReady);
|
||||
this.storageActor.off("window-destroyed", this.onWindowDestroyed);
|
||||
|
||||
this.hostVsStores.clear();
|
||||
|
||||
protocol.Actor.prototype.destroy.call(this);
|
||||
|
||||
this.storageActor = null;
|
||||
},
|
||||
|
||||
setupChildProcess() {
|
||||
const ppmm = this.conn.parentMessageManager;
|
||||
extensionStorageHelpers.setPpmm(ppmm);
|
||||
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
this.conn.setupInParent({
|
||||
module: "devtools/server/actors/storage",
|
||||
setupParent: "setupParentProcessForExtensionStorage",
|
||||
});
|
||||
|
||||
extensionStorageHelpers.onChangedChildListeners.add(
|
||||
this.onStorageChange
|
||||
);
|
||||
this.setupStorageInParent = extensionStorageHelpers.callParentProcessAsync.bind(
|
||||
extensionStorageHelpers,
|
||||
"setupStorageInParent"
|
||||
);
|
||||
|
||||
// Add a message listener in the child process to receive messages from the parent
|
||||
// process
|
||||
ppmm.addMessageListener(
|
||||
"debug:storage-extensionStorage-request-child",
|
||||
extensionStorageHelpers.handleParentRequest.bind(
|
||||
extensionStorageHelpers
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* This fires when the extension changes storage data while the storage
|
||||
* inspector is open. Ensures this.hostVsStores stays up-to-date and
|
||||
* passes the changes on to update the client.
|
||||
*/
|
||||
onStorageChange({ addonId, changes }) {
|
||||
if (addonId !== this.addonId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const host = this.extensionHostURL;
|
||||
const storeMap = this.hostVsStores.get(host);
|
||||
|
||||
function isStructuredCloneHolder(value) {
|
||||
return (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
Cu.getClassName(value, true) === "StructuredCloneHolder"
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in changes) {
|
||||
const storageChange = changes[key];
|
||||
let { newValue, oldValue } = storageChange;
|
||||
if (isStructuredCloneHolder(newValue)) {
|
||||
newValue = newValue.deserialize(this);
|
||||
}
|
||||
if (isStructuredCloneHolder(oldValue)) {
|
||||
oldValue = oldValue.deserialize(this);
|
||||
}
|
||||
|
||||
let action;
|
||||
if (typeof newValue === "undefined") {
|
||||
action = "deleted";
|
||||
storeMap.delete(key);
|
||||
} else if (typeof oldValue === "undefined") {
|
||||
action = "added";
|
||||
storeMap.set(key, newValue);
|
||||
} else {
|
||||
action = "changed";
|
||||
storeMap.set(key, newValue);
|
||||
}
|
||||
|
||||
this.storageActor.update(action, this.typeName, { [host]: [key] });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Purpose of this method is same as populateStoresForHosts but this is async.
|
||||
* This exact same operation cannot be performed in populateStoresForHosts
|
||||
* method, as that method is called in initialize method of the actor, which
|
||||
* cannot be asynchronous.
|
||||
*/
|
||||
async preListStores() {
|
||||
// Ensure the actor's target is an extension and it is enabled
|
||||
if (!this.addonId || !WebExtensionPolicy.getByID(this.addonId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.populateStoresForHost(this.extensionHostURL);
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is overriden and left blank as for extensionStorage, this operation
|
||||
* cannot be performed synchronously. Thus, the preListStores method exists to
|
||||
* do the same task asynchronously.
|
||||
*/
|
||||
populateStoresForHosts() {},
|
||||
|
||||
/**
|
||||
* This method asynchronously reads the storage data for the target extension
|
||||
* and caches this data into this.hostVsStores.
|
||||
* @param {String} host - the hostname for the extension
|
||||
*/
|
||||
async populateStoresForHost(host) {
|
||||
if (host !== this.extensionHostURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = ExtensionProcessScript.getExtensionChild(
|
||||
this.addonId
|
||||
);
|
||||
if (!extension || !extension.hasPermission("storage")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure storeMap is defined and set in this.hostVsStores before subscribing
|
||||
// a storage onChanged listener in the parent process
|
||||
const storeMap = new Map();
|
||||
this.hostVsStores.set(host, storeMap);
|
||||
|
||||
const storagePrincipal = await this.getStoragePrincipal(extension.id);
|
||||
|
||||
if (!storagePrincipal) {
|
||||
// This could happen if the extension fails to be migrated to the
|
||||
// IndexedDB backend
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await ExtensionStorageIDB.open(storagePrincipal);
|
||||
this.dbConnectionForHost.set(host, db);
|
||||
const data = await db.get();
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
storeMap.set(key, value);
|
||||
}
|
||||
|
||||
// Show the storage actor in the add-on storage inspector even when there
|
||||
// is no extension page currently open
|
||||
const storageData = {};
|
||||
storageData[host] = this.getNamesForHost(host);
|
||||
this.storageActor.update("added", this.typeName, storageData);
|
||||
},
|
||||
|
||||
async getStoragePrincipal(addonId) {
|
||||
const {
|
||||
backendEnabled,
|
||||
storagePrincipal,
|
||||
} = await this.setupStorageInParent(addonId);
|
||||
|
||||
if (!backendEnabled) {
|
||||
// IDB backend disabled; give up.
|
||||
return null;
|
||||
}
|
||||
return storagePrincipal;
|
||||
},
|
||||
|
||||
getValuesForHost(host, name) {
|
||||
const result = [];
|
||||
|
||||
if (!this.hostVsStores.has(host)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
return [{ name, value: this.hostVsStores.get(host).get(name) }];
|
||||
}
|
||||
|
||||
for (const [key, value] of Array.from(
|
||||
this.hostVsStores.get(host).entries()
|
||||
)) {
|
||||
result.push({ name: key, value });
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a storage item to an "extensionobject" as defined in
|
||||
* devtools/shared/specs/storage.js
|
||||
* @param {Object} item - The storage item to convert
|
||||
* @param {String} item.name - The storage item key
|
||||
* @param {*} item.value - The storage item value
|
||||
* @return {extensionobject}
|
||||
*/
|
||||
toStoreObject(item) {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, value } = item;
|
||||
|
||||
let newValue;
|
||||
if (typeof value === "string") {
|
||||
newValue = value;
|
||||
} else {
|
||||
try {
|
||||
newValue = JSON.stringify(value) || String(value);
|
||||
} catch (error) {
|
||||
// throws for bigint
|
||||
newValue = String(value);
|
||||
}
|
||||
|
||||
// JavaScript objects that are not JSON stringifiable will be represented
|
||||
// by the string "Object"
|
||||
if (newValue === "{}") {
|
||||
newValue = "Object";
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Bug 1318029 - Due to a bug that is thrown whenever a
|
||||
// LongStringActor string reaches DebuggerServer.LONG_STRING_LENGTH we need
|
||||
// to trim the value. When the bug is fixed we should stop trimming the
|
||||
// string here.
|
||||
const maxLength = DebuggerServer.LONG_STRING_LENGTH - 1;
|
||||
if (newValue.length > maxLength) {
|
||||
newValue = newValue.substr(0, maxLength);
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value: new LongStringActor(this.conn, newValue || ""),
|
||||
area: "local", // Bug 1542038, 1542039: set the correct storage area
|
||||
};
|
||||
},
|
||||
|
||||
getFields() {
|
||||
return [
|
||||
{ name: "name", editable: false },
|
||||
{ name: "value", editable: false },
|
||||
{ name: "area", editable: false },
|
||||
];
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
StorageActors.createActor(
|
||||
{
|
||||
typeName: "Cache",
|
||||
@ -2730,6 +3222,11 @@ const StorageActor = protocol.ActorClassWithSpec(specs.storageSpec, {
|
||||
|
||||
// Initialize the registered store types
|
||||
for (const [store, ActorConstructor] of storageTypePool) {
|
||||
// Only create the extensionStorage actor when the debugging target
|
||||
// is an extension.
|
||||
if (store === "extensionStorage" && !this.parentActor.addonId) {
|
||||
continue;
|
||||
}
|
||||
this.childActorPool.set(store, new ActorConstructor(this));
|
||||
}
|
||||
|
||||
@ -2810,6 +3307,14 @@ const StorageActor = protocol.ActorClassWithSpec(specs.storageSpec, {
|
||||
return null;
|
||||
},
|
||||
|
||||
isIncludedInTargetExtension(subject) {
|
||||
const { document } = subject;
|
||||
return (
|
||||
document.nodePrincipal.addonId &&
|
||||
document.nodePrincipal.addonId === this.parentActor.addonId
|
||||
);
|
||||
},
|
||||
|
||||
isIncludedInTopLevelWindow(window) {
|
||||
return isWindowIncluded(this.window, window);
|
||||
},
|
||||
@ -2848,9 +3353,13 @@ const StorageActor = protocol.ActorClassWithSpec(specs.storageSpec, {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We don't want to try to find a top level window for an extension page, as
|
||||
// in many cases (e.g. background page), it is not loaded in a tab, and
|
||||
// 'isIncludedInTopLevelWindow' throws an error
|
||||
if (
|
||||
topic == "content-document-global-created" &&
|
||||
this.isIncludedInTopLevelWindow(subject)
|
||||
(this.isIncludedInTargetExtension(subject) ||
|
||||
this.isIncludedInTopLevelWindow(subject))
|
||||
) {
|
||||
this.childWindowPool.add(subject);
|
||||
this.emit("window-ready", subject);
|
||||
|
@ -147,6 +147,7 @@ fail-if = fission
|
||||
fail-if = fission
|
||||
[browser_storage_updates.js]
|
||||
fail-if = fission
|
||||
[browser_storage_webext_storage_local.js]
|
||||
[browser_styles_getRuleText.js]
|
||||
[browser_stylesheets_getTextEmpty.js]
|
||||
[browser_stylesheets_nested-iframes.js]
|
||||
|
@ -0,0 +1,39 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Pref remains in effect until test completes and is automatically cleared afterwards
|
||||
add_task(async function set_enable_extensionStorage_pref() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["devtools.storage.extensionStorage.enabled", true]],
|
||||
});
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_extensionStorage_disabled_for_non_extension_target() {
|
||||
info(
|
||||
"Setting up and connecting Debugger Server and Client in main process"
|
||||
);
|
||||
initDebuggerServer();
|
||||
const transport = DebuggerServer.connectPipe();
|
||||
const client = new DebuggerClient(transport);
|
||||
await client.connect();
|
||||
|
||||
info("Opening a non-extension page in a tab");
|
||||
const target = await addTabTarget("data:text/html;charset=utf-8,");
|
||||
|
||||
info("Getting all stores for the target process");
|
||||
const storageFront = await target.getFront("storage");
|
||||
const stores = await storageFront.listStores();
|
||||
|
||||
ok(
|
||||
!("extensionStorage" in stores),
|
||||
"Should not have an extensionStorage store when non-extension process is targeted"
|
||||
);
|
||||
|
||||
await target.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
);
|
861
devtools/server/tests/unit/test_extension_storage_actor.js
Normal file
861
devtools/server/tests/unit/test_extension_storage_actor.js
Normal file
@ -0,0 +1,861 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/* globals browser */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { AddonTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/AddonTestUtils.jsm"
|
||||
);
|
||||
|
||||
const { FileUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/FileUtils.jsm"
|
||||
);
|
||||
|
||||
const { ExtensionTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/ExtensionXPCShellUtils.jsm"
|
||||
);
|
||||
|
||||
// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed.
|
||||
const { PromiseTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/PromiseTestUtils.jsm"
|
||||
);
|
||||
PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
|
||||
|
||||
const { createAppInfo, promiseStartupManager } = AddonTestUtils;
|
||||
|
||||
const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
|
||||
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
|
||||
const EXTENSION_STORAGE_ENABLED_PREF =
|
||||
"devtools.storage.extensionStorage.enabled";
|
||||
|
||||
AddonTestUtils.init(this);
|
||||
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
|
||||
|
||||
ExtensionTestUtils.init(this);
|
||||
|
||||
// This storage actor is gated behind a pref, so make sure it is enabled first
|
||||
Services.prefs.setBoolPref(EXTENSION_STORAGE_ENABLED_PREF, true);
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref(EXTENSION_STORAGE_ENABLED_PREF);
|
||||
});
|
||||
|
||||
/**
|
||||
* Starts up Debugger server and connects a new Debugger client.
|
||||
*
|
||||
* @return {Promise} Resolves with a client object when the debugger has started up.
|
||||
*/
|
||||
async function startDebugger() {
|
||||
DebuggerServer.init();
|
||||
DebuggerServer.registerAllActors();
|
||||
const transport = DebuggerServer.connectPipe();
|
||||
const client = new DebuggerClient(transport);
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the equivalent of an `about:debugging` toolbox for a given extension, minus
|
||||
* the toolbox.
|
||||
*
|
||||
* @param {String} id - The id for the extension to be targeted by the toolbox.
|
||||
* @return {Object} Resolves with the web extension actor front and target objects when
|
||||
* the debugger has been connected to the extension.
|
||||
*/
|
||||
async function setupExtensionDebugging(id) {
|
||||
const client = await startDebugger();
|
||||
const front = await client.mainRoot.getAddon({ id });
|
||||
// Starts a DevTools server in the extension child process.
|
||||
const target = await front.connect();
|
||||
return { front, target };
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and starts up a test extension given the provided extension configuration.
|
||||
*
|
||||
* @param {Object} extConfig - The extension configuration object
|
||||
* @return {ExtensionWrapper} extension - Resolves with an extension object once the
|
||||
* extension has started up.
|
||||
*/
|
||||
async function startupExtension(extConfig) {
|
||||
const extension = ExtensionTestUtils.loadExtension(extConfig);
|
||||
|
||||
await extension.startup();
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the extensionStorage actor for a target extension. This is effectively
|
||||
* what happens when the addon storage panel is opened in the browser.
|
||||
*
|
||||
* @param {String} - id, The addon id
|
||||
* @return {Object} - Resolves with the web extension actor target and extensionStorage
|
||||
* store objects when the panel has been opened.
|
||||
*/
|
||||
async function openAddonStoragePanel(id) {
|
||||
const { target } = await setupExtensionDebugging(id);
|
||||
|
||||
const storageFront = await target.getFront("storage");
|
||||
const stores = await storageFront.listStores();
|
||||
const extensionStorage = stores.extensionStorage || null;
|
||||
|
||||
return { target, extensionStorage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the extension configuration object passed into ExtensionTestUtils.loadExtension
|
||||
*
|
||||
* @param {Object} options - Options, if any, to add to the configuration
|
||||
* @param {Function} options.background - A function comprising the test extension's
|
||||
* background script if provided
|
||||
* @param {Object} options.files - An object whose keys correspond to file names and
|
||||
* values map to the file contents
|
||||
* @param {Object} options.manifest - An object representing the extension's manifest
|
||||
* @return {Object} - The extension configuration object
|
||||
*/
|
||||
function getExtensionConfig(options = {}) {
|
||||
const { manifest, ...otherOptions } = options;
|
||||
const baseConfig = {
|
||||
manifest: {
|
||||
...manifest,
|
||||
permissions: ["storage"],
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
};
|
||||
return {
|
||||
...baseConfig,
|
||||
...otherOptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An extension script that can be used in any extension context (e.g. as a background
|
||||
* script or as an extension page script loaded in a tab).
|
||||
*/
|
||||
async function extensionScriptWithMessageListener() {
|
||||
let fireOnChanged = false;
|
||||
browser.storage.onChanged.addListener(() => {
|
||||
if (fireOnChanged) {
|
||||
// Do not fire it again until explicitly requested again using the "storage-local-fireOnChanged" test message.
|
||||
fireOnChanged = false;
|
||||
browser.test.sendMessage("storage-local-onChanged");
|
||||
}
|
||||
});
|
||||
|
||||
browser.test.onMessage.addListener(async (msg, ...args) => {
|
||||
let value = null;
|
||||
switch (msg) {
|
||||
case "storage-local-set":
|
||||
await browser.storage.local.set(args[0]);
|
||||
break;
|
||||
case "storage-local-get":
|
||||
value = (await browser.storage.local.get(args[0]))[args[0]];
|
||||
break;
|
||||
case "storage-local-remove":
|
||||
await browser.storage.local.remove(args[0]);
|
||||
break;
|
||||
case "storage-local-clear":
|
||||
await browser.storage.local.clear();
|
||||
break;
|
||||
case "storage-local-fireOnChanged": {
|
||||
// Allow the storage.onChanged listener to send a test event
|
||||
// message when onChanged is being fired.
|
||||
fireOnChanged = true;
|
||||
// Do not fire fireOnChanged:done.
|
||||
return;
|
||||
}
|
||||
default:
|
||||
browser.test.fail(`Unexpected test message: ${msg}`);
|
||||
}
|
||||
|
||||
browser.test.sendMessage(`${msg}:done`, value);
|
||||
});
|
||||
browser.test.sendMessage("extension-origin", window.location.origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared files for a test extension that has no background page but adds storage
|
||||
* items via a transient extension page in a tab
|
||||
*/
|
||||
const ext_no_bg = {
|
||||
files: {
|
||||
"extension_page_in_tab.html": `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Extension Page in a Tab</h1>
|
||||
<script src="extension_page_in_tab.js"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
"extension_page_in_tab.js": extensionScriptWithMessageListener,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Shutdown procedure common to all tasks.
|
||||
*
|
||||
* @param {Object} extension - The test extension
|
||||
* @param {Object} target - The web extension actor targeted by the DevTools client
|
||||
*/
|
||||
async function shutdown(extension, target) {
|
||||
if (target) {
|
||||
await target.destroy();
|
||||
}
|
||||
await extension.unload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the missing 'storage/permanent' directory needed by the "indexedDB"
|
||||
* storage actor's 'preListStores' method (called when 'listStores' is called). This
|
||||
* directory exists in a full browser i.e. mochitest.
|
||||
*/
|
||||
function createMissingIndexedDBDirs() {
|
||||
const dir = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
|
||||
dir.append("storage");
|
||||
if (!dir.exists()) {
|
||||
dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
}
|
||||
dir.append("permanent");
|
||||
if (!dir.exists()) {
|
||||
dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
}
|
||||
Assert.ok(
|
||||
dir.exists(),
|
||||
"Should have a 'storage/permanent' dir in the profile dir"
|
||||
);
|
||||
}
|
||||
|
||||
add_task(async function setup() {
|
||||
await promiseStartupManager();
|
||||
createMissingIndexedDBDirs();
|
||||
});
|
||||
|
||||
add_task(async function test_extension_store_exists() {
|
||||
const extension = await startupExtension(getExtensionConfig());
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
ok(extensionStorage, "Should have an extensionStorage store");
|
||||
|
||||
await shutdown(extension, target);
|
||||
});
|
||||
|
||||
add_task(async function test_extension_origin_matches_debugger_target() {
|
||||
async function background() {
|
||||
browser.test.sendMessage("extension-origin", window.location.origin);
|
||||
}
|
||||
|
||||
const extension = await startupExtension(getExtensionConfig({ background }));
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
const { hosts } = extensionStorage;
|
||||
const expectedHost = await extension.awaitMessage("extension-origin");
|
||||
ok(
|
||||
expectedHost in hosts,
|
||||
"Should have the expected extension host in the extensionStorage store"
|
||||
);
|
||||
|
||||
await shutdown(extension, target);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test case: Background page modifies items while storage panel is open.
|
||||
* - Load extension with background page.
|
||||
* - Open the add-on debugger storage panel.
|
||||
* - With the panel still open, from the extension background page:
|
||||
* - Bulk add storage items
|
||||
* - Edit the values of some of the storage items
|
||||
* - Remove some storage items
|
||||
* - Clear all storage items
|
||||
* - For each modification, the storage data in the panel should match the
|
||||
* changes made by the extension.
|
||||
*/
|
||||
add_task(async function test_panel_live_updates() {
|
||||
const extension = await startupExtension(
|
||||
getExtensionConfig({ background: extensionScriptWithMessageListener })
|
||||
);
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
let { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(data, [], "Got the expected results on empty storage.local");
|
||||
|
||||
info("Waiting for extension to bulk add 50 items to storage local");
|
||||
const bulkStorageItems = {};
|
||||
// limited by MAX_STORE_OBJECT_COUNT in devtools/server/actors/storage.js
|
||||
const numItems = 2;
|
||||
for (let i = 1; i <= numItems; i++) {
|
||||
bulkStorageItems[i] = i;
|
||||
}
|
||||
|
||||
// fireOnChanged avoids the race condition where the extension
|
||||
// modifies storage then immediately tries to access storage before
|
||||
// the storage actor has finished updating.
|
||||
extension.sendMessage("storage-local-fireOnChanged");
|
||||
extension.sendMessage("storage-local-set", {
|
||||
...bulkStorageItems,
|
||||
a: 123,
|
||||
b: [4, 5],
|
||||
c: { d: 678 },
|
||||
d: true,
|
||||
e: "hi",
|
||||
f: null,
|
||||
});
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
await extension.awaitMessage("storage-local-onChanged");
|
||||
|
||||
info(
|
||||
"Confirming items added by extension match items in extensionStorage store"
|
||||
);
|
||||
const bulkStorageObjects = [];
|
||||
for (const [name, value] of Object.entries(bulkStorageItems)) {
|
||||
bulkStorageObjects.push({
|
||||
area: "local",
|
||||
name,
|
||||
value: { str: String(value) },
|
||||
});
|
||||
}
|
||||
data = (await extensionStorage.getStoreObjects(host)).data;
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[
|
||||
...bulkStorageObjects,
|
||||
{ area: "local", name: "a", value: { str: "123" } },
|
||||
{ area: "local", name: "b", value: { str: "[4,5]" } },
|
||||
{ area: "local", name: "c", value: { str: '{"d":678}' } },
|
||||
{ area: "local", name: "d", value: { str: "true" } },
|
||||
{ area: "local", name: "e", value: { str: "hi" } },
|
||||
{ area: "local", name: "f", value: { str: "null" } },
|
||||
],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
info("Waiting for extension to edit a few storage item values");
|
||||
extension.sendMessage("storage-local-fireOnChanged");
|
||||
extension.sendMessage("storage-local-set", {
|
||||
a: ["c", "d"],
|
||||
b: 456,
|
||||
c: false,
|
||||
});
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
await extension.awaitMessage("storage-local-onChanged");
|
||||
|
||||
info(
|
||||
"Confirming items edited by extension match items in extensionStorage store"
|
||||
);
|
||||
data = (await extensionStorage.getStoreObjects(host)).data;
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[
|
||||
...bulkStorageObjects,
|
||||
{ area: "local", name: "a", value: { str: '["c","d"]' } },
|
||||
{ area: "local", name: "b", value: { str: "456" } },
|
||||
{ area: "local", name: "c", value: { str: "false" } },
|
||||
{ area: "local", name: "d", value: { str: "true" } },
|
||||
{ area: "local", name: "e", value: { str: "hi" } },
|
||||
{ area: "local", name: "f", value: { str: "null" } },
|
||||
],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
info("Waiting for extension to remove a few storage item values");
|
||||
extension.sendMessage("storage-local-fireOnChanged");
|
||||
extension.sendMessage("storage-local-remove", ["d", "e", "f"]);
|
||||
await extension.awaitMessage("storage-local-remove:done");
|
||||
await extension.awaitMessage("storage-local-onChanged");
|
||||
|
||||
info(
|
||||
"Confirming items removed by extension were removed in extensionStorage store"
|
||||
);
|
||||
data = (await extensionStorage.getStoreObjects(host)).data;
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[
|
||||
...bulkStorageObjects,
|
||||
{ area: "local", name: "a", value: { str: '["c","d"]' } },
|
||||
{ area: "local", name: "b", value: { str: "456" } },
|
||||
{ area: "local", name: "c", value: { str: "false" } },
|
||||
],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
info("Waiting for extension to remove all remaining storage items");
|
||||
extension.sendMessage("storage-local-fireOnChanged");
|
||||
extension.sendMessage("storage-local-clear");
|
||||
await extension.awaitMessage("storage-local-clear:done");
|
||||
await extension.awaitMessage("storage-local-onChanged");
|
||||
|
||||
info("Confirming extensionStorage store was cleared");
|
||||
data = (await extensionStorage.getStoreObjects(host)).data;
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
await shutdown(extension, target);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test case: No bg page. Transient page adds item before storage panel opened.
|
||||
* - Load extension with no background page.
|
||||
* - Open an extension page in a tab that adds a local storage item.
|
||||
* - With the extension page still open, open the add-on storage panel.
|
||||
* - The data in the storage panel should match the items added by the extension.
|
||||
*/
|
||||
add_task(
|
||||
async function test_panel_data_matches_extension_with_transient_page_open() {
|
||||
const extension = await startupExtension(
|
||||
getExtensionConfig({ files: ext_no_bg.files })
|
||||
);
|
||||
|
||||
const url = extension.extension.baseURI.resolve(
|
||||
"extension_page_in_tab.html"
|
||||
);
|
||||
const contentPage = await ExtensionTestUtils.loadContentPage(url, {
|
||||
extension,
|
||||
});
|
||||
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
extension.sendMessage("storage-local-set", { a: 123 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
const { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[{ area: "local", name: "a", value: { str: "123" } }],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
await contentPage.close();
|
||||
await shutdown(extension, target);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Test case: No bg page. Transient page adds item then closes before storage panel opened.
|
||||
* - Load extension with no background page.
|
||||
* - Open an extension page in a tab that adds a local storage item.
|
||||
* - Close all extension pages.
|
||||
* - Open the add-on storage panel.
|
||||
* - The data in the storage panel should match the item added by the extension.
|
||||
*/
|
||||
add_task(async function test_panel_data_matches_extension_with_no_pages_open() {
|
||||
const extension = await startupExtension(
|
||||
getExtensionConfig({ files: ext_no_bg.files })
|
||||
);
|
||||
|
||||
const url = extension.extension.baseURI.resolve("extension_page_in_tab.html");
|
||||
const contentPage = await ExtensionTestUtils.loadContentPage(url, {
|
||||
extension,
|
||||
});
|
||||
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
extension.sendMessage("storage-local-set", { a: 123 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
|
||||
await contentPage.close();
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
const { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[{ area: "local", name: "a", value: { str: "123" } }],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
await shutdown(extension, target);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test case: No bg page. Storage panel live updates when a transient page adds an item.
|
||||
* - Load extension with no background page.
|
||||
* - Open the add-on storage panel.
|
||||
* - With the storage panel still open, open an extension page in a new tab that adds an
|
||||
* item.
|
||||
* - Assert:
|
||||
* - The data in the storage panel should live update to match the item added by the
|
||||
* extension.
|
||||
* - If an extension page adds the same data again, the data in the storage panel should
|
||||
* not change.
|
||||
*/
|
||||
add_task(
|
||||
async function test_panel_data_live_updates_for_extension_without_bg_page() {
|
||||
const extension = await startupExtension(
|
||||
getExtensionConfig({ files: ext_no_bg.files })
|
||||
);
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
const url = extension.extension.baseURI.resolve(
|
||||
"extension_page_in_tab.html"
|
||||
);
|
||||
const contentPage = await ExtensionTestUtils.loadContentPage(url, {
|
||||
extension,
|
||||
});
|
||||
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
let { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[],
|
||||
"Got the expected results on empty storage.local"
|
||||
);
|
||||
|
||||
extension.sendMessage("storage-local-fireOnChanged");
|
||||
extension.sendMessage("storage-local-set", { a: 123 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
await extension.awaitMessage("storage-local-onChanged");
|
||||
|
||||
data = (await extensionStorage.getStoreObjects(host)).data;
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[{ area: "local", name: "a", value: { str: "123" } }],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
extension.sendMessage("storage-local-fireOnChanged");
|
||||
extension.sendMessage("storage-local-set", { a: 123 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
await extension.awaitMessage("storage-local-onChanged");
|
||||
|
||||
data = (await extensionStorage.getStoreObjects(host)).data;
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[{ area: "local", name: "a", value: { str: "123" } }],
|
||||
"The results are unchanged when an extension page adds duplicate items"
|
||||
);
|
||||
|
||||
await contentPage.close();
|
||||
await shutdown(extension, target);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Test case: Storage panel shows extension storage data added prior to extension startup
|
||||
* - Load extension that adds a storage item
|
||||
* - Uninstall the extension
|
||||
* - Reinstall the extension
|
||||
* - Open the add-on storage panel.
|
||||
* - The data in the storage panel should match the data added the first time the extension
|
||||
* was installed
|
||||
* Related test case: Storage panel shows extension storage data when an extension that has
|
||||
* already migrated to the IndexedDB storage backend prior to extension startup adds
|
||||
* another storage item.
|
||||
* - (Building from previous steps)
|
||||
* - The reinstalled extension adds a storage item
|
||||
* - The data in the storage panel should live update with both items: the item added from
|
||||
* the first and the item added from the reinstall.
|
||||
*/
|
||||
add_task(
|
||||
async function test_panel_data_matches_data_added_prior_to_ext_startup() {
|
||||
// The pref to leave the addonid->uuid mapping around after uninstall so that we can
|
||||
// re-attach to the same storage
|
||||
Services.prefs.setBoolPref(LEAVE_UUID_PREF, true);
|
||||
|
||||
// The pref to prevent cleaning up storage on uninstall
|
||||
Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, true);
|
||||
|
||||
let extension = await startupExtension(
|
||||
getExtensionConfig({ background: extensionScriptWithMessageListener })
|
||||
);
|
||||
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
extension.sendMessage("storage-local-set", { a: 123 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
|
||||
await shutdown(extension);
|
||||
|
||||
// Reinstall the same extension
|
||||
extension = await startupExtension(
|
||||
getExtensionConfig({ background: extensionScriptWithMessageListener })
|
||||
);
|
||||
|
||||
await extension.awaitMessage("extension-origin");
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
let { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[{ area: "local", name: "a", value: { str: "123" } }],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
// Related test case
|
||||
extension.sendMessage("storage-local-fireOnChanged");
|
||||
extension.sendMessage("storage-local-set", { b: 456 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
await extension.awaitMessage("storage-local-onChanged");
|
||||
|
||||
data = (await extensionStorage.getStoreObjects(host)).data;
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[
|
||||
{ area: "local", name: "a", value: { str: "123" } },
|
||||
{ area: "local", name: "b", value: { str: "456" } },
|
||||
],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false);
|
||||
Services.prefs.setBoolPref(LEAVE_UUID_PREF, false);
|
||||
|
||||
await shutdown(extension, target);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
function cleanup_for_test_panel_data_matches_data_added_prior_to_ext_startup() {
|
||||
Services.prefs.clearUserPref(LEAVE_UUID_PREF);
|
||||
Services.prefs.clearUserPref(LEAVE_STORAGE_PREF);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Test case: Bg page adds an item to storage. With storage panel open, reload extension.
|
||||
* - Load extension with background page that adds a storage item on message.
|
||||
* - Open the add-on storage panel.
|
||||
* - With the storage panel still open, reload the extension.
|
||||
* - The data in the storage panel should match the item added prior to reloading.
|
||||
*/
|
||||
add_task(async function test_panel_live_reload() {
|
||||
const EXTENSION_ID = "test_panel_live_reload@xpcshell.mozilla.org";
|
||||
let manifest = {
|
||||
version: "1.0",
|
||||
applications: {
|
||||
gecko: {
|
||||
id: EXTENSION_ID,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
info("Loading extension version 1.0");
|
||||
const extension = await startupExtension(
|
||||
getExtensionConfig({
|
||||
manifest,
|
||||
background: extensionScriptWithMessageListener,
|
||||
})
|
||||
);
|
||||
|
||||
info("Waiting for message from test extension");
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
info("Adding storage item");
|
||||
extension.sendMessage("storage-local-set", { a: 123 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
manifest = {
|
||||
...manifest,
|
||||
version: "2.0",
|
||||
};
|
||||
// "Reload" is most similar to an upgrade, as e.g. storage data is preserved
|
||||
info("Update to version 2.0");
|
||||
await extension.upgrade(
|
||||
getExtensionConfig({
|
||||
manifest,
|
||||
background: extensionScriptWithMessageListener,
|
||||
})
|
||||
);
|
||||
|
||||
await extension.awaitMessage("extension-origin");
|
||||
|
||||
const { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[{ area: "local", name: "a", value: { str: "123" } }],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
await shutdown(extension, target);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test case: Transient page adds an item to storage. With storage panel open,
|
||||
* reload extension.
|
||||
* - Load extension with no background page.
|
||||
* - Open transient page that adds a storage item on message.
|
||||
* - Open the add-on storage panel.
|
||||
* - With the storage panel still open, reload the extension.
|
||||
* - The data in the storage panel should match the item added prior to reloading.
|
||||
*/
|
||||
add_task(async function test_panel_live_reload_for_extension_without_bg_page() {
|
||||
const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org";
|
||||
let manifest = {
|
||||
version: "1.0",
|
||||
applications: {
|
||||
gecko: {
|
||||
id: EXTENSION_ID,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
info("Loading and starting extension version 1.0");
|
||||
const extension = await startupExtension(
|
||||
getExtensionConfig({
|
||||
manifest,
|
||||
files: ext_no_bg.files,
|
||||
})
|
||||
);
|
||||
|
||||
info("Opening extension page in a tab");
|
||||
const url = extension.extension.baseURI.resolve("extension_page_in_tab.html");
|
||||
const contentPage = await ExtensionTestUtils.loadContentPage(url, {
|
||||
extension,
|
||||
});
|
||||
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
info("Waiting for extension page in a tab to add storage item");
|
||||
extension.sendMessage("storage-local-set", { a: 123 });
|
||||
await extension.awaitMessage("storage-local-set:done");
|
||||
|
||||
await contentPage.close();
|
||||
|
||||
info("Opening storage panel");
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
manifest = {
|
||||
...manifest,
|
||||
version: "2.0",
|
||||
};
|
||||
// "Reload" is most similar to an upgrade, as e.g. storage data is preserved
|
||||
info("Updating extension to version 2.0");
|
||||
await extension.upgrade(
|
||||
getExtensionConfig({
|
||||
manifest,
|
||||
files: ext_no_bg.files,
|
||||
})
|
||||
);
|
||||
|
||||
const { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[{ area: "local", name: "a", value: { str: "123" } }],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
await shutdown(extension, target);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test case: Bg page auto adds item(s). With storage panel open, reload extension.
|
||||
* - Load extension with background page that automatically adds a storage item on startup.
|
||||
* - Open the add-on storage panel.
|
||||
* - With the storage panel still open, reload the extension.
|
||||
* - The data in the storage panel should match the item(s) added by the reloaded
|
||||
* extension.
|
||||
*/
|
||||
add_task(
|
||||
async function test_panel_live_reload_when_extension_auto_adds_items() {
|
||||
async function background() {
|
||||
await browser.storage.local.set({ a: { b: 123 }, c: { d: 456 } });
|
||||
browser.test.sendMessage("extension-origin", window.location.origin);
|
||||
}
|
||||
const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org";
|
||||
let manifest = {
|
||||
version: "1.0",
|
||||
applications: {
|
||||
gecko: {
|
||||
id: EXTENSION_ID,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
info("Loading and starting extension version 1.0");
|
||||
const extension = await startupExtension(
|
||||
getExtensionConfig({ manifest, background })
|
||||
);
|
||||
|
||||
info("Waiting for message from test extension");
|
||||
const host = await extension.awaitMessage("extension-origin");
|
||||
|
||||
info("Opening storage panel");
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
manifest = {
|
||||
...manifest,
|
||||
version: "2.0",
|
||||
};
|
||||
// "Reload" is most similar to an upgrade, as e.g. storage data is preserved
|
||||
info("Update to version 2.0");
|
||||
await extension.upgrade(
|
||||
getExtensionConfig({
|
||||
manifest,
|
||||
background,
|
||||
})
|
||||
);
|
||||
|
||||
await extension.awaitMessage("extension-origin");
|
||||
|
||||
const { data } = await extensionStorage.getStoreObjects(host);
|
||||
Assert.deepEqual(
|
||||
data,
|
||||
[
|
||||
{ area: "local", name: "a", value: { str: '{"b":123}' } },
|
||||
{ area: "local", name: "c", value: { str: '{"d":456}' } },
|
||||
],
|
||||
"Got the expected results on populated storage.local"
|
||||
);
|
||||
|
||||
await shutdown(extension, target);
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* This task should be last, as it sets a pref to disable the extensionStorage
|
||||
* storage actor. Since this pref is set at the beginning of the file, it
|
||||
* already will be cleared via registerCleanupFunction when the test finishes.
|
||||
*/
|
||||
add_task(async function test_extensionStorage_store_disabled_on_pref() {
|
||||
Services.prefs.setBoolPref(EXTENSION_STORAGE_ENABLED_PREF, false);
|
||||
|
||||
const extension = await startupExtension(getExtensionConfig());
|
||||
|
||||
const { target, extensionStorage } = await openAddonStoragePanel(
|
||||
extension.id
|
||||
);
|
||||
|
||||
ok(
|
||||
extensionStorage === null,
|
||||
"Should not have an extensionStorage store when pref disabled"
|
||||
);
|
||||
|
||||
await shutdown(extension, target);
|
||||
});
|
@ -60,6 +60,7 @@ skip-if = (os == 'win' && bits == 32) #Bug 1543156
|
||||
[test_blackboxing-05.js]
|
||||
[test_blackboxing-07.js]
|
||||
[test_blackboxing-08.js]
|
||||
[test_extension_storage_actor.js]
|
||||
[test_frameactor-01.js]
|
||||
[test_frameactor-02.js]
|
||||
[test_frameactor-03.js]
|
||||
|
@ -167,6 +167,23 @@ createStorageSpec({
|
||||
methods: storageMethods,
|
||||
});
|
||||
|
||||
types.addDictType("extensionobject", {
|
||||
name: "nullable:string",
|
||||
value: "nullable:longstring",
|
||||
});
|
||||
|
||||
types.addDictType("extensionstoreobject", {
|
||||
total: "number",
|
||||
offset: "number",
|
||||
data: "array:nullable:extensionobject",
|
||||
});
|
||||
|
||||
createStorageSpec({
|
||||
typeName: "extensionStorage",
|
||||
storeObjectType: "extensionstoreobject",
|
||||
methods: {},
|
||||
});
|
||||
|
||||
types.addDictType("cacheobject", {
|
||||
url: "string",
|
||||
status: "string",
|
||||
|
@ -695,6 +695,9 @@ this.ExtensionStorageIDB = {
|
||||
|
||||
promise = migrateJSONFileData(extension, storagePrincipal)
|
||||
.then(() => {
|
||||
extension.setSharedData("storageIDBBackend", true);
|
||||
extension.setSharedData("storageIDBPrincipal", storagePrincipal);
|
||||
Services.ppmm.sharedData.flush();
|
||||
return {
|
||||
backendEnabled: true,
|
||||
storagePrincipal: serializedPrincipal,
|
||||
@ -714,6 +717,9 @@ this.ExtensionStorageIDB = {
|
||||
"JSONFile backend is being kept enabled by an unexpected " +
|
||||
`IDBBackend failure: ${err.message}::${err.stack}`
|
||||
);
|
||||
extension.setSharedData("storageIDBBackend", false);
|
||||
Services.ppmm.sharedData.flush();
|
||||
|
||||
return { backendEnabled: false };
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user