From fe77c891cdf4ed565378d485385d085fb3277c2e Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Mon, 17 Jul 2017 16:31:51 +0100 Subject: [PATCH] Adding back in legacy code without message passing code. --- index.js | 930 ++++++++++++++- index.js.bk | 1060 ------------------ package.json | 2 +- webextension/js/background/identityState.js | 17 - webextension/js/background/index.html | 3 + webextension/js/background/tabPageCounter.js | 1 + webextension/js/popup.js | 12 +- webextension/manifest.json | 2 +- 8 files changed, 941 insertions(+), 1086 deletions(-) delete mode 100644 index.js.bk diff --git a/index.js b/index.js index 84fb0ee..92b4228 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,930 @@ +/* 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 XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +const IDENTITY_COLORS = [ + { name: "blue", color: "#00a7e0" }, + { name: "turquoise", color: "#01bdad" }, + { name: "green", color: "#7dc14c" }, + { name: "yellow", color: "#ffcb00" }, + { name: "orange", color: "#f89c24" }, + { name: "red", color: "#d92215" }, + { name: "pink", color: "#ee5195" }, + { name: "purple", color: "#7a2f7a" }, +]; + +const IDENTITY_ICONS = [ + { name: "fingerprint", image: "chrome://browser/skin/usercontext/personal.svg" }, + { name: "briefcase", image: "chrome://browser/skin/usercontext/work.svg" }, + { name: "dollar", image: "chrome://browser/skin/usercontext/banking.svg" }, + { name: "cart", image: "chrome://browser/skin/usercontext/shopping.svg" }, + // All of these do not exist in gecko + { name: "gift", image: "gift" }, + { name: "vacation", image: "vacation" }, + { name: "food", image: "food" }, + { name: "fruit", image: "fruit" }, + { name: "pet", image: "pet" }, + { name: "tree", image: "tree" }, + { name: "chill", image: "chill" }, + { name: "circle", image: "circle" }, +]; + +const IDENTITY_COLORS_STANDARD = [ + "blue", "orange", "green", "pink", +]; + +const IDENTITY_ICONS_STANDARD = [ + "fingerprint", "briefcase", "dollar", "cart", +]; + +const PREFS = [ + [ "privacy.userContext.enabled", true ], + [ "privacy.userContext.longPressBehavior", 2 ], + [ "privacy.userContext.ui.enabled", false ], + [ "privacy.usercontext.about_newtab_segregation.enabled", true ], +]; + +const { attachTo, detachFrom } = require("sdk/content/mod"); +const { Cu } = require("chrome"); +const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm"); +const Metrics = require("./testpilot-metrics"); +const { modelFor } = require("sdk/model/core"); +const prefService = require("sdk/preferences/service"); +const self = require("sdk/self"); +const { Services } = require("resource://gre/modules/Services.jsm"); +const ss = require("sdk/simple-storage"); +const { study } = require("./study"); +const { Style } = require("sdk/stylesheet/style"); +const tabs = require("sdk/tabs"); +const uuid = require("sdk/util/uuid"); +const { viewFor } = require("sdk/view/core"); const webExtension = require("sdk/webextension"); -webExtension.startup(); +const windows = require("sdk/windows"); +const windowUtils = require("sdk/window/utils"); + +Cu.import("resource:///modules/CustomizableUI.jsm"); +Cu.import("resource:///modules/CustomizableWidgets.jsm"); +Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + + +// ContextualIdentityProxy +const ContextualIdentityProxy = { + getIdentities() { + let response; + if ("getPublicIdentities" in ContextualIdentityService) { + response = ContextualIdentityService.getPublicIdentities(); + } else { + response = ContextualIdentityService.getIdentities(); + } + + return response.map((identity) => { + return this._convert(identity); + }); + }, + + getIdentityFromId(userContextId) { + let response; + if ("getPublicIdentityFromId" in ContextualIdentityService) { + response = ContextualIdentityService.getPublicIdentityFromId(userContextId); + } else { + response = ContextualIdentityService.getIdentityFromId(userContextId); + } + if (response) { + return this._convert(response); + } + return response; + }, + + _convert(identity) { + return { + name: ContextualIdentityService.getUserContextLabel(identity.userContextId), + icon: identity.icon, + color: identity.color, + userContextId: identity.userContextId, + }; + }, +}; + +// ---------------------------------------------------------------------------- +// ContainerService + +const ContainerService = { + _windowMap: new Map(), + _containerWasEnabled: false, + _onBackgroundConnectCallback: null, + + async init(installation, reason) { + // If we are just been installed, we must store some information for the + // uninstallation. This object contains also a version number, in case we + // need to implement a migration in the future. + // In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild + if (!("savedConfiguration" in ss.storage) || + !("prefs" in ss.storage.savedConfiguration) || + (installation && reason !== "upgrade")) { + let preInstalledIdentities = []; // eslint-disable-line prefer-const + ContextualIdentityProxy.getIdentities().forEach(identity => { + preInstalledIdentities.push(identity.userContextId); + }); + + const object = { + version: 1, + prefs: {}, + metricsUUID: uuid.uuid().toString(), + preInstalledIdentities: preInstalledIdentities + }; + + PREFS.forEach(pref => { + object.prefs[pref[0]] = prefService.get(pref[0]); + }); + + ss.storage.savedConfiguration = object; + + if (prefService.get("privacy.userContext.enabled") !== true) { + // Maybe rename the Banking container. + const identity = ContextualIdentityProxy.getIdentityFromId(3); + if (identity && identity.l10nID === "userContextBanking.label") { + ContextualIdentityService.update(identity.userContextId, + "Finance", + identity.icon, + identity.color); + } + + // Let's create the default containers in case there are none. + if (ss.storage.savedConfiguration.preInstalledIdentities.length === 0) { + // Note: we have to create them in this way because there is no way to + // reuse the same ID and the localized strings. + ContextualIdentityService.create("Personal", "fingerprint", "blue"); + ContextualIdentityService.create("Work", "briefcase", "orange"); + ContextualIdentityService.create("Finance", "dollar", "green"); + ContextualIdentityService.create("Shopping", "cart", "pink"); + } + } + } + + // TOCHECK should this run on all code + ContextualIdentityProxy.getIdentities().forEach(identity => { + const newIcon = this._fromIconToName(identity.icon); + const newColor = this._fromColorToName(identity.color); + if (newIcon !== identity.icon || newColor !== identity.color) { + ContextualIdentityService.update(identity.userContextId, + ContextualIdentityService.getUserContextLabel(identity.userContextId), + newIcon, + newColor); + } + }); + + // Let's see if containers were enabled before this addon. + this._containerWasEnabled = + ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"]; + + // Enabling preferences + + PREFS.forEach((pref) => { + prefService.set(pref[0], pref[1]); + }); + + this._metricsUUID = ss.storage.savedConfiguration.metricsUUID; + + // Disabling the customizable container panel. + CustomizableUI.destroyWidget("containers-panelmenu"); + + tabs.on("open", tab => { + this._restyleTab(tab); + }); + + tabs.on("activate", tab => { + this._restyleActiveTab(tab).catch(() => {}); + this._configureActiveWindows(); + }); + + // Modify CSS and other stuff for each window. + + this._configureWindows().catch(() => {}); + + windows.browserWindows.on("open", window => { + this._configureWindow(viewFor(window)).catch(() => {}); + }); + + windows.browserWindows.on("close", window => { + this.closeWindow(viewFor(window)); + }); + + // WebExtension startup + + try { + const api = await webExtension.startup(); + this.registerBackgroundConnection(api); + } catch (e) { + throw new Error("WebExtension startup failed. Unable to continue."); + } + + this._sendEvent = new Metrics({ + type: "sdk", + id: self.id, + version: self.version + }).sendEvent; + + // Begin-Of-Hack + ContextualIdentityService.workaroundForCookieManager = function(method, userContextId) { + let identity = method.call(ContextualIdentityService, userContextId); + if (!identity && userContextId) { + identity = { + userContextId, + icon: "", + color: "", + name: "Pending to be deleted", + public: true, + }; + } + + return identity; + }; + + if (!this._oldGetIdentityFromId) { + this._oldGetIdentityFromId = ContextualIdentityService.getIdentityFromId; + } + ContextualIdentityService.getIdentityFromId = function(userContextId) { + return this.workaroundForCookieManager(ContainerService._oldGetIdentityFromId, userContextId); + }; + + if ("getPublicIdentityFromId" in ContextualIdentityService) { + if (!this._oldGetPublicIdentityFromId) { + this._oldGetPublicIdentityFromId = ContextualIdentityService.getPublicIdentityFromId; + } + ContextualIdentityService.getPublicIdentityFromId = function(userContextId) { + return this.workaroundForCookieManager(ContainerService._oldGetPublicIdentityFromId, userContextId); + }; + } + // End-Of-Hack + + if (self.id === "@shield-study-containers") { + study.startup(reason); + this.shieldStudyVariation = study.variation; + } + }, + + registerBackgroundConnection(api) { + // This is only used for theme notifications and new tab + api.browser.runtime.onConnect.addListener((port) => { + this._onBackgroundConnectCallback = (message, topic) => { + port.postMessage({ + type: topic, + message + }); + }; + }); + }, + + triggerBackgroundCallback(message, topic) { + if (this._onBackgroundConnectCallback) { + this._onBackgroundConnectCallback(message, topic); + } + }, + + // In FF 50-51, the icon is the full path, in 52 and following + // releases, we have IDs to be used with a svg file. In this function + // we map URLs to svg IDs. + + // Helper methods for converting colors to names and names to colors. + + _fromNameToColor(name) { + return this._fromNameOrColor(name, "color"); + }, + + _fromColorToName(color) { + return this._fromNameOrColor(color, "name"); + }, + + _fromNameOrColor(what, attribute) { + for (let color of IDENTITY_COLORS) { // eslint-disable-line prefer-const + if (what === color.color || what === color.name) { + return color[attribute]; + } + } + return ""; + }, + + // Helper methods for converting icons to names and names to icons. + + _fromIconToName(icon) { + return this._fromNameOrIcon(icon, "name", "circle"); + }, + + _fromNameOrIcon(what, attribute, defaultValue) { + for (let icon of IDENTITY_ICONS) { // eslint-disable-line prefer-const + if (what === icon.image || what === icon.name) { + return icon[attribute]; + } + } + return defaultValue; + }, + + // Tab Helpers + + _getUserContextIdFromTab(tab) { + return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10); + }, + + _matchTabsByContainer(userContextId) { + const matchedTabs = []; + for (const tab of tabs) { + if (userContextId === this._getUserContextIdFromTab(tab)) { + matchedTabs.push(tab); + } + } + return matchedTabs; + }, + + async _closeTabs(tabsToClose) { + // We create a new tab only if the current operation closes all the + // existing ones. + if (tabs.length === tabsToClose.length) { + await this.openTab({}); + } + + for (const tab of tabsToClose) { + // after .close() window is null. Let's take it now. + const window = viewFor(tab.window); + + tab.close(); + + // forget about this tab. 0 is the index of the forgotten tab and 0 + // means the last one. + try { + SessionStore.forgetClosedTab(window, 0); + } catch (e) {} // eslint-disable-line no-empty + } + }, + + _recentBrowserWindow() { + const browserWin = windowUtils.getMostRecentBrowserWindow(); + + // This should not really happen. + if (!browserWin || !browserWin.gBrowser) { + return Promise.resolve(null); + } + + return Promise.resolve(browserWin); + }, + + // Tabs management + + openTab(args) { + return this.triggerBackgroundCallback(args, "open-tab"); + }, + + // Identities management + + queryIdentities() { + return new Promise(resolve => { + const identities = ContextualIdentityProxy.getIdentities(); + resolve(identities); + }); + }, + + // Styling the window + + _configureWindows() { + const promises = []; + for (let window of windows.browserWindows) { // eslint-disable-line prefer-const + promises.push(this._configureWindow(viewFor(window))); + } + return Promise.all(promises); + }, + + _configureWindow(window) { + return this._getOrCreateContainerWindow(window).configure(); + }, + + _configureActiveWindows() { + const promises = []; + for (let window of windows.browserWindows) { // eslint-disable-line prefer-const + promises.push(this._configureActiveWindow(viewFor(window))); + } + return Promise.all(promises); + }, + + _configureActiveWindow(window) { + return this._getOrCreateContainerWindow(window).configureActive(); + }, + + closeWindow(window) { + this._windowMap.delete(window); + }, + + _getOrCreateContainerWindow(window) { + if (!(this._windowMap.has(window))) { + this._windowMap.set(window, new ContainerWindow(window)); + } + + return this._windowMap.get(window); + }, + + refreshNeeded() { + return this._configureWindows(); + }, + + _restyleActiveTab(tab) { + if (!tab) { + return Promise.resolve(null); + } + + const userContextId = ContainerService._getUserContextIdFromTab(tab); + const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); + const hbox = viewFor(tab.window).document.getElementById("userContext-icons"); + + if (!identity) { + hbox.setAttribute("data-identity-color", ""); + return Promise.resolve(null); + } + + hbox.setAttribute("data-identity-color", identity.color); + + const label = viewFor(tab.window).document.getElementById("userContext-label"); + label.setAttribute("value", identity.name); + label.style.color = ContainerService._fromNameToColor(identity.color); + + const indicator = viewFor(tab.window).document.getElementById("userContext-indicator"); + indicator.setAttribute("data-identity-icon", identity.icon); + indicator.style.listStyleImage = ""; + + return this._restyleTab(tab); + }, + + _restyleTab(tab) { + if (!tab) { + return Promise.resolve(null); + } + const userContextId = ContainerService._getUserContextIdFromTab(tab); + const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); + if (!identity) { + return Promise.resolve(null); + } + return Promise.resolve(viewFor(tab).setAttribute("data-identity-color", identity.color)); + }, + + // Uninstallation + uninstall(reason) { + const data = ss.storage.savedConfiguration; + if (!data) { + throw new DOMError("ERROR - No saved configuration!!"); + } + + if (data.version !== 1) { + throw new DOMError("ERROR - Unknown version!!"); + } + + if (reason !== "upgrade") { + PREFS.forEach(pref => { + if (pref[0] in data.prefs) { + prefService.set(pref[0], data.prefs[pref[0]]); + } + }); + } + + // Note: We cannot go back renaming the Finance identity back to Banking: + // the locale system doesn't work with renamed containers. + + // Restore the customizable container panel. + const widget = CustomizableWidgets.find(widget => widget.id === "containers-panelmenu"); + if (widget) { + CustomizableUI.createWidget(widget); + } + + for (let window of windows.browserWindows) { // eslint-disable-line prefer-const + // Let's close all the container tabs. + // Note: We cannot use _closeTabs() because at this point tab.window is + // null. + if (!this._containerWasEnabled && reason !== "upgrade") { + for (let tab of window.tabs) { // eslint-disable-line prefer-const + if (this._getUserContextIdFromTab(tab)) { + tab.close(); + try { + SessionStore.forgetClosedTab(viewFor(window), 0); + } catch(e) {} // eslint-disable-line no-empty + } + } + } + + this._getOrCreateContainerWindow(viewFor(window)).shutdown(); + } + + // all the configuration must go away now. + this._windowMap = new Map(); + + if (reason !== "upgrade") { + // Let's forget all the previous closed tabs. + this._forgetIdentity(); + + const preInstalledIdentities = data.preInstalledIdentities; + ContextualIdentityProxy.getIdentities().forEach(identity => { + if (!preInstalledIdentities.includes(identity.userContextId)) { + ContextualIdentityService.remove(identity.userContextId); + } else { + // Let's cleanup all the cookies for this container. + Services.obs.notifyObservers(null, "clear-origin-attributes-data", + JSON.stringify({ userContextId: identity.userContextId })); + } + }); + + // Let's delete the configuration. + delete ss.storage.savedConfiguration; + } + + // Begin-Of-Hack + if (this._oldGetIdentityFromId) { + ContextualIdentityService.getIdentityFromId = this._oldGetIdentityFromId; + } + + if (this._oldGetPublicIdentityFromId) { + ContextualIdentityService.getPublicIdentityFromId = this._oldGetPublicIdentityFromId; + } + // End-Of-Hack + }, + + forgetIdentityAndRefresh(args) { + this._forgetIdentity(args.userContextId); + return this.refreshNeeded(); + }, + + _forgetIdentity(userContextId = 0) { + for (let window of windows.browserWindows) { // eslint-disable-line prefer-const + window = viewFor(window); + const closedTabData = JSON.parse(SessionStore.getClosedTabData(window)); + for (let i = closedTabData.length - 1; i >= 0; --i) { + if (!closedTabData[i].state.userContextId) { + continue; + } + + if (userContextId === 0 || + closedTabData[i].state.userContextId === userContextId) { + try { + SessionStore.forgetClosedTab(window, i); + } catch(e) {} // eslint-disable-line no-empty + } + } + } + }, +}; + +// ---------------------------------------------------------------------------- +// ContainerWindow + +// This object is used to configure a single window. +function ContainerWindow(window) { + this._init(window); +} + +ContainerWindow.prototype = { + _window: null, + _style: null, + _panelElement: null, + _timeoutStore: new Map(), + _elementCache: new Map(), + _tooltipCache: new Map(), + _tabsElement: null, + + _init(window) { + this._window = window; + this._tabsElement = this._window.document.getElementById("tabbrowser-tabs"); + this._style = Style({ uri: self.data.url("usercontext.css") }); + this._plusButton = this._window.document.getAnonymousElementByAttribute(this._tabsElement, "anonid", "tabs-newtab-button"); + this._overflowPlusButton = this._window.document.getElementById("new-tab-button"); + + // Only hack the normal plus button as the alltabs is done elsewhere + this.attachMenuEvent("plus-button", this._plusButton); + + attachTo(this._style, this._window); + }, + + attachMenuEvent(source, button) { + const popup = button.querySelector(".new-tab-popup"); + popup.addEventListener("popupshown", () => { + popup.querySelector("menuseparator").remove(); + const popupMenuItems = [...popup.querySelectorAll("menuitem")]; + popupMenuItems.forEach((item) => { + const userContextId = item.getAttribute("data-usercontextid"); + if (!userContextId) { + item.remove(); + } + item.setAttribute("command", ""); + item.addEventListener("command", (e) => { + e.stopPropagation(); + e.preventDefault(); + ContainerService.openTab({ + userContextId: userContextId, + source: source + }); + }); + }); + }); + }, + + configure() { + return Promise.all([ + this._configureActiveTab(), + this._configureFileMenu(), + this._configureAllTabsMenu(), + this._configureTabStyle(), + this.configureActive(), + ]); + }, + + configureActive() { + return this._configureContextMenu(); + }, + + _configureTabStyle() { + const promises = []; + for (let tab of modelFor(this._window).tabs) { // eslint-disable-line prefer-const + promises.push(ContainerService._restyleTab(tab)); + } + return Promise.all(promises); + }, + + _configureActiveTab() { + const tab = modelFor(this._window).tabs.activeTab; + return ContainerService._restyleActiveTab(tab); + }, + + _configureFileMenu() { + return this._configureMenu("menu_newUserContext", null, e => { + const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); + ContainerService.openTab({ + userContextId: userContextId, + source: "file-menu" + }); + }); + }, + + _configureAllTabsMenu() { + return this._configureMenu("alltabs_containersTab", null, e => { + const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); + ContainerService.showTabs({ + userContextId, + nofocus: true, + window: this._window, + }).then(() => { + return ContainerService.openTab({ + userContextId, + source: "alltabs-menu" + }); + }).catch(() => {}); + }); + }, + + _configureContextMenu() { + return Promise.all([ + this._configureMenu("context-openlinkinusercontext-menu", + () => { + // This userContextId is what we want to exclude. + const tab = modelFor(this._window).tabs.activeTab; + return ContainerService._getUserContextIdFromTab(tab); + }, + e => { + // This is a super internal method. Hopefully it will be stable in the + // next FF releases. + this._window.gContextMenu.openLinkInTab(e); + + const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); + ContainerService.showTabs({ + userContextId, + nofocus: true, + window: this._window, + }); + } + ), + this._configureContextMenuOpenLink(), + ]); + }, + + _configureContextMenuOpenLink() { + return new Promise(resolve => { + const self = this; + this._window.gSetUserContextIdAndClick = function(event) { + const tab = modelFor(self._window).tabs.activeTab; + const userContextId = ContainerService._getUserContextIdFromTab(tab); + event.target.setAttribute("data-usercontextid", userContextId); + self._window.gContextMenu.openLinkInTab(event); + }; + + let item = this._window.document.getElementById("context-openlinkincontainertab"); + item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)"); + + item = this._window.document.getElementById("context-openlinkintab"); + item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)"); + + resolve(); + }); + }, + + // Generic menu configuration. + _configureMenu(menuId, excludedContainerCb, clickCb) { + const menu = this._window.document.getElementById(menuId); + if (!this._disableElement(menu)) { + // Delete stale menu that isn't native elements + while (menu.firstChild) { + menu.removeChild(menu.firstChild); + } + } + + const menupopup = this._window.document.createElementNS(XUL_NS, "menupopup"); + menu.appendChild(menupopup); + + menupopup.addEventListener("command", clickCb); + return this._createMenu(menupopup, excludedContainerCb); + }, + + _createMenu(target, excludedContainerCb) { + while (target.hasChildNodes()) { + target.removeChild(target.firstChild); + } + + return new Promise((resolve, reject) => { + ContainerService.queryIdentities().then(identities => { + const fragment = this._window.document.createDocumentFragment(); + + const excludedUserContextId = excludedContainerCb ? excludedContainerCb() : 0; + if (excludedUserContextId) { + const bundle = this._window.document.getElementById("bundle_browser"); + + const menuitem = this._window.document.createElementNS(XUL_NS, "menuitem"); + menuitem.setAttribute("data-usercontextid", "0"); + menuitem.setAttribute("label", bundle.getString("userContextNone.label")); + menuitem.setAttribute("accesskey", bundle.getString("userContextNone.accesskey")); + + fragment.appendChild(menuitem); + + const menuseparator = this._window.document.createElementNS(XUL_NS, "menuseparator"); + fragment.appendChild(menuseparator); + } + + identities.forEach(identity => { + if (identity.userContextId === excludedUserContextId) { + return; + } + + const menuitem = this._window.document.createElementNS(XUL_NS, "menuitem"); + menuitem.setAttribute("label", identity.name); + menuitem.classList.add("menuitem-iconic"); + menuitem.setAttribute("data-usercontextid", identity.userContextId); + menuitem.setAttribute("data-identity-color", identity.color); + menuitem.setAttribute("data-identity-icon", identity.icon); + fragment.appendChild(menuitem); + }); + + target.appendChild(fragment); + resolve(); + }).catch(() => {reject();}); + }); + }, + + // This timer is used to hide the panel auto-magically if it's not used in + // the following X seconds. This is need to avoid the leaking of the panel + // when the mouse goes out of of the 'plus' button. + _createTimeout(key, callback, timeoutTime) { + this._cleanTimeout(key); + this._timeoutStore.set(key, this._window.setTimeout(() => { + callback(); + this._timeoutStore.delete(key); + }, timeoutTime)); + }, + + _cleanAllTimeouts() { + for (let key of this._timeoutStore.keys()) { // eslint-disable-line prefer-const + this._cleanTimeout(key); + } + }, + + _cleanTimeout(key) { + if (this._timeoutStore.has(key)) { + this._window.clearTimeout(this._timeoutStore.get(key)); + this._timeoutStore.delete(key); + } + }, + + shutdown() { + // CSS must be removed. + detachFrom(this._style, this._window); + + this._shutdownFileMenu(); + this._shutdownAllTabsMenu(); + this._shutdownContextMenu(); + + this._shutdownContainers(); + }, + + _shutDownPlusButtonMenuElement(buttonElement) { + if (buttonElement) { + this._shutdownElement(buttonElement); + buttonElement.setAttribute("tooltip", this._tooltipCache.get(buttonElement)); + + buttonElement.removeEventListener("mouseover", this); + buttonElement.removeEventListener("click", this); + buttonElement.removeEventListener("mouseout", this); + } + }, + + _shutdownFileMenu() { + this._shutdownMenu("menu_newUserContext"); + }, + + _shutdownAllTabsMenu() { + this._shutdownMenu("alltabs_containersTab"); + }, + + _shutdownContextMenu() { + this._shutdownMenu("context-openlinkinusercontext-menu"); + }, + + _shutdownMenu(menuId) { + const menu = this._window.document.getElementById(menuId); + this._shutdownElement(menu); + }, + + _shutdownElement(element) { + // Let's remove our elements. + while (element.firstChild) { + element.firstChild.remove(); + } + + const elementCache = this._elementCache.get(element); + if (elementCache) { + for (let e of elementCache) { // eslint-disable-line prefer-const + element.appendChild(e); + } + } + }, + + _disableElement(element) { + // Nothing to disable. + if (!element || this._elementCache.has(element)) { + return false; + } + const cacheArray = []; + + // Let's store the previous elements so that we can repopulate it in case + // the addon is uninstalled. + while (element.firstChild) { + cacheArray.push(element.removeChild(element.firstChild)); + } + + this._elementCache.set(element, cacheArray); + + return true; + }, + + _shutdownContainers() { + ContextualIdentityProxy.getIdentities().forEach(identity => { + if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) !== -1 && + IDENTITY_COLORS_STANDARD.indexOf(identity.color) !== -1) { + return; + } + + if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) === -1) { + if (identity.userContextId <= IDENTITY_ICONS_STANDARD.length) { + identity.icon = IDENTITY_ICONS_STANDARD[identity.userContextId - 1]; + } else { + identity.icon = IDENTITY_ICONS_STANDARD[0]; + } + } + + if (IDENTITY_COLORS_STANDARD.indexOf(identity.color) === -1) { + if (identity.userContextId <= IDENTITY_COLORS_STANDARD.length) { + identity.color = IDENTITY_COLORS_STANDARD[identity.userContextId - 1]; + } else { + identity.color = IDENTITY_COLORS_STANDARD[0]; + } + } + + ContextualIdentityService.update(identity.userContextId, + identity.name, + identity.icon, + identity.color); + }); + } +}; + +// uninstall/install events --------------------------------------------------- + +exports.main = function (options) { + const installation = options.loadReason === "install" || + options.loadReason === "downgrade" || + options.loadReason === "enable" || + options.loadReason === "upgrade"; + + // Let's start :) + ContainerService.init(installation, options.loadReason); +}; + +exports.onUnload = function (reason) { + if (reason === "disable" || + reason === "downgrade" || + reason === "uninstall" || + reason === "upgrade") { + ContainerService.uninstall(reason); + } +}; diff --git a/index.js.bk b/index.js.bk deleted file mode 100644 index f38c596..0000000 --- a/index.js.bk +++ /dev/null @@ -1,1060 +0,0 @@ -/* 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 XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - -const INCOMPATIBLE_ADDON_IDS = [ - "pulse@mozilla.com", - "snoozetabs@mozilla.com", - "jid1-NeEaf3sAHdKHPA@jetpack" // PageShot -]; - -const IDENTITY_COLORS = [ - { name: "blue", color: "#00a7e0" }, - { name: "turquoise", color: "#01bdad" }, - { name: "green", color: "#7dc14c" }, - { name: "yellow", color: "#ffcb00" }, - { name: "orange", color: "#f89c24" }, - { name: "red", color: "#d92215" }, - { name: "pink", color: "#ee5195" }, - { name: "purple", color: "#7a2f7a" }, -]; - -const IDENTITY_ICONS = [ - { name: "fingerprint", image: "chrome://browser/skin/usercontext/personal.svg" }, - { name: "briefcase", image: "chrome://browser/skin/usercontext/work.svg" }, - { name: "dollar", image: "chrome://browser/skin/usercontext/banking.svg" }, - { name: "cart", image: "chrome://browser/skin/usercontext/shopping.svg" }, - // All of these do not exist in gecko - { name: "gift", image: "gift" }, - { name: "vacation", image: "vacation" }, - { name: "food", image: "food" }, - { name: "fruit", image: "fruit" }, - { name: "pet", image: "pet" }, - { name: "tree", image: "tree" }, - { name: "chill", image: "chill" }, - { name: "circle", image: "circle" }, -]; - -const IDENTITY_COLORS_STANDARD = [ - "blue", "orange", "green", "pink", -]; - -const IDENTITY_ICONS_STANDARD = [ - "fingerprint", "briefcase", "dollar", "cart", -]; - -const PREFS = [ - [ "privacy.userContext.enabled", true ], - [ "privacy.userContext.longPressBehavior", 2 ], - [ "privacy.userContext.ui.enabled", false ], - [ "privacy.usercontext.about_newtab_segregation.enabled", true ], -]; - -const { AddonManager } = require("resource://gre/modules/AddonManager.jsm"); -const { attachTo, detachFrom } = require("sdk/content/mod"); -const { Cu } = require("chrome"); -const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm"); -const { LightweightThemeManager } = Cu.import("resource://gre/modules/LightweightThemeManager.jsm", {}); -const Metrics = require("./testpilot-metrics"); -const { modelFor } = require("sdk/model/core"); -const prefService = require("sdk/preferences/service"); -const self = require("sdk/self"); -const { Services } = require("resource://gre/modules/Services.jsm"); -const ss = require("sdk/simple-storage"); -const { study } = require("./study"); -const { Style } = require("sdk/stylesheet/style"); -const tabs = require("sdk/tabs"); -const uuid = require("sdk/util/uuid"); -const { viewFor } = require("sdk/view/core"); -const webExtension = require("sdk/webextension"); -const windows = require("sdk/windows"); -const windowUtils = require("sdk/window/utils"); - -Cu.import("resource:///modules/CustomizableUI.jsm"); -Cu.import("resource:///modules/CustomizableWidgets.jsm"); -Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - - -// ContextualIdentityProxy -const ContextualIdentityProxy = { - getIdentities() { - let response; - if ("getPublicIdentities" in ContextualIdentityService) { - response = ContextualIdentityService.getPublicIdentities(); - } else { - response = ContextualIdentityService.getIdentities(); - } - - return response.map((identity) => { - return this._convert(identity); - }); - }, - - getIdentityFromId(userContextId) { - let response; - if ("getPublicIdentityFromId" in ContextualIdentityService) { - response = ContextualIdentityService.getPublicIdentityFromId(userContextId); - } else { - response = ContextualIdentityService.getIdentityFromId(userContextId); - } - if (response) { - return this._convert(response); - } - return response; - }, - - _convert(identity) { - return { - name: ContextualIdentityService.getUserContextLabel(identity.userContextId), - icon: identity.icon, - color: identity.color, - userContextId: identity.userContextId, - }; - }, -}; - -// ---------------------------------------------------------------------------- -// ContainerService - -const ContainerService = { - _windowMap: new Map(), - _containerWasEnabled: false, - _onBackgroundConnectCallback: null, - - async init(installation, reason) { - // If we are just been installed, we must store some information for the - // uninstallation. This object contains also a version number, in case we - // need to implement a migration in the future. - // In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild - if (!("savedConfiguration" in ss.storage) || - !("prefs" in ss.storage.savedConfiguration) || - (installation && reason !== "upgrade")) { - let preInstalledIdentities = []; // eslint-disable-line prefer-const - ContextualIdentityProxy.getIdentities().forEach(identity => { - preInstalledIdentities.push(identity.userContextId); - }); - - const object = { - version: 1, - prefs: {}, - metricsUUID: uuid.uuid().toString(), - preInstalledIdentities: preInstalledIdentities - }; - - PREFS.forEach(pref => { - object.prefs[pref[0]] = prefService.get(pref[0]); - }); - - ss.storage.savedConfiguration = object; - - if (prefService.get("privacy.userContext.enabled") !== true) { - // Maybe rename the Banking container. - const identity = ContextualIdentityProxy.getIdentityFromId(3); - if (identity && identity.l10nID === "userContextBanking.label") { - ContextualIdentityService.update(identity.userContextId, - "Finance", - identity.icon, - identity.color); - } - - // Let's create the default containers in case there are none. - if (ss.storage.savedConfiguration.preInstalledIdentities.length === 0) { - // Note: we have to create them in this way because there is no way to - // reuse the same ID and the localized strings. - ContextualIdentityService.create("Personal", "fingerprint", "blue"); - ContextualIdentityService.create("Work", "briefcase", "orange"); - ContextualIdentityService.create("Finance", "dollar", "green"); - ContextualIdentityService.create("Shopping", "cart", "pink"); - } - } - } - - // TOCHECK should this run on all code - ContextualIdentityProxy.getIdentities().forEach(identity => { - const newIcon = this._fromIconToName(identity.icon); - const newColor = this._fromColorToName(identity.color); - if (newIcon !== identity.icon || newColor !== identity.color) { - ContextualIdentityService.update(identity.userContextId, - ContextualIdentityService.getUserContextLabel(identity.userContextId), - newIcon, - newColor); - } - }); - - // Let's see if containers were enabled before this addon. - this._containerWasEnabled = - ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"]; - - // Enabling preferences - - PREFS.forEach((pref) => { - prefService.set(pref[0], pref[1]); - }); - - this._metricsUUID = ss.storage.savedConfiguration.metricsUUID; - - // Disabling the customizable container panel. - CustomizableUI.destroyWidget("containers-panelmenu"); - - // Message routing - - // only these methods are allowed. We have a 1:1 mapping between messages - // and methods. These methods must return a promise. - const methods = [ - "showTabs", - "sortTabs", - "getIdentity", - "getPreference", - "sendTelemetryPayload", - "getTheme", - "getShieldStudyVariation", - "refreshNeeded", - "forgetIdentityAndRefresh", - "checkIncompatibleAddons" - ]; - - // Map of identities. - ContextualIdentityProxy.getIdentities().forEach(identity => { - this._remapTabsIfMissing(identity.userContextId); - }); - - // Let's restore the hidden tabs from the previous session. - if (prefService.get("browser.startup.page") === 3 && - "identitiesData" in ss.storage) { - // TODO move Restore State? - /* - ContextualIdentityProxy.getIdentities().forEach(identity => { - if (identity.userContextId in ss.storage.identitiesData && - "hiddenTabs" in ss.storage.identitiesData[identity.userContextId]) { - this._identitiesState[identity.userContextId].hiddenTabs = - ss.storage.identitiesData[identity.userContextId].hiddenTabs; - } - }); -*/ - } - - tabs.on("open", tab => { - this._restyleTab(tab); - this._remapTab(tab); - }); - - tabs.on("close", tab => { - this._remapTab(tab); - }); - - tabs.on("activate", tab => { - this._restyleActiveTab(tab).catch(() => {}); - this._configureActiveWindows(); - this._remapTab(tab); - }); - - // Modify CSS and other stuff for each window. - - this._configureWindows().catch(() => {}); - - windows.browserWindows.on("open", window => { - this._configureWindow(viewFor(window)).catch(() => {}); - }); - - windows.browserWindows.on("close", window => { - this.closeWindow(viewFor(window)); - }); - - // WebExtension startup - - try { - const api = await webExtension.startup(); - api.browser.runtime.onMessage.addListener((message, sender, sendReply) => { - if ("method" in message && methods.indexOf(message.method) !== -1) { - sendReply(this[message.method](message)); - } - }); - - this.registerBackgroundConnection(api); - } catch (e) { - throw new Error("WebExtension startup failed. Unable to continue."); - } - - this._sendEvent = new Metrics({ - type: "sdk", - id: self.id, - version: self.version - }).sendEvent; - - // Begin-Of-Hack - ContextualIdentityService.workaroundForCookieManager = function(method, userContextId) { - let identity = method.call(ContextualIdentityService, userContextId); - if (!identity && userContextId) { - identity = { - userContextId, - icon: "", - color: "", - name: "Pending to be deleted", - public: true, - }; - } - - return identity; - }; - - if (!this._oldGetIdentityFromId) { - this._oldGetIdentityFromId = ContextualIdentityService.getIdentityFromId; - } - ContextualIdentityService.getIdentityFromId = function(userContextId) { - return this.workaroundForCookieManager(ContainerService._oldGetIdentityFromId, userContextId); - }; - - if ("getPublicIdentityFromId" in ContextualIdentityService) { - if (!this._oldGetPublicIdentityFromId) { - this._oldGetPublicIdentityFromId = ContextualIdentityService.getPublicIdentityFromId; - } - ContextualIdentityService.getPublicIdentityFromId = function(userContextId) { - return this.workaroundForCookieManager(ContainerService._oldGetPublicIdentityFromId, userContextId); - }; - } - // End-Of-Hack - - Services.obs.addObserver(this, "lightweight-theme-changed", false); - - if (self.id === "@shield-study-containers") { - study.startup(reason); - this.shieldStudyVariation = study.variation; - } - }, - - registerBackgroundConnection(api) { - // This is only used for theme notifications and new tab - api.browser.runtime.onConnect.addListener((port) => { - this._onBackgroundConnectCallback = (message, topic) => { - port.postMessage({ - type: topic, - message - }); - }; - }); - }, - - triggerBackgroundCallback(message, topic) { - if (this._onBackgroundConnectCallback) { - this._onBackgroundConnectCallback(message, topic); - } - }, - - async observe(subject, topic) { - if (topic === "lightweight-theme-changed") { - try { - const theme = await this.getTheme(); - this.triggerBackgroundCallback(theme, topic); - } catch (e) { - throw new Error("Unable to get theme"); - } - } - }, - - getTheme() { - const defaultTheme = "firefox-compact-light@mozilla.org"; - return new Promise(function (resolve) { - let theme = defaultTheme; - if (LightweightThemeManager.currentTheme && LightweightThemeManager.currentTheme.id) { - theme = LightweightThemeManager.currentTheme.id; - } - resolve(theme); - }); - }, - - getShieldStudyVariation() { - return this.shieldStudyVariation; - }, - - // In FF 50-51, the icon is the full path, in 52 and following - // releases, we have IDs to be used with a svg file. In this function - // we map URLs to svg IDs. - - // Helper methods for converting colors to names and names to colors. - - _fromNameToColor(name) { - return this._fromNameOrColor(name, "color"); - }, - - _fromColorToName(color) { - return this._fromNameOrColor(color, "name"); - }, - - _fromNameOrColor(what, attribute) { - for (let color of IDENTITY_COLORS) { // eslint-disable-line prefer-const - if (what === color.color || what === color.name) { - return color[attribute]; - } - } - return ""; - }, - - // Helper methods for converting icons to names and names to icons. - - _fromIconToName(icon) { - return this._fromNameOrIcon(icon, "name", "circle"); - }, - - _fromNameOrIcon(what, attribute, defaultValue) { - for (let icon of IDENTITY_ICONS) { // eslint-disable-line prefer-const - if (what === icon.image || what === icon.name) { - return icon[attribute]; - } - } - return defaultValue; - }, - - // Tab Helpers - - _getUserContextIdFromTab(tab) { - return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10); - }, - - _matchTabsByContainer(userContextId) { - const matchedTabs = []; - for (const tab of tabs) { - if (userContextId === this._getUserContextIdFromTab(tab)) { - matchedTabs.push(tab); - } - } - return matchedTabs; - }, - - async _closeTabs(tabsToClose) { - // We create a new tab only if the current operation closes all the - // existing ones. - if (tabs.length === tabsToClose.length) { - await this.openTab({}); - } - - for (const tab of tabsToClose) { - // after .close() window is null. Let's take it now. - const window = viewFor(tab.window); - - tab.close(); - - // forget about this tab. 0 is the index of the forgotten tab and 0 - // means the last one. - try { - SessionStore.forgetClosedTab(window, 0); - } catch (e) {} // eslint-disable-line no-empty - } - }, - - _recentBrowserWindow() { - const browserWin = windowUtils.getMostRecentBrowserWindow(); - - // This should not really happen. - if (!browserWin || !browserWin.gBrowser) { - return Promise.resolve(null); - } - - return Promise.resolve(browserWin); - }, - - - sendTelemetryPayload(args = {}) { - // when pings come from popup, delete "method" prop - delete args.method; - let payload = { // eslint-disable-line prefer-const - "uuid": this._metricsUUID - }; - Object.assign(payload, args); - - this._sendEvent(payload); - }, - - checkIncompatibleAddons() { - return new Promise(resolve => { - AddonManager.getAddonsByIDs(INCOMPATIBLE_ADDON_IDS, (addons) => { - addons = addons.filter((a) => a && a.isActive); - const incompatibleAddons = addons.length !== 0; - if (incompatibleAddons) { - this.sendTelemetryPayload({ - "event": "incompatible-addons-detected" - }); - } - resolve(incompatibleAddons); - }); - }); - }, - - // Tabs management - - openTab(args) { - return this.triggerBackgroundCallback(args, "open-tab"); - }, - - // Identities management - - queryIdentities() { - return new Promise(resolve => { - const identities = ContextualIdentityProxy.getIdentities(); - identities.forEach(identity => { - this._remapTabsIfMissing(identity.userContextId); - }); - - resolve(identities); - }); - }, - - // Preferences - - getPreference(args) { - if (!("pref" in args)) { - return Promise.reject("getPreference must be called with pref argument."); - } - - return Promise.resolve(prefService.get(args.pref)); - }, - - // Styling the window - - _configureWindows() { - const promises = []; - for (let window of windows.browserWindows) { // eslint-disable-line prefer-const - promises.push(this._configureWindow(viewFor(window))); - } - return Promise.all(promises); - }, - - _configureWindow(window) { - return this._getOrCreateContainerWindow(window).configure(); - }, - - _configureActiveWindows() { - const promises = []; - for (let window of windows.browserWindows) { // eslint-disable-line prefer-const - promises.push(this._configureActiveWindow(viewFor(window))); - } - return Promise.all(promises); - }, - - _configureActiveWindow(window) { - return this._getOrCreateContainerWindow(window).configureActive(); - }, - - closeWindow(window) { - this._windowMap.delete(window); - }, - - _getOrCreateContainerWindow(window) { - if (!(this._windowMap.has(window))) { - this._windowMap.set(window, new ContainerWindow(window)); - } - - return this._windowMap.get(window); - }, - - refreshNeeded() { - return this._configureWindows(); - }, - - _restyleActiveTab(tab) { - if (!tab) { - return Promise.resolve(null); - } - - const userContextId = ContainerService._getUserContextIdFromTab(tab); - const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); - const hbox = viewFor(tab.window).document.getElementById("userContext-icons"); - - if (!identity) { - hbox.setAttribute("data-identity-color", ""); - return Promise.resolve(null); - } - - hbox.setAttribute("data-identity-color", identity.color); - - const label = viewFor(tab.window).document.getElementById("userContext-label"); - label.setAttribute("value", identity.name); - label.style.color = ContainerService._fromNameToColor(identity.color); - - const indicator = viewFor(tab.window).document.getElementById("userContext-indicator"); - indicator.setAttribute("data-identity-icon", identity.icon); - indicator.style.listStyleImage = ""; - - return this._restyleTab(tab); - }, - - _restyleTab(tab) { - if (!tab) { - return Promise.resolve(null); - } - const userContextId = ContainerService._getUserContextIdFromTab(tab); - const identity = ContextualIdentityProxy.getIdentityFromId(userContextId); - if (!identity) { - return Promise.resolve(null); - } - return Promise.resolve(viewFor(tab).setAttribute("data-identity-color", identity.color)); - }, - - // Uninstallation - uninstall(reason) { - const data = ss.storage.savedConfiguration; - if (!data) { - throw new DOMError("ERROR - No saved configuration!!"); - } - - if (data.version !== 1) { - throw new DOMError("ERROR - Unknown version!!"); - } - - if (reason !== "upgrade") { - PREFS.forEach(pref => { - if (pref[0] in data.prefs) { - prefService.set(pref[0], data.prefs[pref[0]]); - } - }); - } - - // Note: We cannot go back renaming the Finance identity back to Banking: - // the locale system doesn't work with renamed containers. - - // Restore the customizable container panel. - const widget = CustomizableWidgets.find(widget => widget.id === "containers-panelmenu"); - if (widget) { - CustomizableUI.createWidget(widget); - } - - for (let window of windows.browserWindows) { // eslint-disable-line prefer-const - // Let's close all the container tabs. - // Note: We cannot use _closeTabs() because at this point tab.window is - // null. - if (!this._containerWasEnabled && reason !== "upgrade") { - for (let tab of window.tabs) { // eslint-disable-line prefer-const - if (this._getUserContextIdFromTab(tab)) { - tab.close(); - try { - SessionStore.forgetClosedTab(viewFor(window), 0); - } catch(e) {} // eslint-disable-line no-empty - } - } - } - - this._getOrCreateContainerWindow(viewFor(window)).shutdown(); - } - - // all the configuration must go away now. - this._windowMap = new Map(); - - if (reason !== "upgrade") { - // Let's forget all the previous closed tabs. - this._forgetIdentity(); - - const preInstalledIdentities = data.preInstalledIdentities; - ContextualIdentityProxy.getIdentities().forEach(identity => { - if (!preInstalledIdentities.includes(identity.userContextId)) { - ContextualIdentityService.remove(identity.userContextId); - } else { - // Let's cleanup all the cookies for this container. - Services.obs.notifyObservers(null, "clear-origin-attributes-data", - JSON.stringify({ userContextId: identity.userContextId })); - } - }); - - // Let's delete the configuration. - delete ss.storage.savedConfiguration; - } - - // Begin-Of-Hack - if (this._oldGetIdentityFromId) { - ContextualIdentityService.getIdentityFromId = this._oldGetIdentityFromId; - } - - if (this._oldGetPublicIdentityFromId) { - ContextualIdentityService.getPublicIdentityFromId = this._oldGetPublicIdentityFromId; - } - // End-Of-Hack - }, - - forgetIdentityAndRefresh(args) { - this._forgetIdentity(args.userContextId); - return this.refreshNeeded(); - }, - - _forgetIdentity(userContextId = 0) { - for (let window of windows.browserWindows) { // eslint-disable-line prefer-const - window = viewFor(window); - const closedTabData = JSON.parse(SessionStore.getClosedTabData(window)); - for (let i = closedTabData.length - 1; i >= 0; --i) { - if (!closedTabData[i].state.userContextId) { - continue; - } - - if (userContextId === 0 || - closedTabData[i].state.userContextId === userContextId) { - try { - SessionStore.forgetClosedTab(window, i); - } catch(e) {} // eslint-disable-line no-empty - } - } - } - }, -}; - -// ---------------------------------------------------------------------------- -// ContainerWindow - -// This object is used to configure a single window. -function ContainerWindow(window) { - this._init(window); -} - -ContainerWindow.prototype = { - _window: null, - _style: null, - _panelElement: null, - _timeoutStore: new Map(), - _elementCache: new Map(), - _tooltipCache: new Map(), - _tabsElement: null, - - _init(window) { - this._window = window; - this._tabsElement = this._window.document.getElementById("tabbrowser-tabs"); - this._style = Style({ uri: self.data.url("usercontext.css") }); - this._plusButton = this._window.document.getAnonymousElementByAttribute(this._tabsElement, "anonid", "tabs-newtab-button"); - this._overflowPlusButton = this._window.document.getElementById("new-tab-button"); - - // Only hack the normal plus button as the alltabs is done elsewhere - this.attachMenuEvent("plus-button", this._plusButton); - - attachTo(this._style, this._window); - }, - - attachMenuEvent(source, button) { - const popup = button.querySelector(".new-tab-popup"); - popup.addEventListener("popupshown", () => { - ContainerService.sendTelemetryPayload({ - "event": "show-plus-button-menu", - "eventSource": source - }); - popup.querySelector("menuseparator").remove(); - const popupMenuItems = [...popup.querySelectorAll("menuitem")]; - popupMenuItems.forEach((item) => { - const userContextId = item.getAttribute("data-usercontextid"); - if (!userContextId) { - item.remove(); - } - item.setAttribute("command", ""); - item.addEventListener("command", (e) => { - e.stopPropagation(); - e.preventDefault(); - ContainerService.openTab({ - userContextId: userContextId, - source: source - }); - }); - }); - }); - }, - - configure() { - return Promise.all([ - this._configureActiveTab(), - this._configureFileMenu(), - this._configureAllTabsMenu(), - this._configureTabStyle(), - this.configureActive(), - ]); - }, - - configureActive() { - return this._configureContextMenu(); - }, - - _configureTabStyle() { - const promises = []; - for (let tab of modelFor(this._window).tabs) { // eslint-disable-line prefer-const - promises.push(ContainerService._restyleTab(tab)); - } - return Promise.all(promises); - }, - - _configureActiveTab() { - const tab = modelFor(this._window).tabs.activeTab; - return ContainerService._restyleActiveTab(tab); - }, - - _configureFileMenu() { - return this._configureMenu("menu_newUserContext", null, e => { - const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); - ContainerService.openTab({ - userContextId: userContextId, - source: "file-menu" - }); - }); - }, - - _configureAllTabsMenu() { - return this._configureMenu("alltabs_containersTab", null, e => { - const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); - ContainerService.showTabs({ - userContextId, - nofocus: true, - window: this._window, - }).then(() => { - return ContainerService.openTab({ - userContextId, - source: "alltabs-menu" - }); - }).catch(() => {}); - }); - }, - - _configureContextMenu() { - return Promise.all([ - this._configureMenu("context-openlinkinusercontext-menu", - () => { - // This userContextId is what we want to exclude. - const tab = modelFor(this._window).tabs.activeTab; - return ContainerService._getUserContextIdFromTab(tab); - }, - e => { - // This is a super internal method. Hopefully it will be stable in the - // next FF releases. - this._window.gContextMenu.openLinkInTab(e); - - const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); - ContainerService.showTabs({ - userContextId, - nofocus: true, - window: this._window, - }); - } - ), - this._configureContextMenuOpenLink(), - ]); - }, - - _configureContextMenuOpenLink() { - return new Promise(resolve => { - const self = this; - this._window.gSetUserContextIdAndClick = function(event) { - const tab = modelFor(self._window).tabs.activeTab; - const userContextId = ContainerService._getUserContextIdFromTab(tab); - event.target.setAttribute("data-usercontextid", userContextId); - self._window.gContextMenu.openLinkInTab(event); - }; - - let item = this._window.document.getElementById("context-openlinkincontainertab"); - item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)"); - - item = this._window.document.getElementById("context-openlinkintab"); - item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)"); - - resolve(); - }); - }, - - // Generic menu configuration. - _configureMenu(menuId, excludedContainerCb, clickCb) { - const menu = this._window.document.getElementById(menuId); - if (!this._disableElement(menu)) { - // Delete stale menu that isn't native elements - while (menu.firstChild) { - menu.removeChild(menu.firstChild); - } - } - - const menupopup = this._window.document.createElementNS(XUL_NS, "menupopup"); - menu.appendChild(menupopup); - - menupopup.addEventListener("command", clickCb); - return this._createMenu(menupopup, excludedContainerCb); - }, - - _createMenu(target, excludedContainerCb) { - while (target.hasChildNodes()) { - target.removeChild(target.firstChild); - } - - return new Promise((resolve, reject) => { - ContainerService.queryIdentities().then(identities => { - const fragment = this._window.document.createDocumentFragment(); - - const excludedUserContextId = excludedContainerCb ? excludedContainerCb() : 0; - if (excludedUserContextId) { - const bundle = this._window.document.getElementById("bundle_browser"); - - const menuitem = this._window.document.createElementNS(XUL_NS, "menuitem"); - menuitem.setAttribute("data-usercontextid", "0"); - menuitem.setAttribute("label", bundle.getString("userContextNone.label")); - menuitem.setAttribute("accesskey", bundle.getString("userContextNone.accesskey")); - - fragment.appendChild(menuitem); - - const menuseparator = this._window.document.createElementNS(XUL_NS, "menuseparator"); - fragment.appendChild(menuseparator); - } - - identities.forEach(identity => { - if (identity.userContextId === excludedUserContextId) { - return; - } - - const menuitem = this._window.document.createElementNS(XUL_NS, "menuitem"); - menuitem.setAttribute("label", identity.name); - menuitem.classList.add("menuitem-iconic"); - menuitem.setAttribute("data-usercontextid", identity.userContextId); - menuitem.setAttribute("data-identity-color", identity.color); - menuitem.setAttribute("data-identity-icon", identity.icon); - fragment.appendChild(menuitem); - }); - - target.appendChild(fragment); - resolve(); - }).catch(() => {reject();}); - }); - }, - - // This timer is used to hide the panel auto-magically if it's not used in - // the following X seconds. This is need to avoid the leaking of the panel - // when the mouse goes out of of the 'plus' button. - _createTimeout(key, callback, timeoutTime) { - this._cleanTimeout(key); - this._timeoutStore.set(key, this._window.setTimeout(() => { - callback(); - this._timeoutStore.delete(key); - }, timeoutTime)); - }, - - _cleanAllTimeouts() { - for (let key of this._timeoutStore.keys()) { // eslint-disable-line prefer-const - this._cleanTimeout(key); - } - }, - - _cleanTimeout(key) { - if (this._timeoutStore.has(key)) { - this._window.clearTimeout(this._timeoutStore.get(key)); - this._timeoutStore.delete(key); - } - }, - - shutdown() { - // CSS must be removed. - detachFrom(this._style, this._window); - - this._shutdownFileMenu(); - this._shutdownAllTabsMenu(); - this._shutdownContextMenu(); - - this._shutdownContainers(); - }, - - _shutDownPlusButtonMenuElement(buttonElement) { - if (buttonElement) { - this._shutdownElement(buttonElement); - buttonElement.setAttribute("tooltip", this._tooltipCache.get(buttonElement)); - - buttonElement.removeEventListener("mouseover", this); - buttonElement.removeEventListener("click", this); - buttonElement.removeEventListener("mouseout", this); - } - }, - - _shutdownFileMenu() { - this._shutdownMenu("menu_newUserContext"); - }, - - _shutdownAllTabsMenu() { - this._shutdownMenu("alltabs_containersTab"); - }, - - _shutdownContextMenu() { - this._shutdownMenu("context-openlinkinusercontext-menu"); - }, - - _shutdownMenu(menuId) { - const menu = this._window.document.getElementById(menuId); - this._shutdownElement(menu); - }, - - _shutdownElement(element) { - // Let's remove our elements. - while (element.firstChild) { - element.firstChild.remove(); - } - - const elementCache = this._elementCache.get(element); - if (elementCache) { - for (let e of elementCache) { // eslint-disable-line prefer-const - element.appendChild(e); - } - } - }, - - _disableElement(element) { - // Nothing to disable. - if (!element || this._elementCache.has(element)) { - return false; - } - const cacheArray = []; - - // Let's store the previous elements so that we can repopulate it in case - // the addon is uninstalled. - while (element.firstChild) { - cacheArray.push(element.removeChild(element.firstChild)); - } - - this._elementCache.set(element, cacheArray); - - return true; - }, - - _shutdownContainers() { - ContextualIdentityProxy.getIdentities().forEach(identity => { - if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) !== -1 && - IDENTITY_COLORS_STANDARD.indexOf(identity.color) !== -1) { - return; - } - - if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) === -1) { - if (identity.userContextId <= IDENTITY_ICONS_STANDARD.length) { - identity.icon = IDENTITY_ICONS_STANDARD[identity.userContextId - 1]; - } else { - identity.icon = IDENTITY_ICONS_STANDARD[0]; - } - } - - if (IDENTITY_COLORS_STANDARD.indexOf(identity.color) === -1) { - if (identity.userContextId <= IDENTITY_COLORS_STANDARD.length) { - identity.color = IDENTITY_COLORS_STANDARD[identity.userContextId - 1]; - } else { - identity.color = IDENTITY_COLORS_STANDARD[0]; - } - } - - ContextualIdentityService.update(identity.userContextId, - identity.name, - identity.icon, - identity.color); - }); - } -}; - -// uninstall/install events --------------------------------------------------- - -exports.main = function (options) { - const installation = options.loadReason === "install" || - options.loadReason === "downgrade" || - options.loadReason === "enable" || - options.loadReason === "upgrade"; - - // Let's start :) - ContainerService.init(installation, options.loadReason); -}; - -exports.onUnload = function (reason) { - if (reason === "disable" || - reason === "downgrade" || - reason === "uninstall" || - reason === "upgrade") { - ContainerService.uninstall(reason); - } -}; diff --git a/package.json b/package.json index 5ea8d37..85177f0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "testpilot-containers", "title": "Containers Experiment", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", - "version": "2.4.1", + "version": "3.0.0", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "bugs": { "url": "https://github.com/mozilla/testpilot-containers/issues" diff --git a/webextension/js/background/identityState.js b/webextension/js/background/identityState.js index 2bc6205..b43a2f6 100644 --- a/webextension/js/background/identityState.js +++ b/webextension/js/background/identityState.js @@ -63,7 +63,6 @@ const identityState = { const containers = await browser.contextualIdentities.query({}); for (const id in containers) { const container = containers[id]; - const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(container.cookieStoreId); await this.remapTabsIfMissing(container.cookieStoreId); const containerState = await this.storageArea.get(container.cookieStoreId); if (containerState.openTabs > 0) { @@ -118,14 +117,6 @@ const identityState = { async remapTabsIfMissing(cookieStoreId) { // We already know this cookieStoreId. const containerState = await this.storageArea.get(cookieStoreId) || this._createIdentityState(); -//REINSTATE THIS TODO, currently buggy -/* - if (containerState !== null) { - return; - } -*/ -const hiddenTabs = containerState.hiddenTabs; -//END REINSTATE await this.storageArea.set(cookieStoreId, containerState); await this.remapTabsFromUserContextId(cookieStoreId); @@ -141,14 +132,6 @@ const hiddenTabs = containerState.hiddenTabs; containerState.openTabs = tabsByContainer.length; await this.storageArea.set(cookieStoreId, containerState); }, -/*TODO check if used - remapTab(tab) { - const userContextId = this._getUserContextIdFromTab(tab); - if (userContextId) { - this.remapTabsFromUserContextId(userContextId); - } - }, -*/ _createIdentityState() { return { diff --git a/webextension/js/background/index.html b/webextension/js/background/index.html index 24a0eba..2a2924d 100644 --- a/webextension/js/background/index.html +++ b/webextension/js/background/index.html @@ -1,4 +1,7 @@ + + +