From 12a6bb3b9bfa773c91b97f8bc82d8b9dae7c2950 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Tue, 11 Jul 2017 13:40:01 +0100 Subject: [PATCH] Break web extensions code --- webextension/background.js | 1122 ----------------- webextension/js/.eslintrc.js | 14 + webextension/js/background/assignManager.js | 330 +++++ webextension/js/background/backgroundLogic.js | 345 +++++ webextension/js/background/badge.js | 18 + webextension/js/background/identityState.js | 159 +++ webextension/js/background/index.html | 25 + webextension/js/background/init.js | 27 + webextension/js/background/messageHandler.js | 177 +++ webextension/js/background/tabPageCounter.js | 64 + webextension/js/background/themeManager.js | 51 + webextension/js/popup.js | 185 +-- webextension/manifest.json | 2 +- 13 files changed, 1304 insertions(+), 1215 deletions(-) delete mode 100644 webextension/background.js create mode 100644 webextension/js/.eslintrc.js create mode 100644 webextension/js/background/assignManager.js create mode 100644 webextension/js/background/backgroundLogic.js create mode 100644 webextension/js/background/badge.js create mode 100644 webextension/js/background/identityState.js create mode 100644 webextension/js/background/index.html create mode 100644 webextension/js/background/init.js create mode 100644 webextension/js/background/messageHandler.js create mode 100644 webextension/js/background/tabPageCounter.js create mode 100644 webextension/js/background/themeManager.js diff --git a/webextension/background.js b/webextension/background.js deleted file mode 100644 index b160e6c..0000000 --- a/webextension/background.js +++ /dev/null @@ -1,1122 +0,0 @@ -const MAJOR_VERSIONS = ["2.3.0", "2.4.0"]; -const THEME_BUILD_DATE = 20170630; -const DEFAULT_TAB = "about:newtab"; - -const assignManager = { - MENU_ASSIGN_ID: "open-in-this-container", - MENU_REMOVE_ID: "remove-open-in-this-container", - storageArea: { - area: browser.storage.local, - exemptedTabs: {}, - - getSiteStoreKey(pageUrl) { - const url = new window.URL(pageUrl); - const storagePrefix = "siteContainerMap@@_"; - return `${storagePrefix}${url.hostname}`; - }, - - setExempted(pageUrl, tabId) { - const siteStoreKey = this.getSiteStoreKey(pageUrl); - if (!(siteStoreKey in this.exemptedTabs)) { - this.exemptedTabs[siteStoreKey] = []; - } - this.exemptedTabs[siteStoreKey].push(tabId); - }, - - removeExempted(pageUrl) { - const siteStoreKey = this.getSiteStoreKey(pageUrl); - this.exemptedTabs[siteStoreKey] = []; - }, - - isExempted(pageUrl, tabId) { - const siteStoreKey = this.getSiteStoreKey(pageUrl); - if (!(siteStoreKey in this.exemptedTabs)) { - return false; - } - return this.exemptedTabs[siteStoreKey].includes(tabId); - }, - - get(pageUrl) { - const siteStoreKey = this.getSiteStoreKey(pageUrl); - return new Promise((resolve, reject) => { - this.area.get([siteStoreKey]).then((storageResponse) => { - if (storageResponse && siteStoreKey in storageResponse) { - resolve(storageResponse[siteStoreKey]); - } - resolve(null); - }).catch((e) => { - reject(e); - }); - }); - }, - - set(pageUrl, data, exemptedTabIds) { - const siteStoreKey = this.getSiteStoreKey(pageUrl); - if (exemptedTabIds) { - exemptedTabIds.forEach((tabId) => { - this.setExempted(pageUrl, tabId); - }); - } - return this.area.set({ - [siteStoreKey]: data - }); - }, - - remove(pageUrl) { - const siteStoreKey = this.getSiteStoreKey(pageUrl); - // When we remove an assignment we should clear all the exemptions - this.removeExempted(pageUrl); - return this.area.remove([siteStoreKey]); - }, - - async deleteContainer(userContextId) { - const sitesByContainer = await this.getByContainer(userContextId); - this.area.remove(Object.keys(sitesByContainer)); - }, - - async getByContainer(userContextId) { - const sites = {}; - const siteConfigs = await this.area.get(); - Object.keys(siteConfigs).forEach((key) => { - // For some reason this is stored as string... lets check them both as that - if (String(siteConfigs[key].userContextId) === String(userContextId)) { - const site = siteConfigs[key]; - // In hindsight we should have stored this - // TODO file a follow up to clean the storage onLoad - site.hostname = key.replace(/^siteContainerMap@@_/, ""); - sites[key] = site; - } - }); - return sites; - } - }, - - _neverAsk(m) { - const pageUrl = m.pageUrl; - if (m.neverAsk === true) { - // If we have existing data and for some reason it hasn't been deleted etc lets update it - this.storageArea.get(pageUrl).then((siteSettings) => { - if (siteSettings) { - siteSettings.neverAsk = true; - this.storageArea.set(pageUrl, siteSettings); - } - }).catch((e) => { - throw e; - }); - } - }, - - // We return here so the confirm page can load the tab when exempted - async _exemptTab(m) { - const pageUrl = m.pageUrl; - this.storageArea.setExempted(pageUrl, m.tabId); - return true; - }, - - init() { - browser.contextMenus.onClicked.addListener((info, tab) => { - this._onClickedHandler(info, tab); - }); - - // Before a request is handled by the browser we decide if we should route through a different container - browser.webRequest.onBeforeRequest.addListener((options) => { - if (options.frameId !== 0 || options.tabId === -1) { - return {}; - } - this.removeContextMenu(); - return Promise.all([ - browser.tabs.get(options.tabId), - this.storageArea.get(options.url) - ]).then(([tab, siteSettings]) => { - const userContextId = this.getUserContextIdFromCookieStore(tab); - if (!siteSettings - || userContextId === siteSettings.userContextId - || tab.incognito - || this.storageArea.isExempted(options.url, tab.id)) { - return {}; - } - - this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk); - this.calculateContextMenu(tab); - - /* Removal of existing tabs: - We aim to open the new assigned container tab / warning prompt in it's own tab: - - As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back() - - When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421 - If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history - Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history: - - Redirects happen from Short URLs and tracking links that act as a gateway - - Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this - however they don't run on about:blank so this would likely be just as hacky. - We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history. - */ - if (backgroundLogic.NEW_TAB_PAGES.has(tab.url) - || (messageHandler.lastCreatedTab - && messageHandler.lastCreatedTab.id === tab.id)) { - browser.tabs.remove(tab.id); - } - return { - cancel: true, - }; - }).catch((e) => { - throw e; - }); - },{urls: [""], types: ["main_frame"]}, ["blocking"]); - }, - - async _onClickedHandler(info, tab) { - const userContextId = this.getUserContextIdFromCookieStore(tab); - // Mapping ${URL(info.pageUrl).hostname} to ${userContextId} - if (userContextId) { - // let actionName; - let remove; - if (info.menuItemId === this.MENU_ASSIGN_ID) { - remove = false; - } else { - remove = true; - } - await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove); - } - }, - - - deleteContainer(userContextId) { - this.storageArea.deleteContainer(userContextId); - }, - - getUserContextIdFromCookieStore(tab) { - if (!("cookieStoreId" in tab)) { - return false; - } - return backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId); - }, - - isTabPermittedAssign(tab) { - // Ensure we are not an important about url - // Ensure we are not in incognito mode - const url = new URL(tab.url); - if (url.protocol === "about:" - || url.protocol === "moz-extension:" - || tab.incognito) { - return false; - } - return true; - }, - - async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) { - let actionName; - - // https://github.com/mozilla/testpilot-containers/issues/626 - // Context menu has stored context IDs as strings, so we need to coerce - // the value to a string for accurate checking - userContextId = String(userContextId); - - if (!remove) { - const tabs = await browser.tabs.query({}); - const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl); - const exemptedTabIds = tabs.filter((tab) => { - const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url); - /* Auto exempt all tabs that exist for this hostname that are not in the same container */ - if (tabStoreKey === assignmentStoreKey && - this.getUserContextIdFromCookieStore(tab) !== userContextId) { - return true; - } - return false; - }).map((tab) => { - return tab.id; - }); - - await this.storageArea.set(pageUrl, { - userContextId, - neverAsk: false - }, exemptedTabIds); - actionName = "added"; - } else { - await this.storageArea.remove(pageUrl); - actionName = "removed"; - } - browser.tabs.sendMessage(tabId, { - text: `Successfully ${actionName} site to always open in this container` - }); - backgroundLogic.sendTelemetryPayload({ - event: `${actionName}-container-assignment`, - userContextId: userContextId, - }); - const tab = await browser.tabs.get(tabId); - this.calculateContextMenu(tab); - }, - - async _getAssignment(tab) { - const cookieStore = this.getUserContextIdFromCookieStore(tab); - // Ensure we have a cookieStore to assign to - if (cookieStore - && this.isTabPermittedAssign(tab)) { - return await this.storageArea.get(tab.url); - } - return false; - }, - - _getByContainer(userContextId) { - return this.storageArea.getByContainer(userContextId); - }, - - removeContextMenu() { - // There is a focus issue in this menu where if you change window with a context menu click - // you get the wrong menu display because of async - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16 - // We also can't change for always private mode - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102 - browser.contextMenus.remove(this.MENU_ASSIGN_ID); - browser.contextMenus.remove(this.MENU_REMOVE_ID); - }, - - async calculateContextMenu(tab) { - this.removeContextMenu(); - const siteSettings = await this._getAssignment(tab); - // Return early and not add an item if we have false - // False represents assignment is not permitted - if (siteSettings === false) { - return false; - } - // ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418 - let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick - let menuId = this.MENU_ASSIGN_ID; - const tabUserContextId = this.getUserContextIdFromCookieStore(tab); - if (siteSettings && - Number(siteSettings.userContextId) === Number(tabUserContextId)) { - prefix = "✓"; - menuId = this.MENU_REMOVE_ID; - } - browser.contextMenus.create({ - id: menuId, - title: `${prefix} Always Open in This Container`, - checked: true, - contexts: ["all"], - }); - }, - - reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) { - const cookieStoreId = backgroundLogic.cookieStoreId(userContextId); - const loadPage = browser.extension.getURL("confirm-page.html"); - // False represents assignment is not permitted - // If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there - if (neverAsk) { - browser.tabs.create({url, cookieStoreId, index}); - backgroundLogic.sendTelemetryPayload({ - event: "auto-reload-page-in-container", - userContextId: userContextId, - }); - } else { - backgroundLogic.sendTelemetryPayload({ - event: "prompt-to-reload-page-in-container", - userContextId: userContextId, - }); - let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`; - let currentCookieStoreId; - if (currentUserContextId) { - currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId); - confirmUrl += `¤tCookieStoreId=${currentCookieStoreId}`; - } - browser.tabs.create({ - url: confirmUrl, - cookieStoreId: currentCookieStoreId, - index - }).then(() => { - // We don't want to sync this URL ever nor clutter the users history - browser.history.deleteUrl({url: confirmUrl}); - }).catch((e) => { - throw e; - }); - } - } -}; - -const identityState = { - storageArea: { - area: browser.storage.local, - - getContainerStoreKey(cookieStoreId) { - const storagePrefix = "identitiesState@@_"; - return `${storagePrefix}${cookieStoreId}`; - }, - - get(cookieStoreId) { - const storeKey = this.getContainerStoreKey(cookieStoreId); - return new Promise((resolve, reject) => { - this.area.get([storeKey]).then((storageResponse) => { - if (storageResponse && storeKey in storageResponse) { - resolve(storageResponse[storeKey]); - } - resolve(null); - }).catch((e) => { - reject(e); - }); - }); - }, - - set(cookieStoreId, data) { - const storeKey = this.getContainerStoreKey(cookieStoreId); - return this.area.set({ - [storeKey]: data - }); - }, - - remove(cookieStoreId) { - const storeKey = this.getContainerStoreKey(cookieStoreId); - return this.area.remove([storeKey]); - } - }, - - _createTabObject(tab) { - return Object.assign({}, tab); - }, - - async containersCounts() { - let containersCounts = { // eslint-disable-line prefer-const - "shown": 0, - "hidden": 0, - "total": 0 - }; - const containers = await browser.contextualIdentities.query({}); - for (const container in containers) { - const containerState = await this.storageArea.get(container.cookieStoreId); - if (containerState.openTabs > 0) { - ++containersCounts.shown; - ++containersCounts.total; - continue; - } else if (containerState.hiddenTabs.length > 0) { - ++containersCounts.hidden; - ++containersCounts.total; - continue; - } - } - return containersCounts; - }, - - containerTabCount(userContextId) { - // Returns the total of open and hidden tabs with this userContextId - let containerTabsCount = 0; - containerTabsCount += this._identitiesState[userContextId].openTabs; - containerTabsCount += this._identitiesState[userContextId].hiddenTabs.length; - return containerTabsCount; - }, - - totalContainerTabsCount() { - // Returns the number of total open tabs across ALL containers - let totalContainerTabsCount = 0; - for (const userContextId in this._identitiesState) { - totalContainerTabsCount += this._identitiesState[userContextId].openTabs; - } - return totalContainerTabsCount; - }, - - async totalNonContainerTabsCount() { - // Returns the number of open tabs NOT IN a container - let totalNonContainerTabsCount = 0; - const tabs = await browser.tabs.query({}); - for (const tab of tabs) { - if (this._getUserContextIdFromTab(tab) === 0) { - ++totalNonContainerTabsCount; - } - } - return totalNonContainerTabsCount; - }, - - remapTabsIfMissing(userContextId) { - // We already know this userContextId. - if (userContextId in this._identitiesState) { - return; - } - - this._identitiesState[userContextId] = this._createIdentityState(); - this.remapTabsFromUserContextId(userContextId); - }, - - remapTabsFromUserContextId(userContextId) { - this._identitiesState[userContextId].openTabs = this._matchTabsByContainer(userContextId).length; - }, -/*TODO check if used - remapTab(tab) { - const userContextId = this._getUserContextIdFromTab(tab); - if (userContextId) { - this.remapTabsFromUserContextId(userContextId); - } - }, -*/ - - _createIdentityState() { - return { - hiddenTabs: [], - openTabs: 0 - }; - }, -}; - -const backgroundLogic = { - NEW_TAB_PAGES: new Set([ - "about:startpage", - "about:newtab", - "about:home", - "about:blank" - ]), - - getUserContextIdFromCookieStoreId(cookieStoreId) { - if (!cookieStoreId) { - return false; - } - const container = cookieStoreId.replace("firefox-container-", ""); - if (container !== cookieStoreId) { - return container; - } - return false; - }, - - deleteContainer(userContextId) { - this.sendTelemetryPayload({ - event: "delete-container", - userContextId - }); - - const removeTabsPromise = this._closeTabs(userContextId); - - return new Promise((resolve) => { - removeTabsPromise.then(() => { - const removed = browser.contextualIdentities.remove(this.cookieStoreId(userContextId)); - removed.then(() => { - assignManager.deleteContainer(userContextId); - browser.runtime.sendMessage({ - method: "forgetIdentityAndRefresh" - }).then(() => { - resolve({done: true, userContextId}); - }).catch((e) => {throw e;}); - }).catch((e) => {throw e;}); - }).catch((e) => {throw e;}); - }); - }, - - createOrUpdateContainer(options) { - let donePromise; - if (options.userContextId !== "new") { - donePromise = browser.contextualIdentities.update( - this.cookieStoreId(options.userContextId), - options.params - ); - this.sendTelemetryPayload({ - event: "edit-container", - userContextId: options.userContextId - }); - } else { - donePromise = browser.contextualIdentities.create(options.params); - this.sendTelemetryPayload({ - event: "add-container" - }); - } - return donePromise.then(() => { - browser.runtime.sendMessage({ - method: "refreshNeeded" - }); - }); - }, - - openTab(options) { - let url = options.url || undefined; - const userContextId = ("userContextId" in options) ? options.userContextId : 0; - const active = ("nofocus" in options) ? options.nofocus : true; - const source = ("source" in options) ? options.source : null; - - const cookieStoreId = backgroundLogic.cookieStoreId(options.userContextId); - // Only send telemetry for tabs opened by UI - i.e., not via showTabs - if (source && userContextId) { - this.sendTelemetryPayload({ - "event": "open-tab", - "eventSource": source, - "userContextId": userContextId, - "clickedContainerTabCount": identityState.containerTabCount(cookieStoreId) - }); - } - // Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072 - - // We can't open new tab pages, so open a blank tab. Used in tab un-hide - if (this.NEW_TAB_PAGES.has(url)) { - url = undefined; - } - - // Unhide all hidden tabs - this.showTabs({ - cookieStoreId - }); - return browser.tabs.create({ - url, - active, - pinned: options.pinned || false, - cookieStoreId - }); - }, - - async getTabs(options) { - if (!("cookieStoreId" in options)) { - return new Error("getTabs must be called with cookieStoreId argument."); - } - - const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); - this._remapTabsIfMissing(userContextId); - if (!this._isKnownContainer(userContextId)) { - return []; - } - - const list = []; - await this._containerTabs(userContextId).then((tabs) => { - tabs.forEach((tab) => { - list.push(this._createTabObject(tab)); - }); - }); - - const containerState = await identityState.storageArea.get(options.cookieStoreId); - return list.concat(containerState.hiddenTabs); - }, - - async moveTabsToWindow(options) { - if (!("cookieStoreId" in options)) { - return new Error("moveTabsToWindow must be called with cookieStoreId argument."); - } - - const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); - identityState.remapTabsIfMissing(userContextId); - if (!identityState.isKnownContainer(userContextId)) { - return null; - } - - this.sendTelemetryPayload({ - "event": "move-tabs-to-window", - "userContextId": userContextId, - "clickedContainerTabCount": this._containerTabCount(userContextId), - }); - - const list = this._matchTabsByContainer(userContextId); - - // Nothing to do - if (list.length === 0 && - this._identitiesState[userContextId].hiddenTabs.length === 0) { - return; - } -//TODO check list returns ids - const window = await browser.windows.create({ - tabId: list.shift() - }); - browser.tabs.move(list, { - windowId: window.id, - index: -1 - }); - - const containerState = await identityState.storageArea.get(options.cookieStoreId); - // Let's show the hidden tabs. - for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const - browser.tabs.create(object.url || DEFAULT_TAB, { - cookieStoreId: options.cookieStoreId - }); - } - - containerState.hiddenTabs = []; - - // Let's close all the normal tab in the new window. In theory it - // should be only the first tab, but maybe there are addons doing - // crazy stuff. - const tabs = browser.tabs.query({windowId: window.id}); - for (let tab of tabs) { // eslint-disable-line prefer-const - if (tabs.cookieStoreId !== options.cookieStoreId) { - browser.tabs.remove(tab.id); - } - } - }, - - _closeTabs(userContextId) { - return this._containerTabs(userContextId).then((tabs) => { - const tabIds = tabs.map((tab) => tab.id); - return browser.tabs.remove(tabIds); - }); - }, - - async queryIdentitiesState() { - const identities = await browser.contextualIdentities.query({}); - const identitiesMap = await Promise.all(identities.map(async function (identity) { - identityState.remapTabsIfMissing(identity.userContextId); - const containerState = await identityState.get(identity.cookieStoreId); - return { - hasHiddenTabs: !!containerState.hiddenTabs.length, - hasOpenTabs: !!containerState.openTabs - }; - })); - - return identitiesMap; - }, - - _isKnownContainer(userContextId) { - return userContextId in this._identitiesState; - }, - - - - async sortTabs() { - const containersCounts = identityState.containersCounts(); - this.sendTelemetryPayload({ - "event": "sort-tabs", - "shownContainersCount": containersCounts.shown, - "totalContainerTabsCount": identityState.totalContainerTabsCount(), - "totalNonContainerTabsCount": await identityState.totalNonContainerTabsCount() - }); - return new Promise(resolve => { -//TODO fix this -/* - for (let window of windows.browserWindows) { // eslint-disable-line prefer-const - // First the pinned tabs, then the normal ones. - this._sortTabsInternal(window, true); - this._sortTabsInternal(window, false); - } -*/ - resolve(null); - }); - }, - - _sortTabsInternal(window, pinnedTabs) { - // From model to XUL window. - const xulWindow = viewFor(window); - - const tabs = browser.tabs.query({windowId: browser.windows.WINDOW_ID_CURRENT}); - let pos = 0; - - // Let's collect UCIs/tabs for this window. - const map = new Map; - for (const tab of tabs) { - if (pinnedTabs && !tabsUtils.isPinned(tab)) { - // We don't have, or we already handled all the pinned tabs. - break; - } - - if (!pinnedTabs && tabsUtils.isPinned(tab)) { - // pinned tabs must be consider as taken positions. - ++pos; - continue; - } - - const userContextId = this._getUserContextIdFromTab(tab); - if (!map.has(userContextId)) { - map.set(userContextId, []); - } - map.get(userContextId).push(tab); - } - - // Let's sort the map. - const sortMap = new Map([...map.entries()].sort((a, b) => a[0] > b[0])); - - // Let's move tabs. - sortMap.forEach(tabs => { - for (const tab of tabs) { - xulWindow.gBrowser.moveTabTo(tab, pos++); - } - }); - }, - - async hideTabs(options) { - if (!("cookieStoreId" in options)) { - return new Error("hideTabs must be called with cookieStoreId option."); - } - - const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); - this._remapTabsIfMissing(userContextId); - if (!this._isKnownContainer(userContextId)) { - return null; - } - - const containersCounts = identityState.containersCounts(); - this.sendTelemetryPayload({ - "event": "hide-tabs", - "userContextId": userContextId, - "clickedContainerTabCount": this._containerTabCount(options.userContextId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total - }); - - const containerState = await identityState.storageArea.get(options.cookieStoreId); - - await this._containerTabs(userContextId).then((tabs) => { - tabs.forEach((tab) => { - const tabObject = this._createTabObject(tab); - // This tab is going to be closed. Let's mark this tabObject as - // non-active. - tabObject.active = false; - containerState.hiddenTabs.push(tabObject); - }); - }); - - await this._closeTabs(userContextId); - - return identityState.storageArea.set(options.cookieStoreId, containerState); - }, - - async showTabs(options) { - if (!("cookieStoreId" in options)) { - return Promise.reject("showTabs must be called with cookieStoreId argument."); - } - - const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); - identityState.remapTabsIfMissing(options.cookieStoreId); - if (!identityState.isKnownContainer(userContextId)) { - return null; - } - - const containersCounts = identityState.containersCounts(); - this.sendTelemetryPayload({ - "event": "show-tabs", - "userContextId": userContextId, - "clickedContainerTabCount": identityState.containerTabCount(options.userContextId), - "shownContainersCount": containersCounts.shown, - "hiddenContainersCount": containersCounts.hidden, - "totalContainersCount": containersCounts.total - }); - - const promises = []; - - const containerState = await identityState.storageArea.get(options.cookieStoreId); - - for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const - promises.push(this.openTab({ - userContextId: userContextId, - url: object.url, - nofocus: options.nofocus || false, - pinned: object.pinned, - })); - } - - containerState.hiddenTabs = []; - - await Promise.all(promises); - return await identityState.storageArea.set(options.cookieStoreId, containerState); - }, - - - sendTelemetryPayload(message = {}) { - if (!message.event) { - throw new Error("Missing event name for telemetry"); - } - message.method = "sendTelemetryPayload"; - browser.runtime.sendMessage(message); - }, - - cookieStoreId(userContextId) { - return `firefox-container-${userContextId}`; - }, - - _containerTabs(userContextId) { - return browser.tabs.query({ - cookieStoreId: this.cookieStoreId(userContextId) - }).catch((e) => {throw e;}); - }, -}; - -const messageHandler = { - // After the timer completes we assume it's a tab the user meant to keep open - // We use this to catch redirected tabs that have just opened - // If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click - LAST_CREATED_TAB_TIMER: 2000, - - init() { - // Handles messages from webextension code - browser.runtime.onMessage.addListener((m) => { - let response; - - switch (m.method) { - case "deleteContainer": - response = backgroundLogic.deleteContainer(m.message.userContextId); - break; - case "createOrUpdateContainer": - response = backgroundLogic.createOrUpdateContainer(m.message); - break; - case "openTab": - // Same as open-tab for index.js - response = backgroundLogic.openTab(m.message); - break; - case "neverAsk": - assignManager._neverAsk(m); - break; - case "getAssignment": - response = browser.tabs.get(m.tabId).then((tab) => { - return assignManager._getAssignment(tab); - }); - break; - case "getAssignmentObjectByContainer": - response = assignManager._getByContainer(m.message.userContextId); - break; - case "setOrRemoveAssignment": - // m.tabId is used for where to place the in content message - // m.url is the assignment to be removed/added - response = browser.tabs.get(m.tabId).then((tab) => { - return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value); - }); - break; - case "exemptContainerAssignment": - response = assignManager._exemptTab(m); - break; - } - return response; - }); - - // Handles messages from sdk code - const port = browser.runtime.connect(); - port.onMessage.addListener(m => { - switch (m.type) { - case "lightweight-theme-changed": - themeManager.update(m.message); - break; - case "open-tab": - backgroundLogic.openTab(m.message); - break; - default: - throw new Error(`Unhandled message type: ${m.message}`); - } - }); - - browser.tabs.onCreated.addListener((tab) => { - // This works at capturing the tabs as they are created - // However we need onFocusChanged and onActivated to capture the initial tab - if (tab.id === -1) { - return {}; - } - tabPageCounter.initTabCounter(tab); - }); - - browser.tabs.onRemoved.addListener((tabId) => { - if (tabId === -1) { - return {}; - } - tabPageCounter.sendTabCountAndDelete(tabId); - }); - - browser.tabs.onActivated.addListener((info) => { - assignManager.removeContextMenu(); - browser.tabs.get(info.tabId).then((tab) => { - tabPageCounter.initTabCounter(tab); - assignManager.calculateContextMenu(tab); - }).catch((e) => { - throw e; - }); - }); - - browser.windows.onFocusChanged.addListener((windowId) => { - assignManager.removeContextMenu(); - // browserAction loses background color in new windows ... - // https://bugzil.la/1314674 - // https://github.com/mozilla/testpilot-containers/issues/608 - // ... so re-call displayBrowserActionBadge on window changes - displayBrowserActionBadge(); - browser.tabs.query({active: true, windowId}).then((tabs) => { - if (tabs && tabs[0]) { - tabPageCounter.initTabCounter(tabs[0]); - assignManager.calculateContextMenu(tabs[0]); - } - }).catch((e) => { - throw e; - }); - }); - - browser.idle.onStateChanged.addListener((newState) => { - browser.tabs.query({}).then(tabs => { - for (let tab of tabs) { // eslint-disable-line prefer-const - if (newState === "idle") { - tabPageCounter.sendTabCountAndDelete(tab.id, "user-went-idle"); - } else if (newState === "active" && tab.active) { - tabPageCounter.initTabCounter(tab); - } - } - }).catch(e => { - throw e; - }); - }); - - browser.webRequest.onCompleted.addListener((details) => { - if (details.frameId !== 0 || details.tabId === -1) { - return {}; - } - assignManager.removeContextMenu(); - - browser.tabs.get(details.tabId).then((tab) => { - tabPageCounter.incrementTabCount(tab); - assignManager.calculateContextMenu(tab); - }).catch((e) => { - throw e; - }); - }, {urls: [""], types: ["main_frame"]}); - - // lets remember the last tab created so we can close it if it looks like a redirect - browser.tabs.onCreated.addListener((details) => { - this.lastCreatedTab = details; - setTimeout(() => { - this.lastCreatedTab = null; - }, this.LAST_CREATED_TAB_TIMER); - }); - - } -}; - -const themeManager = { - existingTheme: null, - disabled: false, - async init() { - const browserInfo = await browser.runtime.getBrowserInfo(); - if (Number(browserInfo.buildID.substring(0, 8)) >= THEME_BUILD_DATE) { - this.disabled = true; - } - this.check(); - }, - setPopupIcon(theme) { - if (this.disabled) { - return; - } - let icons = { - 16: "img/container-site-d-24.png", - 32: "img/container-site-d-48.png" - }; - if (theme === "firefox-compact-dark@mozilla.org") { - icons = { - 16: "img/container-site-w-24.png", - 32: "img/container-site-w-48.png" - }; - } - browser.browserAction.setIcon({ - path: icons - }); - }, - check() { - browser.runtime.sendMessage({ - method: "getTheme" - }).then((theme) => { - this.update(theme); - }).catch(() => { - throw new Error("Unable to get theme"); - }); - }, - update(theme) { - if (this.existingTheme !== theme) { - this.setPopupIcon(theme); - this.existingTheme = theme; - } - } -}; - -const tabPageCounter = { - counters: {}, - - initTabCounter(tab) { - if (tab.id in this.counters) { - if (!("activity" in this.counters[tab.id])) { - this.counters[tab.id].activity = { - "cookieStoreId": tab.cookieStoreId, - "pageRequests": 0 - }; - } - if (!("tab" in this.counters[tab.id])) { - this.counters[tab.id].tab = { - "cookieStoreId": tab.cookieStoreId, - "pageRequests": 0 - }; - } - } else { - this.counters[tab.id] = {}; - this.counters[tab.id].tab = { - "cookieStoreId": tab.cookieStoreId, - "pageRequests": 0 - }; - this.counters[tab.id].activity = { - "cookieStoreId": tab.cookieStoreId, - "pageRequests": 0 - }; - } - }, - - sendTabCountAndDelete(tabId, why = "user-closed-tab") { - if (!(this.counters[tabId])) { - return; - } - if (why === "user-closed-tab" && this.counters[tabId].tab) { - backgroundLogic.sendTelemetryPayload({ - event: "page-requests-completed-per-tab", - userContextId: this.counters[tabId].tab.cookieStoreId, - pageRequestCount: this.counters[tabId].tab.pageRequests - }); - // When we send the ping because the user closed the tab, - // delete both the 'tab' and 'activity' counters - delete this.counters[tabId]; - } else if (why === "user-went-idle" && this.counters[tabId].activity) { - backgroundLogic.sendTelemetryPayload({ - event: "page-requests-completed-per-activity", - userContextId: this.counters[tabId].activity.cookieStoreId, - pageRequestCount: this.counters[tabId].activity.pageRequests - }); - // When we send the ping because the user went idle, - // only reset the 'activity' counter - this.counters[tabId].activity = { - "cookieStoreId": this.counters[tabId].tab.cookieStoreId, - "pageRequests": 0 - }; - } - }, - - incrementTabCount(tab) { - this.counters[tab.id].tab.pageRequests++; - this.counters[tab.id].activity.pageRequests++; - } -}; - -console.log("aaa, startting"); - -assignManager.init(); -themeManager.init(); -// Lets do this last as theme manager did a check before connecting before -messageHandler.init(); - -browser.runtime.sendMessage({ - method: "getPreference", - pref: "browser.privatebrowsing.autostart" -}).then(pbAutoStart => { - - // We don't want to disable the addon if we are in auto private-browsing. - if (!pbAutoStart) { - browser.tabs.onCreated.addListener(tab => { - if (tab.incognito) { - disableAddon(tab.id); - } - }); - - browser.tabs.query({}).then(tabs => { - for (let tab of tabs) { // eslint-disable-line prefer-const - if (tab.incognito) { - disableAddon(tab.id); - } - } - }).catch(() => {}); - } -}).catch(() => {}); - -function disableAddon(tabId) { - browser.browserAction.disable(tabId); - browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" }); -} - -async function getExtensionInfo() { - const manifestPath = browser.extension.getURL("manifest.json"); - const response = await fetch(manifestPath); - const extensionInfo = await response.json(); - return extensionInfo; -} - -async function displayBrowserActionBadge() { - const extensionInfo = await getExtensionInfo(); - const storage = await browser.storage.local.get({browserActionBadgesClicked: []}); - - if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 && - storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) { - browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"}); - browser.browserAction.setBadgeText({text: "NEW"}); - } -} -displayBrowserActionBadge(); diff --git a/webextension/js/.eslintrc.js b/webextension/js/.eslintrc.js new file mode 100644 index 0000000..620e9c6 --- /dev/null +++ b/webextension/js/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + "extends": [ + "../../.eslintrc.js" + ], + "globals": { + "assignManager": true, + "badge": true, + "backgroundLogic": true, + "identityState": true, + "messageHandler": true, + "tabPageCounter": true, + "themeManager": true + } +}; diff --git a/webextension/js/background/assignManager.js b/webextension/js/background/assignManager.js new file mode 100644 index 0000000..e356c6d --- /dev/null +++ b/webextension/js/background/assignManager.js @@ -0,0 +1,330 @@ +const assignManager = { + MENU_ASSIGN_ID: "open-in-this-container", + MENU_REMOVE_ID: "remove-open-in-this-container", + storageArea: { + area: browser.storage.local, + exemptedTabs: {}, + + getSiteStoreKey(pageUrl) { + const url = new window.URL(pageUrl); + const storagePrefix = "siteContainerMap@@_"; + return `${storagePrefix}${url.hostname}`; + }, + + setExempted(pageUrl, tabId) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + if (!(siteStoreKey in this.exemptedTabs)) { + this.exemptedTabs[siteStoreKey] = []; + } + this.exemptedTabs[siteStoreKey].push(tabId); + }, + + removeExempted(pageUrl) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + this.exemptedTabs[siteStoreKey] = []; + }, + + isExempted(pageUrl, tabId) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + if (!(siteStoreKey in this.exemptedTabs)) { + return false; + } + return this.exemptedTabs[siteStoreKey].includes(tabId); + }, + + get(pageUrl) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + return new Promise((resolve, reject) => { + this.area.get([siteStoreKey]).then((storageResponse) => { + if (storageResponse && siteStoreKey in storageResponse) { + resolve(storageResponse[siteStoreKey]); + } + resolve(null); + }).catch((e) => { + reject(e); + }); + }); + }, + + set(pageUrl, data, exemptedTabIds) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + if (exemptedTabIds) { + exemptedTabIds.forEach((tabId) => { + this.setExempted(pageUrl, tabId); + }); + } + return this.area.set({ + [siteStoreKey]: data + }); + }, + + remove(pageUrl) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + // When we remove an assignment we should clear all the exemptions + this.removeExempted(pageUrl); + return this.area.remove([siteStoreKey]); + }, + + async deleteContainer(userContextId) { + const sitesByContainer = await this.getByContainer(userContextId); + this.area.remove(Object.keys(sitesByContainer)); + }, + + async getByContainer(userContextId) { + const sites = {}; + const siteConfigs = await this.area.get(); + Object.keys(siteConfigs).forEach((key) => { + // For some reason this is stored as string... lets check them both as that + if (String(siteConfigs[key].userContextId) === String(userContextId)) { + const site = siteConfigs[key]; + // In hindsight we should have stored this + // TODO file a follow up to clean the storage onLoad + site.hostname = key.replace(/^siteContainerMap@@_/, ""); + sites[key] = site; + } + }); + return sites; + } + }, + + _neverAsk(m) { + const pageUrl = m.pageUrl; + if (m.neverAsk === true) { + // If we have existing data and for some reason it hasn't been deleted etc lets update it + this.storageArea.get(pageUrl).then((siteSettings) => { + if (siteSettings) { + siteSettings.neverAsk = true; + this.storageArea.set(pageUrl, siteSettings); + } + }).catch((e) => { + throw e; + }); + } + }, + + // We return here so the confirm page can load the tab when exempted + async _exemptTab(m) { + const pageUrl = m.pageUrl; + this.storageArea.setExempted(pageUrl, m.tabId); + return true; + }, + + init() { + browser.contextMenus.onClicked.addListener((info, tab) => { + this._onClickedHandler(info, tab); + }); + + // Before a request is handled by the browser we decide if we should route through a different container + browser.webRequest.onBeforeRequest.addListener((options) => { + if (options.frameId !== 0 || options.tabId === -1) { + return {}; + } + this.removeContextMenu(); + return Promise.all([ + browser.tabs.get(options.tabId), + this.storageArea.get(options.url) + ]).then(([tab, siteSettings]) => { + const userContextId = this.getUserContextIdFromCookieStore(tab); + if (!siteSettings + || userContextId === siteSettings.userContextId + || tab.incognito + || this.storageArea.isExempted(options.url, tab.id)) { + return {}; + } + + this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk); + this.calculateContextMenu(tab); + + /* Removal of existing tabs: + We aim to open the new assigned container tab / warning prompt in it's own tab: + - As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back() + - When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421 + If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history + Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history: + - Redirects happen from Short URLs and tracking links that act as a gateway + - Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this + however they don't run on about:blank so this would likely be just as hacky. + We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history. + */ + if (backgroundLogic.NEW_TAB_PAGES.has(tab.url) + || (messageHandler.lastCreatedTab + && messageHandler.lastCreatedTab.id === tab.id)) { + browser.tabs.remove(tab.id); + } + return { + cancel: true, + }; + }).catch((e) => { + throw e; + }); + },{urls: [""], types: ["main_frame"]}, ["blocking"]); + }, + + async _onClickedHandler(info, tab) { + const userContextId = this.getUserContextIdFromCookieStore(tab); + // Mapping ${URL(info.pageUrl).hostname} to ${userContextId} + if (userContextId) { + // let actionName; + let remove; + if (info.menuItemId === this.MENU_ASSIGN_ID) { + remove = false; + } else { + remove = true; + } + await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove); + } + }, + + + deleteContainer(userContextId) { + this.storageArea.deleteContainer(userContextId); + }, + + getUserContextIdFromCookieStore(tab) { + if (!("cookieStoreId" in tab)) { + return false; + } + return backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId); + }, + + isTabPermittedAssign(tab) { + // Ensure we are not an important about url + // Ensure we are not in incognito mode + const url = new URL(tab.url); + if (url.protocol === "about:" + || url.protocol === "moz-extension:" + || tab.incognito) { + return false; + } + return true; + }, + + async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) { + let actionName; + + // https://github.com/mozilla/testpilot-containers/issues/626 + // Context menu has stored context IDs as strings, so we need to coerce + // the value to a string for accurate checking + userContextId = String(userContextId); + + if (!remove) { + const tabs = await browser.tabs.query({}); + const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl); + const exemptedTabIds = tabs.filter((tab) => { + const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url); + /* Auto exempt all tabs that exist for this hostname that are not in the same container */ + if (tabStoreKey === assignmentStoreKey && + this.getUserContextIdFromCookieStore(tab) !== userContextId) { + return true; + } + return false; + }).map((tab) => { + return tab.id; + }); + + await this.storageArea.set(pageUrl, { + userContextId, + neverAsk: false + }, exemptedTabIds); + actionName = "added"; + } else { + await this.storageArea.remove(pageUrl); + actionName = "removed"; + } + browser.tabs.sendMessage(tabId, { + text: `Successfully ${actionName} site to always open in this container` + }); + backgroundLogic.sendTelemetryPayload({ + event: `${actionName}-container-assignment`, + userContextId: userContextId, + }); + const tab = await browser.tabs.get(tabId); + this.calculateContextMenu(tab); + }, + + async _getAssignment(tab) { + const cookieStore = this.getUserContextIdFromCookieStore(tab); + // Ensure we have a cookieStore to assign to + if (cookieStore + && this.isTabPermittedAssign(tab)) { + return await this.storageArea.get(tab.url); + } + return false; + }, + + _getByContainer(userContextId) { + return this.storageArea.getByContainer(userContextId); + }, + + removeContextMenu() { + // There is a focus issue in this menu where if you change window with a context menu click + // you get the wrong menu display because of async + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16 + // We also can't change for always private mode + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102 + browser.contextMenus.remove(this.MENU_ASSIGN_ID); + browser.contextMenus.remove(this.MENU_REMOVE_ID); + }, + + async calculateContextMenu(tab) { + this.removeContextMenu(); + const siteSettings = await this._getAssignment(tab); + // Return early and not add an item if we have false + // False represents assignment is not permitted + if (siteSettings === false) { + return false; + } + // ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418 + let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick + let menuId = this.MENU_ASSIGN_ID; + const tabUserContextId = this.getUserContextIdFromCookieStore(tab); + if (siteSettings && + Number(siteSettings.userContextId) === Number(tabUserContextId)) { + prefix = "✓"; + menuId = this.MENU_REMOVE_ID; + } + browser.contextMenus.create({ + id: menuId, + title: `${prefix} Always Open in This Container`, + checked: true, + contexts: ["all"], + }); + }, + + reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) { + const cookieStoreId = backgroundLogic.cookieStoreId(userContextId); + const loadPage = browser.extension.getURL("confirm-page.html"); + // False represents assignment is not permitted + // If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there + if (neverAsk) { + browser.tabs.create({url, cookieStoreId, index}); + backgroundLogic.sendTelemetryPayload({ + event: "auto-reload-page-in-container", + userContextId: userContextId, + }); + } else { + backgroundLogic.sendTelemetryPayload({ + event: "prompt-to-reload-page-in-container", + userContextId: userContextId, + }); + let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`; + let currentCookieStoreId; + if (currentUserContextId) { + currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId); + confirmUrl += `¤tCookieStoreId=${currentCookieStoreId}`; + } + browser.tabs.create({ + url: confirmUrl, + cookieStoreId: currentCookieStoreId, + index + }).then(() => { + // We don't want to sync this URL ever nor clutter the users history + browser.history.deleteUrl({url: confirmUrl}); + }).catch((e) => { + throw e; + }); + } + } +}; + +assignManager.init(); diff --git a/webextension/js/background/backgroundLogic.js b/webextension/js/background/backgroundLogic.js new file mode 100644 index 0000000..42ee382 --- /dev/null +++ b/webextension/js/background/backgroundLogic.js @@ -0,0 +1,345 @@ +const DEFAULT_TAB = "about:newtab"; +const backgroundLogic = { + NEW_TAB_PAGES: new Set([ + "about:startpage", + "about:newtab", + "about:home", + "about:blank" + ]), + + async getExtensionInfo() { + const manifestPath = browser.extension.getURL("manifest.json"); + const response = await fetch(manifestPath); + const extensionInfo = await response.json(); + return extensionInfo; + }, + + getUserContextIdFromCookieStoreId(cookieStoreId) { + if (!cookieStoreId) { + return false; + } + const container = cookieStoreId.replace("firefox-container-", ""); + if (container !== cookieStoreId) { + return container; + } + return false; + }, + + async deleteContainer(userContextId) { + this.sendTelemetryPayload({ + event: "delete-container", + userContextId + }); + + await this._closeTabs(userContextId); + await browser.contextualIdentities.remove(this.cookieStoreId(userContextId)); + assignManager.deleteContainer(userContextId); + await browser.runtime.sendMessage({ + method: "forgetIdentityAndRefresh" + }); + return {done: true, userContextId}; + }, + + async createOrUpdateContainer(options) { + let donePromise; + if (options.userContextId !== "new") { + donePromise = browser.contextualIdentities.update( + this.cookieStoreId(options.userContextId), + options.params + ); + this.sendTelemetryPayload({ + event: "edit-container", + userContextId: options.userContextId + }); + } else { + donePromise = browser.contextualIdentities.create(options.params); + this.sendTelemetryPayload({ + event: "add-container" + }); + } + await donePromise; + browser.runtime.sendMessage({ + method: "refreshNeeded" + }); + }, + + async openTab(options) { + let url = options.url || undefined; + const userContextId = ("userContextId" in options) ? options.userContextId : 0; + const active = ("nofocus" in options) ? options.nofocus : true; + const source = ("source" in options) ? options.source : null; + + const cookieStoreId = backgroundLogic.cookieStoreId(userContextId); + // Only send telemetry for tabs opened by UI - i.e., not via showTabs + if (source && userContextId) { + this.sendTelemetryPayload({ + "event": "open-tab", + "eventSource": source, + "userContextId": userContextId, + "clickedContainerTabCount": await identityState.containerTabCount(cookieStoreId) + }); + } + // Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072 + + // We can't open new tab pages, so open a blank tab. Used in tab un-hide + if (this.NEW_TAB_PAGES.has(url)) { + url = undefined; + } + + // Unhide all hidden tabs + this.showTabs({ + cookieStoreId + }); + return browser.tabs.create({ + url, + active, + pinned: options.pinned || false, + cookieStoreId + }); + }, + + async getTabs(options) { + if (!("cookieStoreId" in options)) { + return new Error("getTabs must be called with cookieStoreId argument."); + } + + const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); + await identityState.remapTabsIfMissing(options.cookieStoreId); + const isKnownContainer = await identityState._isKnownContainer(userContextId); + if (!isKnownContainer) { + return []; + } + + const list = []; + const tabs = await this._containerTabs(options.cookieStoreId); + tabs.forEach((tab) => { + list.push(identityState._createTabObject(tab)); + }); + + const containerState = await identityState.storageArea.get(options.cookieStoreId); + return list.concat(containerState.hiddenTabs); + }, + + async moveTabsToWindow(options) { + if (!("cookieStoreId" in options)) { + return new Error("moveTabsToWindow must be called with cookieStoreId argument."); + } + + const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); + await identityState.remapTabsIfMissing(options.cookieStoreId); + if (!identityState._isKnownContainer(userContextId)) { + return null; + } + + this.sendTelemetryPayload({ + "event": "move-tabs-to-window", + "userContextId": userContextId, + "clickedContainerTabCount": identityState.containerTabCount(userContextId), + }); + + const list = await identityState._matchTabsByContainer(options.cookieStoreId); + + const containerState = await identityState.storageArea.get(options.cookieStoreId); + // Nothing to do + if (list.length === 0 && + containerState.hiddenTabs.length === 0) { + return; + } + const window = await browser.windows.create({ + tabId: list.shift().id + }); + browser.tabs.move(list, { + windowId: window.id, + index: -1 + }); + + // Let's show the hidden tabs. + for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const + browser.tabs.create(object.url || DEFAULT_TAB, { + windowId: window.id, + cookieStoreId: options.cookieStoreId + }); + } + + containerState.hiddenTabs = []; + + // Let's close all the normal tab in the new window. In theory it + // should be only the first tab, but maybe there are addons doing + // crazy stuff. + const tabs = browser.tabs.query({windowId: window.id}); + for (let tab of tabs) { // eslint-disable-line prefer-const + if (tabs.cookieStoreId !== options.cookieStoreId) { + browser.tabs.remove(tab.id); + } + } + return await identityState.storageArea.set(options.cookieStoreId, containerState); + }, + + async _closeTabs(userContextId) { + const cookieStoreId = this.cookieStoreId(userContextId); + const tabs = await this._containerTabs(cookieStoreId); + const tabIds = tabs.map((tab) => tab.id); + return browser.tabs.remove(tabIds); + }, + + async queryIdentitiesState() { + const identities = await browser.contextualIdentities.query({}); + const identitiesOutput = {}; + const identitiesPromise = identities.map(async function (identity) { + await identityState.remapTabsIfMissing(identity.cookieStoreId); + const containerState = await identityState.storageArea.get(identity.cookieStoreId); + identitiesOutput[identity.cookieStoreId] = { + hasHiddenTabs: !!containerState.hiddenTabs.length, + hasOpenTabs: !!containerState.openTabs + }; + return; + }); + await Promise.all(identitiesPromise); + return identitiesOutput; + }, + + async sortTabs() { + const containersCounts = identityState.containersCounts(); + this.sendTelemetryPayload({ + "event": "sort-tabs", + "shownContainersCount": containersCounts.shown, + "totalContainerTabsCount": await identityState.totalContainerTabsCount(), + "totalNonContainerTabsCount": await identityState.totalNonContainerTabsCount() + }); + const windows = await browser.windows.getAll(); + for (let window of windows) { // eslint-disable-line prefer-const + // First the pinned tabs, then the normal ones. + await this._sortTabsInternal(window, true); + await this._sortTabsInternal(window, false); + } + }, + + async _sortTabsInternal(window, pinnedTabs) { + const tabs = await browser.tabs.query({windowId: window.id}); + let pos = 0; + + // Let's collect UCIs/tabs for this window. + const map = new Map; + for (const tab of tabs) { + if (pinnedTabs && !tab.pinned) { + // We don't have, or we already handled all the pinned tabs. + break; + } + + if (!pinnedTabs && tab.pinned) { + // pinned tabs must be consider as taken positions. + ++pos; + continue; + } + + const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId); + if (!map.has(userContextId)) { + map.set(userContextId, []); + } + map.get(userContextId).push(tab); + } + + // Let's sort the map. + const sortMap = new Map([...map.entries()].sort((a, b) => a[0] > b[0])); + + // Let's move tabs. + sortMap.forEach(tabs => { + for (const tab of tabs) { + ++pos; + browser.tabs.move(tab.id, { + windowId: window.id, + index: pos + }); + //xulWindow.gBrowser.moveTabTo(tab, pos++); + } + }); + }, + + async hideTabs(options) { + if (!("cookieStoreId" in options)) { + return new Error("hideTabs must be called with cookieStoreId option."); + } + + const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); + await identityState.remapTabsIfMissing(options.cookieStoreId); + const isKnownContainer = await identityState._isKnownContainer(userContextId); + if (!isKnownContainer) { + return null; + } + + const containersCounts = identityState.containersCounts(); + this.sendTelemetryPayload({ + "event": "hide-tabs", + "userContextId": userContextId, + "clickedContainerTabCount": identityState.containerTabCount(userContextId), + "shownContainersCount": containersCounts.shown, + "hiddenContainersCount": containersCounts.hidden, + "totalContainersCount": containersCounts.total + }); + + const containerState = await identityState.storeHidden(options.cookieStoreId); + await this._closeTabs(userContextId); + return containerState; + }, + + async showTabs(options) { + if (!("cookieStoreId" in options)) { + return Promise.reject("showTabs must be called with cookieStoreId argument."); + } + + const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId); + await identityState.remapTabsIfMissing(options.cookieStoreId); + if (!identityState._isKnownContainer(userContextId)) { + return null; + } + + const containersCounts = identityState.containersCounts(); + this.sendTelemetryPayload({ + "event": "show-tabs", + "userContextId": userContextId, + "clickedContainerTabCount": await identityState.containerTabCount(options.cookieStoreId), + "shownContainersCount": containersCounts.shown, + "hiddenContainersCount": containersCounts.hidden, + "totalContainersCount": containersCounts.total + }); + + const promises = []; + + const containerState = await identityState.storageArea.get(options.cookieStoreId); + + for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const + promises.push(this.openTab({ + userContextId: userContextId, + url: object.url, + nofocus: options.nofocus || false, + pinned: object.pinned, + })); + } + + containerState.hiddenTabs = []; + + await Promise.all(promises); + return await identityState.storageArea.set(options.cookieStoreId, containerState); + }, + + + sendTelemetryPayload(message = {}) { + if (!message.event) { + throw new Error("Missing event name for telemetry"); + } + message.method = "sendTelemetryPayload"; + //TODO decide where this goes + // browser.runtime.sendMessage(message); + }, + + cookieStoreId(userContextId) { + return `firefox-container-${userContextId}`; + }, + + _containerTabs(cookieStoreId) { + return browser.tabs.query({ + cookieStoreId + }).catch((e) => {throw e;}); + }, +}; + diff --git a/webextension/js/background/badge.js b/webextension/js/background/badge.js new file mode 100644 index 0000000..3a9abc7 --- /dev/null +++ b/webextension/js/background/badge.js @@ -0,0 +1,18 @@ +const MAJOR_VERSIONS = ["2.3.0", "2.4.0"]; +const badge = { + init() { + this.displayBrowserActionBadge(); + }, + async displayBrowserActionBadge() { + const extensionInfo = await backgroundLogic.getExtensionInfo(); + const storage = await browser.storage.local.get({browserActionBadgesClicked: []}); + + if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 && + storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) { + browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"}); + browser.browserAction.setBadgeText({text: "NEW"}); + } + } +}; + +badge.init(); diff --git a/webextension/js/background/identityState.js b/webextension/js/background/identityState.js new file mode 100644 index 0000000..2bc6205 --- /dev/null +++ b/webextension/js/background/identityState.js @@ -0,0 +1,159 @@ +const identityState = { + storageArea: { + area: browser.storage.local, + + getContainerStoreKey(cookieStoreId) { + const storagePrefix = "identitiesState@@_"; + return `${storagePrefix}${cookieStoreId}`; + }, + + async get(cookieStoreId) { + const storeKey = this.getContainerStoreKey(cookieStoreId); + const storageResponse = await this.area.get([storeKey]); + if (storageResponse && storeKey in storageResponse) { + return storageResponse[storeKey]; + } + return null; + }, + + set(cookieStoreId, data) { + const storeKey = this.getContainerStoreKey(cookieStoreId); + return this.area.set({ + [storeKey]: data + }); + }, + + remove(cookieStoreId) { + const storeKey = this.getContainerStoreKey(cookieStoreId); + return this.area.remove([storeKey]); + } + }, + + async _isKnownContainer(userContextId) { + const cookieStoreId = backgroundLogic.cookieStoreId(userContextId); + const state = await this.storageArea.get(cookieStoreId); + return !!state; + }, + + _createTabObject(tab) { + return Object.assign({}, tab); + }, + + async storeHidden(cookieStoreId) { + const containerState = await this.storageArea.get(cookieStoreId); + const tabsByContainer = await this._matchTabsByContainer(cookieStoreId); + tabsByContainer.forEach((tab) => { + const tabObject = this._createTabObject(tab); + // This tab is going to be closed. Let's mark this tabObject as + // non-active. + tabObject.active = false; + tabObject.hiddenState = true; + containerState.hiddenTabs.push(tabObject); + }); + + return this.storageArea.set(cookieStoreId, containerState); + }, + + async containersCounts() { + let containersCounts = { // eslint-disable-line prefer-const + "shown": 0, + "hidden": 0, + "total": 0 + }; + 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) { + ++containersCounts.shown; + ++containersCounts.total; + continue; + } else if (containerState.hiddenTabs.length > 0) { + ++containersCounts.hidden; + ++containersCounts.total; + continue; + } + } + return containersCounts; + }, + + async containerTabCount(cookieStoreId) { + // Returns the total of open and hidden tabs with this userContextId + let containerTabsCount = 0; + await identityState.remapTabsIfMissing(cookieStoreId); + const containerState = await this.storageArea.get(cookieStoreId); + containerTabsCount += containerState.openTabs; + containerTabsCount += containerState.hiddenTabs.length; + return containerTabsCount; + }, + + async totalContainerTabsCount() { + // Returns the number of total open tabs across ALL containers + let totalContainerTabsCount = 0; + const containers = await browser.contextualIdentities.query({}); + for (const id in containers) { + const container = containers[id]; + const cookieStoreId = container.cookieStoreId; + await identityState.remapTabsIfMissing(cookieStoreId); + totalContainerTabsCount += await this.storageArea.get(cookieStoreId).openTabs; + } + return totalContainerTabsCount; + }, + + async totalNonContainerTabsCount() { + // Returns the number of open tabs NOT IN a container + let totalNonContainerTabsCount = 0; + const tabs = await browser.tabs.query({}); + for (const tab of tabs) { + const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId); + if (userContextId === 0) { + ++totalNonContainerTabsCount; + } + } + return totalNonContainerTabsCount; + }, + + 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); + }, + + _matchTabsByContainer(cookieStoreId) { + return browser.tabs.query({cookieStoreId}); + }, + + async remapTabsFromUserContextId(cookieStoreId) { + const tabsByContainer = await this._matchTabsByContainer(cookieStoreId); + const containerState = await this.storageArea.get(cookieStoreId); + 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 { + hiddenTabs: [], + openTabs: 0 + }; + }, +}; diff --git a/webextension/js/background/index.html b/webextension/js/background/index.html new file mode 100644 index 0000000..24a0eba --- /dev/null +++ b/webextension/js/background/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/webextension/js/background/init.js b/webextension/js/background/init.js new file mode 100644 index 0000000..3875580 --- /dev/null +++ b/webextension/js/background/init.js @@ -0,0 +1,27 @@ +browser.runtime.sendMessage({ + method: "getPreference", + pref: "browser.privatebrowsing.autostart" +}).then(pbAutoStart => { + + // We don't want to disable the addon if we are in auto private-browsing. + if (!pbAutoStart) { + browser.tabs.onCreated.addListener(tab => { + if (tab.incognito) { + disableAddon(tab.id); + } + }); + + browser.tabs.query({}).then(tabs => { + for (let tab of tabs) { // eslint-disable-line prefer-const + if (tab.incognito) { + disableAddon(tab.id); + } + } + }).catch(() => {}); + } +}).catch(() => {}); + +function disableAddon(tabId) { + browser.browserAction.disable(tabId); + browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" }); +} diff --git a/webextension/js/background/messageHandler.js b/webextension/js/background/messageHandler.js new file mode 100644 index 0000000..5a73d2b --- /dev/null +++ b/webextension/js/background/messageHandler.js @@ -0,0 +1,177 @@ +const messageHandler = { + // After the timer completes we assume it's a tab the user meant to keep open + // We use this to catch redirected tabs that have just opened + // If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click + LAST_CREATED_TAB_TIMER: 2000, + + init() { + // Handles messages from webextension code + browser.runtime.onMessage.addListener((m) => { + let response; + + switch (m.method) { + case "deleteContainer": + response = backgroundLogic.deleteContainer(m.message.userContextId); + break; + case "createOrUpdateContainer": + response = backgroundLogic.createOrUpdateContainer(m.message); + break; + case "openTab": + // Same as open-tab for index.js + response = backgroundLogic.openTab(m.message); + break; + case "neverAsk": + assignManager._neverAsk(m); + break; + case "getAssignment": + response = browser.tabs.get(m.tabId).then((tab) => { + return assignManager._getAssignment(tab); + }); + break; + case "getAssignmentObjectByContainer": + response = assignManager._getByContainer(m.message.userContextId); + break; + case "setOrRemoveAssignment": + // m.tabId is used for where to place the in content message + // m.url is the assignment to be removed/added + response = browser.tabs.get(m.tabId).then((tab) => { + return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value); + }); + break; + case "sendTelemetryPayload": + // TODO + break; + case "sortTabs": + backgroundLogic.sortTabs(); + break; + case "showTabs": + backgroundLogic.showTabs({cookieStoreId: m.cookieStoreId}); + break; + case "hideTabs": + backgroundLogic.hideTabs({cookieStoreId: m.cookieStoreId}); + break; + case "checkIncompatibleAddons": + // TODO + break; + case "getShieldStudyVariation": + // TODO + break; + case "moveTabsToWindow": + response = backgroundLogic.moveTabsToWindow({ + cookieStoreId: m.cookieStoreId + }); + break; + case "getTabs": + response = backgroundLogic.getTabs({ + cookieStoreId: m.cookieStoreId + }); + break; + case "queryIdentitiesState": + response = backgroundLogic.queryIdentitiesState(); + break; + case "exemptContainerAssignment": + response = assignManager._exemptTab(m); + break; + } + return response; + }); + + // Handles messages from sdk code + const port = browser.runtime.connect(); + port.onMessage.addListener(m => { + switch (m.type) { + case "lightweight-theme-changed": + themeManager.update(m.message); + break; + case "open-tab": + backgroundLogic.openTab(m.message); + break; + default: + throw new Error(`Unhandled message type: ${m.message}`); + } + }); + + browser.tabs.onCreated.addListener((tab) => { + // This works at capturing the tabs as they are created + // However we need onFocusChanged and onActivated to capture the initial tab + if (tab.id === -1) { + return {}; + } + tabPageCounter.initTabCounter(tab); + }); + + browser.tabs.onRemoved.addListener((tabId) => { + if (tabId === -1) { + return {}; + } + tabPageCounter.sendTabCountAndDelete(tabId); + }); + + browser.tabs.onActivated.addListener((info) => { + assignManager.removeContextMenu(); + browser.tabs.get(info.tabId).then((tab) => { + tabPageCounter.initTabCounter(tab); + assignManager.calculateContextMenu(tab); + }).catch((e) => { + throw e; + }); + }); + + browser.windows.onFocusChanged.addListener((windowId) => { + assignManager.removeContextMenu(); + // browserAction loses background color in new windows ... + // https://bugzil.la/1314674 + // https://github.com/mozilla/testpilot-containers/issues/608 + // ... so re-call displayBrowserActionBadge on window changes + badge.displayBrowserActionBadge(); + browser.tabs.query({active: true, windowId}).then((tabs) => { + if (tabs && tabs[0]) { + tabPageCounter.initTabCounter(tabs[0]); + assignManager.calculateContextMenu(tabs[0]); + } + }).catch((e) => { + throw e; + }); + }); + + browser.idle.onStateChanged.addListener((newState) => { + browser.tabs.query({}).then(tabs => { + for (let tab of tabs) { // eslint-disable-line prefer-const + if (newState === "idle") { + tabPageCounter.sendTabCountAndDelete(tab.id, "user-went-idle"); + } else if (newState === "active" && tab.active) { + tabPageCounter.initTabCounter(tab); + } + } + }).catch(e => { + throw e; + }); + }); + + browser.webRequest.onCompleted.addListener((details) => { + if (details.frameId !== 0 || details.tabId === -1) { + return {}; + } + assignManager.removeContextMenu(); + + browser.tabs.get(details.tabId).then((tab) => { + tabPageCounter.incrementTabCount(tab); + assignManager.calculateContextMenu(tab); + }).catch((e) => { + throw e; + }); + }, {urls: [""], types: ["main_frame"]}); + + // lets remember the last tab created so we can close it if it looks like a redirect + browser.tabs.onCreated.addListener((details) => { + this.lastCreatedTab = details; + setTimeout(() => { + this.lastCreatedTab = null; + }, this.LAST_CREATED_TAB_TIMER); + }); + + } +}; + +// Lets do this last as theme manager did a check before connecting before +messageHandler.init(); diff --git a/webextension/js/background/tabPageCounter.js b/webextension/js/background/tabPageCounter.js new file mode 100644 index 0000000..f639f5e --- /dev/null +++ b/webextension/js/background/tabPageCounter.js @@ -0,0 +1,64 @@ +const tabPageCounter = { + counters: {}, + + initTabCounter(tab) { + if (tab.id in this.counters) { + if (!("activity" in this.counters[tab.id])) { + this.counters[tab.id].activity = { + "cookieStoreId": tab.cookieStoreId, + "pageRequests": 0 + }; + } + if (!("tab" in this.counters[tab.id])) { + this.counters[tab.id].tab = { + "cookieStoreId": tab.cookieStoreId, + "pageRequests": 0 + }; + } + } else { + this.counters[tab.id] = {}; + this.counters[tab.id].tab = { + "cookieStoreId": tab.cookieStoreId, + "pageRequests": 0 + }; + this.counters[tab.id].activity = { + "cookieStoreId": tab.cookieStoreId, + "pageRequests": 0 + }; + } + }, + + sendTabCountAndDelete(tabId, why = "user-closed-tab") { + if (!(this.counters[tabId])) { + return; + } + if (why === "user-closed-tab" && this.counters[tabId].tab) { + backgroundLogic.sendTelemetryPayload({ + event: "page-requests-completed-per-tab", + userContextId: this.counters[tabId].tab.cookieStoreId, + pageRequestCount: this.counters[tabId].tab.pageRequests + }); + // When we send the ping because the user closed the tab, + // delete both the 'tab' and 'activity' counters + delete this.counters[tabId]; + } else if (why === "user-went-idle" && this.counters[tabId].activity) { + backgroundLogic.sendTelemetryPayload({ + event: "page-requests-completed-per-activity", + userContextId: this.counters[tabId].activity.cookieStoreId, + pageRequestCount: this.counters[tabId].activity.pageRequests + }); + // When we send the ping because the user went idle, + // only reset the 'activity' counter + this.counters[tabId].activity = { + "cookieStoreId": this.counters[tabId].tab.cookieStoreId, + "pageRequests": 0 + }; + } + }, + + incrementTabCount(tab) { + this.initTabCounter(tab); + this.counters[tab.id].tab.pageRequests++; + this.counters[tab.id].activity.pageRequests++; + } +}; diff --git a/webextension/js/background/themeManager.js b/webextension/js/background/themeManager.js new file mode 100644 index 0000000..b77ade6 --- /dev/null +++ b/webextension/js/background/themeManager.js @@ -0,0 +1,51 @@ +const THEME_BUILD_DATE = 20170630; +const themeManager = { + existingTheme: null, + disabled: false, + async init() { + const browserInfo = await browser.runtime.getBrowserInfo(); + if (Number(browserInfo.buildID.substring(0, 8)) >= THEME_BUILD_DATE) { + this.disabled = true; + } else { + this.check(); + } + }, + setPopupIcon(theme) { + if (this.disabled) { + return; + } + let icons = { + 16: "img/container-site-d-24.png", + 32: "img/container-site-d-48.png" + }; + if (theme === "firefox-compact-dark@mozilla.org") { + icons = { + 16: "img/container-site-w-24.png", + 32: "img/container-site-w-48.png" + }; + } + browser.browserAction.setIcon({ + path: icons + }); + }, + check() { + if (this.disabled) { + return; + } + browser.runtime.sendMessage({ + method: "getTheme" + }).then((theme) => { + this.update(theme); + }).catch(() => { + throw new Error("Unable to get theme"); + }); + }, + update(theme) { + if (this.existingTheme !== theme) { + this.setPopupIcon(theme); + this.existingTheme = theme; + } + } +}; + +themeManager.init(); diff --git a/webextension/js/popup.js b/webextension/js/popup.js index c8f77bc..bcf133c 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -183,22 +183,21 @@ const Logic = { return false; }, - refreshIdentities() { - return Promise.all([ + async refreshIdentities() { + const [identities, state] = await Promise.all([ browser.contextualIdentities.query({}), browser.runtime.sendMessage({ method: "queryIdentitiesState" }) - ]).then(([identities, state]) => { - this._identities = identities.map((identity) => { - const stateObject = state[Logic.userContextId(identity.cookieStoreId)]; - if (stateObject) { - identity.hasOpenTabs = stateObject.hasOpenTabs; - identity.hasHiddenTabs = stateObject.hasHiddenTabs; - } - return identity; - }); - }).catch((e) => {throw e;}); + ]); + this._identities = identities.map((identity) => { + const stateObject = state[identity.cookieStoreId]; + if (stateObject) { + identity.hasOpenTabs = stateObject.hasOpenTabs; + identity.hasHiddenTabs = stateObject.hasHiddenTabs; + } + return identity; + }); }, getPanelSelector(panel) { @@ -313,12 +312,11 @@ const Logic = { }); }, - getShieldStudyVariation() { - return browser.runtime.sendMessage({ + async getShieldStudyVariation() { + const variation = await browser.runtime.sendMessage({ method: "getShieldStudyVariation" - }).then(variation => { - this._onboardingVariation = variation; }); + this._onboardingVariation = variation; }, generateIdentityName() { @@ -477,14 +475,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, { Logic.showPanel(P_CONTAINERS_EDIT); }); - Logic.addEnterHandler(document.querySelector("#sort-containers-link"), () => { - browser.runtime.sendMessage({ - method: "sortTabs" - }).then(() => { + Logic.addEnterHandler(document.querySelector("#sort-containers-link"), async function () { + try { + await browser.runtime.sendMessage({ + method: "sortTabs" + }); window.close(); - }).catch(() => { + } catch (e) { window.close(); - }); + } }); document.addEventListener("keydown", (e) => { @@ -492,13 +491,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, { function next() { const nextElement = element.nextElementSibling; if (nextElement) { - nextElement.querySelector("td[tabindex=0]").focus(); +//TOFIX, dunno when this broke + nextElement.querySelector(`td[tabindex="0"]`).focus(); } } function previous() { const previousElement = element.previousElementSibling; if (previousElement) { - previousElement.querySelector("td[tabindex=0]").focus(); +//TOFIX, dunno when this broke + previousElement.querySelector(`td[tabindex="0"]`).focus(); } } switch (e.keyCode) { @@ -608,21 +609,22 @@ Logic.registerPanel(P_CONTAINERS_LIST, { tr.appendChild(manage); } - Logic.addEnterHandler(tr, e => { + Logic.addEnterHandler(tr, async function (e) { if (e.target.matches(".open-newtab") || e.target.parentNode.matches(".open-newtab") || e.type === "keydown") { - browser.runtime.sendMessage({ - method: "openTab", - message: { - userContextId: Logic.userContextId(identity.cookieStoreId), - source: "pop-up" - } - }).then(() => { + try { + await browser.runtime.sendMessage({ + method: "openTab", + message: { + userContextId: Logic.userContextId(identity.cookieStoreId), + source: "pop-up" + } + }); window.close(); - }).catch(() => { + } catch (e) { window.close(); - }); + } } else if (hasTabs) { Logic.showPanel(P_CONTAINER_INFO, identity); } @@ -657,27 +659,29 @@ Logic.registerPanel(P_CONTAINER_INFO, { panelSelector: "#container-info-panel", // This method is called when the object is registered. - initialize() { + async initialize() { Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => { Logic.showPreviousPanel(); }); - Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), () => { + Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), async function () { const identity = Logic.currentIdentity(); - browser.runtime.sendMessage({ - method: identity.hasHiddenTabs ? "showTabs" : "hideTabs", - cookieStoreId: Logic.currentCookieStoreId() - }).then(() => { + try { + browser.runtime.sendMessage({ + method: identity.hasHiddenTabs ? "showTabs" : "hideTabs", + cookieStoreId: Logic.currentCookieStoreId() + }); window.close(); - }).catch(() => { + } catch (e) { window.close(); - }); + } }); // Check if the user has incompatible add-ons installed - browser.runtime.sendMessage({ - method: "checkIncompatibleAddons" - }).then(incompatible => { + try { + const incompatible = await browser.runtime.sendMessage({ + method: "checkIncompatibleAddons" + }); const moveTabsEl = document.querySelector("#container-info-movetabs"); if (incompatible) { const fragment = document.createDocumentFragment(); @@ -693,22 +697,21 @@ Logic.registerPanel(P_CONTAINER_INFO, { moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling); } else { - Logic.addEnterHandler(moveTabsEl, () => { - browser.runtime.sendMessage({ + Logic.addEnterHandler(moveTabsEl, async function () { + await browser.runtime.sendMessage({ method: "moveTabsToWindow", - userContextId: Logic.userContextId(Logic.currentIdentity().cookieStoreId), - }).then(() => { - window.close(); - }).catch((e) => { throw e; }); + cookieStoreId: Logic.currentIdentity().cookieStoreId, + }); + window.close(); }); } - }).catch(() => { + } catch (e) { throw new Error("Could not check for incompatible add-ons."); - }); + } }, // This method is called when the panel is shown. - prepare() { + async prepare() { const identity = Logic.currentIdentity(); // Populating the panel: name and icon @@ -736,10 +739,11 @@ Logic.registerPanel(P_CONTAINER_INFO, { } // Let's retrieve the list of tabs. - return browser.runtime.sendMessage({ + const tabs = await browser.runtime.sendMessage({ method: "getTabs", - userContextId: Logic.currentUserContextId(), - }).then(this.buildInfoTable); + cookieStoreId: Logic.currentIdentity().cookieStoreId + }); + return this.buildInfoTable(tabs); }, buildInfoTable(tabs) { @@ -752,15 +756,14 @@ Logic.registerPanel(P_CONTAINER_INFO, { tr.innerHTML = escaped` ${tab.title}`; - tr.querySelector("td").appendChild(Utils.createFavIconElement(tab.favicon)); + tr.querySelector("td").appendChild(Utils.createFavIconElement(tab.favIconUrl)); // On click, we activate this tab. But only if this tab is active. - if (tab.active) { + if (!tab.hiddenState) { tr.classList.add("clickable"); - Logic.addEnterHandler(tr, () => { - browser.tabs.update(tab.id, {selected: true}).then(() => { - window.close(); - }); + Logic.addEnterHandler(tr, async function () { + await browser.tabs.update(tab.id, {active: true}); + window.close(); }); } } @@ -871,25 +874,25 @@ Logic.registerPanel(P_CONTAINER_EDIT, { }, - _submitForm() { + async _submitForm() { const formValues = new FormData(this._editForm); - return browser.runtime.sendMessage({ - method: "createOrUpdateContainer", - message: { - userContextId: formValues.get("container-id") || NEW_CONTAINER_ID, - params: { - name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), - icon: formValues.get("container-icon") || DEFAULT_ICON, - color: formValues.get("container-color") || DEFAULT_COLOR, + try { + await browser.runtime.sendMessage({ + method: "createOrUpdateContainer", + message: { + userContextId: formValues.get("container-id") || NEW_CONTAINER_ID, + params: { + name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), + icon: formValues.get("container-icon") || DEFAULT_ICON, + color: formValues.get("container-color") || DEFAULT_COLOR, + } } - } - }).then(() => { - return Logic.refreshIdentities(); - }).then(() => { + }); + await Logic.refreshIdentities(); Logic.showPreviousPanel(); - }).catch(() => { + } catch (e) { Logic.showPanel(P_CONTAINERS_LIST); - }); + } }, showAssignedContainers(assignments) { @@ -919,17 +922,15 @@ Logic.registerPanel(P_CONTAINER_EDIT, { src="/img/container-delete.svg" />`; const deleteButton = trElement.querySelector(".delete-assignment"); - Logic.addEnterHandler(deleteButton, () => { + const that = this; + Logic.addEnterHandler(deleteButton, async function () { const userContextId = Logic.currentUserContextId(); // Lets show the message to the current tab // TODO remove then when firefox supports arrow fn async - Logic.currentTab().then((currentTab) => { - Logic.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true); - delete assignments[siteKey]; - this.showAssignedContainers(assignments); - }).catch((e) => { - throw e; - }); + const currentTab = await Logic.currentTab(); + Logic.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true); + delete assignments[siteKey]; + that.showAssignedContainers(assignments); }); trElement.classList.add("container-info-tab-row", "clickable"); tableElement.appendChild(trElement); @@ -1002,19 +1003,19 @@ Logic.registerPanel(P_CONTAINER_DELETE, { Logic.showPreviousPanel(); }); - Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), () => { + Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), async function () { /* This promise wont resolve if the last tab was removed from the window. as the message async callback stops listening, this isn't an issue for us however it might be in future if you want to do anything post delete do it in the background script. Browser console currently warns about not listening also. */ - Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)).then(() => { - return Logic.refreshIdentities(); - }).then(() => { + try { + await Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)); + await Logic.refreshIdentities(); Logic.showPreviousPanel(); - }).catch(() => { + } catch(e) { Logic.showPanel(P_CONTAINERS_LIST); - }); + } }); }, diff --git a/webextension/manifest.json b/webextension/manifest.json index 9d0d04c..474d8b3 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -85,7 +85,7 @@ }, "background": { - "scripts": ["background.js"] + "page": "js/background/index.html" }, "content_scripts": [