diff options
Diffstat (limited to 'toolkit/components/xulstore/XULStore.js')
-rw-r--r-- | toolkit/components/xulstore/XULStore.js | 336 |
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]); |