Bug 1811230 - [devtools] Consider extension storage inspection always enabled. r=devtools-reviewers,nchevobbe

This pref has been true for a while and isn't meant to be disabled by the user.

Differential Revision: https://phabricator.services.mozilla.com/D166660
This commit is contained in:
Alexandre Poirot 2023-01-19 17:16:34 +00:00
parent ec1c1ce4c1
commit d42dc60869
6 changed files with 313 additions and 377 deletions

View file

@ -70,8 +70,8 @@ pref("extensions.langpacks.signatures.required", true);
pref("xpinstall.signatures.required", true); pref("xpinstall.signatures.required", true);
pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing"); pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
// Enable extensionStorage storage actor by default // Enable the unified extensions UI by default.
pref("devtools.storage.extensionStorage.enabled", true); pref("extensions.unifiedExtensions.enabled", true);
// Dictionary download preference // Dictionary download preference
pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/language-tools/"); pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/language-tools/");

View file

@ -6,9 +6,7 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; "use strict";
add_task(async function set_enable_extensionStorage_pref() { add_setup(async function() {
await pushPref("devtools.storage.extensionStorage.enabled", true);
// Always on top mode mess up with toolbox focus and openStoragePanelForAddon would timeout // Always on top mode mess up with toolbox focus and openStoragePanelForAddon would timeout
// waiting for toolbox focus. // waiting for toolbox focus.
await pushPref("devtools.toolbox.alwaysOnTop", false); await pushPref("devtools.toolbox.alwaysOnTop", false);

View file

@ -33,9 +33,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
Sqlite: "resource://gre/modules/Sqlite.sys.mjs", Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
}); });
const EXTENSION_STORAGE_ENABLED_PREF =
"devtools.storage.extensionStorage.enabled";
const DEFAULT_VALUE = "value"; const DEFAULT_VALUE = "value";
loader.lazyRequireGetter( loader.lazyRequireGetter(
@ -1784,369 +1781,363 @@ exports.setupParentProcessForExtensionStorage = function({ mm, prefix }) {
/** /**
* The Extension Storage actor. * The Extension Storage actor.
*/ */
if (Services.prefs.getBoolPref(EXTENSION_STORAGE_ENABLED_PREF, false)) { StorageActors.createActor(
StorageActors.createActor( {
{ typeName: "extensionStorage",
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);
}, },
{
initialize(storageActor) {
protocol.Actor.prototype.initialize.call(this, null);
this.storageActor = storageActor; getExtensionPolicy() {
return WebExtensionPolicy.getByID(this.addonId);
},
this.addonId = this.storageActor.parentActor.addonId; destroy() {
extensionStorageHelpers.onChangedChildListeners.delete(
this.onStorageChange
);
// Retrieve the base moz-extension url for the extension this.storageActor.off("window-ready", this.onWindowReady);
// (and also remove the final '/' from it). this.storageActor.off("window-destroyed", this.onWindowDestroyed);
this.extensionHostURL = this.getExtensionPolicy()
.getURL()
.slice(0, -1);
// Map<host, ExtensionStorageIDB db connection> this.hostVsStores.clear();
// Bug 1542038, 1542039: Each storage area will need its own protocol.Actor.prototype.destroy.call(this);
// 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.storageActor = null;
// 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); setupChildProcess() {
const ppmm = this.conn.parentMessageManager;
extensionStorageHelpers.setPpmm(ppmm);
this.setupChildProcess(); // eslint-disable-next-line no-restricted-properties
this.conn.setupInParent({
module: "devtools/server/actors/storage",
setupParent: "setupParentProcessForExtensionStorage",
});
this.onWindowReady = this.onWindowReady.bind(this); extensionStorageHelpers.onChangedChildListeners.add(this.onStorageChange);
this.onWindowDestroyed = this.onWindowDestroyed.bind(this); this.setupStorageInParent = extensionStorageHelpers.callParentProcessAsync.bind(
this.storageActor.on("window-ready", this.onWindowReady); extensionStorageHelpers,
this.storageActor.on("window-destroyed", this.onWindowDestroyed); "setupStorageInParent"
}, );
getExtensionPolicy() { // Add a message listener in the child process to receive messages from the parent
return WebExtensionPolicy.getByID(this.addonId); // process
}, ppmm.addMessageListener(
"debug:storage-extensionStorage-request-child",
extensionStorageHelpers.handleParentRequest.bind(
extensionStorageHelpers
)
);
},
destroy() { /**
extensionStorageHelpers.onChangedChildListeners.delete( * This fires when the extension changes storage data while the storage
this.onStorageChange * 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"
); );
}
this.storageActor.off("window-ready", this.onWindowReady); for (const key in changes) {
this.storageActor.off("window-destroyed", this.onWindowDestroyed); const storageChange = changes[key];
let { newValue, oldValue } = storageChange;
this.hostVsStores.clear(); if (isStructuredCloneHolder(newValue)) {
protocol.Actor.prototype.destroy.call(this); newValue = newValue.deserialize(this);
}
this.storageActor = null; if (isStructuredCloneHolder(oldValue)) {
}, oldValue = oldValue.deserialize(this);
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; let action;
const storeMap = this.hostVsStores.get(host); if (typeof newValue === "undefined") {
action = "deleted";
function isStructuredCloneHolder(value) { storeMap.delete(key);
return ( } else if (typeof oldValue === "undefined") {
value && action = "added";
typeof value === "object" && storeMap.set(key, newValue);
Cu.getClassName(value, true) === "StructuredCloneHolder" } else {
); action = "changed";
storeMap.set(key, newValue);
} }
for (const key in changes) { this.storageActor.update(action, this.typeName, { [host]: [key] });
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") { * Purpose of this method is same as populateStoresForHosts but this is async.
action = "deleted"; * This exact same operation cannot be performed in populateStoresForHosts
storeMap.delete(key); * method, as that method is called in initialize method of the actor, which
} else if (typeof oldValue === "undefined") { * cannot be asynchronous.
action = "added"; */
storeMap.set(key, newValue); async preListStores() {
} else { // Ensure the actor's target is an extension and it is enabled
action = "changed"; if (!this.addonId || !WebExtensionPolicy.getByID(this.addonId)) {
storeMap.set(key, newValue); return;
} }
this.storageActor.update(action, this.typeName, { [host]: [key] }); await this.populateStoresForHost(this.extensionHostURL);
} },
},
/** /**
* Purpose of this method is same as populateStoresForHosts but this is async. * This method is overriden and left blank as for extensionStorage, this operation
* This exact same operation cannot be performed in populateStoresForHosts * cannot be performed synchronously. Thus, the preListStores method exists to
* method, as that method is called in initialize method of the actor, which * do the same task asynchronously.
* cannot be asynchronous. */
*/ populateStoresForHosts() {},
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 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);
* This method is overriden and left blank as for extensionStorage, this operation if (!extension || !extension.hasPermission("storage")) {
* cannot be performed synchronously. Thus, the preListStores method exists to return;
* do the same task asynchronously. }
*/
populateStoresForHosts() {},
/** // Make sure storeMap is defined and set in this.hostVsStores before subscribing
* This method asynchronously reads the storage data for the target extension // a storage onChanged listener in the parent process
* and caches this data into this.hostVsStores. const storeMap = new Map();
* @param {String} host - the hostname for the extension this.hostVsStores.set(host, storeMap);
*/
async populateStoresForHost(host) {
if (host !== this.extensionHostURL) {
return;
}
const extension = ExtensionProcessScript.getExtensionChild( const storagePrincipal = await this.getStoragePrincipal(extension.id);
this.addonId
);
if (!extension || !extension.hasPermission("storage")) {
return;
}
// Make sure storeMap is defined and set in this.hostVsStores before subscribing if (!storagePrincipal) {
// a storage onChanged listener in the parent process // This could happen if the extension fails to be migrated to the
const storeMap = new Map(); // IndexedDB backend
this.hostVsStores.set(host, storeMap); return;
}
const storagePrincipal = await this.getStoragePrincipal(extension.id); const db = await ExtensionStorageIDB.open(storagePrincipal);
this.dbConnectionForHost.set(host, db);
const data = await db.get();
if (!storagePrincipal) { for (const [key, value] of Object.entries(data)) {
// This could happen if the extension fails to be migrated to the storeMap.set(key, value);
// IndexedDB backend }
return;
}
const db = await ExtensionStorageIDB.open(storagePrincipal); if (this.storageActor.parentActor.fallbackWindow) {
this.dbConnectionForHost.set(host, db); // Show the storage actor in the add-on storage inspector even when there
const data = await db.get(); // is no extension page currently open
// This strategy may need to change depending on the outcome of Bug 1597900
const storageData = {};
storageData[host] = this.getNamesForHost(host);
this.storageActor.update("added", this.typeName, storageData);
}
},
for (const [key, value] of Object.entries(data)) { async getStoragePrincipal(addonId) {
storeMap.set(key, value); const {
} backendEnabled,
storagePrincipal,
} = await this.setupStorageInParent(addonId);
if (this.storageActor.parentActor.fallbackWindow) { if (!backendEnabled) {
// Show the storage actor in the add-on storage inspector even when there // IDB backend disabled; give up.
// is no extension page currently open return null;
// This strategy may need to change depending on the outcome of Bug 1597900 }
const storageData = {}; return storagePrincipal;
storageData[host] = this.getNamesForHost(host); },
this.storageActor.update("added", this.typeName, storageData);
}
},
async getStoragePrincipal(addonId) { getValuesForHost(host, name) {
const { const result = [];
backendEnabled,
storagePrincipal,
} = await this.setupStorageInParent(addonId);
if (!backendEnabled) { if (!this.hostVsStores.has(host)) {
// 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; return result;
}, }
/** if (name) {
* Converts a storage item to an "extensionobject" as defined in return [{ name, value: this.hostVsStores.get(host).get(name) }];
* devtools/shared/specs/storage.js. Behavior largely mirrors the "indexedDB" storage actor, }
* except where it would throw an unhandled error (i.e. for a `BigInt` or `undefined`
* `item.value`).
* @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;
}
let { name, value } = item; for (const [key, value] of Array.from(
const isValueEditable = extensionStorageHelpers.isEditable(value); this.hostVsStores.get(host).entries()
)) {
result.push({ name: key, value });
}
return result;
},
// `JSON.stringify()` throws for `BigInt`, adds extra quotes to strings and `Date` strings, /**
// and doesn't modify `undefined`. * Converts a storage item to an "extensionobject" as defined in
switch (typeof value) { * devtools/shared/specs/storage.js. Behavior largely mirrors the "indexedDB" storage actor,
case "bigint": * except where it would throw an unhandled error (i.e. for a `BigInt` or `undefined`
value = `${value.toString()}n`; * `item.value`).
* @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;
}
let { name, value } = item;
const isValueEditable = extensionStorageHelpers.isEditable(value);
// `JSON.stringify()` throws for `BigInt`, adds extra quotes to strings and `Date` strings,
// and doesn't modify `undefined`.
switch (typeof value) {
case "bigint":
value = `${value.toString()}n`;
break;
case "string":
break;
case "undefined":
value = "undefined";
break;
default:
value = JSON.stringify(value);
if (
// can't use `instanceof` across frame boundaries
Object.prototype.toString.call(item.value) === "[object Date]"
) {
value = JSON.parse(value);
}
}
return {
name,
value: new LongStringActor(this.conn, value),
area: "local", // Bug 1542038, 1542039: set the correct storage area
isValueEditable,
};
},
getFields() {
return [
{ name: "name", editable: false },
{ name: "value", editable: true },
{ name: "area", editable: false },
{ name: "isValueEditable", editable: false, private: true },
];
},
onItemUpdated(action, host, names) {
this.storageActor.update(action, this.typeName, {
[host]: names,
});
},
async editItem({ host, field, items, oldValue }) {
const db = this.dbConnectionForHost.get(host);
if (!db) {
return;
}
const { name, value } = items;
let parsedValue = parseItemValue(value);
if (parsedValue === value) {
const { typesFromString } = extensionStorageHelpers;
for (const { test, parse } of Object.values(typesFromString)) {
if (test(value)) {
parsedValue = parse(value);
break; break;
case "string":
break;
case "undefined":
value = "undefined";
break;
default:
value = JSON.stringify(value);
if (
// can't use `instanceof` across frame boundaries
Object.prototype.toString.call(item.value) === "[object Date]"
) {
value = JSON.parse(value);
}
}
return {
name,
value: new LongStringActor(this.conn, value),
area: "local", // Bug 1542038, 1542039: set the correct storage area
isValueEditable,
};
},
getFields() {
return [
{ name: "name", editable: false },
{ name: "value", editable: true },
{ name: "area", editable: false },
{ name: "isValueEditable", editable: false, private: true },
];
},
onItemUpdated(action, host, names) {
this.storageActor.update(action, this.typeName, {
[host]: names,
});
},
async editItem({ host, field, items, oldValue }) {
const db = this.dbConnectionForHost.get(host);
if (!db) {
return;
}
const { name, value } = items;
let parsedValue = parseItemValue(value);
if (parsedValue === value) {
const { typesFromString } = extensionStorageHelpers;
for (const { test, parse } of Object.values(typesFromString)) {
if (test(value)) {
parsedValue = parse(value);
break;
}
} }
} }
const changes = await db.set({ [name]: parsedValue }); }
this.fireOnChangedExtensionEvent(host, changes); const changes = await db.set({ [name]: parsedValue });
this.fireOnChangedExtensionEvent(host, changes);
this.onItemUpdated("changed", host, [name]); this.onItemUpdated("changed", host, [name]);
}, },
async removeItem(host, name) { async removeItem(host, name) {
const db = this.dbConnectionForHost.get(host); const db = this.dbConnectionForHost.get(host);
if (!db) { if (!db) {
return; return;
} }
const changes = await db.remove(name); const changes = await db.remove(name);
this.fireOnChangedExtensionEvent(host, changes); this.fireOnChangedExtensionEvent(host, changes);
this.onItemUpdated("deleted", host, [name]); this.onItemUpdated("deleted", host, [name]);
}, },
async removeAll(host) { async removeAll(host) {
const db = this.dbConnectionForHost.get(host); const db = this.dbConnectionForHost.get(host);
if (!db) { if (!db) {
return; return;
} }
const changes = await db.clear(); const changes = await db.clear();
this.fireOnChangedExtensionEvent(host, changes); this.fireOnChangedExtensionEvent(host, changes);
this.onItemUpdated("cleared", host, []); this.onItemUpdated("cleared", host, []);
}, },
/** /**
* Let the extension know that storage data has been changed by the user from * Let the extension know that storage data has been changed by the user from
* the storage inspector. * the storage inspector.
*/ */
fireOnChangedExtensionEvent(host, changes) { fireOnChangedExtensionEvent(host, changes) {
// Bug 1542038, 1542039: Which message to send depends on the storage area // Bug 1542038, 1542039: Which message to send depends on the storage area
const uuid = new URL(host).host; const uuid = new URL(host).host;
Services.cpmm.sendAsyncMessage( Services.cpmm.sendAsyncMessage(
`Extension:StorageLocalOnChanged:${uuid}`, `Extension:StorageLocalOnChanged:${uuid}`,
changes changes
); );
}, },
} }
); );
}
StorageActors.createActor( StorageActors.createActor(
{ {

View file

@ -4,13 +4,6 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; "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( add_task(
async function test_extensionStorage_disabled_for_non_extension_target() { async function test_extensionStorage_disabled_for_non_extension_target() {
if (isFissionEnabled()) { if (isFissionEnabled()) {

View file

@ -41,20 +41,12 @@ const { createAppInfo, promiseStartupManager } = AddonTestUtils;
const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
const EXTENSION_STORAGE_ENABLED_PREF =
"devtools.storage.extensionStorage.enabled";
AddonTestUtils.init(this); AddonTestUtils.init(this);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
ExtensionTestUtils.init(this); 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);
});
add_setup(async function setup() { add_setup(async function setup() {
await promiseStartupManager(); await promiseStartupManager();
const dir = createMissingIndexedDBDirs(); const dir = createMissingIndexedDBDirs();
@ -1157,32 +1149,3 @@ add_task(async function test_live_update_with_no_extension_listener() {
await shutdown(extension, target); 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(
{
// This test fails if the extension runs in the main process
// like in Thunderbird (see bug 1575183 comment #15 for details).
skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions,
},
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);
}
);

View file

@ -35,20 +35,11 @@ PromiseTestUtils.allowMatchingRejectionsGlobally(
const { createAppInfo, promiseStartupManager } = AddonTestUtils; const { createAppInfo, promiseStartupManager } = AddonTestUtils;
const EXTENSION_STORAGE_ENABLED_PREF =
"devtools.storage.extensionStorage.enabled";
AddonTestUtils.init(this); AddonTestUtils.init(this);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
ExtensionTestUtils.init(this); 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);
});
add_task(async function setup() { add_task(async function setup() {
await promiseStartupManager(); await promiseStartupManager();
const dir = createMissingIndexedDBDirs(); const dir = createMissingIndexedDBDirs();