summaryrefslogtreecommitdiffstats
path: root/toolkit/components/contextualidentity
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/contextualidentity')
-rw-r--r--toolkit/components/contextualidentity/ContextualIdentityService.jsm344
-rw-r--r--toolkit/components/contextualidentity/moz.build11
-rw-r--r--toolkit/components/contextualidentity/tests/unit/test_basic.js67
-rw-r--r--toolkit/components/contextualidentity/tests/unit/xpcshell.ini3
4 files changed, 425 insertions, 0 deletions
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.jsm b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
new file mode 100644
index 000000000..6aae3673d
--- /dev/null
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
@@ -0,0 +1,344 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["ContextualIdentityService"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const DEFAULT_TAB_COLOR = "#909090";
+const SAVE_DELAY_MS = 1500;
+
+XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
+ return Services.strings.createBundle("chrome://browser/locale/browser.properties");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
+ return new TextDecoder();
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
+ return new TextEncoder();
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+function _ContextualIdentityService(path) {
+ this.init(path);
+}
+
+_ContextualIdentityService.prototype = {
+ _defaultIdentities: [
+ { userContextId: 1,
+ public: true,
+ icon: "fingerprint",
+ color: "blue",
+ l10nID: "userContextPersonal.label",
+ accessKey: "userContextPersonal.accesskey",
+ telemetryId: 1,
+ },
+ { userContextId: 2,
+ public: true,
+ icon: "briefcase",
+ color: "orange",
+ l10nID: "userContextWork.label",
+ accessKey: "userContextWork.accesskey",
+ telemetryId: 2,
+ },
+ { userContextId: 3,
+ public: true,
+ icon: "dollar",
+ color: "green",
+ l10nID: "userContextBanking.label",
+ accessKey: "userContextBanking.accesskey",
+ telemetryId: 3,
+ },
+ { userContextId: 4,
+ public: true,
+ icon: "cart",
+ color: "pink",
+ l10nID: "userContextShopping.label",
+ accessKey: "userContextShopping.accesskey",
+ telemetryId: 4,
+ },
+ { userContextId: 5,
+ public: false,
+ icon: "",
+ color: "",
+ name: "userContextIdInternal.thumbnail",
+ accessKey: "" },
+ ],
+
+ _identities: null,
+ _openedIdentities: new Set(),
+ _lastUserContextId: 0,
+
+ _path: null,
+ _dataReady: false,
+
+ _saver: null,
+
+ init(path) {
+ this._path = path;
+ this._saver = new DeferredTask(() => this.save(), SAVE_DELAY_MS);
+ AsyncShutdown.profileBeforeChange.addBlocker("ContextualIdentityService: writing data",
+ () => this._saver.finalize());
+
+ this.load();
+ },
+
+ load() {
+ OS.File.read(this._path).then(bytes => {
+ // If synchronous loading happened in the meantime, exit now.
+ if (this._dataReady) {
+ return;
+ }
+
+ try {
+ let data = JSON.parse(gTextDecoder.decode(bytes));
+ if (data.version == 1) {
+ this.resetDefault();
+ }
+ if (data.version != 2) {
+ dump("ERROR - ContextualIdentityService - Unknown version found in " + this._path + "\n");
+ this.loadError(null);
+ return;
+ }
+
+ this._identities = data.identities;
+ this._lastUserContextId = data.lastUserContextId;
+
+ this._dataReady = true;
+ } catch (error) {
+ this.loadError(error);
+ }
+ }, (error) => {
+ this.loadError(error);
+ });
+ },
+
+ resetDefault() {
+ this._identities = this._defaultIdentities;
+ this._lastUserContextId = this._defaultIdentities.length;
+
+ this._dataReady = true;
+
+ this.saveSoon();
+ },
+
+ loadError(error) {
+ if (error != null &&
+ !(error instanceof OS.File.Error && error.becauseNoSuchFile) &&
+ !(error instanceof Components.Exception &&
+ error.result == Cr.NS_ERROR_FILE_NOT_FOUND)) {
+ // Let's report the error.
+ Cu.reportError(error);
+ }
+
+ // If synchronous loading happened in the meantime, exit now.
+ if (this._dataReady) {
+ return;
+ }
+
+ this.resetDefault();
+ },
+
+ saveSoon() {
+ this._saver.arm();
+ },
+
+ save() {
+ let object = {
+ version: 2,
+ lastUserContextId: this._lastUserContextId,
+ identities: this._identities
+ };
+
+ let bytes = gTextEncoder.encode(JSON.stringify(object));
+ return OS.File.writeAtomic(this._path, bytes,
+ { tmpPath: this._path + ".tmp" });
+ },
+
+ create(name, icon, color) {
+ let identity = {
+ userContextId: ++this._lastUserContextId,
+ public: true,
+ icon,
+ color,
+ name
+ };
+
+ this._identities.push(identity);
+ this.saveSoon();
+
+ return Cu.cloneInto(identity, {});
+ },
+
+ update(userContextId, name, icon, color) {
+ let identity = this._identities.find(identity => identity.userContextId == userContextId &&
+ identity.public);
+ if (identity && name) {
+ identity.name = name;
+ identity.color = color;
+ identity.icon = icon;
+ delete identity.l10nID;
+ delete identity.accessKey;
+ this.saveSoon();
+ }
+
+ return !!identity;
+ },
+
+ remove(userContextId) {
+ let index = this._identities.findIndex(i => i.userContextId == userContextId && i.public);
+ if (index == -1) {
+ return false;
+ }
+
+ Services.obs.notifyObservers(null, "clear-origin-attributes-data",
+ JSON.stringify({ userContextId }));
+
+ this._identities.splice(index, 1);
+ this._openedIdentities.delete(userContextId);
+ this.saveSoon();
+
+ return true;
+ },
+
+ ensureDataReady() {
+ if (this._dataReady) {
+ return;
+ }
+
+ try {
+ // This reads the file and automatically detects the UTF-8 encoding.
+ let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ inputStream.init(new FileUtils.File(this._path),
+ FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ try {
+ let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+ let data = json.decodeFromStream(inputStream,
+ inputStream.available());
+ this._identities = data.identities;
+ this._lastUserContextId = data.lastUserContextId;
+
+ this._dataReady = true;
+ } finally {
+ inputStream.close();
+ }
+ } catch (error) {
+ this.loadError(error);
+ return;
+ }
+ },
+
+ getIdentities() {
+ this.ensureDataReady();
+ return Cu.cloneInto(this._identities.filter(info => info.public), {});
+ },
+
+ getPrivateIdentity(name) {
+ this.ensureDataReady();
+ return Cu.cloneInto(this._identities.find(info => !info.public && info.name == name), {});
+ },
+
+ getIdentityFromId(userContextId) {
+ this.ensureDataReady();
+ return Cu.cloneInto(this._identities.find(info => info.userContextId == userContextId &&
+ info.public), {});
+ },
+
+ getUserContextLabel(userContextId) {
+ let identity = this.getIdentityFromId(userContextId);
+ if (!identity || !identity.public) {
+ return "";
+ }
+
+ // We cannot localize the user-created identity names.
+ if (identity.name) {
+ return identity.name;
+ }
+
+ return gBrowserBundle.GetStringFromName(identity.l10nID);
+ },
+
+ setTabStyle(tab) {
+ if (!tab.hasAttribute("usercontextid")) {
+ return;
+ }
+
+ let userContextId = tab.getAttribute("usercontextid");
+ let identity = this.getIdentityFromId(userContextId);
+ tab.setAttribute("data-identity-color", identity ? identity.color : "");
+ },
+
+ countContainerTabs() {
+ let count = 0;
+ this._forEachContainerTab(function() { ++count; });
+ return count;
+ },
+
+ closeAllContainerTabs() {
+ this._forEachContainerTab(function(tab, tabbrowser) {
+ tabbrowser.removeTab(tab);
+ });
+ },
+
+ _forEachContainerTab(callback) {
+ let windowList = Services.wm.getEnumerator("navigator:browser");
+ while (windowList.hasMoreElements()) {
+ let win = windowList.getNext();
+
+ if (win.closed || !win.gBrowser) {
+ continue;
+ }
+
+ let tabbrowser = win.gBrowser;
+ for (let i = tabbrowser.tabContainer.childNodes.length - 1; i >= 0; --i) {
+ let tab = tabbrowser.tabContainer.childNodes[i];
+ if (tab.hasAttribute("usercontextid")) {
+ callback(tab, tabbrowser);
+ }
+ }
+ }
+ },
+
+ telemetry(userContextId) {
+ let identity = this.getIdentityFromId(userContextId);
+
+ // Let's ignore unknown identities for now.
+ if (!identity || !identity.public) {
+ return;
+ }
+
+ if (!this._openedIdentities.has(userContextId)) {
+ this._openedIdentities.add(userContextId);
+ Services.telemetry.getHistogramById("UNIQUE_CONTAINERS_OPENED").add(1);
+ }
+
+ Services.telemetry.getHistogramById("TOTAL_CONTAINERS_OPENED").add(1);
+
+ if (identity.telemetryId) {
+ Services.telemetry.getHistogramById("CONTAINER_USED")
+ .add(identity.telemetryId);
+ }
+ },
+
+ createNewInstanceForTesting(path) {
+ return new _ContextualIdentityService(path);
+ },
+};
+
+let path = OS.Path.join(OS.Constants.Path.profileDir, "containers.json");
+this.ContextualIdentityService = new _ContextualIdentityService(path);
diff --git a/toolkit/components/contextualidentity/moz.build b/toolkit/components/contextualidentity/moz.build
new file mode 100644
index 000000000..9188421f9
--- /dev/null
+++ b/toolkit/components/contextualidentity/moz.build
@@ -0,0 +1,11 @@
+# -*- 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/.
+
+EXTRA_JS_MODULES += [
+ 'ContextualIdentityService.jsm',
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
diff --git a/toolkit/components/contextualidentity/tests/unit/test_basic.js b/toolkit/components/contextualidentity/tests/unit/test_basic.js
new file mode 100644
index 000000000..4d17b9a26
--- /dev/null
+++ b/toolkit/components/contextualidentity/tests/unit/test_basic.js
@@ -0,0 +1,67 @@
+"use strict";
+
+do_get_profile();
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ContextualIdentityService.jsm");
+
+const TEST_STORE_FILE_NAME = "test-containers.json";
+
+let cis;
+
+// Basic tests
+add_task(function() {
+ ok(!!ContextualIdentityService, "ContextualIdentityService exists");
+
+ cis = ContextualIdentityService.createNewInstanceForTesting(TEST_STORE_FILE_NAME);
+ ok(!!cis, "We have our instance of ContextualIdentityService");
+
+ equal(cis.getIdentities().length, 4, "By default, 4 containers.");
+ equal(cis.getIdentityFromId(0), null, "No identity with id 0");
+
+ ok(!!cis.getIdentityFromId(1), "Identity 1 exists");
+ ok(!!cis.getIdentityFromId(2), "Identity 2 exists");
+ ok(!!cis.getIdentityFromId(3), "Identity 3 exists");
+ ok(!!cis.getIdentityFromId(4), "Identity 4 exists");
+});
+
+// Create a new identity
+add_task(function() {
+ equal(cis.getIdentities().length, 4, "By default, 4 containers.");
+
+ let identity = cis.create("New Container", "Icon", "Color");
+ ok(!!identity, "New container created");
+ equal(identity.name, "New Container", "Name matches");
+ equal(identity.icon, "Icon", "Icon matches");
+ equal(identity.color, "Color", "Color matches");
+
+ equal(cis.getIdentities().length, 5, "Expected 5 containers.");
+
+ ok(!!cis.getIdentityFromId(identity.userContextId), "Identity exists");
+ equal(cis.getIdentityFromId(identity.userContextId).name, "New Container", "Identity name is OK");
+ equal(cis.getIdentityFromId(identity.userContextId).icon, "Icon", "Identity icon is OK");
+ equal(cis.getIdentityFromId(identity.userContextId).color, "Color", "Identity color is OK");
+ equal(cis.getUserContextLabel(identity.userContextId), "New Container", "Identity label is OK");
+
+ // Remove an identity
+ equal(cis.remove(-1), false, "cis.remove() returns false if identity doesn't exist.");
+ equal(cis.remove(1), true, "cis.remove() returns true if identity exists.");
+
+ equal(cis.getIdentities().length, 4, "Expected 4 containers.");
+});
+
+// Update an identity
+add_task(function() {
+ ok(!!cis.getIdentityFromId(2), "Identity 2 exists");
+
+ equal(cis.update(-1, "Container", "Icon", "Color"), false, "Update returns false if the identity doesn't exist");
+
+ equal(cis.update(2, "Container", "Icon", "Color"), true, "Update returns true if everything is OK");
+
+ ok(!!cis.getIdentityFromId(2), "Identity exists");
+ equal(cis.getIdentityFromId(2).name, "Container", "Identity name is OK");
+ equal(cis.getIdentityFromId(2).icon, "Icon", "Identity icon is OK");
+ equal(cis.getIdentityFromId(2).color, "Color", "Identity color is OK");
+ equal(cis.getUserContextLabel(2), "Container", "Identity label is OK");
+});
diff --git a/toolkit/components/contextualidentity/tests/unit/xpcshell.ini b/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..b45ff2c30
--- /dev/null
+++ b/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[test_basic.js]