summaryrefslogtreecommitdiffstats
path: root/toolkit/components/xulstore
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/xulstore')
-rw-r--r--toolkit/components/xulstore/XULStore.cpp108
-rw-r--r--toolkit/components/xulstore/XULStore.h56
-rw-r--r--toolkit/components/xulstore/XULStore.sys.mjs329
-rw-r--r--toolkit/components/xulstore/components.conf16
-rw-r--r--toolkit/components/xulstore/moz.build25
-rw-r--r--toolkit/components/xulstore/nsIXULStore.idl94
-rw-r--r--toolkit/components/xulstore/tests/chrome/chrome.ini5
-rw-r--r--toolkit/components/xulstore/tests/chrome/test_persistence.xhtml30
-rw-r--r--toolkit/components/xulstore/tests/chrome/window_persistence.xhtml67
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore.js150
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/xpcshell.ini4
11 files changed, 884 insertions, 0 deletions
diff --git a/toolkit/components/xulstore/XULStore.cpp b/toolkit/components/xulstore/XULStore.cpp
new file mode 100644
index 0000000000..a544746c6e
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.cpp
@@ -0,0 +1,108 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/XULStore.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+
+// The XULStore API is implemented in Rust and exposed to C++ via a set of
+// C functions with the "xulstore_" prefix. We declare them in this anonymous
+// namespace to prevent C++ code outside this file from accessing them,
+// as they are an internal implementation detail, and C++ code should use
+// the mozilla::XULStore::* functions and mozilla::XULStoreIterator class
+// declared in XULStore.h.
+namespace {
+extern "C" {
+void xulstore_new_service(nsIXULStore** result);
+nsresult xulstore_set_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr, const nsAString* value);
+nsresult xulstore_has_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr, bool* has_value);
+nsresult xulstore_get_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr, nsAString* value);
+nsresult xulstore_remove_value(const nsAString* doc, const nsAString* id,
+ const nsAString* attr);
+XULStoreIterator* xulstore_get_ids(const nsAString* doc, nsresult* result);
+XULStoreIterator* xulstore_get_attrs(const nsAString* doc, const nsAString* id,
+ nsresult* result);
+bool xulstore_iter_has_more(const XULStoreIterator*);
+nsresult xulstore_iter_get_next(XULStoreIterator*, nsAString* value);
+void xulstore_iter_free(XULStoreIterator* iterator);
+nsresult xulstore_shutdown();
+}
+
+// A static reference to the nsIXULStore singleton that JS uses to access
+// the store. Retrieved via mozilla::XULStore::GetService().
+static StaticRefPtr<nsIXULStore> sXULStore;
+} // namespace
+
+bool XULStoreIterator::HasMore() const { return xulstore_iter_has_more(this); }
+
+nsresult XULStoreIterator::GetNext(nsAString* item) {
+ return xulstore_iter_get_next(this, item);
+}
+
+void DefaultDelete<XULStoreIterator>::operator()(XULStoreIterator* ptr) const {
+ xulstore_iter_free(ptr);
+}
+
+namespace XULStore {
+already_AddRefed<nsIXULStore> GetService() {
+ nsCOMPtr<nsIXULStore> xulStore;
+
+ if (sXULStore) {
+ xulStore = sXULStore;
+ } else {
+ xulstore_new_service(getter_AddRefs(xulStore));
+ sXULStore = xulStore;
+ mozilla::ClearOnShutdown(&sXULStore);
+ }
+
+ return xulStore.forget();
+}
+
+nsresult SetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, const nsAString& value) {
+ return xulstore_set_value(&doc, &id, &attr, &value);
+}
+nsresult HasValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, bool& has_value) {
+ return xulstore_has_value(&doc, &id, &attr, &has_value);
+}
+nsresult GetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, nsAString& value) {
+ return xulstore_get_value(&doc, &id, &attr, &value);
+}
+nsresult RemoveValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr) {
+ return xulstore_remove_value(&doc, &id, &attr);
+}
+nsresult GetIDs(const nsAString& doc, UniquePtr<XULStoreIterator>& iter) {
+ // We assign the value of the iter here in C++ via a return value
+ // rather than in the Rust function via an out parameter in order
+ // to ensure that any old value is deleted, since the UniquePtr's
+ // assignment operator won't delete the old value if the assignment
+ // happens in Rust.
+ nsresult result;
+ iter.reset(xulstore_get_ids(&doc, &result));
+ return result;
+}
+nsresult GetAttrs(const nsAString& doc, const nsAString& id,
+ UniquePtr<XULStoreIterator>& iter) {
+ // We assign the value of the iter here in C++ via a return value
+ // rather than in the Rust function via an out parameter in order
+ // to ensure that any old value is deleted, since the UniquePtr's
+ // assignment operator won't delete the old value if the assignment
+ // happens in Rust.
+ nsresult result;
+ iter.reset(xulstore_get_attrs(&doc, &id, &result));
+ return result;
+}
+nsresult Shutdown() { return xulstore_shutdown(); }
+
+}; // namespace XULStore
+}; // namespace mozilla
diff --git a/toolkit/components/xulstore/XULStore.h b/toolkit/components/xulstore/XULStore.h
new file mode 100644
index 0000000000..25b69ed12e
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.h
@@ -0,0 +1,56 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file declares the XULStore API for C++ via the mozilla::XULStore
+ * namespace and the mozilla::XULStoreIterator class. It also declares
+ * the mozilla::XULStore::GetService() function that the component manager
+ * uses to instantiate and retrieve the nsIXULStore singleton.
+ */
+
+#ifndef mozilla_XULStore_h
+#define mozilla_XULStore_h
+
+#include "nsIXULStore.h"
+
+namespace mozilla {
+class XULStoreIterator final {
+ public:
+ bool HasMore() const;
+ nsresult GetNext(nsAString* item);
+
+ private:
+ XULStoreIterator() = delete;
+ XULStoreIterator(const XULStoreIterator&) = delete;
+ XULStoreIterator& operator=(const XULStoreIterator&) = delete;
+ ~XULStoreIterator() = delete;
+};
+
+template <>
+class DefaultDelete<XULStoreIterator> {
+ public:
+ void operator()(XULStoreIterator* ptr) const;
+};
+
+namespace XULStore {
+// Instantiates and retrieves the nsIXULStore singleton that JS uses to access
+// the store. C++ code should use the mozilla::XULStore::* functions instead.
+already_AddRefed<nsIXULStore> GetService();
+
+nsresult SetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, const nsAString& value);
+nsresult HasValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, bool& has_value);
+nsresult GetValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr, nsAString& value);
+nsresult RemoveValue(const nsAString& doc, const nsAString& id,
+ const nsAString& attr);
+nsresult GetIDs(const nsAString& doc, UniquePtr<XULStoreIterator>& iter);
+nsresult GetAttrs(const nsAString& doc, const nsAString& id,
+ UniquePtr<XULStoreIterator>& iter);
+nsresult Shutdown();
+}; // namespace XULStore
+}; // namespace mozilla
+
+#endif // mozilla_XULStore_h
diff --git a/toolkit/components/xulstore/XULStore.sys.mjs b/toolkit/components/xulstore/XULStore.sys.mjs
new file mode 100644
index 0000000000..1683358ad5
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.sys.mjs
@@ -0,0 +1,329 @@
+/* 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/. */
+
+// 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_CID = Components.ID("{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}");
+const STOREDB_FILENAME = "xulstore.json";
+
+export function XULStore() {
+ if (!Services.appinfo.inSafeMode) {
+ this.load();
+ }
+}
+
+XULStore.prototype = {
+ classID: XULSTORE_CID,
+ name: "XULStore",
+ QueryInterface: ChromeUtils.generateQI([
+ "nsINamed",
+ "nsIObserver",
+ "nsIXULStore",
+ "nsISupportsWeakReference",
+ ]),
+
+ /* ---------- 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() {
+ Services.obs.addObserver(this, "profile-before-change", true);
+
+ try {
+ this._storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ } catch (ex) {
+ try {
+ this._storeFile = Services.dirsvc.get("ProfDS", Ci.nsIFile);
+ } catch (ex) {
+ throw new Error("Can't find profile directory.");
+ }
+ }
+ this._storeFile.append(STOREDB_FILENAME);
+
+ this.readFile();
+ },
+
+ observe(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(message) {
+ if (!debugMode) {
+ return;
+ }
+ console.log("XULStore: " + message);
+ },
+
+ readFile() {
+ try {
+ this._data = JSON.parse(Cu.readUTF8File(this._storeFile));
+ } catch (e) {
+ this.log("Error reading JSON: " + e);
+ // This exception could mean that the file didn't exist.
+ // We'll just ignore the error and start with a blank slate.
+ }
+ },
+
+ async writeFile() {
+ if (!this._needsSaving) {
+ return;
+ }
+
+ this._needsSaving = false;
+
+ this.log("Writing to xulstore.json");
+
+ try {
+ await IOUtils.writeJSON(this._storeFile.path, this._data, {
+ tmpPath: this._storeFile.path + ".tmp",
+ });
+ } catch (e) {
+ this.log("Failed to write xulstore.json: " + e);
+ throw e;
+ }
+ },
+
+ markAsChanged() {
+ 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 ---------- */
+
+ persist(node, attr) {
+ if (!node.id) {
+ throw new Error("Node without ID passed into persist()");
+ }
+
+ const uri = node.ownerDocument.documentURI;
+ const value = node.getAttribute(attr);
+
+ if (node.localName == "window") {
+ this.log("Persisting attributes to windows is handled by AppWindow.");
+ return;
+ }
+
+ // See Bug 1476680 - we could drop the `hasValue` check so that
+ // any time there's an empty attribute it gets removed from the
+ // store. Since this is copying behavior from document.persist,
+ // callers would need to be updated with that change.
+ if (!value && this.hasValue(uri, node.id, attr)) {
+ this.removeValue(uri, node.id, attr);
+ } else {
+ this.setValue(uri, node.id, attr, value);
+ }
+ },
+
+ setValue(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(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(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(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) {
+ delete ids[id];
+
+ if (!Object.getOwnPropertyNames(ids).length) {
+ delete this._data[docURI];
+ }
+ }
+
+ this.markAsChanged();
+ }
+ }
+ },
+
+ removeDocument(docURI) {
+ this.log("remove store values for doc=" + docURI);
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage(
+ "XULStore: Changes after profile-before-change are ignored!"
+ );
+ return;
+ }
+
+ if (this._data[docURI]) {
+ delete this._data[docURI];
+ this.markAsChanged();
+ }
+ },
+
+ getIDsEnumerator(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(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: ChromeUtils.generateQI(["nsIStringEnumerator"]),
+ _nextIndex: 0,
+ [Symbol.iterator]() {
+ return this._items.values();
+ },
+ hasMore() {
+ return this._nextIndex < this._items.length;
+ },
+ getNext() {
+ if (!this.hasMore()) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ return this._items[this._nextIndex++];
+ },
+};
diff --git a/toolkit/components/xulstore/components.conf b/toolkit/components/xulstore/components.conf
new file mode 100644
index 0000000000..533b5544b5
--- /dev/null
+++ b/toolkit/components/xulstore/components.conf
@@ -0,0 +1,16 @@
+# -*- 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/.
+
+Classes = [
+ {
+ 'js_name': 'xulStore',
+ 'cid': '{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}',
+ 'contract_ids': ['@mozilla.org/xul/xulstore;1'],
+ 'interfaces': ['nsIXULStore'],
+ 'esModule': 'resource://gre/modules/XULStore.sys.mjs',
+ 'constructor': 'XULStore',
+ },
+]
diff --git a/toolkit/components/xulstore/moz.build b/toolkit/components/xulstore/moz.build
new file mode 100644
index 0000000000..eebaef5baf
--- /dev/null
+++ b/toolkit/components/xulstore/moz.build
@@ -0,0 +1,25 @@
+# -*- 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Startup and Profile System")
+
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+
+XPIDL_MODULE = "toolkit_xulstore"
+
+XPIDL_SOURCES += [
+ "nsIXULStore.idl",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXTRA_JS_MODULES += [
+ "XULStore.sys.mjs",
+]
diff --git a/toolkit/components/xulstore/nsIXULStore.idl b/toolkit/components/xulstore/nsIXULStore.idl
new file mode 100644
index 0000000000..832c07dc90
--- /dev/null
+++ b/toolkit/components/xulstore/nsIXULStore.idl
@@ -0,0 +1,94 @@
+/* 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;
+webidl Node;
+
+/**
+ * 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 for a specified node's attribute, except in
+ * the case below:
+ * If the value is empty and if calling `hasValue` with the node's
+ * document and ID and `attr` would return true, then the
+ * value instead gets removed from the store (see Bug 1476680).
+ *
+ * @param node - DOM node
+ * @param attr - attribute to store
+ */
+ void persist(in Node aNode, in AString attr);
+
+ /**
+ * 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);
+
+ /**
+ * Removes all values related to the given document.
+ *
+ * @param doc - document URI
+ */
+ void removeDocument(in AString doc);
+
+ /**
+ * 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/chrome.ini b/toolkit/components/xulstore/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..c8f4ddf073
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ window_persistence.xhtml
+
+[test_persistence.xhtml]
diff --git a/toolkit/components/xulstore/tests/chrome/test_persistence.xhtml b/toolkit/components/xulstore/tests/chrome/test_persistence.xhtml
new file mode 100644
index 0000000000..b3e65fb050
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/test_persistence.xhtml
@@ -0,0 +1,30 @@
+<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function runTest() {
+ window.openDialog("window_persistence.xhtml", "_blank", "chrome,noopener", true, window);
+ }
+
+ function windowOpened() {
+ window.openDialog("window_persistence.xhtml", "_blank", "chrome,noopener", false, window);
+ }
+
+ 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.xhtml b/toolkit/components/xulstore/tests/chrome/window_persistence.xhtml
new file mode 100644
index 0000000000..d474891cfc
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/window_persistence.xhtml
@@ -0,0 +1,67 @@
+<?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"/>
+<button id="button3" label="Button3" value="Normal" persist="hidden" hidden="true"/>
+
+<script>
+<![CDATA[
+
+const XULStore = Services.xulStore;
+let URI = "chrome://mochitests/content/chrome/toolkit/components/xulstore/tests/chrome/window_persistence.xhtml";
+
+function opened()
+{
+ runTest();
+}
+
+function runTest()
+{
+ var firstRun = window.arguments[0];
+ var button1 = document.getElementById("button1");
+ var button2 = document.getElementById("button2");
+ var button3 = document.getElementById("button3");
+ if (firstRun) {
+ button1.setAttribute("value", "Pressed");
+ button2.removeAttribute("value");
+
+ button2.setAttribute("foo", "bar");
+ XULStore.persist(button2, "foo");
+ is(XULStore.getValue(URI, "button2", "foo"), "bar", "attribute persisted");
+ button2.removeAttribute("foo");
+ XULStore.persist(button2, "foo");
+ is(XULStore.hasValue(URI, "button2", "foo"), false, "attribute removed");
+
+ button3.removeAttribute("hidden");
+
+ window.close();
+ window.arguments[1].windowOpened();
+ }
+ else {
+ is(button1.getAttribute("value"), "Pressed",
+ "Attribute set");
+ is(button2.hasAttribute("value"), false,
+ "Attribute cleared");
+ is(button2.hasAttribute("foo"), false,
+ "Attribute cleared");
+
+ is(button3.hasAttribute("hidden"), false,
+ "Attribute cleared");
+
+ window.close();
+ window.arguments[1].testDone();
+ }
+}
+
+function is(l, r, n) { window.arguments[1].SimpleTest.is(l,r,n); }
+
+]]></script>
+
+</window>
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 0000000000..d100592e81
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/◦
+*/
+
+"use strict";
+
+var XULStore = null;
+var browserURI = "chrome://browser/content/browser.xhtml";
+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);
+ Assert.equal(value, reference);
+}
+
+function checkValueExists(uri, id, attr, exists) {
+ Assert.equal(XULStore.hasValue(uri, id, attr), exists);
+}
+
+function getIDs(uri) {
+ return Array.from(XULStore.getIDsEnumerator(uri)).sort();
+}
+
+function getAttributes(uri, id) {
+ return Array.from(XULStore.getAttributeEnumerator(uri, id)).sort();
+}
+
+function checkArrays(a, b) {
+ a.sort();
+ b.sort();
+ Assert.equal(a.toString(), b.toString());
+}
+
+add_task(async function setup() {
+ // Set a value that a future test depends on manually
+ XULStore = Services.xulStore;
+ XULStore.setValue(browserURI, "main-window", "width", "994");
+});
+
+add_task(async 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");
+ Assert.ok(dos.length == 4096);
+ XULStore.removeValue(browserURI, "dos", "dos");
+});
+
+add_task(async function testGetValue() {
+ // Get non-existing property
+ checkValue(browserURI, "side-window", "height", "");
+
+ // Get existing property
+ checkValue(browserURI, "main-window", "width", "994");
+});
+
+add_task(async function testHasValue() {
+ // Check non-existing property
+ checkValueExists(browserURI, "side-window", "height", false);
+
+ // Check existing property
+ checkValueExists(browserURI, "main-window", "width", true);
+});
+
+add_task(async function testSetValue() {
+ // Set new attribute
+ checkValue(browserURI, "side-bar", "width", "");
+ XULStore.setValue(browserURI, "side-bar", "width", "1000");
+ checkValue(browserURI, "side-bar", "width", "1000");
+ checkArrays(["main-window", "side-bar"], 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(["main-window", "side-bar"], 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(["main-window", "side-bar"], getIDs(browserURI));
+ checkArrays(["width", "height"], getAttributes(browserURI, "side-bar"));
+});
+
+add_task(async 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(["main-window", "side-bar"], 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(["main-window"], 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 0000000000..accfb1c95b
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+skip-if = toolkit == 'android'
+
+[test_XULStore.js]