/* 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"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/AsyncShutdown.jsm"); /* globals OS ExtensionStorage */ var Path = OS.Path; var profileDir = OS.Constants.Path.profileDir; this.ExtensionStorage = { cache: new Map(), listeners: new Map(), extensionDir: Path.join(profileDir, "browser-extension-data"), getExtensionDir(extensionId) { return Path.join(this.extensionDir, extensionId); }, getStorageFile(extensionId) { return 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(() => { 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: 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) { return this.read(extensionId).then(extData => { let changes = {}; for (let prop in items) { changes[prop] = {oldValue: extData[prop], newValue: items[prop]}; extData[prop] = items[prop]; } this.notifyListeners(extensionId, changes); return this.write(extensionId); }); }, remove(extensionId, items) { return this.read(extensionId).then(extData => { let changes = {}; if (Array.isArray(items)) { for (let prop of items) { changes[prop] = {oldValue: extData[prop]}; delete extData[prop]; } } else { let prop = 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 = {}; if (extData) { 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 if (typeof(keys) == "string") { let prop = keys; if (prop in extData) { result[prop] = extData[prop]; } } else { for (let prop of 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() { 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(); } }, }; ExtensionStorage.init();