diff options
Diffstat (limited to 'toolkit/components/contextualidentity')
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] |