Merge pull request #605 from jonathanKingston/sdk-mamoth-breakage
Shirk my SDK code
This commit is contained in:
commit
ee8c69b73e
15 changed files with 1333 additions and 1362 deletions
515
index.js
515
index.js
|
@ -3,14 +3,6 @@
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||||
const DEFAULT_TAB = "about:newtab";
|
|
||||||
const LOOKUP_KEY = "$ref";
|
|
||||||
|
|
||||||
const INCOMPATIBLE_ADDON_IDS = [
|
|
||||||
"pulse@mozilla.com",
|
|
||||||
"snoozetabs@mozilla.com",
|
|
||||||
"jid1-NeEaf3sAHdKHPA@jetpack" // PageShot
|
|
||||||
];
|
|
||||||
|
|
||||||
const IDENTITY_COLORS = [
|
const IDENTITY_COLORS = [
|
||||||
{ name: "blue", color: "#00a7e0" },
|
{ name: "blue", color: "#00a7e0" },
|
||||||
|
@ -54,12 +46,9 @@ const PREFS = [
|
||||||
[ "privacy.usercontext.about_newtab_segregation.enabled", true ],
|
[ "privacy.usercontext.about_newtab_segregation.enabled", true ],
|
||||||
];
|
];
|
||||||
|
|
||||||
const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
|
|
||||||
const { attachTo, detachFrom } = require("sdk/content/mod");
|
const { attachTo, detachFrom } = require("sdk/content/mod");
|
||||||
const { Cu } = require("chrome");
|
const { Cu } = require("chrome");
|
||||||
const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm");
|
const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm");
|
||||||
const { getFavicon } = require("sdk/places/favicon");
|
|
||||||
const { LightweightThemeManager } = Cu.import("resource://gre/modules/LightweightThemeManager.jsm", {});
|
|
||||||
const Metrics = require("./testpilot-metrics");
|
const Metrics = require("./testpilot-metrics");
|
||||||
const { modelFor } = require("sdk/model/core");
|
const { modelFor } = require("sdk/model/core");
|
||||||
const prefService = require("sdk/preferences/service");
|
const prefService = require("sdk/preferences/service");
|
||||||
|
@ -69,7 +58,6 @@ const ss = require("sdk/simple-storage");
|
||||||
const { study } = require("./study");
|
const { study } = require("./study");
|
||||||
const { Style } = require("sdk/stylesheet/style");
|
const { Style } = require("sdk/stylesheet/style");
|
||||||
const tabs = require("sdk/tabs");
|
const tabs = require("sdk/tabs");
|
||||||
const tabsUtils = require("sdk/tabs/utils");
|
|
||||||
const uuid = require("sdk/util/uuid");
|
const uuid = require("sdk/util/uuid");
|
||||||
const { viewFor } = require("sdk/view/core");
|
const { viewFor } = require("sdk/view/core");
|
||||||
const webExtension = require("sdk/webextension");
|
const webExtension = require("sdk/webextension");
|
||||||
|
@ -81,8 +69,8 @@ Cu.import("resource:///modules/CustomizableWidgets.jsm");
|
||||||
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
|
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
// ContextualIdentityProxy
|
|
||||||
|
|
||||||
|
// ContextualIdentityProxy
|
||||||
const ContextualIdentityProxy = {
|
const ContextualIdentityProxy = {
|
||||||
getIdentities() {
|
getIdentities() {
|
||||||
let response;
|
let response;
|
||||||
|
@ -124,7 +112,6 @@ const ContextualIdentityProxy = {
|
||||||
// ContainerService
|
// ContainerService
|
||||||
|
|
||||||
const ContainerService = {
|
const ContainerService = {
|
||||||
_identitiesState: {},
|
|
||||||
_windowMap: new Map(),
|
_windowMap: new Map(),
|
||||||
_containerWasEnabled: false,
|
_containerWasEnabled: false,
|
||||||
_onBackgroundConnectCallback: null,
|
_onBackgroundConnectCallback: null,
|
||||||
|
@ -204,58 +191,13 @@ const ContainerService = {
|
||||||
// Disabling the customizable container panel.
|
// Disabling the customizable container panel.
|
||||||
CustomizableUI.destroyWidget("containers-panelmenu");
|
CustomizableUI.destroyWidget("containers-panelmenu");
|
||||||
|
|
||||||
// Message routing
|
|
||||||
|
|
||||||
// only these methods are allowed. We have a 1:1 mapping between messages
|
|
||||||
// and methods. These methods must return a promise.
|
|
||||||
const methods = [
|
|
||||||
"hideTabs",
|
|
||||||
"showTabs",
|
|
||||||
"sortTabs",
|
|
||||||
"getTabs",
|
|
||||||
"showTab",
|
|
||||||
"moveTabsToWindow",
|
|
||||||
"queryIdentitiesState",
|
|
||||||
"getIdentity",
|
|
||||||
"getPreference",
|
|
||||||
"sendTelemetryPayload",
|
|
||||||
"getTheme",
|
|
||||||
"getShieldStudyVariation",
|
|
||||||
"refreshNeeded",
|
|
||||||
"forgetIdentityAndRefresh",
|
|
||||||
"checkIncompatibleAddons"
|
|
||||||
];
|
|
||||||
|
|
||||||
// Map of identities.
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
|
||||||
this._remapTabsIfMissing(identity.userContextId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Let's restore the hidden tabs from the previous session.
|
|
||||||
if (prefService.get("browser.startup.page") === 3 &&
|
|
||||||
"identitiesData" in ss.storage) {
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
|
||||||
if (identity.userContextId in ss.storage.identitiesData &&
|
|
||||||
"hiddenTabs" in ss.storage.identitiesData[identity.userContextId]) {
|
|
||||||
this._identitiesState[identity.userContextId].hiddenTabs =
|
|
||||||
ss.storage.identitiesData[identity.userContextId].hiddenTabs;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tabs.on("open", tab => {
|
tabs.on("open", tab => {
|
||||||
this._restyleTab(tab);
|
this._restyleTab(tab);
|
||||||
this._remapTab(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
tabs.on("close", tab => {
|
|
||||||
this._remapTab(tab);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tabs.on("activate", tab => {
|
tabs.on("activate", tab => {
|
||||||
this._restyleActiveTab(tab).catch(() => {});
|
this._restyleActiveTab(tab).catch(() => {});
|
||||||
this._configureActiveWindows();
|
this._configureActiveWindows();
|
||||||
this._remapTab(tab);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modify CSS and other stuff for each window.
|
// Modify CSS and other stuff for each window.
|
||||||
|
@ -274,12 +216,6 @@ const ContainerService = {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const api = await webExtension.startup();
|
const api = await webExtension.startup();
|
||||||
api.browser.runtime.onMessage.addListener((message, sender, sendReply) => {
|
|
||||||
if ("method" in message && methods.indexOf(message.method) !== -1) {
|
|
||||||
sendReply(this[message.method](message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.registerBackgroundConnection(api);
|
this.registerBackgroundConnection(api);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error("WebExtension startup failed. Unable to continue.");
|
throw new Error("WebExtension startup failed. Unable to continue.");
|
||||||
|
@ -324,8 +260,6 @@ const ContainerService = {
|
||||||
}
|
}
|
||||||
// End-Of-Hack
|
// End-Of-Hack
|
||||||
|
|
||||||
Services.obs.addObserver(this, "lightweight-theme-changed", false);
|
|
||||||
|
|
||||||
if (self.id === "@shield-study-containers") {
|
if (self.id === "@shield-study-containers") {
|
||||||
study.startup(reason);
|
study.startup(reason);
|
||||||
this.shieldStudyVariation = study.variation;
|
this.shieldStudyVariation = study.variation;
|
||||||
|
@ -350,82 +284,6 @@ const ContainerService = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async observe(subject, topic) {
|
|
||||||
if (topic === "lightweight-theme-changed") {
|
|
||||||
try {
|
|
||||||
const theme = await this.getTheme();
|
|
||||||
this.triggerBackgroundCallback(theme, topic);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Unable to get theme");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getTheme() {
|
|
||||||
const defaultTheme = "firefox-compact-light@mozilla.org";
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
let theme = defaultTheme;
|
|
||||||
if (LightweightThemeManager.currentTheme && LightweightThemeManager.currentTheme.id) {
|
|
||||||
theme = LightweightThemeManager.currentTheme.id;
|
|
||||||
}
|
|
||||||
resolve(theme);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getShieldStudyVariation() {
|
|
||||||
return this.shieldStudyVariation;
|
|
||||||
},
|
|
||||||
|
|
||||||
// utility methods
|
|
||||||
|
|
||||||
_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;
|
|
||||||
},
|
|
||||||
|
|
||||||
_totalNonContainerTabsCount() {
|
|
||||||
// Returns the number of open tabs NOT IN a container
|
|
||||||
let totalNonContainerTabsCount = 0;
|
|
||||||
for (const tab of tabs) {
|
|
||||||
if (this._getUserContextIdFromTab(tab) === 0) {
|
|
||||||
++totalNonContainerTabsCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return totalNonContainerTabsCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
_containersCounts() {
|
|
||||||
let containersCounts = { // eslint-disable-line prefer-const
|
|
||||||
"shown": 0,
|
|
||||||
"hidden": 0,
|
|
||||||
"total": 0
|
|
||||||
};
|
|
||||||
for (const userContextId in this._identitiesState) {
|
|
||||||
if (this._identitiesState[userContextId].openTabs > 0) {
|
|
||||||
++containersCounts.shown;
|
|
||||||
++containersCounts.total;
|
|
||||||
continue;
|
|
||||||
} else if (this._identitiesState[userContextId].hiddenTabs.length > 0) {
|
|
||||||
++containersCounts.hidden;
|
|
||||||
++containersCounts.total;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return containersCounts;
|
|
||||||
},
|
|
||||||
|
|
||||||
// In FF 50-51, the icon is the full path, in 52 and following
|
// In FF 50-51, the icon is the full path, in 52 and following
|
||||||
// releases, we have IDs to be used with a svg file. In this function
|
// releases, we have IDs to be used with a svg file. In this function
|
||||||
// we map URLs to svg IDs.
|
// we map URLs to svg IDs.
|
||||||
|
@ -470,23 +328,6 @@ const ContainerService = {
|
||||||
return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10);
|
return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10);
|
||||||
},
|
},
|
||||||
|
|
||||||
async _createTabObject(tab) {
|
|
||||||
let url;
|
|
||||||
try {
|
|
||||||
url = await getFavicon(tab.url);
|
|
||||||
} catch (e) {
|
|
||||||
url = "";
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title: tab.title,
|
|
||||||
url: tab.url,
|
|
||||||
favicon: url,
|
|
||||||
id: tab.id,
|
|
||||||
active: true,
|
|
||||||
pinned: tabsUtils.isPinned(viewFor(tab))
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
_matchTabsByContainer(userContextId) {
|
_matchTabsByContainer(userContextId) {
|
||||||
const matchedTabs = [];
|
const matchedTabs = [];
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
|
@ -497,38 +338,6 @@ const ContainerService = {
|
||||||
return matchedTabs;
|
return matchedTabs;
|
||||||
},
|
},
|
||||||
|
|
||||||
_createIdentityState() {
|
|
||||||
return {
|
|
||||||
hiddenTabs: [],
|
|
||||||
openTabs: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
_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;
|
|
||||||
},
|
|
||||||
|
|
||||||
_remapTab(tab) {
|
|
||||||
const userContextId = this._getUserContextIdFromTab(tab);
|
|
||||||
if (userContextId) {
|
|
||||||
this._remapTabsFromUserContextId(userContextId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_isKnownContainer(userContextId) {
|
|
||||||
return userContextId in this._identitiesState;
|
|
||||||
},
|
|
||||||
|
|
||||||
async _closeTabs(tabsToClose) {
|
async _closeTabs(tabsToClose) {
|
||||||
// We create a new tab only if the current operation closes all the
|
// We create a new tab only if the current operation closes all the
|
||||||
// existing ones.
|
// existing ones.
|
||||||
|
@ -561,339 +370,21 @@ const ContainerService = {
|
||||||
return Promise.resolve(browserWin);
|
return Promise.resolve(browserWin);
|
||||||
},
|
},
|
||||||
|
|
||||||
_syncTabs() {
|
|
||||||
// Let's store all what we have.
|
|
||||||
ss.storage.identitiesData = this._identitiesState;
|
|
||||||
},
|
|
||||||
|
|
||||||
sendTelemetryPayload(args = {}) {
|
|
||||||
// when pings come from popup, delete "method" prop
|
|
||||||
delete args.method;
|
|
||||||
let payload = { // eslint-disable-line prefer-const
|
|
||||||
"uuid": this._metricsUUID
|
|
||||||
};
|
|
||||||
Object.assign(payload, args);
|
|
||||||
|
|
||||||
/* This is to masage the data whilst it is still active in the SDK side */
|
|
||||||
const containersCounts = this._containersCounts();
|
|
||||||
Object.keys(payload).forEach((keyName) => {
|
|
||||||
let value = payload[keyName];
|
|
||||||
if (value === LOOKUP_KEY) {
|
|
||||||
switch (keyName) {
|
|
||||||
case "clickedContainerTabCount":
|
|
||||||
value = this._containerTabCount(payload.userContextId);
|
|
||||||
break;
|
|
||||||
case "shownContainersCount":
|
|
||||||
value = containersCounts.shown;
|
|
||||||
break;
|
|
||||||
case "hiddenContainersCount":
|
|
||||||
value = containersCounts.hidden;
|
|
||||||
break;
|
|
||||||
case "totalContainersCount":
|
|
||||||
value = containersCounts.total;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
payload[keyName] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._sendEvent(payload);
|
|
||||||
},
|
|
||||||
|
|
||||||
checkIncompatibleAddons() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
AddonManager.getAddonsByIDs(INCOMPATIBLE_ADDON_IDS, (addons) => {
|
|
||||||
addons = addons.filter((a) => a && a.isActive);
|
|
||||||
const incompatibleAddons = addons.length !== 0;
|
|
||||||
if (incompatibleAddons) {
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "incompatible-addons-detected"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
resolve(incompatibleAddons);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tabs management
|
// Tabs management
|
||||||
|
|
||||||
async hideTabs(args) {
|
|
||||||
if (!("userContextId" in args)) {
|
|
||||||
return new Error("hideTabs must be called with userContextId argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._remapTabsIfMissing(args.userContextId);
|
|
||||||
if (!this._isKnownContainer(args.userContextId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "hide-tabs",
|
|
||||||
"userContextId": args.userContextId,
|
|
||||||
"clickedContainerTabCount": LOOKUP_KEY,
|
|
||||||
"shownContainersCount": LOOKUP_KEY,
|
|
||||||
"hiddenContainersCount": LOOKUP_KEY,
|
|
||||||
"totalContainersCount": LOOKUP_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const tabsToClose = [];
|
|
||||||
|
|
||||||
const tabObjects = await Promise.all(this._matchTabsByContainer(args.userContextId).map((tab) => {
|
|
||||||
tabsToClose.push(tab);
|
|
||||||
return this._createTabObject(tab);
|
|
||||||
}));
|
|
||||||
|
|
||||||
tabObjects.forEach((object) => {
|
|
||||||
// This tab is going to be closed. Let's mark this tabObject as
|
|
||||||
// non-active.
|
|
||||||
object.active = false;
|
|
||||||
|
|
||||||
this._identitiesState[args.userContextId].hiddenTabs.push(object);
|
|
||||||
});
|
|
||||||
|
|
||||||
await this._closeTabs(tabsToClose);
|
|
||||||
|
|
||||||
return this._syncTabs();
|
|
||||||
},
|
|
||||||
|
|
||||||
async showTabs(args) {
|
|
||||||
if (!("userContextId" in args)) {
|
|
||||||
return Promise.reject("showTabs must be called with userContextId argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._remapTabsIfMissing(args.userContextId);
|
|
||||||
if (!this._isKnownContainer(args.userContextId)) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "show-tabs",
|
|
||||||
"userContextId": args.userContextId,
|
|
||||||
"clickedContainerTabCount": LOOKUP_KEY,
|
|
||||||
"shownContainersCount": LOOKUP_KEY,
|
|
||||||
"hiddenContainersCount": LOOKUP_KEY,
|
|
||||||
"totalContainersCount": LOOKUP_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
const hiddenTabs = this._identitiesState[args.userContextId].hiddenTabs;
|
|
||||||
this._identitiesState[args.userContextId].hiddenTabs = [];
|
|
||||||
|
|
||||||
for (let object of hiddenTabs) { // eslint-disable-line prefer-const
|
|
||||||
promises.push(this.openTab({
|
|
||||||
userContextId: args.userContextId,
|
|
||||||
url: object.url,
|
|
||||||
nofocus: args.nofocus || false,
|
|
||||||
pinned: object.pinned,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._identitiesState[args.userContextId].hiddenTabs = [];
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
return this._syncTabs();
|
|
||||||
},
|
|
||||||
|
|
||||||
sortTabs() {
|
|
||||||
const containersCounts = this._containersCounts();
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "sort-tabs",
|
|
||||||
"shownContainersCount": containersCounts.shown,
|
|
||||||
"totalContainerTabsCount": this._totalContainerTabsCount(),
|
|
||||||
"totalNonContainerTabsCount": this._totalNonContainerTabsCount()
|
|
||||||
});
|
|
||||||
return new Promise(resolve => {
|
|
||||||
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 = tabsUtils.getTabs(xulWindow);
|
|
||||||
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 getTabs(args) {
|
|
||||||
if (!("userContextId" in args)) {
|
|
||||||
return new Error("getTabs must be called with userContextId argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._remapTabsIfMissing(args.userContextId);
|
|
||||||
if (!this._isKnownContainer(args.userContextId)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
this._matchTabsByContainer(args.userContextId).forEach((tab) => {
|
|
||||||
promises.push(this._createTabObject(tab));
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = await Promise.all(promises);
|
|
||||||
return list.concat(this._identitiesState[args.userContextId].hiddenTabs);
|
|
||||||
},
|
|
||||||
|
|
||||||
showTab(args) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!("tabId" in args)) {
|
|
||||||
reject("showTab must be called with tabId argument.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tab of tabs) {
|
|
||||||
if (tab.id === args.tabId) {
|
|
||||||
tab.window.activate();
|
|
||||||
tab.activate();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
moveTabsToWindow(args) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!("userContextId" in args)) {
|
|
||||||
reject("moveTabsToWindow must be called with userContextId argument.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._remapTabsIfMissing(args.userContextId);
|
|
||||||
if (!this._isKnownContainer(args.userContextId)) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "move-tabs-to-window",
|
|
||||||
"userContextId": args.userContextId,
|
|
||||||
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = this._matchTabsByContainer(args.userContextId);
|
|
||||||
|
|
||||||
// Nothing to do
|
|
||||||
if (list.length === 0 &&
|
|
||||||
this._identitiesState[args.userContextId].hiddenTabs.length === 0) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
windows.browserWindows.open({
|
|
||||||
url: "about:blank",
|
|
||||||
onOpen: window => {
|
|
||||||
const newBrowserWindow = viewFor(window);
|
|
||||||
let pos = 0;
|
|
||||||
|
|
||||||
// Let's move the tab to the new window.
|
|
||||||
for (let tab of list) { // eslint-disable-line prefer-const
|
|
||||||
newBrowserWindow.gBrowser.adoptTab(viewFor(tab), pos++, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's show the hidden tabs.
|
|
||||||
for (let object of this._identitiesState[args.userContextId].hiddenTabs) { // eslint-disable-line prefer-const
|
|
||||||
newBrowserWindow.gBrowser.addTab(object.url || DEFAULT_TAB, { userContextId: args.userContextId });
|
|
||||||
}
|
|
||||||
|
|
||||||
this._identitiesState[args.userContextId].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.
|
|
||||||
for (let tab of window.tabs) { // eslint-disable-line prefer-const
|
|
||||||
const userContextId = this._getUserContextIdFromTab(tab);
|
|
||||||
if (args.userContextId !== userContextId) {
|
|
||||||
newBrowserWindow.gBrowser.removeTab(viewFor(tab));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(null);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
openTab(args) {
|
openTab(args) {
|
||||||
return this.triggerBackgroundCallback(args, "open-tab");
|
return this.triggerBackgroundCallback(args, "open-tab");
|
||||||
},
|
},
|
||||||
|
|
||||||
// Identities management
|
// Identities management
|
||||||
queryIdentitiesState() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const identities = {};
|
|
||||||
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
|
||||||
this._remapTabsIfMissing(identity.userContextId);
|
|
||||||
const convertedIdentity = {
|
|
||||||
hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length,
|
|
||||||
hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs
|
|
||||||
};
|
|
||||||
|
|
||||||
identities[identity.userContextId] = convertedIdentity;
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve(identities);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
queryIdentities() {
|
queryIdentities() {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const identities = ContextualIdentityProxy.getIdentities();
|
const identities = ContextualIdentityProxy.getIdentities();
|
||||||
identities.forEach(identity => {
|
|
||||||
this._remapTabsIfMissing(identity.userContextId);
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve(identities);
|
resolve(identities);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Preferences
|
|
||||||
|
|
||||||
getPreference(args) {
|
|
||||||
if (!("pref" in args)) {
|
|
||||||
return Promise.reject("getPreference must be called with pref argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(prefService.get(args.pref));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Styling the window
|
// Styling the window
|
||||||
|
|
||||||
_configureWindows() {
|
_configureWindows() {
|
||||||
|
@ -1114,10 +605,6 @@ ContainerWindow.prototype = {
|
||||||
attachMenuEvent(source, button) {
|
attachMenuEvent(source, button) {
|
||||||
const popup = button.querySelector(".new-tab-popup");
|
const popup = button.querySelector(".new-tab-popup");
|
||||||
popup.addEventListener("popupshown", () => {
|
popup.addEventListener("popupshown", () => {
|
||||||
ContainerService.sendTelemetryPayload({
|
|
||||||
"event": "show-plus-button-menu",
|
|
||||||
"eventSource": source
|
|
||||||
});
|
|
||||||
popup.querySelector("menuseparator").remove();
|
popup.querySelector("menuseparator").remove();
|
||||||
const popupMenuItems = [...popup.querySelectorAll("menuitem")];
|
const popupMenuItems = [...popup.querySelectorAll("menuitem")];
|
||||||
popupMenuItems.forEach((item) => {
|
popupMenuItems.forEach((item) => {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "testpilot-containers",
|
"name": "testpilot-containers",
|
||||||
"title": "Containers Experiment",
|
"title": "Containers Experiment",
|
||||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||||
"version": "2.5.0",
|
"version": "3.0.0",
|
||||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
||||||
|
|
|
@ -1,746 +0,0 @@
|
||||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
|
||||||
const LOOKUP_KEY = "$ref";
|
|
||||||
|
|
||||||
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: ["<all_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;
|
|
||||||
}
|
|
||||||
const cookieStore = tab.cookieStoreId;
|
|
||||||
const container = cookieStore.replace("firefox-container-", "");
|
|
||||||
if (container !== cookieStore) {
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
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 backgroundLogic = {
|
|
||||||
NEW_TAB_PAGES: new Set([
|
|
||||||
"about:startpage",
|
|
||||||
"about:newtab",
|
|
||||||
"about:home",
|
|
||||||
"about:blank"
|
|
||||||
]),
|
|
||||||
|
|
||||||
deleteContainer(userContextId) {
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
event: "delete-container",
|
|
||||||
userContextId
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeTabsPromise = this._containerTabs(userContextId).then((tabs) => {
|
|
||||||
const tabIds = tabs.map((tab) => tab.id);
|
|
||||||
return browser.tabs.remove(tabIds);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 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": LOOKUP_KEY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
browser.runtime.sendMessage({
|
|
||||||
method: "showTabs",
|
|
||||||
userContextId: options.userContextId
|
|
||||||
});
|
|
||||||
return browser.tabs.create({
|
|
||||||
url,
|
|
||||||
active,
|
|
||||||
pinned: options.pinned || false,
|
|
||||||
cookieStoreId: backgroundLogic.cookieStoreId(options.userContextId)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
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: ["<all_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,
|
|
||||||
init() {
|
|
||||||
this.check();
|
|
||||||
},
|
|
||||||
setPopupIcon(theme) {
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
14
webextension/js/.eslintrc.js
Normal file
14
webextension/js/.eslintrc.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
"extends": [
|
||||||
|
"../../.eslintrc.js"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"assignManager": true,
|
||||||
|
"badge": true,
|
||||||
|
"backgroundLogic": true,
|
||||||
|
"identityState": true,
|
||||||
|
"messageHandler": true,
|
||||||
|
"tabPageCounter": true,
|
||||||
|
"themeManager": true
|
||||||
|
}
|
||||||
|
};
|
330
webextension/js/background/assignManager.js
Normal file
330
webextension/js/background/assignManager.js
Normal file
|
@ -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: ["<all_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();
|
345
webextension/js/background/backgroundLogic.js
Normal file
345
webextension/js/background/backgroundLogic.js
Normal file
|
@ -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;});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
18
webextension/js/background/badge.js
Normal file
18
webextension/js/background/badge.js
Normal file
|
@ -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();
|
142
webextension/js/background/identityState.js
Normal file
142
webextension/js/background/identityState.js
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
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];
|
||||||
|
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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
_createIdentityState() {
|
||||||
|
return {
|
||||||
|
hiddenTabs: [],
|
||||||
|
openTabs: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
28
webextension/js/background/index.html
Normal file
28
webextension/js/background/index.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
This didn't work for debugging in the manifest.
|
||||||
|
"scripts": [
|
||||||
|
"js/background/backgroundLogic.js",
|
||||||
|
"js/background/assignManager.js",
|
||||||
|
"js/background/badge.js",
|
||||||
|
"js/background/identityState.js",
|
||||||
|
"js/background/messageHandler.js",
|
||||||
|
"js/background/tabPageCounter.js",
|
||||||
|
"js/background/themeManager.js",
|
||||||
|
"js/backdround/init.js"
|
||||||
|
]
|
||||||
|
-->
|
||||||
|
<script type="text/javascript" src="backgroundLogic.js"></script>
|
||||||
|
<script type="text/javascript" src="assignManager.js"></script>
|
||||||
|
<script type="text/javascript" src="badge.js"></script>
|
||||||
|
<script type="text/javascript" src="identityState.js"></script>
|
||||||
|
<script type="text/javascript" src="messageHandler.js"></script>
|
||||||
|
<script type="text/javascript" src="tabPageCounter.js"></script>
|
||||||
|
<script type="text/javascript" src="themeManager.js"></script>
|
||||||
|
<script type="text/javascript" src="init.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
webextension/js/background/init.js
Normal file
27
webextension/js/background/init.js
Normal file
|
@ -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" });
|
||||||
|
}
|
177
webextension/js/background/messageHandler.js
Normal file
177
webextension/js/background/messageHandler.js
Normal file
|
@ -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: ["<all_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();
|
65
webextension/js/background/tabPageCounter.js
Normal file
65
webextension/js/background/tabPageCounter.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
};
|
51
webextension/js/background/themeManager.js
Normal file
51
webextension/js/background/themeManager.js
Normal file
|
@ -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();
|
|
@ -183,22 +183,21 @@ const Logic = {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshIdentities() {
|
async refreshIdentities() {
|
||||||
return Promise.all([
|
const [identities, state] = await Promise.all([
|
||||||
browser.contextualIdentities.query({}),
|
browser.contextualIdentities.query({}),
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "queryIdentitiesState"
|
method: "queryIdentitiesState"
|
||||||
})
|
})
|
||||||
]).then(([identities, state]) => {
|
]);
|
||||||
this._identities = identities.map((identity) => {
|
this._identities = identities.map((identity) => {
|
||||||
const stateObject = state[Logic.userContextId(identity.cookieStoreId)];
|
const stateObject = state[identity.cookieStoreId];
|
||||||
if (stateObject) {
|
if (stateObject) {
|
||||||
identity.hasOpenTabs = stateObject.hasOpenTabs;
|
identity.hasOpenTabs = stateObject.hasOpenTabs;
|
||||||
identity.hasHiddenTabs = stateObject.hasHiddenTabs;
|
identity.hasHiddenTabs = stateObject.hasHiddenTabs;
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
});
|
});
|
||||||
}).catch((e) => {throw e;});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getPanelSelector(panel) {
|
getPanelSelector(panel) {
|
||||||
|
@ -265,6 +264,11 @@ const Logic = {
|
||||||
return Logic.userContextId(identity.cookieStoreId);
|
return Logic.userContextId(identity.cookieStoreId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currentCookieStoreId() {
|
||||||
|
const identity = Logic.currentIdentity();
|
||||||
|
return identity.cookieStoreId;
|
||||||
|
},
|
||||||
|
|
||||||
sendTelemetryPayload(message = {}) {
|
sendTelemetryPayload(message = {}) {
|
||||||
if (!message.event) {
|
if (!message.event) {
|
||||||
throw new Error("Missing event name for telemetry");
|
throw new Error("Missing event name for telemetry");
|
||||||
|
@ -308,12 +312,11 @@ const Logic = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getShieldStudyVariation() {
|
async getShieldStudyVariation() {
|
||||||
return browser.runtime.sendMessage({
|
const variation = await browser.runtime.sendMessage({
|
||||||
method: "getShieldStudyVariation"
|
method: "getShieldStudyVariation"
|
||||||
}).then(variation => {
|
|
||||||
this._onboardingVariation = variation;
|
|
||||||
});
|
});
|
||||||
|
this._onboardingVariation = variation;
|
||||||
},
|
},
|
||||||
|
|
||||||
generateIdentityName() {
|
generateIdentityName() {
|
||||||
|
@ -472,28 +475,31 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), async function () {
|
||||||
browser.runtime.sendMessage({
|
try {
|
||||||
method: "sortTabs"
|
await browser.runtime.sendMessage({
|
||||||
}).then(() => {
|
method: "sortTabs"
|
||||||
|
});
|
||||||
window.close();
|
window.close();
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
|
const selectables = [...document.querySelectorAll("[tabindex='0'], [tabindex='-1']")];
|
||||||
const element = document.activeElement;
|
const element = document.activeElement;
|
||||||
|
const index = selectables.indexOf(element) || 0;
|
||||||
function next() {
|
function next() {
|
||||||
const nextElement = element.nextElementSibling;
|
const nextElement = selectables[index + 1];
|
||||||
if (nextElement) {
|
if (nextElement) {
|
||||||
nextElement.querySelector("td[tabindex=0]").focus();
|
nextElement.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function previous() {
|
function previous() {
|
||||||
const previousElement = element.previousElementSibling;
|
const previousElement = selectables[index - 1];
|
||||||
if (previousElement) {
|
if (previousElement) {
|
||||||
previousElement.querySelector("td[tabindex=0]").focus();
|
previousElement.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
|
@ -603,21 +609,22 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
tr.appendChild(manage);
|
tr.appendChild(manage);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logic.addEnterHandler(tr, e => {
|
Logic.addEnterHandler(tr, async function (e) {
|
||||||
if (e.target.matches(".open-newtab")
|
if (e.target.matches(".open-newtab")
|
||||||
|| e.target.parentNode.matches(".open-newtab")
|
|| e.target.parentNode.matches(".open-newtab")
|
||||||
|| e.type === "keydown") {
|
|| e.type === "keydown") {
|
||||||
browser.runtime.sendMessage({
|
try {
|
||||||
method: "openTab",
|
await browser.runtime.sendMessage({
|
||||||
message: {
|
method: "openTab",
|
||||||
userContextId: Logic.userContextId(identity.cookieStoreId),
|
message: {
|
||||||
source: "pop-up"
|
userContextId: Logic.userContextId(identity.cookieStoreId),
|
||||||
}
|
source: "pop-up"
|
||||||
}).then(() => {
|
}
|
||||||
|
});
|
||||||
window.close();
|
window.close();
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
}
|
||||||
} else if (hasTabs) {
|
} else if (hasTabs) {
|
||||||
Logic.showPanel(P_CONTAINER_INFO, identity);
|
Logic.showPanel(P_CONTAINER_INFO, identity);
|
||||||
}
|
}
|
||||||
|
@ -652,27 +659,29 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
panelSelector: "#container-info-panel",
|
panelSelector: "#container-info-panel",
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
async initialize() {
|
||||||
Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => {
|
Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => {
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), () => {
|
Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), async function () {
|
||||||
const identity = Logic.currentIdentity();
|
const identity = Logic.currentIdentity();
|
||||||
browser.runtime.sendMessage({
|
try {
|
||||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
browser.runtime.sendMessage({
|
||||||
userContextId: Logic.currentUserContextId()
|
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||||
}).then(() => {
|
cookieStoreId: Logic.currentCookieStoreId()
|
||||||
|
});
|
||||||
window.close();
|
window.close();
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if the user has incompatible add-ons installed
|
// Check if the user has incompatible add-ons installed
|
||||||
browser.runtime.sendMessage({
|
try {
|
||||||
method: "checkIncompatibleAddons"
|
const incompatible = await browser.runtime.sendMessage({
|
||||||
}).then(incompatible => {
|
method: "checkIncompatibleAddons"
|
||||||
|
});
|
||||||
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
||||||
if (incompatible) {
|
if (incompatible) {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
|
@ -688,22 +697,21 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
|
|
||||||
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
||||||
} else {
|
} else {
|
||||||
Logic.addEnterHandler(moveTabsEl, () => {
|
Logic.addEnterHandler(moveTabsEl, async function () {
|
||||||
browser.runtime.sendMessage({
|
await browser.runtime.sendMessage({
|
||||||
method: "moveTabsToWindow",
|
method: "moveTabsToWindow",
|
||||||
userContextId: Logic.userContextId(Logic.currentIdentity().cookieStoreId),
|
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
||||||
}).then(() => {
|
});
|
||||||
window.close();
|
window.close();
|
||||||
}).catch((e) => { throw e; });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
throw new Error("Could not check for incompatible add-ons.");
|
throw new Error("Could not check for incompatible add-ons.");
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// This method is called when the panel is shown.
|
// This method is called when the panel is shown.
|
||||||
prepare() {
|
async prepare() {
|
||||||
const identity = Logic.currentIdentity();
|
const identity = Logic.currentIdentity();
|
||||||
|
|
||||||
// Populating the panel: name and icon
|
// Populating the panel: name and icon
|
||||||
|
@ -731,10 +739,11 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's retrieve the list of tabs.
|
// Let's retrieve the list of tabs.
|
||||||
return browser.runtime.sendMessage({
|
const tabs = await browser.runtime.sendMessage({
|
||||||
method: "getTabs",
|
method: "getTabs",
|
||||||
userContextId: Logic.currentUserContextId(),
|
cookieStoreId: Logic.currentIdentity().cookieStoreId
|
||||||
}).then(this.buildInfoTable);
|
});
|
||||||
|
return this.buildInfoTable(tabs);
|
||||||
},
|
},
|
||||||
|
|
||||||
buildInfoTable(tabs) {
|
buildInfoTable(tabs) {
|
||||||
|
@ -747,20 +756,14 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
tr.innerHTML = escaped`
|
tr.innerHTML = escaped`
|
||||||
<td></td>
|
<td></td>
|
||||||
<td class="container-info-tab-title truncate-text" title="${tab.url}" >${tab.title}</td>`;
|
<td class="container-info-tab-title truncate-text" title="${tab.url}" >${tab.title}</td>`;
|
||||||
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.
|
// On click, we activate this tab. But only if this tab is active.
|
||||||
if (tab.active) {
|
if (!tab.hiddenState) {
|
||||||
tr.classList.add("clickable");
|
tr.classList.add("clickable");
|
||||||
Logic.addEnterHandler(tr, () => {
|
Logic.addEnterHandler(tr, async function () {
|
||||||
browser.runtime.sendMessage({
|
await browser.tabs.update(tab.id, {active: true});
|
||||||
method: "showTab",
|
window.close();
|
||||||
tabId: tab.id,
|
|
||||||
}).then(() => {
|
|
||||||
window.close();
|
|
||||||
}).catch(() => {
|
|
||||||
window.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -871,25 +874,25 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_submitForm() {
|
async _submitForm() {
|
||||||
const formValues = new FormData(this._editForm);
|
const formValues = new FormData(this._editForm);
|
||||||
return browser.runtime.sendMessage({
|
try {
|
||||||
method: "createOrUpdateContainer",
|
await browser.runtime.sendMessage({
|
||||||
message: {
|
method: "createOrUpdateContainer",
|
||||||
userContextId: formValues.get("container-id") || NEW_CONTAINER_ID,
|
message: {
|
||||||
params: {
|
userContextId: formValues.get("container-id") || NEW_CONTAINER_ID,
|
||||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
params: {
|
||||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||||
color: formValues.get("container-color") || DEFAULT_COLOR,
|
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||||
|
color: formValues.get("container-color") || DEFAULT_COLOR,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}).then(() => {
|
await Logic.refreshIdentities();
|
||||||
return Logic.refreshIdentities();
|
|
||||||
}).then(() => {
|
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
Logic.showPanel(P_CONTAINERS_LIST);
|
Logic.showPanel(P_CONTAINERS_LIST);
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showAssignedContainers(assignments) {
|
showAssignedContainers(assignments) {
|
||||||
|
@ -919,17 +922,15 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
src="/img/container-delete.svg"
|
src="/img/container-delete.svg"
|
||||||
/>`;
|
/>`;
|
||||||
const deleteButton = trElement.querySelector(".delete-assignment");
|
const deleteButton = trElement.querySelector(".delete-assignment");
|
||||||
Logic.addEnterHandler(deleteButton, () => {
|
const that = this;
|
||||||
|
Logic.addEnterHandler(deleteButton, async function () {
|
||||||
const userContextId = Logic.currentUserContextId();
|
const userContextId = Logic.currentUserContextId();
|
||||||
// Lets show the message to the current tab
|
// Lets show the message to the current tab
|
||||||
// TODO remove then when firefox supports arrow fn async
|
// TODO remove then when firefox supports arrow fn async
|
||||||
Logic.currentTab().then((currentTab) => {
|
const currentTab = await Logic.currentTab();
|
||||||
Logic.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true);
|
Logic.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true);
|
||||||
delete assignments[siteKey];
|
delete assignments[siteKey];
|
||||||
this.showAssignedContainers(assignments);
|
that.showAssignedContainers(assignments);
|
||||||
}).catch((e) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
trElement.classList.add("container-info-tab-row", "clickable");
|
trElement.classList.add("container-info-tab-row", "clickable");
|
||||||
tableElement.appendChild(trElement);
|
tableElement.appendChild(trElement);
|
||||||
|
@ -1002,19 +1003,19 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||||
Logic.showPreviousPanel();
|
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.
|
/* 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
|
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.
|
if you want to do anything post delete do it in the background script.
|
||||||
Browser console currently warns about not listening also.
|
Browser console currently warns about not listening also.
|
||||||
*/
|
*/
|
||||||
Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)).then(() => {
|
try {
|
||||||
return Logic.refreshIdentities();
|
await Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId));
|
||||||
}).then(() => {
|
await Logic.refreshIdentities();
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
}).catch(() => {
|
} catch(e) {
|
||||||
Logic.showPanel(P_CONTAINERS_LIST);
|
Logic.showPanel(P_CONTAINERS_LIST);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Containers Experiment",
|
"name": "Containers Experiment",
|
||||||
"version": "2.5.0",
|
"version": "3.0.0",
|
||||||
|
|
||||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
@ -48,12 +48,44 @@
|
||||||
"16": "img/container-site-d-24.png",
|
"16": "img/container-site-d-24.png",
|
||||||
"32": "img/container-site-d-48.png"
|
"32": "img/container-site-d-48.png"
|
||||||
},
|
},
|
||||||
|
"theme_icons": [
|
||||||
|
{
|
||||||
|
"size": 16,
|
||||||
|
"dark": "img/container-site-d-24.png",
|
||||||
|
"light": "img/container-site-w-24.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": 24,
|
||||||
|
"dark": "img/container-site-d-24.png",
|
||||||
|
"light": "img/container-site-w-24.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": 32,
|
||||||
|
"dark": "img/container-site-d-48.png",
|
||||||
|
"light": "img/container-site-w-48.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": 48,
|
||||||
|
"dark": "img/container-site-d-48.png",
|
||||||
|
"light": "img/container-site-w-48.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": 96,
|
||||||
|
"dark": "img/container-site-d-96.png",
|
||||||
|
"light": "img/container-site-w-96.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": 192,
|
||||||
|
"dark": "img/container-site-d-192.png",
|
||||||
|
"light": "img/container-site-w-192.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
"default_title": "Containers",
|
"default_title": "Containers",
|
||||||
"default_popup": "popup.html"
|
"default_popup": "popup.html"
|
||||||
},
|
},
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"]
|
"page": "js/background/index.html"
|
||||||
},
|
},
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
|
|
Loading…
Add table
Reference in a new issue