From 50419d6bb1f086807326b4fca91cb0d70198d80e Mon Sep 17 00:00:00 2001 From: luke crouch Date: Tue, 3 Jan 2017 09:06:10 -0600 Subject: [PATCH] re-integrate ContextualIdentityService into SDK code (#27) * bring ContextualIdentityService back into SDK code * restore show/hide functionality in WebExtension * move identitiesState to SDK index to preserve it When a user closes the pop-up, preserve the hidden tabs for the containers, so they can still quickly re-open the tabs. --- index.js | 179 +++- webextension-experiment/LICENSE | 27 - webextension-experiment/README.md | 7 - webextension-experiment/api.js | 1477 --------------------------- webextension-experiment/install.rdf | 31 - webextension-experiment/schema.json | 131 --- webextension/js/popup.js | 69 +- webextension/manifest.json | 2 - 8 files changed, 225 insertions(+), 1698 deletions(-) delete mode 100644 webextension-experiment/LICENSE delete mode 100644 webextension-experiment/README.md delete mode 100644 webextension-experiment/api.js delete mode 100644 webextension-experiment/install.rdf delete mode 100644 webextension-experiment/schema.json diff --git a/index.js b/index.js index 34660cf..b9c8e8b 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,186 @@ /* global require */ +const {ContextualIdentityService} = require('resource://gre/modules/ContextualIdentityService.jsm'); + const tabs = require('sdk/tabs'); const webExtension = require('sdk/webextension'); +const CONTAINER_STORE = 'firefox-container-'; + +const identitiesState = { +}; + +function getCookieStoreIdForContainer(containerId) { + return CONTAINER_STORE + containerId; +} + +function convert(identity) { + const cookieStoreId = getCookieStoreIdForContainer(identity.userContextId); + let hiddenTabUrls = []; + + if (cookieStoreId in identitiesState) { + hiddenTabUrls = identitiesState[cookieStoreId].hiddenTabUrls; + } + const result = { + name: ContextualIdentityService.getUserContextLabel(identity.userContextId), + icon: identity.icon, + color: identity.color, + cookieStoreId: cookieStoreId, + hiddenTabUrls: hiddenTabUrls + }; + + return result; +} + +function isContainerCookieStoreId(storeId) { + return storeId !== null && storeId.startsWith(CONTAINER_STORE); +} + +function getContainerForCookieStoreId(storeId) { + if (!isContainerCookieStoreId(storeId)) { + return null; + } + + const containerId = storeId.substring(CONTAINER_STORE.length); + + if (ContextualIdentityService.getIdentityFromId(containerId)) { + return parseInt(containerId, 10); + } + + return null; +} + +function getContainer(cookieStoreId) { + const containerId = getContainerForCookieStoreId(cookieStoreId); + + if (!containerId) { + return Promise.resolve(null); + } + + const identity = ContextualIdentityService.getIdentityFromId(containerId); + + return Promise.resolve(convert(identity)); +} + +function queryContainers(details) { + const identities = []; + + ContextualIdentityService.getIdentities().forEach(identity=> { + if (details && details.name && + ContextualIdentityService.getUserContextLabel(identity.userContextId) !== details.name) { + return; + } + + const convertedIdentity = convert(identity); + + identities.push(convertedIdentity); + if (!(convertedIdentity.cookieStoreId in identitiesState)) { + identitiesState[convertedIdentity.cookieStoreId] = {hiddenTabUrls: []}; + } + }); + + return Promise.resolve(identities); +} + +function createContainer(details) { + const identity = ContextualIdentityService.create(details.name, + details.icon, + details.color); + + return Promise.resolve(convert(identity)); +} + +function updateContainer(cookieStoreId, details) { + const containerId = getContainerForCookieStoreId(cookieStoreId); + + if (!containerId) { + return Promise.resolve(null); + } + + const identity = ContextualIdentityService.getIdentityFromId(containerId); + + if (!identity) { + return Promise.resolve(null); + } + + if (details.name !== null) { + identity.name = details.name; + } + + if (details.color !== null) { + identity.color = details.color; + } + + if (details.icon !== null) { + identity.icon = details.icon; + } + + if (!ContextualIdentityService.update(identity.userContextId, + identity.name, identity.icon, + identity.color)) { + return Promise.resolve(null); + } + + return Promise.resolve(convert(identity)); +} + +function removeContainer(cookieStoreId) { + const containerId = getContainerForCookieStoreId(cookieStoreId); + + if (!containerId) { + return Promise.resolve(null); + } + + const identity = ContextualIdentityService.getIdentityFromId(containerId); + + if (!identity) { + return Promise.resolve(null); + } + + // We have to create the identity object before removing it. + const convertedIdentity = convert(identity); + + if (!ContextualIdentityService.remove(identity.userContextId)) { + return Promise.resolve(null); + } + + return Promise.resolve(convertedIdentity); +} + +const contextualIdentities = { + get: getContainer, + query: queryContainers, + create: createContainer, + update: updateContainer, + remove: removeContainer +}; + function handleWebExtensionMessage(message, sender, sendReply) { - switch (message) { + switch (message.method) { + case 'query': + sendReply(contextualIdentities.query(message.arguments)); + break; + case 'hide': + identitiesState[message.cookieStoreId].hiddenTabUrls = message.tabUrlsToSave; + break; + case 'show': + sendReply(identitiesState[message.cookieStoreId].hiddenTabUrls); + identitiesState[message.cookieStoreId].hiddenTabUrls = []; + break; + case 'get': + sendReply(contextualIdentities.get(message.arguments)); + break; + case 'create': + sendReply(contextualIdentities.create(message.arguments)); + break; + case 'update': + sendReply(contextualIdentities.update(message.arguments)); + break; + case 'remove': + sendReply(contextualIdentities.remove(message.arguments)); + break; + case 'getIdentitiesState': + sendReply(identitiesState); + break; case 'open-containers-preferences': tabs.open('about:preferences#containers'); sendReply({content: 'opened'}); diff --git a/webextension-experiment/LICENSE b/webextension-experiment/LICENSE deleted file mode 100644 index f979cc3..0000000 --- a/webextension-experiment/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2010 - 2016, Mozilla Corporation -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -* Neither the name of the Mozilla Corporation nor the names of its contributors -may be used to endorse or promote products derived from this software without -specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/webextension-experiment/README.md b/webextension-experiment/README.md deleted file mode 100644 index 17794f1..0000000 --- a/webextension-experiment/README.md +++ /dev/null @@ -1,7 +0,0 @@ - -## Webextension contextual identities API extension - -This project contains the implementation of the Firefox -browser.contextualIdentites API. - -Based on: https://bugzilla.mozilla.org/show_bug.cgi?id=1322856 diff --git a/webextension-experiment/api.js b/webextension-experiment/api.js deleted file mode 100644 index 8ec50b8..0000000 --- a/webextension-experiment/api.js +++ /dev/null @@ -1,1477 +0,0 @@ -"use strict"; -const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; -const {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", - "resource://gre/modules/ContextualIdentityService.jsm"); - -//////////////////////////////////////////////////////////////////////////// -/// ext-cookies -//////////////////////////////////////////////////////////////////////////// -const DEFAULT_STORE = "firefox-default"; -const PRIVATE_STORE = "firefox-private"; -const CONTAINER_STORE = "firefox-container-"; - -const getCookieStoreIdForTab = function(data, tab) { - if (data.incognito) { - return PRIVATE_STORE; - } - - if (tab.userContextId) { - return getCookieStoreIdForContainer(tab.userContextId); - } - - return DEFAULT_STORE; -}; -//////////////////////////////////////////////////////////////////////////// -/// end ext-cookies -//////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////// -/// ext-utils -//////////////////////////////////////////////////////////////////////////// - -/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set sts=2 sw=2 et tw=80: */ -"use strict"; - -XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", - "resource:///modules/CustomizableUI.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService", - "@mozilla.org/content/style-sheet-service;1", - "nsIStyleSheetService"); - -XPCOMUtils.defineLazyGetter(this, "colorUtils", () => { - return require("devtools/shared/css/color").colorUtils; -}); - -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); -Cu.import("resource://gre/modules/AppConstants.jsm"); - -const POPUP_LOAD_TIMEOUT_MS = 200; - -const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - -var { - DefaultWeakMap, - EventManager, - promiseEvent, -} = ExtensionUtils; - -// This file provides some useful code for the |tabs| and |windows| -// modules. All of the code is installed on |global|, which is a scope -// shared among the different ext-*.js scripts. - -const makeWidgetId = id => { - id = id.toLowerCase(); - // FIXME: This allows for collisions. - return id.replace(/[^a-z0-9_-]/g, "_"); -}; - -function promisePopupShown(popup) { - return new Promise(resolve => { - if (popup.state == "open") { - resolve(); - } else { - popup.addEventListener("popupshown", function onPopupShown(event) { - popup.removeEventListener("popupshown", onPopupShown); - resolve(); - }); - } - }); -} - -XPCOMUtils.defineLazyGetter(this, "popupStylesheets", () => { - let stylesheets = ["chrome://browser/content/extension.css"]; - - if (AppConstants.platform === "macosx") { - stylesheets.push("chrome://browser/content/extension-mac.css"); - } - return stylesheets; -}); - -XPCOMUtils.defineLazyGetter(this, "standaloneStylesheets", () => { - let stylesheets = []; - - if (AppConstants.platform === "macosx") { - stylesheets.push("chrome://browser/content/extension-mac-panel.css"); - } - if (AppConstants.platform === "win") { - stylesheets.push("chrome://browser/content/extension-win-panel.css"); - } - return stylesheets; -}); - -class BasePopup { - constructor(extension, viewNode, popupURL, browserStyle, fixedWidth = false) { - this.extension = extension; - this.popupURL = popupURL; - this.viewNode = viewNode; - this.browserStyle = browserStyle; - this.window = viewNode.ownerGlobal; - this.destroyed = false; - this.fixedWidth = fixedWidth; - - extension.callOnClose(this); - - this.contentReady = new Promise(resolve => { - this._resolveContentReady = resolve; - }); - - this.viewNode.addEventListener(this.DESTROY_EVENT, this); - - let doc = viewNode.ownerDocument; - let arrowContent = doc.getAnonymousElementByAttribute(this.panel, "class", "panel-arrowcontent"); - this.borderColor = doc.defaultView.getComputedStyle(arrowContent).borderTopColor; - - this.browser = null; - this.browserLoaded = new Promise((resolve, reject) => { - this.browserLoadedDeferred = {resolve, reject}; - }); - this.browserReady = this.createBrowser(viewNode, popupURL); - - BasePopup.instances.get(this.window).set(extension, this); - } - - static for(extension, window) { - return BasePopup.instances.get(window).get(extension); - } - - close() { - this.closePopup(); - } - - destroy() { - this.extension.forgetOnClose(this); - - this.destroyed = true; - this.browserLoadedDeferred.reject(new Error("Popup destroyed")); - return this.browserReady.then(() => { - this.destroyBrowser(this.browser, true); - this.browser.remove(); - - if (this.viewNode) { - this.viewNode.removeEventListener(this.DESTROY_EVENT, this); - this.viewNode.style.maxHeight = ""; - } - - if (this.panel) { - this.panel.style.removeProperty("--arrowpanel-background"); - this.panel.style.removeProperty("--panel-arrow-image-vertical"); - } - - BasePopup.instances.get(this.window).delete(this.extension); - - this.browser = null; - this.viewNode = null; - }); - } - - destroyBrowser(browser, finalize = false) { - let mm = browser.messageManager; - // If the browser has already been removed from the document, because the - // popup was closed externally, there will be no message manager here, so - // just replace our receiveMessage method with a stub. - if (mm) { - mm.removeMessageListener("DOMTitleChanged", this); - mm.removeMessageListener("Extension:BrowserBackgroundChanged", this); - mm.removeMessageListener("Extension:BrowserContentLoaded", this); - mm.removeMessageListener("Extension:BrowserResized", this); - mm.removeMessageListener("Extension:DOMWindowClose", this); - } else if (finalize) { - this.receiveMessage = () => {}; - } - } - - // Returns the name of the event fired on `viewNode` when the popup is being - // destroyed. This must be implemented by every subclass. - get DESTROY_EVENT() { - throw new Error("Not implemented"); - } - - get STYLESHEETS() { - let sheets = []; - - if (this.browserStyle) { - sheets.push(...popupStylesheets); - } - if (!this.fixedWidth) { - sheets.push(...standaloneStylesheets); - } - - return sheets; - } - - get panel() { - let panel = this.viewNode; - while (panel && panel.localName != "panel") { - panel = panel.parentNode; - } - return panel; - } - - receiveMessage({name, data}) { - switch (name) { - case "DOMTitleChanged": - this.viewNode.setAttribute("aria-label", this.browser.contentTitle); - break; - - case "Extension:BrowserBackgroundChanged": - this.setBackground(data.background); - break; - - case "Extension:BrowserContentLoaded": - this.browserLoadedDeferred.resolve(); - break; - - case "Extension:BrowserResized": - this._resolveContentReady(); - if (this.ignoreResizes) { - this.dimensions = data; - } else { - this.resizeBrowser(data); - } - break; - - case "Extension:DOMWindowClose": - this.closePopup(); - break; - } - } - - handleEvent(event) { - switch (event.type) { - case this.DESTROY_EVENT: - if (!this.destroyed) { - this.destroy(); - } - break; - } - } - - createBrowser(viewNode, popupURL = null) { - let document = viewNode.ownerDocument; - let browser = document.createElementNS(XUL_NS, "browser"); - browser.setAttribute("type", "content"); - browser.setAttribute("disableglobalhistory", "true"); - browser.setAttribute("transparent", "true"); - browser.setAttribute("class", "webextension-popup-browser"); - browser.setAttribute("webextension-view-type", "popup"); - browser.setAttribute("tooltip", "aHTMLTooltip"); - - if (this.extension.remote) { - browser.setAttribute("remote", "true"); - } - - // We only need flex sizing for the sake of the slide-in sub-views of the - // main menu panel, so that the browser occupies the full width of the view, - // and also takes up any extra height that's available to it. - browser.setAttribute("flex", "1"); - - // Note: When using noautohide panels, the popup manager will add width and - // height attributes to the panel, breaking our resize code, if the browser - // starts out smaller than 30px by 10px. This isn't an issue now, but it - // will be if and when we popup debugging. - - this.browser = browser; - - let readyPromise; - if (this.extension.remote) { - readyPromise = promiseEvent(browser, "XULFrameLoaderCreated"); - } else { - readyPromise = promiseEvent(browser, "load"); - } - - viewNode.appendChild(browser); - - extensions.emit("extension-browser-inserted", browser); - - let setupBrowser = browser => { - let mm = browser.messageManager; - mm.addMessageListener("DOMTitleChanged", this); - mm.addMessageListener("Extension:BrowserBackgroundChanged", this); - mm.addMessageListener("Extension:BrowserContentLoaded", this); - mm.addMessageListener("Extension:BrowserResized", this); - mm.addMessageListener("Extension:DOMWindowClose", this, true); - return browser; - }; - - if (!popupURL) { - // For remote browsers, we can't do any setup until the frame loader is - // created. Non-remote browsers get a message manager immediately, so - // there's no need to wait for the load event. - if (this.extension.remote) { - return readyPromise.then(() => setupBrowser(browser)); - } - return setupBrowser(browser); - } - - return readyPromise.then(() => { - setupBrowser(browser); - let mm = browser.messageManager; - - mm.loadFrameScript( - "chrome://extensions/content/ext-browser-content.js", false); - - mm.sendAsyncMessage("Extension:InitBrowser", { - allowScriptsToClose: true, - fixedWidth: this.fixedWidth, - maxWidth: 800, - maxHeight: 600, - stylesheets: this.STYLESHEETS, - }); - - browser.loadURI(popupURL); - }); - } - - resizeBrowser({width, height, detail}) { - if (this.fixedWidth) { - // Figure out how much extra space we have on the side of the panel - // opposite the arrow. - let side = this.panel.getAttribute("side") == "top" ? "bottom" : "top"; - let maxHeight = this.viewHeight + this.extraHeight[side]; - - height = Math.min(height, maxHeight); - this.browser.style.height = `${height}px`; - - // Set a maximum height on the element to our preferred - // maximum height, so that the PanelUI resizing code can make an accurate - // calculation. If we don't do this, the flex sizing logic will prevent us - // from ever reporting a preferred size smaller than the height currently - // available to us in the panel. - height = Math.max(height, this.viewHeight); - this.viewNode.style.maxHeight = `${height}px`; - } else { - this.browser.style.width = `${width}px`; - this.browser.style.minWidth = `${width}px`; - this.browser.style.height = `${height}px`; - this.browser.style.minHeight = `${height}px`; - } - - let event = new this.window.CustomEvent("WebExtPopupResized", {detail}); - this.browser.dispatchEvent(event); - } - - setBackground(background) { - let panelBackground = ""; - let panelArrow = ""; - - if (background) { - let borderColor = this.borderColor || background; - - panelBackground = background; - panelArrow = `url("data:image/svg+xml,${encodeURIComponent(` - - - - - `)}")`; - } - - this.panel.style.setProperty("--arrowpanel-background", panelBackground); - this.panel.style.setProperty("--panel-arrow-image-vertical", panelArrow); - } -} - -/** - * A map of active popups for a given browser window. - * - * WeakMap[window -> WeakMap[Extension -> BasePopup]] - */ -BasePopup.instances = new DefaultWeakMap(() => new WeakMap()); - -class PanelPopup extends BasePopup { - constructor(extension, imageNode, popupURL, browserStyle) { - let document = imageNode.ownerDocument; - - let panel = document.createElement("panel"); - panel.setAttribute("id", makeWidgetId(extension.id) + "-panel"); - panel.setAttribute("class", "browser-extension-panel"); - panel.setAttribute("tabspecific", "true"); - panel.setAttribute("type", "arrow"); - panel.setAttribute("role", "group"); - - document.getElementById("mainPopupSet").appendChild(panel); - - super(extension, panel, popupURL, browserStyle); - - this.contentReady.then(() => { - panel.openPopup(imageNode, "bottomcenter topright", 0, 0, false, false); - - let event = new this.window.CustomEvent("WebExtPopupLoaded", { - bubbles: true, - detail: {extension}, - }); - this.browser.dispatchEvent(event); - }); - } - - get DESTROY_EVENT() { - return "popuphidden"; - } - - destroy() { - super.destroy(); - this.viewNode.remove(); - this.viewNode = null; - } - - closePopup() { - promisePopupShown(this.viewNode).then(() => { - // Make sure we're not already destroyed, or removed from the DOM. - if (this.viewNode && this.viewNode.hidePopup) { - this.viewNode.hidePopup(); - } - }); - } -} - -class ViewPopup extends BasePopup { - constructor(extension, window, popupURL, browserStyle, fixedWidth) { - let document = window.document; - - // Create a temporary panel to hold the browser while it pre-loads its - // content. This panel will never be shown, but the browser's docShell will - // be swapped with the browser in the real panel when it's ready. - let panel = document.createElement("panel"); - panel.setAttribute("type", "arrow"); - document.getElementById("mainPopupSet").appendChild(panel); - - super(extension, panel, popupURL, browserStyle, fixedWidth); - - this.ignoreResizes = true; - - this.attached = false; - this.shown = false; - this.tempPanel = panel; - - this.browser.classList.add("webextension-preload-browser"); - } - - /** - * Attaches the pre-loaded browser to the given view node, and reserves a - * promise which resolves when the browser is ready. - * - * @param {Element} viewNode - * The node to attach the browser to. - * @returns {Promise} - * Resolves when the browser is ready. Resolves to `false` if the - * browser was destroyed before it was fully loaded, and the popup - * should be closed, or `true` otherwise. - */ - attach(viewNode) { - return Task.spawn(function* () { - this.viewNode = viewNode; - this.viewNode.addEventListener(this.DESTROY_EVENT, this); - - // Wait until the browser element is fully initialized, and give it at least - // a short grace period to finish loading its initial content, if necessary. - // - // In practice, the browser that was created by the mousdown handler should - // nearly always be ready by this point. - yield Promise.all([ - this.browserReady, - Promise.race([ - // This promise may be rejected if the popup calls window.close() - // before it has fully loaded. - this.browserLoaded.catch(() => {}), - new Promise(resolve => setTimeout(resolve, POPUP_LOAD_TIMEOUT_MS)), - ]), - ]); - - if (!this.destroyed && !this.panel) { - this.destroy(); - } - - if (this.destroyed) { - CustomizableUI.hidePanelForNode(viewNode); - return false; - } - - this.attached = true; - - - // Store the initial height of the view, so that we never resize menu panel - // sub-views smaller than the initial height of the menu. - this.viewHeight = this.viewNode.boxObject.height; - - // Calculate the extra height available on the screen above and below the - // menu panel. Use that to calculate the how much the sub-view may grow. - let popupRect = this.panel.getBoundingClientRect(); - - let win = this.window; - let popupBottom = win.mozInnerScreenY + popupRect.bottom; - let popupTop = win.mozInnerScreenY + popupRect.top; - - let screenBottom = win.screen.availTop + win.screen.availHeight; - this.extraHeight = { - bottom: Math.max(0, screenBottom - popupBottom), - top: Math.max(0, popupTop - win.screen.availTop), - }; - - // Create a new browser in the real popup. - let browser = this.browser; - yield this.createBrowser(this.viewNode); - - this.ignoreResizes = false; - - this.browser.swapDocShells(browser); - this.destroyBrowser(browser); - - if (this.dimensions && !this.fixedWidth) { - this.resizeBrowser(this.dimensions); - } - - this.tempPanel.remove(); - this.tempPanel = null; - - this.shown = true; - - if (this.destroyed) { - this.closePopup(); - this.destroy(); - return false; - } - - let event = new this.window.CustomEvent("WebExtPopupLoaded", { - bubbles: true, - detail: {extension: this.extension}, - }); - this.browser.dispatchEvent(event); - - return true; - }.bind(this)); - } - - destroy() { - return super.destroy().then(() => { - if (this.tempPanel) { - this.tempPanel.remove(); - this.tempPanel = null; - } - }); - } - - get DESTROY_EVENT() { - return "ViewHiding"; - } - - closePopup() { - if (this.shown) { - CustomizableUI.hidePanelForNode(this.viewNode); - } else if (this.attached) { - this.destroyed = true; - } else { - this.destroy(); - } - } -} - -//Object.assign(global, {PanelPopup, ViewPopup}); - -// Manages tab-specific context data, and dispatching tab select events -// across all windows. -const TabContext = function TabContext(getDefaults, extension) { - this.extension = extension; - this.getDefaults = getDefaults; - - this.tabData = new WeakMap(); - this.lastLocation = new WeakMap(); - - AllWindowEvents.addListener("progress", this); - AllWindowEvents.addListener("TabSelect", this); - - EventEmitter.decorate(this); -}; - -TabContext.prototype = { - get(tab) { - if (!this.tabData.has(tab)) { - this.tabData.set(tab, this.getDefaults(tab)); - } - - return this.tabData.get(tab); - }, - - clear(tab) { - this.tabData.delete(tab); - }, - - handleEvent(event) { - if (event.type == "TabSelect") { - let tab = event.target; - this.emit("tab-select", tab); - this.emit("location-change", tab); - } - }, - - onStateChange(browser, webProgress, request, stateFlags, statusCode) { - let flags = Ci.nsIWebProgressListener; - - if (!(~stateFlags & (flags.STATE_IS_WINDOW | flags.STATE_START) || - this.lastLocation.has(browser))) { - this.lastLocation.set(browser, request.URI); - } - }, - - onLocationChange(browser, webProgress, request, locationURI, flags) { - let gBrowser = browser.ownerGlobal.gBrowser; - let lastLocation = this.lastLocation.get(browser); - if (browser === gBrowser.selectedBrowser && - !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) { - let tab = gBrowser.getTabForBrowser(browser); - this.emit("location-change", tab, true); - } - this.lastLocation.set(browser, browser.currentURI); - }, - - shutdown() { - AllWindowEvents.removeListener("progress", this); - AllWindowEvents.removeListener("TabSelect", this); - }, -}; - -// Manages tab mappings and permissions for a specific extension. -function ExtensionTabManager(extension) { - this.extension = extension; - - // A mapping of tab objects to the inner window ID the extension currently has - // the active tab permission for. The active permission for a given tab is - // valid only for the inner window that was active when the permission was - // granted. If the tab navigates, the inner window ID changes, and the - // permission automatically becomes stale. - // - // WeakMap[tab => inner-window-id] - this.hasTabPermissionFor = new WeakMap(); -} - -ExtensionTabManager.prototype = { - addActiveTabPermission(tab = TabManager.activeTab) { - if (this.extension.hasPermission("activeTab")) { - // Note that, unlike Chrome, we don't currently clear this permission with - // the tab navigates. If the inner window is revived from BFCache before - // we've granted this permission to a new inner window, the extension - // maintains its permissions for it. - this.hasTabPermissionFor.set(tab, tab.linkedBrowser.innerWindowID); - } - }, - - revokeActiveTabPermission(tab = TabManager.activeTab) { - this.hasTabPermissionFor.delete(tab); - }, - - // Returns true if the extension has the "activeTab" permission for this tab. - // This is somewhat more permissive than the generic "tabs" permission, as - // checked by |hasTabPermission|, in that it also allows programmatic script - // injection without an explicit host permission. - hasActiveTabPermission(tab) { - // This check is redundant with addTabPermission, but cheap. - if (this.extension.hasPermission("activeTab")) { - return (this.hasTabPermissionFor.has(tab) && - this.hasTabPermissionFor.get(tab) === tab.linkedBrowser.innerWindowID); - } - return false; - }, - - hasTabPermission(tab) { - return this.extension.hasPermission("tabs") || this.hasActiveTabPermission(tab); - }, - - convert(tab) { - let window = tab.ownerGlobal; - let browser = tab.linkedBrowser; - - let mutedInfo = {muted: tab.muted}; - if (tab.muteReason === null) { - mutedInfo.reason = "user"; - } else if (tab.muteReason) { - mutedInfo.reason = "extension"; - mutedInfo.extensionId = tab.muteReason; - } - - let result = { - id: TabManager.getId(tab), - index: tab._tPos, - windowId: WindowManager.getId(window), - selected: tab.selected, - highlighted: tab.selected, - active: tab.selected, - pinned: tab.pinned, - status: TabManager.getStatus(tab), - incognito: PrivateBrowsingUtils.isBrowserPrivate(browser), - width: browser.frameLoader.lazyWidth || browser.clientWidth, - height: browser.frameLoader.lazyHeight || browser.clientHeight, - audible: tab.soundPlaying, - mutedInfo, - }; - if (this.extension.hasPermission("cookies")) { - result.cookieStoreId = getCookieStoreIdForTab(result, tab); - } - - if (this.hasTabPermission(tab)) { - result.url = browser.currentURI.spec; - let title = browser.contentTitle || tab.label; - if (title) { - result.title = title; - } - let icon = window.gBrowser.getIcon(tab); - if (icon) { - result.favIconUrl = icon; - } - } - - return result; - }, - - // Converts tabs returned from SessionStore.getClosedTabData and - // SessionStore.getClosedWindowData into API tab objects - convertFromSessionStoreClosedData(tab, window) { - let result = { - sessionId: String(tab.closedId), - index: tab.pos ? tab.pos : 0, - windowId: WindowManager.getId(window), - selected: false, - highlighted: false, - active: false, - pinned: false, - incognito: Boolean(tab.state && tab.state.isPrivate), - }; - - return result; - }, - - getTabs(window) { - return Array.from(window.gBrowser.tabs) - .filter(tab => !tab.closing) - .map(tab => this.convert(tab)); - }, -}; - -function getBrowserInfo(browser) { - if (!browser.ownerGlobal.gBrowser) { - // When we're loaded into a inside about:addons, we need to go up - // one more level. - browser = browser.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .chromeEventHandler; - - if (!browser) { - return {}; - } - } - - let result = {}; - - let window = browser.ownerGlobal; - if (window.gBrowser) { - let tab = window.gBrowser.getTabForBrowser(browser); - if (tab) { - result.tabId = TabManager.getId(tab); - } - - result.windowId = WindowManager.getId(window); - } - - return result; -} -//const getBrowserInfo = getBrowserInfo; - -// Sends the tab and windowId upon request. This is primarily used to support -// the synchronous `browser.extension.getViews` API. -let onGetTabAndWindowId = { - receiveMessage({name, target, sync}) { - let result = getBrowserInfo(target); - - if (result.tabId) { - if (sync) { - return result; - } - target.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", result); - } - }, -}; -/* eslint-disable mozilla/balanced-listeners */ -Services.mm.addMessageListener("Extension:GetTabAndWindowId", onGetTabAndWindowId); -/* eslint-enable mozilla/balanced-listeners */ - - -// Manages global mappings between XUL tabs and extension tab IDs. -const TabManager = { - _tabs: new WeakMap(), - _nextId: 1, - _initialized: false, - - // We begin listening for TabOpen and TabClose events once we've started - // assigning IDs to tabs, so that we can remap the IDs of tabs which are moved - // between windows. - initListener() { - if (this._initialized) { - return; - } - - AllWindowEvents.addListener("TabOpen", this); - AllWindowEvents.addListener("TabClose", this); - WindowListManager.addOpenListener(this.handleWindowOpen.bind(this)); - - this._initialized = true; - }, - - handleEvent(event) { - if (event.type == "TabOpen") { - let {adoptedTab} = event.detail; - if (adoptedTab) { - // This tab is being created to adopt a tab from a different window. - // Copy the ID from the old tab to the new. - let tab = event.target; - this._tabs.set(tab, this.getId(adoptedTab)); - - tab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", { - windowId: WindowManager.getId(tab.ownerGlobal), - }); - } - } else if (event.type == "TabClose") { - let {adoptedBy} = event.detail; - if (adoptedBy) { - // This tab is being closed because it was adopted by a new window. - // Copy its ID to the new tab, in case it was created as the first tab - // of a new window, and did not have an `adoptedTab` detail when it was - // opened. - this._tabs.set(adoptedBy, this.getId(event.target)); - - adoptedBy.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", { - windowId: WindowManager.getId(adoptedBy), - }); - } - } - }, - - handleWindowOpen(window) { - if (window.arguments && window.arguments[0] instanceof window.XULElement) { - // If the first window argument is a XUL element, it means the - // window is about to adopt a tab from another window to replace its - // initial tab. - let adoptedTab = window.arguments[0]; - - this._tabs.set(window.gBrowser.tabs[0], this.getId(adoptedTab)); - } - }, - - getId(tab) { - if (this._tabs.has(tab)) { - return this._tabs.get(tab); - } - this.initListener(); - - let id = this._nextId++; - this._tabs.set(tab, id); - return id; - }, - - getBrowserId(browser) { - let gBrowser = browser.ownerGlobal.gBrowser; - // Some non-browser windows have gBrowser but not - // getTabForBrowser! - if (gBrowser && gBrowser.getTabForBrowser) { - let tab = gBrowser.getTabForBrowser(browser); - if (tab) { - return this.getId(tab); - } - } - return -1; - }, - - /** - * Returns the XUL element associated with the given tab ID. If no tab - * with the given ID exists, and no default value is provided, an error is - * raised, belonging to the scope of the given context. - * - * @param {integer} tabId - * The ID of the tab to retrieve. - * @param {ExtensionContext} context - * The context of the caller. - * This value may be omitted if `default_` is not `undefined`. - * @param {*} default_ - * The value to return if no tab exists with the given ID. - * @returns {Element} - * A XUL element. - */ - getTab(tabId, context, default_ = undefined) { - // FIXME: Speed this up without leaking memory somehow. - for (let window of WindowListManager.browserWindows()) { - if (!window.gBrowser) { - continue; - } - for (let tab of window.gBrowser.tabs) { - if (this.getId(tab) == tabId) { - return tab; - } - } - } - if (default_ !== undefined) { - return default_; - } - throw new context.cloneScope.Error(`Invalid tab ID: ${tabId}`); - }, - - get activeTab() { - let window = WindowManager.topWindow; - if (window && window.gBrowser) { - return window.gBrowser.selectedTab; - } - return null; - }, - - getStatus(tab) { - return tab.getAttribute("busy") == "true" ? "loading" : "complete"; - }, - - convert(extension, tab) { - return TabManager.for(extension).convert(tab); - }, -}; - -// WeakMap[Extension -> ExtensionTabManager] -let tabManagers = new WeakMap(); - -// Returns the extension-specific tab manager for the given extension, or -// creates one if it doesn't already exist. -TabManager.for = function(extension) { - if (!tabManagers.has(extension)) { - tabManagers.set(extension, new ExtensionTabManager(extension)); - } - return tabManagers.get(extension); -}; - -/* eslint-disable mozilla/balanced-listeners */ -//extensions.on("shutdown", (type, extension) => { -// tabManagers.delete(extension); -//}); -/* eslint-enable mozilla/balanced-listeners */ - -// Manages mapping between XUL windows and extension window IDs. -const WindowManager = { - // Note: These must match the values in windows.json. - WINDOW_ID_NONE: -1, - WINDOW_ID_CURRENT: -2, - - get topWindow() { - return Services.wm.getMostRecentWindow("navigator:browser"); - }, - - windowType(window) { - // TODO: Make this work. - - let {chromeFlags} = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIXULWindow); - - if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) { - return "popup"; - } - - return "normal"; - }, - - updateGeometry(window, options) { - if (options.left !== null || options.top !== null) { - let left = options.left !== null ? options.left : window.screenX; - let top = options.top !== null ? options.top : window.screenY; - window.moveTo(left, top); - } - - if (options.width !== null || options.height !== null) { - let width = options.width !== null ? options.width : window.outerWidth; - let height = options.height !== null ? options.height : window.outerHeight; - window.resizeTo(width, height); - } - }, - - getId(window) { - if (!window.QueryInterface) { - return null; - } - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; - }, - - getWindow(id, context) { - if (id == this.WINDOW_ID_CURRENT) { - return currentWindow(context); - } - - for (let window of WindowListManager.browserWindows(true)) { - if (this.getId(window) == id) { - return window; - } - } - return null; - }, - - getState(window) { - const STATES = { - [window.STATE_MAXIMIZED]: "maximized", - [window.STATE_MINIMIZED]: "minimized", - [window.STATE_NORMAL]: "normal", - }; - let state = STATES[window.windowState]; - if (window.fullScreen) { - state = "fullscreen"; - } - return state; - }, - - setState(window, state) { - if (state != "fullscreen" && window.fullScreen) { - window.fullScreen = false; - } - - switch (state) { - case "maximized": - window.maximize(); - break; - - case "minimized": - case "docked": - window.minimize(); - break; - - case "normal": - // Restore sometimes returns the window to its previous state, rather - // than to the "normal" state, so it may need to be called anywhere from - // zero to two times. - window.restore(); - if (window.windowState != window.STATE_NORMAL) { - window.restore(); - } - if (window.windowState != window.STATE_NORMAL) { - // And on OS-X, where normal vs. maximized is basically a heuristic, - // we need to cheat. - window.sizeToContent(); - } - break; - - case "fullscreen": - window.fullScreen = true; - break; - - default: - throw new Error(`Unexpected window state: ${state}`); - } - }, - - convert(extension, window, getInfo) { - let xulWindow = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .treeOwner.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIXULWindow); - - let result = { - id: this.getId(window), - focused: window.document.hasFocus(), - top: window.screenY, - left: window.screenX, - width: window.outerWidth, - height: window.outerHeight, - incognito: PrivateBrowsingUtils.isWindowPrivate(window), - type: this.windowType(window), - state: this.getState(window), - alwaysOnTop: xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ, - }; - - if (getInfo && getInfo.populate) { - result.tabs = TabManager.for(extension).getTabs(window); - } - - return result; - }, - - // Converts windows returned from SessionStore.getClosedWindowData - // into API window objects - convertFromSessionStoreClosedData(window, extension) { - let result = { - sessionId: String(window.closedId), - focused: false, - incognito: false, - type: "normal", // this is always "normal" for a closed window - state: this.getState(window), - alwaysOnTop: false, - }; - - if (window.tabs.length) { - result.tabs = []; - window.tabs.forEach((tab, index) => { - result.tabs.push(TabManager.for(extension).convertFromSessionStoreClosedData(tab, window, index)); - }); - } - - return result; - }, -}; - -// Manages listeners for window opening and closing. A window is -// considered open when the "load" event fires on it. A window is -// closed when a "domwindowclosed" notification fires for it. -const WindowListManager = { - _openListeners: new Set(), - _closeListeners: new Set(), - - // Returns an iterator for all browser windows. Unless |includeIncomplete| is - // true, only fully-loaded windows are returned. - * browserWindows(includeIncomplete = false) { - // The window type parameter is only available once the window's document - // element has been created. This means that, when looking for incomplete - // browser windows, we need to ignore the type entirely for windows which - // haven't finished loading, since we would otherwise skip browser windows - // in their early loading stages. - // This is particularly important given that the "domwindowcreated" event - // fires for browser windows when they're in that in-between state, and just - // before we register our own "domwindowcreated" listener. - - let e = Services.wm.getEnumerator(""); - while (e.hasMoreElements()) { - let window = e.getNext(); - - let ok = includeIncomplete; - if (window.document.readyState == "complete") { - ok = window.document.documentElement.getAttribute("windowtype") == "navigator:browser"; - } - - if (ok) { - yield window; - } - } - }, - - addOpenListener(listener) { - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { - Services.ww.registerNotification(this); - } - this._openListeners.add(listener); - - for (let window of this.browserWindows(true)) { - if (window.document.readyState != "complete") { - window.addEventListener("load", this); - } - } - }, - - removeOpenListener(listener) { - this._openListeners.delete(listener); - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { - Services.ww.unregisterNotification(this); - } - }, - - addCloseListener(listener) { - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { - Services.ww.registerNotification(this); - } - this._closeListeners.add(listener); - }, - - removeCloseListener(listener) { - this._closeListeners.delete(listener); - if (this._openListeners.size == 0 && this._closeListeners.size == 0) { - Services.ww.unregisterNotification(this); - } - }, - - handleEvent(event) { - event.currentTarget.removeEventListener(event.type, this); - let window = event.target.defaultView; - if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") { - return; - } - - for (let listener of this._openListeners) { - listener(window); - } - }, - - observe(window, topic, data) { - if (topic == "domwindowclosed") { - if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") { - return; - } - - window.removeEventListener("load", this); - for (let listener of this._closeListeners) { - listener(window); - } - } else { - window.addEventListener("load", this); - } - }, -}; - -// Provides a facility to listen for DOM events across all XUL windows. -const AllWindowEvents = { - _listeners: new Map(), - - // If |type| is a normal event type, invoke |listener| each time - // that event fires in any open window. If |type| is "progress", add - // a web progress listener that covers all open windows. - addListener(type, listener) { - if (type == "domwindowopened") { - return WindowListManager.addOpenListener(listener); - } else if (type == "domwindowclosed") { - return WindowListManager.addCloseListener(listener); - } - - if (this._listeners.size == 0) { - WindowListManager.addOpenListener(this.openListener); - } - - if (!this._listeners.has(type)) { - this._listeners.set(type, new Set()); - } - let list = this._listeners.get(type); - list.add(listener); - - // Register listener on all existing windows. - for (let window of WindowListManager.browserWindows()) { - this.addWindowListener(window, type, listener); - } - }, - - removeListener(eventType, listener) { - if (eventType == "domwindowopened") { - return WindowListManager.removeOpenListener(listener); - } else if (eventType == "domwindowclosed") { - return WindowListManager.removeCloseListener(listener); - } - - let listeners = this._listeners.get(eventType); - listeners.delete(listener); - if (listeners.size == 0) { - this._listeners.delete(eventType); - if (this._listeners.size == 0) { - WindowListManager.removeOpenListener(this.openListener); - } - } - - // Unregister listener from all existing windows. - let useCapture = eventType === "focus" || eventType === "blur"; - for (let window of WindowListManager.browserWindows()) { - if (eventType == "progress") { - window.gBrowser.removeTabsProgressListener(listener); - } else { - window.removeEventListener(eventType, listener, useCapture); - } - } - }, - - /* eslint-disable mozilla/balanced-listeners */ - addWindowListener(window, eventType, listener) { - let useCapture = eventType === "focus" || eventType === "blur"; - - if (eventType == "progress") { - window.gBrowser.addTabsProgressListener(listener); - } else { - window.addEventListener(eventType, listener, useCapture); - } - }, - /* eslint-enable mozilla/balanced-listeners */ - - // Runs whenever the "load" event fires for a new window. - openListener(window) { - for (let [eventType, listeners] of AllWindowEvents._listeners) { - for (let listener of listeners) { - this.addWindowListener(window, eventType, listener); - } - } - }, -}; - -AllWindowEvents.openListener = AllWindowEvents.openListener.bind(AllWindowEvents); - -// Subclass of EventManager where we just need to call -// add/removeEventListener on each XUL window. -const WindowEventManager = function(context, name, event, listener) { - EventManager.call(this, context, name, fire => { - let listener2 = (...args) => listener(fire, ...args); - AllWindowEvents.addListener(event, listener2); - return () => { - AllWindowEvents.removeListener(event, listener2); - }; - }); -}; - -WindowEventManager.prototype = Object.create(EventManager.prototype); - - -//////////////////////////////////////////////////////////////////////////// -/// end ext-utils -//////////////////////////////////////////////////////////////////////////// - - -function convert(identity) { - let result = { - name: ContextualIdentityService.getUserContextLabel(identity.userContextId), - icon: identity.icon, - color: identity.color, - cookieStoreId: getCookieStoreIdForContainer(identity.userContextId), - }; - - return result; -} - -function getCookieStoreIdForContainer(containerId) { - return CONTAINER_STORE + containerId; -} - -function query(extension, queryInfo) { - let pattern = null; - - function matches(window, tab) { - if (queryInfo.cookieStoreId !== null && - tab.cookieStoreId != queryInfo.cookieStoreId) { - return false; - } - - return true; - } - - let result = []; - for (let window of WindowListManager.browserWindows()) { - let tabs = TabManager.for(extension).getTabs(window); - for (let tab of tabs) { - if (matches(window, tab)) { - result.push(tab); - } - } - } - return Promise.resolve(result); -} - -class API extends ExtensionAPI { - getAPI(context) { - let {extension} = context; - let self = { - contextualIdentities: { - hide(cookieStoreId) { - query(extension, { - cookieStoreId - }).then((tabs) => { - tabs.forEach((tab) => { - let tabObj = TabManager.getTab(tab.id, null); - let gBrowser = tabObj.ownerGlobal.gBrowser; - gBrowser.hideTab(tabObj); - }); - }).catch((e) => { - throw e; - }); - }, - - show(cookieStoreId) { - query(extension, { - cookieStoreId - }).then((tabs) => { - tabs.forEach((tab) => { - let tabObj = TabManager.getTab(tab.id, null); - let gBrowser = tabObj.ownerGlobal.gBrowser; - gBrowser.showTab(tabObj); - }); - }); - }, - - get(cookieStoreId) { - let containerId = getContainerForCookieStoreId(cookieStoreId); - if (!containerId) { - return Promise.resolve(null); - } - - let identity = ContextualIdentityService.getIdentityFromId(containerId); - return Promise.resolve(convert(identity)); - }, - - query(details) { - let identities = []; - ContextualIdentityService.getIdentities().forEach(identity => { - if (details.name && - ContextualIdentityService.getUserContextLabel(identity.userContextId) != details.name) { - return; - } - - identities.push(convert(identity)); - }); - - return Promise.resolve(identities); - }, - - create(details) { - let identity = ContextualIdentityService.create(details.name, - details.icon, - details.color); - return Promise.resolve(convert(identity)); - }, - - update(cookieStoreId, details) { - let containerId = getContainerForCookieStoreId(cookieStoreId); - if (!containerId) { - return Promise.resolve(null); - } - - let identity = ContextualIdentityService.getIdentityFromId(containerId); - if (!identity) { - return Promise.resolve(null); - } - - if (details.name !== null) { - identity.name = details.name; - } - - if (details.color !== null) { - identity.color = details.color; - } - - if (details.icon !== null) { - identity.icon = details.icon; - } - - if (!ContextualIdentityService.update(identity.userContextId, - identity.name, identity.icon, - identity.color)) { - return Promise.resolve(null); - } - - return Promise.resolve(convert(identity)); - }, - - remove(cookieStoreId) { - let containerId = getContainerForCookieStoreId(cookieStoreId); - if (!containerId) { - return Promise.resolve(null); - } - - let identity = ContextualIdentityService.getIdentityFromId(containerId); - if (!identity) { - return Promise.resolve(null); - } - - // We have to create the identity object before removing it. - let convertedIdentity = convert(identity); - - if (!ContextualIdentityService.remove(identity.userContextId)) { - return Promise.resolve(null); - } - - return Promise.resolve(convertedIdentity); - }, - }, - }; - - return self; - } -} diff --git a/webextension-experiment/install.rdf b/webextension-experiment/install.rdf deleted file mode 100644 index e1d4e0b..0000000 --- a/webextension-experiment/install.rdf +++ /dev/null @@ -1,31 +0,0 @@ - - - - contextualidentities@experiments.addons.mozilla.org - Experimental Contextual Identites API - 256 - 0.1 - Experimental Contextual Identities API - groovecoder, baku and jkt - true - - - - - {ec8030f7-c20a-464f-9b0e-13a3a9e97384} - 51.0 - * - - - - - - - xpcshell@tests.mozilla.org - 1 - 2 - - - - - diff --git a/webextension-experiment/schema.json b/webextension-experiment/schema.json deleted file mode 100644 index ec1dcc5..0000000 --- a/webextension-experiment/schema.json +++ /dev/null @@ -1,131 +0,0 @@ -[ - { - "namespace": "contextualIdentities", - "description": "Use the browser.contextualIdentities API to query and modify contextual identity, also called as containers.", - "permissions": ["contextualidentities"], - "types": [ - { - "id": "ContextualIdentity", - "type": "object", - "description": "Represents information about a contextual identity.", - "properties": { - "name": {"type": "string", "description": "The name of the contextual identity."}, - "icon": {"type": "string", "description": "The icon of the contextual identity."}, - "color": {"type": "string", "description": "The color of the contextual identity."}, - "cookieStoreId": {"type": "string", "description": "The cookie store ID of the contextual identity."} - } - } - ], - "functions": [ - { - "name": "get", - "type": "function", - "description": "Retrieves information about a single contextual identity.", - "async": true, - "parameters": [ - { - "type": "string", - "name": "cookiestoreid", - "description": "the id of the contextual identity cookie store. " - } - ] - }, - { - "name": "hide", - "type": "function", - "description": "hides all of a contextual identity.", - "async": true, - "parameters": [ - { - "type": "string", - "name": "cookiestoreid", - "description": "the id of the contextual identity cookie store. " - } - ] - }, - { - "name": "show", - "type": "function", - "description": "unhides all of a contextual identity.", - "async": true, - "parameters": [ - { - "type": "string", - "name": "cookiestoreid", - "description": "the id of the contextual identity cookie store. " - } - ] - }, - { - "name": "query", - "type": "function", - "description": "Retrieves all contextual identities", - "async": true, - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Information to filter the contextual identities being retrieved.", - "properties": { - "name": {"type": "string", "optional": true, "description": "Filters the contextual identity by name."} - } - } - ] - }, - { - "name": "create", - "type": "function", - "description": "Creates a contextual identity with the given data.", - "async": true, - "parameters": [ - { - "type": "object", - "name": "details", - "description": "Details about the contextual identity being created.", - "properties": { - "name": {"type": "string", "optional": false, "description": "The name of the contextual identity." }, - "color": {"type": "string", "optional": false, "description": "The color of the contextual identity." }, - "icon": {"type": "string", "optional": false, "description": "The icon of the contextual identity." } - } - } - ] - }, - { - "name": "update", - "type": "function", - "description": "Updates a contextual identity with the given data.", - "async": true, - "parameters": [ - { - "type": "string", - "name": "cookieStoreId", - "description": "The ID of the contextual identity cookie store. " - }, - { - "type": "object", - "name": "details", - "description": "Details about the contextual identity being created.", - "properties": { - "name": {"type": "string", "optional": true, "description": "The name of the contextual identity." }, - "color": {"type": "string", "optional": true, "description": "The color of the contextual identity." }, - "icon": {"type": "string", "optional": true, "description": "The icon of the contextual identity." } - } - } - ] - }, - { - "name": "remove", - "type": "function", - "description": "Deletes a contetual identity by its cookie Store ID.", - "async": true, - "parameters": [ - { - "type": "string", - "name": "cookieStoreId", - "description": "The ID of the contextual identity cookie store. " - } - ] - } - ] - } -] diff --git a/webextension/js/popup.js b/webextension/js/popup.js index e8481a7..d9b1f1b 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -1,25 +1,54 @@ /* global browser, window, document */ -const identityState = { -}; +const CONTAINER_HIDE_SRC = '/img/container-hide.svg'; +const CONTAINER_UNHIDE_SRC = '/img/container-unhide.svg'; -function hideContainer(containerId) { +function hideContainerTabs(containerId) { + const tabIdsToRemove = []; + const tabUrlsToSave = []; const hideorshowIcon = document.querySelector(`#${containerId}-hideorshow-icon`); - hideorshowIcon.src = '/img/container-unhide.svg'; - browser.contextualIdentities.hide(containerId); + browser.tabs.query({cookieStoreId: containerId}).then(tabs=> { + tabs.forEach(tab=> { + tabIdsToRemove.push(tab.id); + tabUrlsToSave.push(tab.url); + }); + browser.runtime.sendMessage({ + method: 'hide', + cookieStoreId: containerId, + tabUrlsToSave: tabUrlsToSave + }).then(()=> { + browser.tabs.remove(tabIdsToRemove); + hideorshowIcon.src = CONTAINER_UNHIDE_SRC; + }); + }); } -function showContainer(containerId) { +function showContainerTabs(containerId) { const hideorshowIcon = document.querySelector(`#${containerId}-hideorshow-icon`); - hideorshowIcon.src = '/img/container-hide.svg'; - browser.contextualIdentities.show(containerId); + browser.runtime.sendMessage({ + method: 'show', + cookieStoreId: containerId + }).then(hiddenTabUrls=> { + hiddenTabUrls.forEach(url=> { + browser.tabs.create({ + url: url, + cookieStoreId: containerId + }); + }); + }); + hideorshowIcon.src = CONTAINER_HIDE_SRC; } -browser.contextualIdentities.query({}).then(identities=> { +browser.runtime.sendMessage({method: 'query'}).then(identities=> { const identitiesListElement = document.querySelector('.identities-list'); identities.forEach(identity=> { + let hideOrShowIconSrc = CONTAINER_HIDE_SRC; + + if (identity.hiddenTabUrls.length) { + hideOrShowIconSrc = CONTAINER_UNHIDE_SRC; + } const identityRow = `
{ data-identity-cookie-store-id="${identity.cookieStoreId}" id="${identity.cookieStoreId}-hideorshow-icon" class="hideorshow-icon" - src="/img/container-hide.svg" + src="${hideOrShowIconSrc}" /> > `; identitiesListElement.innerHTML += identityRow; - }); const rows = identitiesListElement.querySelectorAll('tr'); @@ -49,16 +77,13 @@ browser.contextualIdentities.query({}).then(identities=> { if (e.target.matches('.hideorshow-icon')) { const containerId = e.target.dataset.identityCookieStoreId; - if (!(containerId in identityState)) { - identityState[containerId] = true; - } - if (identityState[containerId]) { - hideContainer(containerId); - identityState[containerId] = false; - } else { - showContainer(containerId); - identityState[containerId] = true; - } + browser.runtime.sendMessage({method: 'getIdentitiesState'}).then(identitiesState=> { + if (identitiesState[containerId].hiddenTabUrls.length) { + showContainerTabs(containerId); + } else { + hideContainerTabs(containerId); + } + }); } }); }); @@ -81,7 +106,7 @@ function moveTabs(sortedTabsArray) { } document.querySelector('#sort-containers-link').addEventListener('click', ()=> { - browser.contextualIdentities.query({}).then(identities=> { + browser.runtime.sendMessage({method: 'query'}).then(identities=> { identities.unshift({cookieStoreId: 'firefox-default'}); browser.tabs.query({}).then(tabsArray=> { diff --git a/webextension/manifest.json b/webextension/manifest.json index a1195e2..150f073 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -21,8 +21,6 @@ "permissions": [ "cookies", - "experiments.contextualidentities", - "contextualidentities", "tabs", "cookies" ],