summaryrefslogtreecommitdiffstats
path: root/toolkit/components/xulstore/XULStore.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/xulstore/XULStore.js')
-rw-r--r--toolkit/components/xulstore/XULStore.js336
1 files changed, 336 insertions, 0 deletions
diff --git a/toolkit/components/xulstore/XULStore.js b/toolkit/components/xulstore/XULStore.js
new file mode 100644
index 000000000..c2721327c
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.js
@@ -0,0 +1,336 @@
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+// Enables logging and shorter save intervals.
+const debugMode = false;
+
+// Delay when a change is made to when the file is saved.
+// 30 seconds normally, or 3 seconds for testing
+const WRITE_DELAY_MS = (debugMode ? 3 : 30) * 1000;
+
+const XULSTORE_CONTRACTID = "@mozilla.org/xul/xulstore;1";
+const XULSTORE_CID = Components.ID("{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}");
+const STOREDB_FILENAME = "xulstore.json";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+function XULStore() {
+ if (!Services.appinfo.inSafeMode)
+ this.load();
+}
+
+XULStore.prototype = {
+ classID: XULSTORE_CID,
+ classInfo: XPCOMUtils.generateCI({classID: XULSTORE_CID,
+ contractID: XULSTORE_CONTRACTID,
+ classDescription: "XULStore",
+ interfaces: [Ci.nsIXULStore]}),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIXULStore,
+ Ci.nsISupportsWeakReference]),
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(XULStore),
+
+ /* ---------- private members ---------- */
+
+ /*
+ * The format of _data is _data[docuri][elementid][attribute]. For example:
+ * {
+ * "chrome://blah/foo.xul" : {
+ * "main-window" : { aaa : 1, bbb : "c" },
+ * "barColumn" : { ddd : 9, eee : "f" },
+ * },
+ *
+ * "chrome://foopy/b.xul" : { ... },
+ * ...
+ * }
+ */
+ _data: {},
+ _storeFile: null,
+ _needsSaving: false,
+ _saveAllowed: true,
+ _writeTimer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
+
+ load: function () {
+ Services.obs.addObserver(this, "profile-before-change", true);
+
+ this._storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ this._storeFile.append(STOREDB_FILENAME);
+
+ if (!this._storeFile.exists()) {
+ this.import();
+ } else {
+ this.readFile();
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ this.writeFile();
+ if (topic == "profile-before-change") {
+ this._saveAllowed = false;
+ }
+ },
+
+ /*
+ * Internal function for logging debug messages to the Error Console window
+ */
+ log: function (message) {
+ if (!debugMode)
+ return;
+ dump("XULStore: " + message + "\n");
+ Services.console.logStringMessage("XULStore: " + message);
+ },
+
+ import: function() {
+ let localStoreFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+
+ localStoreFile.append("localstore.rdf");
+ if (!localStoreFile.exists()) {
+ return;
+ }
+
+ const RDF = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
+ const persistKey = RDF.GetResource("http://home.netscape.com/NC-rdf#persist");
+
+ this.log("Import localstore from " + localStoreFile.path);
+
+ let localStoreURI = Services.io.newFileURI(localStoreFile).spec;
+ let localStore = RDF.GetDataSourceBlocking(localStoreURI);
+ let resources = localStore.GetAllResources();
+
+ while (resources.hasMoreElements()) {
+ let resource = resources.getNext().QueryInterface(Ci.nsIRDFResource);
+ let uri;
+
+ try {
+ uri = NetUtil.newURI(resource.ValueUTF8);
+ } catch (ex) {
+ continue; // skip invalid uris
+ }
+
+ // If this has a ref, then this is an attribute reference. Otherwise,
+ // this is a document reference.
+ if (!uri.hasRef)
+ continue;
+
+ // Verify that there the persist key is connected up.
+ let docURI = uri.specIgnoringRef;
+
+ if (!localStore.HasAssertion(RDF.GetResource(docURI), persistKey, resource, true))
+ continue;
+
+ let id = uri.ref;
+ let attrs = localStore.ArcLabelsOut(resource);
+
+ while (attrs.hasMoreElements()) {
+ let attr = attrs.getNext().QueryInterface(Ci.nsIRDFResource);
+ let value = localStore.GetTarget(resource, attr, true);
+
+ if (value instanceof Ci.nsIRDFLiteral) {
+ this.setValue(docURI, id, attr.ValueUTF8, value.Value);
+ }
+ }
+ }
+ },
+
+ readFile: function() {
+ const MODE_RDONLY = 0x01;
+ const FILE_PERMS = 0o600;
+
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+ try {
+ stream.init(this._storeFile, MODE_RDONLY, FILE_PERMS, 0);
+ this._data = json.decodeFromStream(stream, stream.available());
+ } catch (e) {
+ this.log("Error reading JSON: " + e);
+ // Ignore problem, we'll just continue on with an empty dataset.
+ } finally {
+ stream.close();
+ }
+ },
+
+ writeFile: Task.async(function* () {
+ if (!this._needsSaving)
+ return;
+
+ this._needsSaving = false;
+
+ this.log("Writing to xulstore.json");
+
+ try {
+ let data = JSON.stringify(this._data);
+ let encoder = new TextEncoder();
+
+ data = encoder.encode(data);
+ yield OS.File.writeAtomic(this._storeFile.path, data,
+ { tmpPath: this._storeFile.path + ".tmp" });
+ } catch (e) {
+ this.log("Failed to write xulstore.json: " + e);
+ throw e;
+ }
+ }),
+
+ markAsChanged: function() {
+ if (this._needsSaving || !this._storeFile)
+ return;
+
+ // Don't write the file more than once every 30 seconds.
+ this._needsSaving = true;
+ this._writeTimer.init(this, WRITE_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /* ---------- interface implementation ---------- */
+
+ setValue: function (docURI, id, attr, value) {
+ this.log("Saving " + attr + "=" + value + " for id=" + id + ", doc=" + docURI);
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage("XULStore: Changes after profile-before-change are ignored!");
+ return;
+ }
+
+ // bug 319846 -- don't save really long attributes or values.
+ if (id.length > 512 || attr.length > 512) {
+ throw Components.Exception("id or attribute name too long", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ if (value.length > 4096) {
+ Services.console.logStringMessage("XULStore: Warning, truncating long attribute value")
+ value = value.substr(0, 4096);
+ }
+
+ let obj = this._data;
+ if (!(docURI in obj)) {
+ obj[docURI] = {};
+ }
+ obj = obj[docURI];
+ if (!(id in obj)) {
+ obj[id] = {};
+ }
+ obj = obj[id];
+
+ // Don't set the value if it is already set to avoid saving the file.
+ if (attr in obj && obj[attr] == value)
+ return;
+
+ obj[attr] = value; // IE, this._data[docURI][id][attr] = value;
+
+ this.markAsChanged();
+ },
+
+ hasValue: function (docURI, id, attr) {
+ this.log("has store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs) {
+ return attr in attrs;
+ }
+ }
+
+ return false;
+ },
+
+ getValue: function (docURI, id, attr) {
+ this.log("get store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs) {
+ return attrs[attr] || "";
+ }
+ }
+
+ return "";
+ },
+
+ removeValue: function (docURI, id, attr) {
+ this.log("remove store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage("XULStore: Changes after profile-before-change are ignored!");
+ return;
+ }
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs && attr in attrs) {
+ delete attrs[attr];
+
+ if (Object.getOwnPropertyNames(attrs).length == 0) {
+ delete ids[id];
+
+ if (Object.getOwnPropertyNames(ids).length == 0) {
+ delete this._data[docURI];
+ }
+ }
+
+ this.markAsChanged();
+ }
+ }
+ },
+
+ getIDsEnumerator: function (docURI) {
+ this.log("Getting ID enumerator for doc=" + docURI);
+
+ if (!(docURI in this._data))
+ return new nsStringEnumerator([]);
+
+ let result = [];
+ let ids = this._data[docURI];
+ if (ids) {
+ for (let id in this._data[docURI]) {
+ result.push(id);
+ }
+ }
+
+ return new nsStringEnumerator(result);
+ },
+
+ getAttributeEnumerator: function (docURI, id) {
+ this.log("Getting attribute enumerator for id=" + id + ", doc=" + docURI);
+
+ if (!(docURI in this._data) || !(id in this._data[docURI]))
+ return new nsStringEnumerator([]);
+
+ let attrs = [];
+ for (let attr in this._data[docURI][id]) {
+ attrs.push(attr);
+ }
+
+ return new nsStringEnumerator(attrs);
+ }
+};
+
+function nsStringEnumerator(items) {
+ this._items = items;
+}
+
+nsStringEnumerator.prototype = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIStringEnumerator]),
+ _nextIndex : 0,
+ hasMore: function() {
+ return this._nextIndex < this._items.length;
+ },
+ getNext : function() {
+ if (!this.hasMore())
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+ return this._items[this._nextIndex++];
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([XULStore]);