summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--toolkit/modules/ExtensionStorage.jsm241
-rw-r--r--toolkit/modules/moz.build1
2 files changed, 242 insertions, 0 deletions
diff --git a/toolkit/modules/ExtensionStorage.jsm b/toolkit/modules/ExtensionStorage.jsm
new file mode 100644
index 000000000..0b0ffb000
--- /dev/null
+++ b/toolkit/modules/ExtensionStorage.jsm
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ExtensionStorage"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+
+function jsonReplacer(key, value) {
+ switch (typeof(value)) {
+ // Serialize primitive types as-is.
+ case "string":
+ case "number":
+ case "boolean":
+ return value;
+
+ case "object":
+ if (value === null) {
+ return value;
+ }
+
+ switch (Cu.getClassName(value, true)) {
+ // Serialize arrays and ordinary objects as-is.
+ case "Array":
+ case "Object":
+ return value;
+
+ // Serialize Date objects and regular expressions as their
+ // string representations.
+ case "Date":
+ case "RegExp":
+ return String(value);
+ }
+ break;
+ }
+
+ if (!key) {
+ // If this is the root object, and we can't serialize it, serialize
+ // the value to an empty object.
+ return {};
+ }
+
+ // Everything else, omit entirely.
+ return undefined;
+}
+
+this.ExtensionStorage = {
+ cache: new Map(),
+ listeners: new Map(),
+
+ /**
+ * Sanitizes the given value, and returns a JSON-compatible
+ * representation of it, based on the privileges of the given global.
+ *
+ * @param {value} value
+ * The value to sanitize.
+ * @param {Context} context
+ * The extension context in which to sanitize the value
+ * @returns {value}
+ * The sanitized value.
+ */
+ sanitize(value, context) {
+ let json = context.jsonStringify(value, jsonReplacer);
+ return JSON.parse(json);
+ },
+
+ getExtensionDir(extensionId) {
+ return OS.Path.join(this.extensionDir, extensionId);
+ },
+
+ getStorageFile(extensionId) {
+ return OS.Path.join(this.extensionDir, extensionId, "storage.js");
+ },
+
+ read(extensionId) {
+ if (this.cache.has(extensionId)) {
+ return this.cache.get(extensionId);
+ }
+
+ let path = this.getStorageFile(extensionId);
+ let decoder = new TextDecoder();
+ let promise = OS.File.read(path);
+ promise = promise.then(array => {
+ return JSON.parse(decoder.decode(array));
+ }).catch((error) => {
+ if (!error.becauseNoSuchFile) {
+ Cu.reportError("Unable to parse JSON data for extension storage.");
+ }
+ return {};
+ });
+ this.cache.set(extensionId, promise);
+ return promise;
+ },
+
+ write(extensionId) {
+ let promise = this.read(extensionId).then(extData => {
+ let encoder = new TextEncoder();
+ let array = encoder.encode(JSON.stringify(extData));
+ let path = this.getStorageFile(extensionId);
+ OS.File.makeDir(this.getExtensionDir(extensionId), {
+ ignoreExisting: true,
+ from: OS.Constants.Path.profileDir,
+ });
+ let promise = OS.File.writeAtomic(path, array);
+ return promise;
+ }).catch(() => {
+ // Make sure this promise is never rejected.
+ Cu.reportError("Unable to write JSON data for extension storage.");
+ });
+
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ "ExtensionStorage: Finish writing extension data",
+ promise);
+
+ return promise.then(() => {
+ AsyncShutdown.profileBeforeChange.removeBlocker(promise);
+ });
+ },
+
+ set(extensionId, items, context) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop in items) {
+ let item = this.sanitize(items[prop], context);
+ changes[prop] = {oldValue: extData[prop], newValue: item};
+ extData[prop] = item;
+ }
+
+ this.notifyListeners(extensionId, changes);
+
+ return this.write(extensionId);
+ });
+ },
+
+ remove(extensionId, items) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop of [].concat(items)) {
+ changes[prop] = {oldValue: extData[prop]};
+ delete extData[prop];
+ }
+
+ this.notifyListeners(extensionId, changes);
+
+ return this.write(extensionId);
+ });
+ },
+
+ clear(extensionId) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop of Object.keys(extData)) {
+ changes[prop] = {oldValue: extData[prop]};
+ delete extData[prop];
+ }
+
+ this.notifyListeners(extensionId, changes);
+
+ return this.write(extensionId);
+ });
+ },
+
+ get(extensionId, keys) {
+ return this.read(extensionId).then(extData => {
+ let result = {};
+ if (keys === null) {
+ Object.assign(result, extData);
+ } else if (typeof(keys) == "object" && !Array.isArray(keys)) {
+ for (let prop in keys) {
+ if (prop in extData) {
+ result[prop] = extData[prop];
+ } else {
+ result[prop] = keys[prop];
+ }
+ }
+ } else {
+ for (let prop of [].concat(keys)) {
+ if (prop in extData) {
+ result[prop] = extData[prop];
+ }
+ }
+ }
+
+ return result;
+ });
+ },
+
+ addOnChangedListener(extensionId, listener) {
+ let listeners = this.listeners.get(extensionId) || new Set();
+ listeners.add(listener);
+ this.listeners.set(extensionId, listeners);
+ },
+
+ removeOnChangedListener(extensionId, listener) {
+ let listeners = this.listeners.get(extensionId);
+ listeners.delete(listener);
+ },
+
+ notifyListeners(extensionId, changes) {
+ let listeners = this.listeners.get(extensionId);
+ if (listeners) {
+ for (let listener of listeners) {
+ listener(changes);
+ }
+ }
+ },
+
+ init() {
+ if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ return;
+ }
+ Services.obs.addObserver(this, "extension-invalidate-storage-cache", false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "xpcom-shutdown") {
+ Services.obs.removeObserver(this, "extension-invalidate-storage-cache");
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ } else if (topic == "extension-invalidate-storage-cache") {
+ this.cache.clear();
+ }
+ },
+};
+
+XPCOMUtils.defineLazyGetter(ExtensionStorage, "extensionDir",
+ () => OS.Path.join(OS.Constants.Path.profileDir, "browser-extension-data"));
+
+ExtensionStorage.init();
diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build
index 062388d24..0e8a4ee89 100644
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -29,6 +29,7 @@ EXTRA_JS_MODULES += [
'debug.js',
'DeferredTask.jsm',
'Deprecated.jsm',
+ 'ExtensionStorage.jsm',
'FileUtils.jsm',
'Finder.jsm',
'FinderHighlighter.jsm',