-
-
Save biancadanforth/fb6aaae07084512a594a8098c971807e to your computer and use it in GitHub Desktop.
diff --git a/devtools/client/locales/en-US/storage.properties b/devtools/client/locales/en-US/storage.properties | |
--- a/devtools/client/locales/en-US/storage.properties | |
+++ b/devtools/client/locales/en-US/storage.properties | |
@@ -31,6 +31,9 @@ tree.labels.localStorage=Local Storage | |
tree.labels.sessionStorage=Session Storage | |
tree.labels.indexedDB=Indexed DB | |
tree.labels.Cache=Cache Storage | |
+# TODO: Figure out how to structure tree for the 3 different types of extension | |
+# storage (browser.storage.local, .managed, .sync). | |
+tree.labels.extensionStorage=Extension Storage | |
# LOCALIZATION NOTE (table.headers.*.*): | |
# These strings are the header names of the columns in the Storage Table for | |
@@ -67,6 +70,10 @@ table.headers.indexedDB.keyPath2=Key Pat | |
table.headers.indexedDB.autoIncrement=Auto Increment | |
table.headers.indexedDB.indexes=Indexes | |
+# TODO: Should name=Key? | |
+table.headers.extensionStorage.name=Name | |
+table.headers.extensionStorage.value=Value | |
+ | |
# LOCALIZATION NOTE (label.expires.session): | |
# This string is displayed in the expires column when the cookie is Session | |
# Cookie | |
diff --git a/devtools/server/actors/storage.js b/devtools/server/actors/storage.js | |
--- a/devtools/server/actors/storage.js | |
+++ b/devtools/server/actors/storage.js | |
@@ -12,6 +12,8 @@ const Services = require("Services"); | |
const defer = require("devtools/shared/defer"); | |
const {isWindowIncluded} = require("devtools/shared/layout/utils"); | |
const specs = require("devtools/shared/specs/storage"); | |
+const {ExtensionProcessScript} = require("resource://gre/modules/ExtensionProcessScript.jsm"); | |
+const {ExtensionStorageIDB} = require("resource://gre/modules/ExtensionStorageIDB.jsm"); | |
const CHROME_ENABLED_PREF = "devtools.chrome.enabled"; | |
const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled"; | |
@@ -1298,6 +1300,270 @@ StorageActors.createActor({ | |
observationTopics: ["dom-storage2-changed", "dom-private-storage2-changed"], | |
}, getObjectForLocalOrSessionStorage("sessionStorage")); | |
+/** | |
+* The Extension Storage actor. | |
+*/ | |
+StorageActors.createActor({ | |
+ typeName: "extensionStorage", | |
+}, { | |
+ initialize(storageActor) { | |
+ protocol.Actor.prototype.initialize.call(this, null); | |
+ | |
+ this.storageActor = storageActor; | |
+ | |
+ this.populateStoresForHosts(); | |
+ | |
+ this.addExtensionStorageListeners(); | |
+ | |
+ 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); | |
+ }, | |
+ | |
+ destroy() { | |
+ this.removeExtensionStorageListeners(); | |
+ | |
+ 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; | |
+ }, | |
+ | |
+ /** | |
+ * Ensures this.hostVsStores stays up-to-date and passes the change on | |
+ * to update the view. | |
+ */ | |
+ onStorageChange(changes, areaName) { | |
+ if (areaName !== "local") { | |
+ return; | |
+ } | |
+ | |
+ for (const key in changes) { | |
+ const storageChange = changes[key]; | |
+ const {newValue, oldValue} = storageChange; | |
+ | |
+ // TODO: Not sure what the best way is to get the current host. | |
+ let host = null; | |
+ for (const window of this.windows) { | |
+ host = this.getHostName(window.location); | |
+ if (host) { | |
+ break; | |
+ } | |
+ } | |
+ | |
+ let action = null; | |
+ const storeMap = this.hostVsStores.get(host); | |
+ | |
+ 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]}); | |
+ } | |
+ }, | |
+ | |
+ // TODO: Don't use WE APIs directly | |
+ addExtensionStorageListeners() { | |
+ this.onStorageChange = this.onStorageChange.bind(this); | |
+ for (const window of this.windows) { | |
+ Cu.waiveXrays(window).browser.storage.onChanged.addListener(this.onStorageChange); | |
+ } | |
+ }, | |
+ | |
+ // TODO: Don't use WE APIs directly | |
+ removeExtensionStorageListeners() { | |
+ for (const window of this.windows) { | |
+ Cu.waiveXrays(window).browser.storage.onChanged.removeListener( | |
+ this.onStorageChange | |
+ ); | |
+ } | |
+ }, | |
+ | |
+ /** | |
+ * 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() {}, | |
+ | |
+ /** | |
+ * 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() { | |
+ this.hostVsStores = new Map(); | |
+ | |
+ // TODO: Don’t use `window` for everything, as an extension may not have a | |
+ // `window` available (e.g. has no background script, and no other scripts | |
+ // are running at a given moment). | |
+ for (const window of this.windows) { | |
+ const host = this.getHostName(window.location); | |
+ if (host) { | |
+ await this.populateStoresForHost(host, window); | |
+ } | |
+ } | |
+ }, | |
+ | |
+ async populateStoreMap(storagePrincipal, storeMap) { | |
+ const db = await ExtensionStorageIDB.open(storagePrincipal); | |
+ const data = await db.get(); | |
+ | |
+ for (const [key, value] of Object.entries(data)) { | |
+ storeMap.set(key, value); | |
+ } | |
+ return storeMap; | |
+ }, | |
+ | |
+ /** | |
+ * This method asynchronously reads the browser.storage.local data for the window | |
+ * (if it's an extension page window and the related extension has the storage | |
+ * permission) and caches this data into this.hostVsStores. | |
+ */ | |
+ async populateStoresForHost(host, window) { | |
+ if (this.hostVsStores.has(host)) { | |
+ return; | |
+ } | |
+ | |
+ let storeMap = new Map(); | |
+ const principal = this.getPrincipal(window); | |
+ const { addonId } = principal; | |
+ // TODO: Get extension object from the parent instead? | |
+ const extension = ExtensionProcessScript.getExtensionChild(addonId); | |
+ | |
+ if (extension && extension.hasPermission("storage")) { | |
+ const isUsingIDBBackend = extension.getSharedData("storageIDBBackend"); | |
+ if (isUsingIDBBackend === null) { | |
+ // Extension has not yet migrated to the IDB backend; trigger migration | |
+ | |
+ // TODO: Create own context object rather than getting from the view? | |
+ let context; | |
+ const {views} = extension; | |
+ for (const view of views) { | |
+ context = view.childManager.context; | |
+ if (context) { | |
+ break; | |
+ } | |
+ } | |
+ | |
+ if (context) { | |
+ const { | |
+ backendEnabled, | |
+ storagePrincipal, | |
+ } = await ExtensionStorageIDB.selectBackend(context); | |
+ | |
+ if (backendEnabled) { | |
+ storeMap = await this.populateStoreMap(storagePrincipal, storeMap); | |
+ } | |
+ } | |
+ } else if (isUsingIDBBackend) { | |
+ const storagePrincipal = extension.getSharedData("storageIDBPrincipal"); | |
+ | |
+ storeMap = await this.populateStoreMap(storagePrincipal, storeMap); | |
+ } | |
+ } | |
+ | |
+ this.hostVsStores.set(host, storeMap); | |
+ }, | |
+ | |
+ getValuesForHost(host, name) { | |
+ if (name) { | |
+ return [{name, value: this.hostVsStores.get(host).get(name)}]; | |
+ } | |
+ | |
+ const result = []; | |
+ for (const [key, value] of Array.from(this.hostVsStores.get(host).entries())) { | |
+ result.push({name: key, value}); | |
+ } | |
+ return result; | |
+ }, | |
+ | |
+ /** | |
+ * This method converts the values returned by getValuesForHost into a StoreObject | |
+ * (currently based off of this same method for IndexedDB). | |
+ */ | |
+ toStoreObject({name, value}) { | |
+ if (!{name, value}) { | |
+ return null; | |
+ } | |
+ | |
+ // Stringify adds redundant quotes to strings | |
+ if (typeof value !== "string") { | |
+ // Not all possible values are stringifiable (e.g. functions) | |
+ value = JSON.stringify(value) || "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 (value.length > maxLength) { | |
+ value = value.substr(0, maxLength); | |
+ } | |
+ | |
+ return { | |
+ name, | |
+ value: new LongStringActor(this.conn, value || ""), | |
+ }; | |
+ }, | |
+ | |
+ getFields() { | |
+ return [ | |
+ // name needs to be editable for the addItem case, where a temporary key-value | |
+ // pair is created that can later be edited via editItem. | |
+ { name: "name", editable: true }, | |
+ { name: "value", editable: true }, | |
+ ]; | |
+ }, | |
+ | |
+ // TODO: Web content storage inspector currently breaks (changing storage from a | |
+ // content script doesn't work). Because Cu.unwaiveXrays etc. don't exist. Easiest | |
+ // fix is not to show anything here if the host looks like an extension id. | |
+ | |
+ // TODO: Don't use WE APIs directly | |
+ async addItem(guid, host) { | |
+ const win = this.storageActor.getWindowFromHost(host); | |
+ await Cu.waiveXrays(win).browser.storage.local.set({[guid]: DEFAULT_VALUE}); | |
+ }, | |
+ | |
+ // TODO: Don't use WE APIs directly | |
+ async editItem({host, field, items, oldValue}) { | |
+ const win = this.storageActor.getWindowFromHost(host); | |
+ const {name, value} = items; | |
+ // If the name changed, remove the previous entry in storage by the old name first | |
+ if (field === "name") { | |
+ await Cu.waiveXrays(win).browser.storage.local.remove(oldValue); | |
+ } | |
+ await Cu.waiveXrays(win).browser.storage.local.set({[name]: value}); | |
+ }, | |
+ | |
+ // TODO: Don't use WE APIs directly | |
+ async removeItem(host, name) { | |
+ const win = this.storageActor.getWindowFromHost(host); | |
+ await Cu.waiveXrays(win).browser.storage.local.remove(name); | |
+ }, | |
+ | |
+ // TODO: Don't use WE APIs directly | |
+ async removeAll(host) { | |
+ const win = this.storageActor.getWindowFromHost(host); | |
+ await Cu.waiveXrays(win).browser.storage.local.clear(); | |
+ }, | |
+}); | |
+ | |
StorageActors.createActor({ | |
typeName: "Cache", | |
}, { | |
diff --git a/devtools/shared/specs/storage.js b/devtools/shared/specs/storage.js | |
--- a/devtools/shared/specs/storage.js | |
+++ b/devtools/shared/specs/storage.js | |
@@ -163,6 +163,42 @@ createStorageSpec({ | |
methods: storageMethods, | |
}); | |
+// TODO: create a new storeObjectType, since extension storage is less | |
+// restrictive than localStorage/sessionStorage and more restrictive than | |
+// IndexedDB. | |
+// Extension store object | |
+types.addDictType("extensionobject", { | |
+ name: "nullable:string", | |
+ value: "nullable:longstring", | |
+}); | |
+ | |
+// Array of Extension Storage store objects | |
+types.addDictType("extensionstoreobject", { | |
+ total: "number", | |
+ offset: "number", | |
+ data: "array:nullable:extensionobject", | |
+}); | |
+ | |
+// TODO: Do we want a different spec for each extension storage type? | |
+// (local, managed, sync) | |
+createStorageSpec({ | |
+ typeName: "extensionStorage", | |
+ storeObjectType: "extensionstoreobject", | |
+ methods: { | |
+ // TODO: May want to define storageMethods differently compared to localStorage | |
+ // and sessionStorage...compare to IDB spec as it is more similar. | |
+ ...storageMethods, | |
+ addExtensionStorageListeners: { | |
+ request: {}, | |
+ response: {}, | |
+ }, | |
+ removeExtensionStorageListeners: { | |
+ request: {}, | |
+ response: {}, | |
+ }, | |
+ }, | |
+}); | |
+ | |
types.addDictType("cacheobject", { | |
"url": "string", | |
"status": "string", |
Revision 3/4:
Accesses browser.storage.local
data for the extension after waiving Xrays and puts it into the Storage tab UI for the extension under the extensionStorage
category.
Note: getValuesForHost
returns an Array of arrays where each element array is a 2-tuple: [key, value] pair in the extension’s storage.
E.g. for my test extension, which has a storage object like (see test WE): { catgifs: true, rainbows: unicorns }, it returns [ [ “catgifs”, true], [“rainbows”, “unicorns”] ].
TODO
(for the first milestone of getting browser.storage.local
working in the DevTools Storage panel)
- Update the view so that it refreshes and updates automatically when the extension changes the stored data
- Use a different
storeObject
spec (possibly create a new one?) than "storagestoreobject", since unlikewindow.localStorage
andwindow.sessionStorage
, extension storage values do not have to be strings. - To get the storage object, use the implementation of
browser.storage.get
instead of the API itself - Get the
extension
object directly from the parent process instead of viaExtensionProcessScript.getExtensionChild()
Revision 5
- Added an override for the
initialize
anddestroy
methods of the storage actor to add/remove storage listeners, respectively. TheonStorageChange
method now fires any time extension storage is changed. This still relies on using WE APIs directly. - Added
addItem
,editItem
,removeItem
andremoveAll
methods which are called when the user modifies the storage object in the Storage tab. ExcepteditItem
, none of these methods are correctly updating the view, though they are correctly updating the extension's storage object. These also still rely on using WE APIs directly.
TODO
Same as above.
Revision 6
Now displays an object literal as a value in the Storage tab for the extension storage.
- Add a new
storageObjectType
called"extensionstoreobject"
based off of the IndexedDBstorageObjectType
. - Update the
storageActor.toStoreObject
method to map more closely to IndexedDB's method of the same name, which stringifies the object and turns it into a long string in the same way.
To help facilitate this, I extended a test storage web extension I'd already made to add the same data to IndexedDB as to extension local storage (browser.storage.local
), and I compared the output in the Storage tab until they looked the same (or similar).
Revision 7
Progress on updating the view on storage changes
addItem
,removeItem
andremoveAll
now correctly update the view by callingthis.storageActor.update
.
TODO
- Fix:
editItem
is failing to update the "Data" column in the Storage tab. - Make sure view is updated when the extension itself updates storage.
Revision 8/9
View is updating correctly! (AFAICT)
...For any changes to extension local storage either by the extension or by the developer in the addon Storage inspector UI. Thanks to mratcliffe for his help debugging the editItem
method and helping me to update the onStorageChange
method (was previously a stub).
Revision 10
- Adds TODOs as comments for all known tasks.
- Add some basic localization information just to be able to add TODOs in that file.
- Get initial extension storage data from IndexedDB backend (only changes the part of the storage actor that is reading the stored data,
populateStoresForHost
) by:- checking in the extension's sharedData (extension data shared across the processes) if the IDB backend is enabled
- retrieving from the extension's sharedData the extension's storage principal
- then use the ExtensionStorageIDB.open (https://searchfox.org/mozilla-central/rev/8ff2cd0a27e3764d9540abdc5a66b2fb1e4e9644/toolkit/components/extensions/ExtensionStorageIDB.jsm#648) method to retrieve an instance of the ExtensionStorageLocalIDB class for that extension (https://searchfox.org/mozilla-central/rev/8ff2cd0a27e3764d9540abdc5a66b2fb1e4e9644/toolkit/components/extensions/ExtensionStorageIDB.jsm#142)
Notes:
- With my test WE which immediately puts a couple items into
browser.storage.local
from a background script, when running withweb-ext run
(and pointing to my local Nightly build with my patch), the extension must be reloaded once before opening the storage inspector to ensure the migration to the IndexedDB backend (which happens lazily) has occurred. - The IDB backend is used by default in Nightly and Beta without needing to set the pref
extensions.webextensions.ExtensionStorageIDB.enabled
totrue
.
Revision 11
- Now correctly displays the test WE's storage items in the Storage Inspector without having to reload the extension by triggering a migration if one has not yet occurred. Does this without relying on message passing between the parent and child processes (i.e. does not need
this.conn.setupInParent
or similar)!
Notes:
- Just like all previous patches, none of these work for the case that the extension does not have a background page, as we are still relying on
window
objects (e.g.Cu.waiveXrays(window).browser.storage...
) in places. Hopefully this will change with my next revision.
Revision 1/2:
Creates a storage spec and actor based on mratcliffe's WIP for a similar bug and referencing the DevTools docs heavily.
There is effectively a base "class" of Storage Actor with default methods which can be overwritten/extended by an override object(example), which is the second argument passed into
StorageActors.createActor
. There are other methods that are called and expected to be present on the class that do not exist on the default class likegetFields
.So far, I am able to get the extension's background page
window
object (via theextension
object) from this new storage actor'spopulateStoresForHost
method, but I have not been able to access any data from the extension's storage yet.