summaryrefslogtreecommitdiffstats
path: root/toolkit/components/xulstore
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/xulstore')
-rw-r--r--toolkit/components/xulstore/XULStore.js336
-rw-r--r--toolkit/components/xulstore/XULStore.manifest2
-rw-r--r--toolkit/components/xulstore/moz.build19
-rw-r--r--toolkit/components/xulstore/nsIXULStore.idl75
-rw-r--r--toolkit/components/xulstore/tests/chrome/.eslintrc.js7
-rw-r--r--toolkit/components/xulstore/tests/chrome/animals.rdf142
-rw-r--r--toolkit/components/xulstore/tests/chrome/chrome.ini6
-rw-r--r--toolkit/components/xulstore/tests/chrome/test_persistence.xul31
-rw-r--r--toolkit/components/xulstore/tests/chrome/window_persistence.xul98
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/localstore.rdf31
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore.js199
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/xpcshell.ini6
13 files changed, 959 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]);
diff --git a/toolkit/components/xulstore/XULStore.manifest b/toolkit/components/xulstore/XULStore.manifest
new file mode 100644
index 000000000..249f0c447
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.manifest
@@ -0,0 +1,2 @@
+component {6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea} XULStore.js
+contract @mozilla.org/xul/xulstore;1 {6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}
diff --git a/toolkit/components/xulstore/moz.build b/toolkit/components/xulstore/moz.build
new file mode 100644
index 000000000..30559fccb
--- /dev/null
+++ b/toolkit/components/xulstore/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsIXULStore.idl',
+]
+
+XPIDL_MODULE = 'toolkit_xulstore'
+
+EXTRA_COMPONENTS += [
+ 'XULStore.js',
+ 'XULStore.manifest',
+]
diff --git a/toolkit/components/xulstore/nsIXULStore.idl b/toolkit/components/xulstore/nsIXULStore.idl
new file mode 100644
index 000000000..57050b31b
--- /dev/null
+++ b/toolkit/components/xulstore/nsIXULStore.idl
@@ -0,0 +1,75 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIStringEnumerator;
+
+/**
+ * The XUL store is used to store information related to a XUL document/application.
+ * Typically it is used to store the persisted state for the document, such as
+ * window location, toolbars that are open and nodes that are open and closed in a tree.
+ *
+ * The data is serialized to [profile directory]/xulstore.json
+ */
+[scriptable, uuid(987c4b35-c426-4dd7-ad49-3c9fa4c65d20)]
+
+interface nsIXULStore: nsISupports
+{
+ /**
+ * Sets a value in the store.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to store
+ * @param value - value of the attribute
+ */
+ void setValue(in AString doc, in AString id, in AString attr, in AString value);
+
+ /**
+ * Returns true if the store contains a value for attr.
+ *
+ * @param doc - URI of the document
+ * @param id - identifier of the node
+ * @param attr - attribute
+ */
+ bool hasValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Retrieves a value in the store, or an empty string if it does not exist.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to retrieve
+ *
+ * @returns the value of the attribute
+ */
+ AString getValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Removes a value in the store.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to remove
+ */
+ void removeValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Iterates over all of the ids associated with a given document uri that
+ * have stored data.
+ *
+ * @param doc - document URI
+ */
+ nsIStringEnumerator getIDsEnumerator(in AString doc);
+
+ /**
+ * Iterates over all of the attributes associated with a given document uri
+ * and id that have stored data.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ */
+ nsIStringEnumerator getAttributeEnumerator(in AString doc, in AString id);
+};
diff --git a/toolkit/components/xulstore/tests/chrome/.eslintrc.js b/toolkit/components/xulstore/tests/chrome/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/xulstore/tests/chrome/animals.rdf b/toolkit/components/xulstore/tests/chrome/animals.rdf
new file mode 100644
index 000000000..c7319e641
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/animals.rdf
@@ -0,0 +1,142 @@
+<?xml version="1.0"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:ANIMALS="http://www.some-fictitious-zoo.com/rdf#">
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/arachnids">
+ <ANIMALS:name>Arachnids</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/arachnids/tarantula">
+ <ANIMALS:name>Tarantula</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/birds">
+ <ANIMALS:name>Birds</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/birds/emu">
+ <ANIMALS:name>Emu</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/birds/barnowl">
+ <ANIMALS:name>Barn Owl</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/birds/raven">
+ <ANIMALS:name>Raven</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/crustaceans">
+ <ANIMALS:name>Crustaceans</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/fish">
+ <ANIMALS:name>Fish</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/fish/cod">
+ <ANIMALS:name>Cod</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/fish/swordfish">
+ <ANIMALS:name>Swordfish</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/mammals">
+ <ANIMALS:name>Mammals</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/lion">
+ <ANIMALS:name>Lion</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/hippopotamus">
+ <ANIMALS:name>HIPPOPOTAMUS</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/africanelephant">
+ <ANIMALS:name>African Elephant</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/llama">
+ <ANIMALS:name>LLAMA</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/polarbear">
+ <ANIMALS:name>Polar Bear</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/aardvark">
+ <ANIMALS:name>aardvark</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/ninebandedarmadillo">
+ <ANIMALS:name>Nine-banded Armadillo</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/gorilla">
+ <ANIMALS:name>Gorilla</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/reptiles">
+ <ANIMALS:name>Reptiles</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/reptiles/anaconda">
+ <ANIMALS:name>Anaconda</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/reptiles/chameleon">
+ <ANIMALS:name>Chameleon</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/some-animals" ANIMALS:name="Zoo Animals">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/arachnids"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds"/>
+ </RDF:Seq>
+
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/all-animals" ANIMALS:name="Zoo Animals">
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/arachnids">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/arachnids/tarantula"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/birds">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds/emu"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds/barnowl"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds/raven"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/crustaceans"/>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/fish">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/fish/cod"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/fish/swordfish"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/mammals">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/lion"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/hippopotamus"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/africanelephant"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/llama"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/polarbear"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/aardvark"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/ninebandedarmadillo"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/gorilla"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/reptiles">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/reptiles/anaconda"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/reptiles/chameleon"/>
+ </RDF:Seq>
+ </RDF:li>
+ </RDF:Seq>
+
+</RDF:RDF>
diff --git a/toolkit/components/xulstore/tests/chrome/chrome.ini b/toolkit/components/xulstore/tests/chrome/chrome.ini
new file mode 100644
index 000000000..91efd5455
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/chrome.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+ window_persistence.xul
+ animals.rdf
+
+[test_persistence.xul]
diff --git a/toolkit/components/xulstore/tests/chrome/test_persistence.xul b/toolkit/components/xulstore/tests/chrome/test_persistence.xul
new file mode 100644
index 000000000..736a067ed
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/test_persistence.xul
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Persistence Tests"
+ onload="runTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function runTest() {
+ window.openDialog("window_persistence.xul", "_blank", "chrome", true);
+ }
+
+ function windowOpened() {
+ window.openDialog("window_persistence.xul", "_blank", "chrome", false);
+ }
+
+ function testDone() {
+ SimpleTest.finish();
+ }
+ </script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"/>
+</body>
+
+</window>
diff --git a/toolkit/components/xulstore/tests/chrome/window_persistence.xul b/toolkit/components/xulstore/tests/chrome/window_persistence.xul
new file mode 100644
index 000000000..4d76fe11d
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/window_persistence.xul
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Persistence Tests"
+ onload="opened()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ persist="screenX screenY width height">
+
+<button id="button1" label="Button1" persist="value"/>
+<button id="button2" label="Button2" value="Normal" persist="value"/>
+
+<tree id="tree" datasources="animals.rdf" ref="http://www.some-fictitious-zoo.com/all-animals"
+ flags="dont-build-content" width="200" height="200">
+ <treecols orient="horizontal" id="treecols">
+ <treecol id="treecol" primary="true" label="Name" flex="1"/>
+ </treecols>
+ <template id="template">
+ <treechildren>
+ <treeitem uri="rdf:*">
+ <treerow>
+ <treecell label="rdf:http://www.some-fictitious-zoo.com/rdf#name"/>
+ <treecell/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </template>
+</tree>
+
+<script>
+<![CDATA[
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+let XULStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+let URI = "chrome://mochitests/content/chrome/toolkit/components/xulstore/tests/chrome/window_persistence.xul";
+
+function opened()
+{
+ // If the data in the tree has not been loaded yet, wait a bit and try again.
+ var treeView = document.getElementById("tree").view;
+ if (treeView.rowCount != 6 && treeView.rowCount != 17) {
+ setTimeout(opened, 50);
+ return;
+ }
+
+ runTest(treeView);
+}
+
+function runTest(treeView)
+{
+ var firstRun = window.arguments[0];
+ if (firstRun) {
+ document.getElementById("button1").setAttribute("value", "Pressed");
+ document.getElementById("button2").removeAttribute("value");
+
+ document.getElementById("button2").setAttribute("foo", "bar");
+ document.persist("button2", "foo");
+ is(XULStore.getValue(URI, "button2", "foo"), "bar", "attribute persisted")
+ document.getElementById("button2").removeAttribute("foo");
+ document.persist("button2", "foo");
+ is(XULStore.hasValue(URI, "button2", "foo"), false, "attribute removed")
+
+ is(treeView.rowCount, 6, "tree rows are closed");
+ treeView.toggleOpenState(1);
+ treeView.toggleOpenState(7);
+
+ window.close();
+ window.opener.windowOpened();
+ }
+ else {
+ is(document.getElementById("button1").getAttribute("value"), "Pressed",
+ "Attribute set");
+ is(document.getElementById("button2").hasAttribute("value"), true,
+ "Attribute cleared");
+ is(document.getElementById("button2").getAttribute("value"), "",
+ "Attribute cleared");
+ is(document.getElementById("button2").hasAttribute("foo"), false,
+ "Attribute cleared");
+ is(document.getElementById("button2").getAttribute("foo"), "",
+ "Attribute cleared");
+
+ is(treeView.rowCount, 17, "tree rows are open");
+ is(treeView.isContainerOpen(0), false, "first closed row");
+ is(treeView.isContainerOpen(1), true, "first open row");
+ is(treeView.isContainerOpen(7), true, "second open row");
+
+ window.close();
+ window.opener.testDone();
+ }
+}
+
+function is(l, r, n) { window.opener.wrappedJSObject.SimpleTest.is(l,r,n); }
+
+]]></script>
+
+</window>
diff --git a/toolkit/components/xulstore/tests/xpcshell/.eslintrc.js b/toolkit/components/xulstore/tests/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/xulstore/tests/xpcshell/localstore.rdf b/toolkit/components/xulstore/tests/xpcshell/localstore.rdf
new file mode 100644
index 000000000..458eb50ea
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/localstore.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul#sidebar-title"
+ value="" />
+ <RDF:Description RDF:about="about:config#prefCol"
+ ordinal="1"
+ sortDirection="ascending" />
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul#addon-bar"
+ collapsed="true" />
+ <RDF:Description RDF:about="about:config">
+ <NC:persist RDF:resource="about:config#prefCol"/>
+ <NC:persist RDF:resource="about:config#lockCol"/>
+ <NC:persist RDF:resource="about:config#typeCol"/>
+ <NC:persist RDF:resource="about:config#valueCol"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="about:config#lockCol"
+ ordinal="3" />
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul">
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#main-window"/>
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#addon-bar"/>
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#sidebar-box"/>
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#sidebar-title"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul#main-window"
+ width="994"
+ height="768"
+ screenX="4"
+ screenY="22"
+ sizemode="normal" />
+</RDF:RDF>
diff --git a/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js b/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js
new file mode 100644
index 000000000..c3c96654b
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/◦
+*/
+
+"use strict"
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/osfile.jsm")
+
+var XULStore = null;
+var browserURI = "chrome://browser/content/browser.xul";
+var aboutURI = "about:config";
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+function checkValue(uri, id, attr, reference) {
+ let value = XULStore.getValue(uri, id, attr);
+ do_check_true(value === reference);
+}
+
+function checkValueExists(uri, id, attr, exists) {
+ do_check_eq(XULStore.hasValue(uri, id, attr), exists);
+}
+
+function getIDs(uri) {
+ let it = XULStore.getIDsEnumerator(uri);
+ let result = [];
+
+ while (it.hasMore()) {
+ let value = it.getNext();
+ result.push(value);
+ }
+
+ result.sort();
+ return result;
+}
+
+function getAttributes(uri, id) {
+ let it = XULStore.getAttributeEnumerator(uri, id);
+
+ let result = [];
+
+ while (it.hasMore()) {
+ let value = it.getNext();
+ result.push(value);
+ }
+
+ result.sort();
+ return result;
+}
+
+function checkArrays(a, b) {
+ a.sort();
+ b.sort();
+ do_check_true(a.toString() == b.toString());
+}
+
+function checkOldStore() {
+ checkArrays(['addon-bar', 'main-window', 'sidebar-title'], getIDs(browserURI));
+ checkArrays(['collapsed'], getAttributes(browserURI, 'addon-bar'));
+ checkArrays(['height', 'screenX', 'screenY', 'sizemode', 'width'],
+ getAttributes(browserURI, 'main-window'));
+ checkArrays(['value'], getAttributes(browserURI, 'sidebar-title'));
+
+ checkValue(browserURI, "addon-bar", "collapsed", "true");
+ checkValue(browserURI, "main-window", "width", "994");
+ checkValue(browserURI, "main-window", "height", "768");
+ checkValue(browserURI, "main-window", "screenX", "4");
+ checkValue(browserURI, "main-window", "screenY", "22");
+ checkValue(browserURI, "main-window", "sizemode", "normal");
+ checkValue(browserURI, "sidebar-title", "value", "");
+
+ checkArrays(['lockCol', 'prefCol'], getIDs(aboutURI));
+ checkArrays(['ordinal'], getAttributes(aboutURI, 'lockCol'));
+ checkArrays(['ordinal', 'sortDirection'], getAttributes(aboutURI, 'prefCol'));
+
+ checkValue(aboutURI, "prefCol", "ordinal", "1");
+ checkValue(aboutURI, "prefCol", "sortDirection", "ascending");
+ checkValue(aboutURI, "lockCol", "ordinal", "3");
+}
+
+add_task(function* testImport() {
+ let src = "localstore.rdf";
+ let dst = OS.Path.join(OS.Constants.Path.profileDir, src);
+
+ yield OS.File.copy(src, dst);
+
+ // Importing relies on XULStore not yet being loaded before this point.
+ XULStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+ checkOldStore();
+});
+
+add_task(function* testTruncation() {
+ let dos = Array(8192).join("~");
+ // Long id names should trigger an exception
+ Assert.throws(() => XULStore.setValue(browserURI, dos, "foo", "foo"), /NS_ERROR_ILLEGAL_VALUE/);
+
+ // Long attr names should trigger an exception
+ Assert.throws(() => XULStore.setValue(browserURI, "foo", dos, "foo"), /NS_ERROR_ILLEGAL_VALUE/);
+
+ // Long values should be truncated
+ XULStore.setValue(browserURI, "dos", "dos", dos);
+ dos =XULStore.getValue(browserURI, "dos", "dos");
+ do_check_true(dos.length == 4096)
+ XULStore.removeValue(browserURI, "dos", "dos")
+});
+
+add_task(function* testGetValue() {
+ // Get non-existing property
+ checkValue(browserURI, "side-window", "height", "");
+
+ // Get existing property
+ checkValue(browserURI, "main-window", "width", "994");
+});
+
+add_task(function* testHasValue() {
+ // Check non-existing property
+ checkValueExists(browserURI, "side-window", "height", false);
+
+ // Check existing property
+ checkValueExists(browserURI, "main-window", "width", true);
+});
+
+add_task(function* testSetValue() {
+ // Set new attribute
+ checkValue(browserURI, "side-bar", "width", "");
+ XULStore.setValue(browserURI, "side-bar", "width", "1000");
+ checkValue(browserURI, "side-bar", "width", "1000");
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["width"], getAttributes(browserURI, 'side-bar'));
+
+ // Modify existing property
+ checkValue(browserURI, "side-bar", "width", "1000");
+ XULStore.setValue(browserURI, "side-bar", "width", "1024");
+ checkValue(browserURI, "side-bar", "width", "1024");
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["width"], getAttributes(browserURI, 'side-bar'));
+
+ // Add another attribute
+ checkValue(browserURI, "side-bar", "height", "");
+ XULStore.setValue(browserURI, "side-bar", "height", "1000");
+ checkValue(browserURI, "side-bar", "height", "1000");
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["width", "height"], getAttributes(browserURI, 'side-bar'));
+});
+
+add_task(function* testRemoveValue() {
+ // Remove first attribute
+ checkValue(browserURI, "side-bar", "width", "1024");
+ XULStore.removeValue(browserURI, "side-bar", "width");
+ checkValue(browserURI, "side-bar", "width", "");
+ checkValueExists(browserURI, "side-bar", "width", false);
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["height"], getAttributes(browserURI, 'side-bar'));
+
+ // Remove second attribute
+ checkValue(browserURI, "side-bar", "height", "1000");
+ XULStore.removeValue(browserURI, "side-bar", "height");
+ checkValue(browserURI, "side-bar", "height", "");
+ checkArrays(["addon-bar", "main-window", "sidebar-title"], getIDs(browserURI));
+
+ // Removing an attribute that doesn't exists shouldn't fail
+ XULStore.removeValue(browserURI, "main-window", "bar");
+
+ // Removing from an id that doesn't exists shouldn't fail
+ XULStore.removeValue(browserURI, "foo", "bar");
+
+ // Removing from a document that doesn't exists shouldn't fail
+ let nonDocURI = "chrome://example/content/other.xul";
+ XULStore.removeValue(nonDocURI, "foo", "bar");
+
+ // Remove all attributes in browserURI
+ XULStore.removeValue(browserURI, "addon-bar", "collapsed");
+ checkArrays([], getAttributes(browserURI, "addon-bar"));
+ XULStore.removeValue(browserURI, "main-window", "width");
+ XULStore.removeValue(browserURI, "main-window", "height");
+ XULStore.removeValue(browserURI, "main-window", "screenX");
+ XULStore.removeValue(browserURI, "main-window", "screenY");
+ XULStore.removeValue(browserURI, "main-window", "sizemode");
+ checkArrays([], getAttributes(browserURI, "main-window"));
+ XULStore.removeValue(browserURI, "sidebar-title", "value");
+ checkArrays([], getAttributes(browserURI, "sidebar-title"));
+ checkArrays([], getIDs(browserURI));
+
+ // Remove all attributes in aboutURI
+ XULStore.removeValue(aboutURI, "prefCol", "ordinal");
+ XULStore.removeValue(aboutURI, "prefCol", "sortDirection");
+ checkArrays([], getAttributes(aboutURI, "prefCol"));
+ XULStore.removeValue(aboutURI, "lockCol", "ordinal");
+ checkArrays([], getAttributes(aboutURI, "lockCol"));
+ checkArrays([], getIDs(aboutURI));
+});
diff --git a/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..d483dae02
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+skip-if = toolkit == 'android'
+support-files =
+ localstore.rdf
+
+[test_XULStore.js]