Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ab705081e | |||
| 15b9dce1a9 | |||
| da3fc2ede2 | |||
| 385c585888 | |||
| 734b97beb0 | |||
| 3aa311a3c1 | |||
| df7d7f9c38 | |||
| 65be77665a | |||
| 44548659db | |||
| 10c4395efd | |||
| 27b2a4b5f2 | |||
| 6f54e7ff7f | |||
| cb6726b667 | |||
| 0964311fa1 | |||
| 2831d019f5 | |||
| 1cc58cad9b | |||
| bc9660f76e | |||
| f526caca50 | |||
| b6a98fb83e | |||
| af2b4b79a9 | |||
| a762b5eca2 | |||
| 3cc40344af | |||
| 278cdb7f69 | |||
| 0be03ebeb7 | |||
| c69f37a2de | |||
| b20ac8169a | |||
| 9e98d35b45 | |||
| 17f2781e07 | |||
| 0acf9cc0e6 | |||
| 70573d0559 | |||
| da239237f7 | |||
| 29f078d2c9 | |||
| 5704a21c97 | |||
| 57a31f7f97 | |||
| d685a58d74 | |||
| a44bf21582 | |||
| 8f8fc322eb | |||
| f03404ad9e | |||
| 78b5de3b44 | |||
| e78f49bec5 |
@@ -1,3 +1 @@
|
|||||||
testpilot-metrics.js
|
|
||||||
lib/shield/*.js
|
|
||||||
lib/testpilot/*.js
|
lib/testpilot/*.js
|
||||||
|
|||||||
+6
-1
@@ -13,7 +13,12 @@ module.exports = {
|
|||||||
"CustomizableUI": true,
|
"CustomizableUI": true,
|
||||||
"CustomizableWidgets": true,
|
"CustomizableWidgets": true,
|
||||||
"SessionStore": true,
|
"SessionStore": true,
|
||||||
"Services": true
|
"Services": true,
|
||||||
|
"Components": true,
|
||||||
|
"XPCOMUtils": true,
|
||||||
|
"OS": true,
|
||||||
|
"ADDON_UNINSTALL": true,
|
||||||
|
"ADDON_DISABLE": true
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"promise",
|
"promise",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Containers Add-on
|
# Firefox Multi-Account Containers
|
||||||
|
|
||||||
[](https://testpilot.firefox.com/experiments/containers)
|
[](https://testpilot.firefox.com/experiments/containers)
|
||||||
|
|
||||||
@@ -20,6 +20,19 @@ For more info, see:
|
|||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Web Extension Development
|
||||||
|
|
||||||
|
Since Firefox 57, this extension can now be run without any of the legacy components that were previously needed.
|
||||||
|
|
||||||
|
1. Install web-ext with npm
|
||||||
|
2. cd webextension; web-ext run -f Nightly
|
||||||
|
|
||||||
|
This will work in other builds of Firefox however certain features won't work and you will need to manually flip preferences to enable containers. All other sections of this guide talk about using the legacy setup with jpm.
|
||||||
|
|
||||||
|
|
||||||
|
## Legacy Development
|
||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
|
|
||||||
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
|
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
|
||||||
@@ -40,6 +53,16 @@ Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs.
|
|||||||
5. Click the gear, and select "Install Add-on From File..."
|
5. Click the gear, and select "Install Add-on From File..."
|
||||||
6. Select the `.xpi` file
|
6. Select the `.xpi` file
|
||||||
|
|
||||||
|
#### Correct prefs
|
||||||
|
|
||||||
|
Whilst this is still using legacy code to test you will need the following in your profile:
|
||||||
|
|
||||||
|
Change the following prefs in about:config:
|
||||||
|
|
||||||
|
- extensions.legacy.enabled = true
|
||||||
|
- xpinstall.signatures.required = false
|
||||||
|
|
||||||
|
|
||||||
#### Run the TxP experiment with `jpm`
|
#### Run the TxP experiment with `jpm`
|
||||||
|
|
||||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
||||||
@@ -49,23 +72,12 @@ Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs.
|
|||||||
|
|
||||||
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
|
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
|
||||||
|
|
||||||
#### Run the shield study with `shield`
|
|
||||||
|
|
||||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
|
||||||
2. `cd testpilot-containers`
|
|
||||||
3. `npm install`
|
|
||||||
4. `npm install -g shield-study-cli`
|
|
||||||
5. `shield run . -- --binary Nightly`
|
|
||||||
|
|
||||||
### Building .xpi
|
### Building .xpi
|
||||||
|
|
||||||
To build a local testpilot-containers.xpi, use the plain [`jpm
|
To build a local testpilot-containers.xpi, use the plain [`jpm
|
||||||
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command,
|
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command,
|
||||||
or run `npm run build`.
|
or run `npm run build`.
|
||||||
|
|
||||||
#### Building a shield .xpi
|
|
||||||
To build a local shield-study-containers.xpi, run `npm run build-shield`.
|
|
||||||
|
|
||||||
### Signing an .xpi
|
### Signing an .xpi
|
||||||
|
|
||||||
To sign an .xpi, use [`jpm
|
To sign an .xpi, use [`jpm
|
||||||
|
|||||||
Vendored
+170
@@ -0,0 +1,170 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const PREFS = [
|
||||||
|
{
|
||||||
|
name: "privacy.userContext.enabled",
|
||||||
|
value: true,
|
||||||
|
type: "bool",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "privacy.userContext.longPressBehavior",
|
||||||
|
value: 2,
|
||||||
|
type: "int",
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "privacy.userContext.ui.enabled",
|
||||||
|
value: true, // Post web ext we will be setting this true
|
||||||
|
type: "bool",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "privacy.usercontext.about_newtab_segregation.enabled",
|
||||||
|
value: true,
|
||||||
|
type: "bool",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
const Cc = Components.classes;
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
const { TextDecoder, TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
|
"resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
|
const JETPACK_DIR_BASENAME = "jetpack";
|
||||||
|
const EXTENSION_ID = "@testpilot-containers";
|
||||||
|
|
||||||
|
function loadStyles(resourceURI) {
|
||||||
|
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
|
||||||
|
.getService(Ci.nsIStyleSheetService);
|
||||||
|
const styleURI = styleSheet(resourceURI);
|
||||||
|
const sheetType = styleSheetService.AGENT_SHEET;
|
||||||
|
styleSheetService.loadAndRegisterSheet(styleURI, sheetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleSheet(resourceURI) {
|
||||||
|
return Services.io.newURI("data/usercontext.css", null, resourceURI);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unloadStyles(resourceURI) {
|
||||||
|
const styleURI = styleSheet(resourceURI);
|
||||||
|
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
|
||||||
|
.getService(Ci.nsIStyleSheetService);
|
||||||
|
const sheetType = styleSheetService.AGENT_SHEET;
|
||||||
|
if (styleSheetService.sheetRegistered(styleURI, sheetType)) {
|
||||||
|
styleSheetService.unregisterSheet(styleURI, sheetType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filename() {
|
||||||
|
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||||
|
storeFile.append(JETPACK_DIR_BASENAME);
|
||||||
|
storeFile.append(EXTENSION_ID);
|
||||||
|
storeFile.append("simple-storage");
|
||||||
|
storeFile.append("store.json");
|
||||||
|
return storeFile.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeFilepath() {
|
||||||
|
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||||
|
storeFile.append(JETPACK_DIR_BASENAME);
|
||||||
|
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
|
||||||
|
storeFile.append(EXTENSION_ID);
|
||||||
|
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
|
||||||
|
storeFile.append("simple-storage");
|
||||||
|
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getConfig() {
|
||||||
|
let savedConfig = {savedConfiguration: {}};
|
||||||
|
try {
|
||||||
|
const bytes = await OS.File.read(filename());
|
||||||
|
const raw = new TextDecoder().decode(bytes) || "";
|
||||||
|
if (raw) {
|
||||||
|
savedConfig = JSON.parse(raw);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore file read errors, sometimes they happen and I'm not sure if we can fix
|
||||||
|
}
|
||||||
|
return savedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initConfig() {
|
||||||
|
const savedConfig = await getConfig();
|
||||||
|
savedConfig.savedConfiguration.version = 2;
|
||||||
|
if (!("prefs" in savedConfig.savedConfiguration)) {
|
||||||
|
savedConfig.savedConfiguration.prefs = {};
|
||||||
|
PREFS.forEach((pref) => {
|
||||||
|
if ("int" === pref.type) {
|
||||||
|
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getIntPref(pref.name);
|
||||||
|
} else {
|
||||||
|
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getBoolPref(pref.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const serialized = JSON.stringify(savedConfig);
|
||||||
|
const bytes = new TextEncoder().encode(serialized) || "";
|
||||||
|
await makeFilepath();
|
||||||
|
await OS.File.writeAtomic(filename(), bytes, { });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPrefs() {
|
||||||
|
PREFS.forEach((pref) => {
|
||||||
|
if ("int" === pref.type) {
|
||||||
|
Services.prefs.setIntPref(pref.name, pref.value);
|
||||||
|
} else {
|
||||||
|
Services.prefs.setBoolPref(pref.name, pref.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
async function install() {
|
||||||
|
await initConfig();
|
||||||
|
setPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
async function uninstall(aData, aReason) {
|
||||||
|
if (aReason === ADDON_UNINSTALL
|
||||||
|
|| aReason === ADDON_DISABLE) {
|
||||||
|
const config = await getConfig();
|
||||||
|
const storedPrefs = config.savedConfiguration.prefs || {};
|
||||||
|
PREFS.forEach((pref) => {
|
||||||
|
let value = pref.default;
|
||||||
|
if (pref.name in storedPrefs) {
|
||||||
|
value = storedPrefs[pref.name];
|
||||||
|
}
|
||||||
|
if ("int" === pref.type) {
|
||||||
|
Services.prefs.setIntPref(pref.name, value);
|
||||||
|
} else {
|
||||||
|
Services.prefs.setBoolPref(pref.name, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function startup({webExtension, resourceURI}) {
|
||||||
|
const version = Services.appinfo.version;
|
||||||
|
const versionMatch = version.match(/^([0-9]+)\./)[1];
|
||||||
|
if (versionMatch === "55"
|
||||||
|
|| versionMatch === "56") {
|
||||||
|
loadStyles(resourceURI);
|
||||||
|
}
|
||||||
|
// Reset prefs that may have changed, or are legacy
|
||||||
|
install();
|
||||||
|
// Start the embedded webextension.
|
||||||
|
webExtension.startup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function shutdown({resourceURI}) {
|
||||||
|
unloadStyles(resourceURI);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,108 +1,3 @@
|
|||||||
/* HACK: Custom Container vars do not propigate correctly
|
|
||||||
until the container tab is blurred and refocused,
|
|
||||||
adding the data-identity-color with the default hex
|
|
||||||
value, or chrome url path as an alternate selector mitiages this bug.*/
|
|
||||||
[data-identity-color="blue"],
|
|
||||||
[data-identity-color="#00a7e0"] {
|
|
||||||
--identity-tab-color: #37adff;
|
|
||||||
--identity-icon-color: #37adff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="turquoise"],
|
|
||||||
[data-identity-color="#01bdad"] {
|
|
||||||
--identity-tab-color: #00c79a;
|
|
||||||
--identity-icon-color: #00c79a;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="green"],
|
|
||||||
[data-identity-color="#7dc14c"] {
|
|
||||||
--identity-tab-color: #51cd00;
|
|
||||||
--identity-icon-color: #51cd00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="yellow"],
|
|
||||||
[data-identity-color="#ffcb00"] {
|
|
||||||
--identity-tab-color: #ffcb00;
|
|
||||||
--identity-icon-color: #ffcb00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="orange"],
|
|
||||||
[data-identity-color="#f89c24"] {
|
|
||||||
--identity-tab-color: #ff9f00;
|
|
||||||
--identity-icon-color: #ff9f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="red"],
|
|
||||||
[data-identity-color="#d92215"] {
|
|
||||||
--identity-tab-color: #ff613d;
|
|
||||||
--identity-icon-color: #ff613d;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="pink"],
|
|
||||||
[data-identity-color="#ee5195"] {
|
|
||||||
--identity-tab-color: #ff4bda;
|
|
||||||
--identity-icon-color: #ff4bda;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="purple"],
|
|
||||||
[data-identity-color="#7a2f7a"] {
|
|
||||||
--identity-tab-color: #af51f5;
|
|
||||||
--identity-icon-color: #af51f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="fingerprint"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#fingerprint");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="briefcase"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#briefcase");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="dollar"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#dollar");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="cart"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/shopping.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#cart");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="circle"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#circle");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="gift"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#gift");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="vacation"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#vacation");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="food"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#food");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="fruit"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#fruit");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="pet"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#pet");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="tree"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#tree");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="chill"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#chill");
|
|
||||||
}
|
|
||||||
|
|
||||||
#userContext-indicator {
|
#userContext-indicator {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
list-style-image: none !important;
|
list-style-image: none !important;
|
||||||
@@ -129,19 +24,6 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userContext-icon,
|
|
||||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon,
|
|
||||||
.subviewbutton[usercontextid] > .toolbarbutton-icon,
|
|
||||||
#userContext-indicator {
|
|
||||||
background-image: var(--identity-icon) !important;
|
|
||||||
background-position: center center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
fill: var(--identity-icon-color) !important;
|
|
||||||
filter: url(/img/filters.svg#fill);
|
|
||||||
filter: url(/data/filters.svg#fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* containers experiment */
|
/* containers experiment */
|
||||||
|
|
||||||
/* reset nightly containers */
|
/* reset nightly containers */
|
||||||
|
|||||||
-285
@@ -1,285 +0,0 @@
|
|||||||
# METRICS
|
|
||||||
|
|
||||||
## Data Analysis
|
|
||||||
The collected data will primarily be used to answer the following questions.
|
|
||||||
Images are used for visualization and are not composed of actual data.
|
|
||||||
|
|
||||||
### Do users install and run this?
|
|
||||||
|
|
||||||
What is the overall engagement of the Containers experiment?
|
|
||||||
**This is the standard Daily Active User (DAU) and Monthly Active User (MAU) analysis.**
|
|
||||||
|
|
||||||
This captures data from the users who have the add-on installed, regardless of
|
|
||||||
whether they are actively interacting with it.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Immediate Questions
|
|
||||||
|
|
||||||
* Do people use the containers feature & how do people create new container tabs?
|
|
||||||
* Click to create new container tab
|
|
||||||
* \+ `entry-point` value: "tab-bar" or "pop-up"
|
|
||||||
* Do people who use the containers feature continue to use it?
|
|
||||||
* Retention: opening a second container tab (second tab in the same container, or a tab in a second container?)
|
|
||||||
* What containers do people use?
|
|
||||||
* userContextId
|
|
||||||
* \+ Number of tabs in the container (when should we measure this? on every tab open?)
|
|
||||||
* Do people edit their containers?
|
|
||||||
* Click on "Edit Containers"
|
|
||||||
* Click to edit a single container
|
|
||||||
* Click "OK"
|
|
||||||
* Click to delete a single container
|
|
||||||
* Click "OK"
|
|
||||||
* Click to add a container
|
|
||||||
* Click "OK"
|
|
||||||
* Do people sort the tabs?
|
|
||||||
* Click sort
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* Average number of container tabs when sort was clicked
|
|
||||||
* Do users show and hide container tabs?
|
|
||||||
* Click hide
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* \+ Number of hidden containers when clicked
|
|
||||||
* Click show
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* \+ Number of shown containers when clicked
|
|
||||||
* Do users move container tabs to new windows?
|
|
||||||
* Click move
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* Average number of container tabs when new window was clicked
|
|
||||||
* How many containers do users have hidden at the same time? (when should we measure this? each time a container is hidden?)
|
|
||||||
* Do users pin container tabs? (do we have existing Telemetry for pinning?)
|
|
||||||
* Do users visit more pages in container tabs than non-container tabs?
|
|
||||||
|
|
||||||
### Follow-up Questions
|
|
||||||
|
|
||||||
What are some follow-up questions we anticipate we will ask based on any of the
|
|
||||||
above answers/data?
|
|
||||||
|
|
||||||
* What is the average lifespan of a container tab? Is that longer or shorter than a regular tab? (if we don't have data on the latter, the former probably isn't worth gathering data on since we will have nothing to compare it to).
|
|
||||||
|
|
||||||
## Data Collection
|
|
||||||
|
|
||||||
### Server Side
|
|
||||||
There is currently no server side component to Containers.
|
|
||||||
|
|
||||||
### Client Side
|
|
||||||
Containers will use Test Pilot Telemetry with no batching of data. Details
|
|
||||||
of when pings are sent are below, along with examples of the `payload` portion
|
|
||||||
of a `testpilottest` telemetry ping for each scenario.
|
|
||||||
|
|
||||||
* The user shows the new tab menu
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "show-plus-button-menu",
|
|
||||||
"eventSource": ["plus-button"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks on a container name to open a tab in that container
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "open-tab",
|
|
||||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"|"alltabs-menu"|"plus-button"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Edit Containers" in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "edit-containers"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking on a container edit icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "edit-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking on a container delete icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "delete-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking to add a container in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "add-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks the sort button/icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "sort-tabs",
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainerTabsCount": <number-of-all-container-tabs>,
|
|
||||||
"totalNonContainerTabsCount": <number-of-all-non-container-tabs>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Hide these container tabs" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "hide-tabs",
|
|
||||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Show these container tabs" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "show-tabs",
|
|
||||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Move tabs to a new window" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "move-tabs-to-window"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* When a user encounters the disabled "move" feature because of incompatible add-ons
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "incompatible-addons-detected"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user closes a tab
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "page-requests-completed-per-tab",
|
|
||||||
"pageRequestCount": <pageRequestCount>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user goes idle
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "page-requests-completed-per-activity",
|
|
||||||
"pageRequestCount": <pageRequestCount>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user chooses "Always Open in this Container" context menu option. (Note: We send two separate event names: one for assigning a site to a container, one for removing a site from a container.)
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "[added|removed]-container-assignment"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Firefox prompts the user to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "prompt-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Open in *assigned* container" to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "click-to-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Open in *Current* container" to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "click-to-reload-page-in-same-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "auto-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### A Redshift schema for the payload:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local schema = {
|
|
||||||
-- column name field type length attributes field name
|
|
||||||
{"uuid", "VARCHAR", 255, nil, "Fields[payload.uuid]"},
|
|
||||||
{"userContextId", "INTEGER", 255, nil, "Fields[payload.userContextId]"},
|
|
||||||
{"clickedContainerTabCount", "INTEGER", 255, nil, "Fields[payload.clickedContainerTabCount]"},
|
|
||||||
{"eventSource", "VARCHAR", 255, nil, "Fields[payload.eventSource]"},
|
|
||||||
{"event", "VARCHAR", 255, nil, "Fields[payload.event]"},
|
|
||||||
{"pageRequestCount", "INTEGER", 255, nil, "Fields[payload.pageRequestCount]"}
|
|
||||||
{"hiddenContainersCount", "INTEGER", 255, nil, "Fields[payload.hiddenContainersCount]"},
|
|
||||||
{"shownContainersCount", "INTEGER", 255, nil, "Fields[payload.shownContainersCount]"},
|
|
||||||
{"totalContainersCount", "INTEGER", 255, nil, "Fields[payload.totalContainersCount]"},
|
|
||||||
{"totalContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalContainerTabsCount]"},
|
|
||||||
{"totalNonContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalNonContainerTabsCount]"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Valid data should be enforced on the server side:
|
|
||||||
|
|
||||||
* `eventSource` should be one of `tab-bar`, `pop-up`, `file-menu`, "alltabs-nmenu" or "plus-button".
|
|
||||||
|
|
||||||
All Mozilla data is kept by default for 180 days and in accordance with our
|
|
||||||
privacy policies.
|
|
||||||
@@ -1,930 +0,0 @@
|
|||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
||||||
|
|
||||||
const IDENTITY_COLORS = [
|
|
||||||
{ name: "blue", color: "#00a7e0" },
|
|
||||||
{ name: "turquoise", color: "#01bdad" },
|
|
||||||
{ name: "green", color: "#7dc14c" },
|
|
||||||
{ name: "yellow", color: "#ffcb00" },
|
|
||||||
{ name: "orange", color: "#f89c24" },
|
|
||||||
{ name: "red", color: "#d92215" },
|
|
||||||
{ name: "pink", color: "#ee5195" },
|
|
||||||
{ name: "purple", color: "#7a2f7a" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const IDENTITY_ICONS = [
|
|
||||||
{ name: "fingerprint", image: "chrome://browser/skin/usercontext/personal.svg" },
|
|
||||||
{ name: "briefcase", image: "chrome://browser/skin/usercontext/work.svg" },
|
|
||||||
{ name: "dollar", image: "chrome://browser/skin/usercontext/banking.svg" },
|
|
||||||
{ name: "cart", image: "chrome://browser/skin/usercontext/shopping.svg" },
|
|
||||||
// All of these do not exist in gecko
|
|
||||||
{ name: "gift", image: "gift" },
|
|
||||||
{ name: "vacation", image: "vacation" },
|
|
||||||
{ name: "food", image: "food" },
|
|
||||||
{ name: "fruit", image: "fruit" },
|
|
||||||
{ name: "pet", image: "pet" },
|
|
||||||
{ name: "tree", image: "tree" },
|
|
||||||
{ name: "chill", image: "chill" },
|
|
||||||
{ name: "circle", image: "circle" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const IDENTITY_COLORS_STANDARD = [
|
|
||||||
"blue", "orange", "green", "pink",
|
|
||||||
];
|
|
||||||
|
|
||||||
const IDENTITY_ICONS_STANDARD = [
|
|
||||||
"fingerprint", "briefcase", "dollar", "cart",
|
|
||||||
];
|
|
||||||
|
|
||||||
const PREFS = [
|
|
||||||
[ "privacy.userContext.enabled", true ],
|
|
||||||
[ "privacy.userContext.longPressBehavior", 2 ],
|
|
||||||
[ "privacy.userContext.ui.enabled", false ],
|
|
||||||
[ "privacy.usercontext.about_newtab_segregation.enabled", true ],
|
|
||||||
];
|
|
||||||
|
|
||||||
const { attachTo, detachFrom } = require("sdk/content/mod");
|
|
||||||
const { Cu } = require("chrome");
|
|
||||||
const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm");
|
|
||||||
const Metrics = require("./testpilot-metrics");
|
|
||||||
const { modelFor } = require("sdk/model/core");
|
|
||||||
const prefService = require("sdk/preferences/service");
|
|
||||||
const self = require("sdk/self");
|
|
||||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
|
||||||
const ss = require("sdk/simple-storage");
|
|
||||||
const { study } = require("./study");
|
|
||||||
const { Style } = require("sdk/stylesheet/style");
|
|
||||||
const tabs = require("sdk/tabs");
|
|
||||||
const uuid = require("sdk/util/uuid");
|
|
||||||
const { viewFor } = require("sdk/view/core");
|
|
||||||
const webExtension = require("sdk/webextension");
|
|
||||||
const windows = require("sdk/windows");
|
|
||||||
const windowUtils = require("sdk/window/utils");
|
|
||||||
|
|
||||||
Cu.import("resource:///modules/CustomizableUI.jsm");
|
|
||||||
Cu.import("resource:///modules/CustomizableWidgets.jsm");
|
|
||||||
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
|
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
|
||||||
|
|
||||||
|
|
||||||
// ContextualIdentityProxy
|
|
||||||
const ContextualIdentityProxy = {
|
|
||||||
getIdentities() {
|
|
||||||
let response;
|
|
||||||
if ("getPublicIdentities" in ContextualIdentityService) {
|
|
||||||
response = ContextualIdentityService.getPublicIdentities();
|
|
||||||
} else {
|
|
||||||
response = ContextualIdentityService.getIdentities();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.map((identity) => {
|
|
||||||
return this._convert(identity);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getIdentityFromId(userContextId) {
|
|
||||||
let response;
|
|
||||||
if ("getPublicIdentityFromId" in ContextualIdentityService) {
|
|
||||||
response = ContextualIdentityService.getPublicIdentityFromId(userContextId);
|
|
||||||
} else {
|
|
||||||
response = ContextualIdentityService.getIdentityFromId(userContextId);
|
|
||||||
}
|
|
||||||
if (response) {
|
|
||||||
return this._convert(response);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
|
|
||||||
_convert(identity) {
|
|
||||||
return {
|
|
||||||
name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
|
|
||||||
icon: identity.icon,
|
|
||||||
color: identity.color,
|
|
||||||
userContextId: identity.userContextId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// ContainerService
|
|
||||||
|
|
||||||
const ContainerService = {
|
|
||||||
_windowMap: new Map(),
|
|
||||||
_containerWasEnabled: false,
|
|
||||||
_onBackgroundConnectCallback: null,
|
|
||||||
|
|
||||||
async init(installation, reason) {
|
|
||||||
// If we are just been installed, we must store some information for the
|
|
||||||
// uninstallation. This object contains also a version number, in case we
|
|
||||||
// need to implement a migration in the future.
|
|
||||||
// In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild
|
|
||||||
if (!("savedConfiguration" in ss.storage) ||
|
|
||||||
!("prefs" in ss.storage.savedConfiguration) ||
|
|
||||||
(installation && reason !== "upgrade")) {
|
|
||||||
let preInstalledIdentities = []; // eslint-disable-line prefer-const
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
|
||||||
preInstalledIdentities.push(identity.userContextId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const object = {
|
|
||||||
version: 1,
|
|
||||||
prefs: {},
|
|
||||||
metricsUUID: uuid.uuid().toString(),
|
|
||||||
preInstalledIdentities: preInstalledIdentities
|
|
||||||
};
|
|
||||||
|
|
||||||
PREFS.forEach(pref => {
|
|
||||||
object.prefs[pref[0]] = prefService.get(pref[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
ss.storage.savedConfiguration = object;
|
|
||||||
|
|
||||||
if (prefService.get("privacy.userContext.enabled") !== true) {
|
|
||||||
// Maybe rename the Banking container.
|
|
||||||
const identity = ContextualIdentityProxy.getIdentityFromId(3);
|
|
||||||
if (identity && identity.l10nID === "userContextBanking.label") {
|
|
||||||
ContextualIdentityService.update(identity.userContextId,
|
|
||||||
"Finance",
|
|
||||||
identity.icon,
|
|
||||||
identity.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's create the default containers in case there are none.
|
|
||||||
if (ss.storage.savedConfiguration.preInstalledIdentities.length === 0) {
|
|
||||||
// Note: we have to create them in this way because there is no way to
|
|
||||||
// reuse the same ID and the localized strings.
|
|
||||||
ContextualIdentityService.create("Personal", "fingerprint", "blue");
|
|
||||||
ContextualIdentityService.create("Work", "briefcase", "orange");
|
|
||||||
ContextualIdentityService.create("Finance", "dollar", "green");
|
|
||||||
ContextualIdentityService.create("Shopping", "cart", "pink");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOCHECK should this run on all code
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
|
||||||
const newIcon = this._fromIconToName(identity.icon);
|
|
||||||
const newColor = this._fromColorToName(identity.color);
|
|
||||||
if (newIcon !== identity.icon || newColor !== identity.color) {
|
|
||||||
ContextualIdentityService.update(identity.userContextId,
|
|
||||||
ContextualIdentityService.getUserContextLabel(identity.userContextId),
|
|
||||||
newIcon,
|
|
||||||
newColor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Let's see if containers were enabled before this addon.
|
|
||||||
this._containerWasEnabled =
|
|
||||||
ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"];
|
|
||||||
|
|
||||||
// Enabling preferences
|
|
||||||
|
|
||||||
PREFS.forEach((pref) => {
|
|
||||||
prefService.set(pref[0], pref[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._metricsUUID = ss.storage.savedConfiguration.metricsUUID;
|
|
||||||
|
|
||||||
// Disabling the customizable container panel.
|
|
||||||
CustomizableUI.destroyWidget("containers-panelmenu");
|
|
||||||
|
|
||||||
tabs.on("open", tab => {
|
|
||||||
this._restyleTab(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
tabs.on("activate", tab => {
|
|
||||||
this._restyleActiveTab(tab).catch(() => {});
|
|
||||||
this._configureActiveWindows();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Modify CSS and other stuff for each window.
|
|
||||||
|
|
||||||
this._configureWindows().catch(() => {});
|
|
||||||
|
|
||||||
windows.browserWindows.on("open", window => {
|
|
||||||
this._configureWindow(viewFor(window)).catch(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
windows.browserWindows.on("close", window => {
|
|
||||||
this.closeWindow(viewFor(window));
|
|
||||||
});
|
|
||||||
|
|
||||||
// WebExtension startup
|
|
||||||
|
|
||||||
try {
|
|
||||||
const api = await webExtension.startup();
|
|
||||||
this.registerBackgroundConnection(api);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("WebExtension startup failed. Unable to continue.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._sendEvent = new Metrics({
|
|
||||||
type: "sdk",
|
|
||||||
id: self.id,
|
|
||||||
version: self.version
|
|
||||||
}).sendEvent;
|
|
||||||
|
|
||||||
// Begin-Of-Hack
|
|
||||||
ContextualIdentityService.workaroundForCookieManager = function(method, userContextId) {
|
|
||||||
let identity = method.call(ContextualIdentityService, userContextId);
|
|
||||||
if (!identity && userContextId) {
|
|
||||||
identity = {
|
|
||||||
userContextId,
|
|
||||||
icon: "",
|
|
||||||
color: "",
|
|
||||||
name: "Pending to be deleted",
|
|
||||||
public: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return identity;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!this._oldGetIdentityFromId) {
|
|
||||||
this._oldGetIdentityFromId = ContextualIdentityService.getIdentityFromId;
|
|
||||||
}
|
|
||||||
ContextualIdentityService.getIdentityFromId = function(userContextId) {
|
|
||||||
return this.workaroundForCookieManager(ContainerService._oldGetIdentityFromId, userContextId);
|
|
||||||
};
|
|
||||||
|
|
||||||
if ("getPublicIdentityFromId" in ContextualIdentityService) {
|
|
||||||
if (!this._oldGetPublicIdentityFromId) {
|
|
||||||
this._oldGetPublicIdentityFromId = ContextualIdentityService.getPublicIdentityFromId;
|
|
||||||
}
|
|
||||||
ContextualIdentityService.getPublicIdentityFromId = function(userContextId) {
|
|
||||||
return this.workaroundForCookieManager(ContainerService._oldGetPublicIdentityFromId, userContextId);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// End-Of-Hack
|
|
||||||
|
|
||||||
if (self.id === "@shield-study-containers") {
|
|
||||||
study.startup(reason);
|
|
||||||
this.shieldStudyVariation = study.variation;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
registerBackgroundConnection(api) {
|
|
||||||
// This is only used for theme notifications and new tab
|
|
||||||
api.browser.runtime.onConnect.addListener((port) => {
|
|
||||||
this._onBackgroundConnectCallback = (message, topic) => {
|
|
||||||
port.postMessage({
|
|
||||||
type: topic,
|
|
||||||
message
|
|
||||||
});
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerBackgroundCallback(message, topic) {
|
|
||||||
if (this._onBackgroundConnectCallback) {
|
|
||||||
this._onBackgroundConnectCallback(message, topic);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// In FF 50-51, the icon is the full path, in 52 and following
|
|
||||||
// releases, we have IDs to be used with a svg file. In this function
|
|
||||||
// we map URLs to svg IDs.
|
|
||||||
|
|
||||||
// Helper methods for converting colors to names and names to colors.
|
|
||||||
|
|
||||||
_fromNameToColor(name) {
|
|
||||||
return this._fromNameOrColor(name, "color");
|
|
||||||
},
|
|
||||||
|
|
||||||
_fromColorToName(color) {
|
|
||||||
return this._fromNameOrColor(color, "name");
|
|
||||||
},
|
|
||||||
|
|
||||||
_fromNameOrColor(what, attribute) {
|
|
||||||
for (let color of IDENTITY_COLORS) { // eslint-disable-line prefer-const
|
|
||||||
if (what === color.color || what === color.name) {
|
|
||||||
return color[attribute];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
|
|
||||||
// Helper methods for converting icons to names and names to icons.
|
|
||||||
|
|
||||||
_fromIconToName(icon) {
|
|
||||||
return this._fromNameOrIcon(icon, "name", "circle");
|
|
||||||
},
|
|
||||||
|
|
||||||
_fromNameOrIcon(what, attribute, defaultValue) {
|
|
||||||
for (let icon of IDENTITY_ICONS) { // eslint-disable-line prefer-const
|
|
||||||
if (what === icon.image || what === icon.name) {
|
|
||||||
return icon[attribute];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tab Helpers
|
|
||||||
|
|
||||||
_getUserContextIdFromTab(tab) {
|
|
||||||
return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10);
|
|
||||||
},
|
|
||||||
|
|
||||||
_matchTabsByContainer(userContextId) {
|
|
||||||
const matchedTabs = [];
|
|
||||||
for (const tab of tabs) {
|
|
||||||
if (userContextId === this._getUserContextIdFromTab(tab)) {
|
|
||||||
matchedTabs.push(tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchedTabs;
|
|
||||||
},
|
|
||||||
|
|
||||||
async _closeTabs(tabsToClose) {
|
|
||||||
// We create a new tab only if the current operation closes all the
|
|
||||||
// existing ones.
|
|
||||||
if (tabs.length === tabsToClose.length) {
|
|
||||||
await this.openTab({});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tab of tabsToClose) {
|
|
||||||
// after .close() window is null. Let's take it now.
|
|
||||||
const window = viewFor(tab.window);
|
|
||||||
|
|
||||||
tab.close();
|
|
||||||
|
|
||||||
// forget about this tab. 0 is the index of the forgotten tab and 0
|
|
||||||
// means the last one.
|
|
||||||
try {
|
|
||||||
SessionStore.forgetClosedTab(window, 0);
|
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_recentBrowserWindow() {
|
|
||||||
const browserWin = windowUtils.getMostRecentBrowserWindow();
|
|
||||||
|
|
||||||
// This should not really happen.
|
|
||||||
if (!browserWin || !browserWin.gBrowser) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(browserWin);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tabs management
|
|
||||||
|
|
||||||
openTab(args) {
|
|
||||||
return this.triggerBackgroundCallback(args, "open-tab");
|
|
||||||
},
|
|
||||||
|
|
||||||
// Identities management
|
|
||||||
|
|
||||||
queryIdentities() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const identities = ContextualIdentityProxy.getIdentities();
|
|
||||||
resolve(identities);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Styling the window
|
|
||||||
|
|
||||||
_configureWindows() {
|
|
||||||
const promises = [];
|
|
||||||
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
|
||||||
promises.push(this._configureWindow(viewFor(window)));
|
|
||||||
}
|
|
||||||
return Promise.all(promises);
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureWindow(window) {
|
|
||||||
return this._getOrCreateContainerWindow(window).configure();
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureActiveWindows() {
|
|
||||||
const promises = [];
|
|
||||||
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
|
||||||
promises.push(this._configureActiveWindow(viewFor(window)));
|
|
||||||
}
|
|
||||||
return Promise.all(promises);
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureActiveWindow(window) {
|
|
||||||
return this._getOrCreateContainerWindow(window).configureActive();
|
|
||||||
},
|
|
||||||
|
|
||||||
closeWindow(window) {
|
|
||||||
this._windowMap.delete(window);
|
|
||||||
},
|
|
||||||
|
|
||||||
_getOrCreateContainerWindow(window) {
|
|
||||||
if (!(this._windowMap.has(window))) {
|
|
||||||
this._windowMap.set(window, new ContainerWindow(window));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._windowMap.get(window);
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshNeeded() {
|
|
||||||
return this._configureWindows();
|
|
||||||
},
|
|
||||||
|
|
||||||
_restyleActiveTab(tab) {
|
|
||||||
if (!tab) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userContextId = ContainerService._getUserContextIdFromTab(tab);
|
|
||||||
const identity = ContextualIdentityProxy.getIdentityFromId(userContextId);
|
|
||||||
const hbox = viewFor(tab.window).document.getElementById("userContext-icons");
|
|
||||||
|
|
||||||
if (!identity) {
|
|
||||||
hbox.setAttribute("data-identity-color", "");
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
hbox.setAttribute("data-identity-color", identity.color);
|
|
||||||
|
|
||||||
const label = viewFor(tab.window).document.getElementById("userContext-label");
|
|
||||||
label.setAttribute("value", identity.name);
|
|
||||||
label.style.color = ContainerService._fromNameToColor(identity.color);
|
|
||||||
|
|
||||||
const indicator = viewFor(tab.window).document.getElementById("userContext-indicator");
|
|
||||||
indicator.setAttribute("data-identity-icon", identity.icon);
|
|
||||||
indicator.style.listStyleImage = "";
|
|
||||||
|
|
||||||
return this._restyleTab(tab);
|
|
||||||
},
|
|
||||||
|
|
||||||
_restyleTab(tab) {
|
|
||||||
if (!tab) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
const userContextId = ContainerService._getUserContextIdFromTab(tab);
|
|
||||||
const identity = ContextualIdentityProxy.getIdentityFromId(userContextId);
|
|
||||||
if (!identity) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
return Promise.resolve(viewFor(tab).setAttribute("data-identity-color", identity.color));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Uninstallation
|
|
||||||
uninstall(reason) {
|
|
||||||
const data = ss.storage.savedConfiguration;
|
|
||||||
if (!data) {
|
|
||||||
throw new DOMError("ERROR - No saved configuration!!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.version !== 1) {
|
|
||||||
throw new DOMError("ERROR - Unknown version!!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reason !== "upgrade") {
|
|
||||||
PREFS.forEach(pref => {
|
|
||||||
if (pref[0] in data.prefs) {
|
|
||||||
prefService.set(pref[0], data.prefs[pref[0]]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: We cannot go back renaming the Finance identity back to Banking:
|
|
||||||
// the locale system doesn't work with renamed containers.
|
|
||||||
|
|
||||||
// Restore the customizable container panel.
|
|
||||||
const widget = CustomizableWidgets.find(widget => widget.id === "containers-panelmenu");
|
|
||||||
if (widget) {
|
|
||||||
CustomizableUI.createWidget(widget);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
|
||||||
// Let's close all the container tabs.
|
|
||||||
// Note: We cannot use _closeTabs() because at this point tab.window is
|
|
||||||
// null.
|
|
||||||
if (!this._containerWasEnabled && reason !== "upgrade") {
|
|
||||||
for (let tab of window.tabs) { // eslint-disable-line prefer-const
|
|
||||||
if (this._getUserContextIdFromTab(tab)) {
|
|
||||||
tab.close();
|
|
||||||
try {
|
|
||||||
SessionStore.forgetClosedTab(viewFor(window), 0);
|
|
||||||
} catch(e) {} // eslint-disable-line no-empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._getOrCreateContainerWindow(viewFor(window)).shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
// all the configuration must go away now.
|
|
||||||
this._windowMap = new Map();
|
|
||||||
|
|
||||||
if (reason !== "upgrade") {
|
|
||||||
// Let's forget all the previous closed tabs.
|
|
||||||
this._forgetIdentity();
|
|
||||||
|
|
||||||
this._resetContainerToCentralIcons();
|
|
||||||
|
|
||||||
const preInstalledIdentities = data.preInstalledIdentities;
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
|
||||||
if (!preInstalledIdentities.includes(identity.userContextId)) {
|
|
||||||
ContextualIdentityService.remove(identity.userContextId);
|
|
||||||
} else {
|
|
||||||
// Let's cleanup all the cookies for this container.
|
|
||||||
Services.obs.notifyObservers(null, "clear-origin-attributes-data",
|
|
||||||
JSON.stringify({ userContextId: identity.userContextId }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Let's delete the configuration.
|
|
||||||
delete ss.storage.savedConfiguration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin-Of-Hack
|
|
||||||
if (this._oldGetIdentityFromId) {
|
|
||||||
ContextualIdentityService.getIdentityFromId = this._oldGetIdentityFromId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._oldGetPublicIdentityFromId) {
|
|
||||||
ContextualIdentityService.getPublicIdentityFromId = this._oldGetPublicIdentityFromId;
|
|
||||||
}
|
|
||||||
// End-Of-Hack
|
|
||||||
},
|
|
||||||
|
|
||||||
forgetIdentityAndRefresh(args) {
|
|
||||||
this._forgetIdentity(args.userContextId);
|
|
||||||
return this.refreshNeeded();
|
|
||||||
},
|
|
||||||
|
|
||||||
_forgetIdentity(userContextId = 0) {
|
|
||||||
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
|
||||||
window = viewFor(window);
|
|
||||||
const closedTabData = JSON.parse(SessionStore.getClosedTabData(window));
|
|
||||||
for (let i = closedTabData.length - 1; i >= 0; --i) {
|
|
||||||
if (!closedTabData[i].state.userContextId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContextId === 0 ||
|
|
||||||
closedTabData[i].state.userContextId === userContextId) {
|
|
||||||
try {
|
|
||||||
SessionStore.forgetClosedTab(window, i);
|
|
||||||
} catch(e) {} // eslint-disable-line no-empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// ContainerWindow
|
|
||||||
|
|
||||||
// This object is used to configure a single window.
|
|
||||||
function ContainerWindow(window) {
|
|
||||||
this._init(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
ContainerWindow.prototype = {
|
|
||||||
_window: null,
|
|
||||||
_style: null,
|
|
||||||
_panelElement: null,
|
|
||||||
_timeoutStore: new Map(),
|
|
||||||
_elementCache: new Map(),
|
|
||||||
_tooltipCache: new Map(),
|
|
||||||
_tabsElement: null,
|
|
||||||
|
|
||||||
_init(window) {
|
|
||||||
this._window = window;
|
|
||||||
this._tabsElement = this._window.document.getElementById("tabbrowser-tabs");
|
|
||||||
this._style = Style({ uri: self.data.url("usercontext.css") });
|
|
||||||
this._plusButton = this._window.document.getAnonymousElementByAttribute(this._tabsElement, "anonid", "tabs-newtab-button");
|
|
||||||
this._overflowPlusButton = this._window.document.getElementById("new-tab-button");
|
|
||||||
|
|
||||||
// Only hack the normal plus button as the alltabs is done elsewhere
|
|
||||||
this.attachMenuEvent("plus-button", this._plusButton);
|
|
||||||
|
|
||||||
attachTo(this._style, this._window);
|
|
||||||
},
|
|
||||||
|
|
||||||
attachMenuEvent(source, button) {
|
|
||||||
const popup = button.querySelector(".new-tab-popup");
|
|
||||||
popup.addEventListener("popupshown", () => {
|
|
||||||
popup.querySelector("menuseparator").remove();
|
|
||||||
const popupMenuItems = [...popup.querySelectorAll("menuitem")];
|
|
||||||
popupMenuItems.forEach((item) => {
|
|
||||||
const userContextId = item.getAttribute("data-usercontextid");
|
|
||||||
if (!userContextId) {
|
|
||||||
item.remove();
|
|
||||||
}
|
|
||||||
item.setAttribute("command", "");
|
|
||||||
item.addEventListener("command", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
ContainerService.openTab({
|
|
||||||
userContextId: userContextId,
|
|
||||||
source: source
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
configure() {
|
|
||||||
return Promise.all([
|
|
||||||
this._configureActiveTab(),
|
|
||||||
this._configureFileMenu(),
|
|
||||||
this._configureAllTabsMenu(),
|
|
||||||
this._configureTabStyle(),
|
|
||||||
this.configureActive(),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
configureActive() {
|
|
||||||
return this._configureContextMenu();
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureTabStyle() {
|
|
||||||
const promises = [];
|
|
||||||
for (let tab of modelFor(this._window).tabs) { // eslint-disable-line prefer-const
|
|
||||||
promises.push(ContainerService._restyleTab(tab));
|
|
||||||
}
|
|
||||||
return Promise.all(promises);
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureActiveTab() {
|
|
||||||
const tab = modelFor(this._window).tabs.activeTab;
|
|
||||||
return ContainerService._restyleActiveTab(tab);
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureFileMenu() {
|
|
||||||
return this._configureMenu("menu_newUserContext", null, e => {
|
|
||||||
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
|
|
||||||
ContainerService.openTab({
|
|
||||||
userContextId: userContextId,
|
|
||||||
source: "file-menu"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureAllTabsMenu() {
|
|
||||||
return this._configureMenu("alltabs_containersTab", null, e => {
|
|
||||||
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
|
|
||||||
ContainerService.showTabs({
|
|
||||||
userContextId,
|
|
||||||
nofocus: true,
|
|
||||||
window: this._window,
|
|
||||||
}).then(() => {
|
|
||||||
return ContainerService.openTab({
|
|
||||||
userContextId,
|
|
||||||
source: "alltabs-menu"
|
|
||||||
});
|
|
||||||
}).catch(() => {});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureContextMenu() {
|
|
||||||
return Promise.all([
|
|
||||||
this._configureMenu("context-openlinkinusercontext-menu",
|
|
||||||
() => {
|
|
||||||
// This userContextId is what we want to exclude.
|
|
||||||
const tab = modelFor(this._window).tabs.activeTab;
|
|
||||||
return ContainerService._getUserContextIdFromTab(tab);
|
|
||||||
},
|
|
||||||
e => {
|
|
||||||
// This is a super internal method. Hopefully it will be stable in the
|
|
||||||
// next FF releases.
|
|
||||||
this._window.gContextMenu.openLinkInTab(e);
|
|
||||||
|
|
||||||
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
|
|
||||||
ContainerService.showTabs({
|
|
||||||
userContextId,
|
|
||||||
nofocus: true,
|
|
||||||
window: this._window,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
),
|
|
||||||
this._configureContextMenuOpenLink(),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
_configureContextMenuOpenLink() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const self = this;
|
|
||||||
this._window.gSetUserContextIdAndClick = function(event) {
|
|
||||||
const tab = modelFor(self._window).tabs.activeTab;
|
|
||||||
const userContextId = ContainerService._getUserContextIdFromTab(tab);
|
|
||||||
event.target.setAttribute("data-usercontextid", userContextId);
|
|
||||||
self._window.gContextMenu.openLinkInTab(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
let item = this._window.document.getElementById("context-openlinkincontainertab");
|
|
||||||
item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)");
|
|
||||||
|
|
||||||
item = this._window.document.getElementById("context-openlinkintab");
|
|
||||||
item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)");
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Generic menu configuration.
|
|
||||||
_configureMenu(menuId, excludedContainerCb, clickCb) {
|
|
||||||
const menu = this._window.document.getElementById(menuId);
|
|
||||||
if (!this._disableElement(menu)) {
|
|
||||||
// Delete stale menu that isn't native elements
|
|
||||||
while (menu.firstChild) {
|
|
||||||
menu.removeChild(menu.firstChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const menupopup = this._window.document.createElementNS(XUL_NS, "menupopup");
|
|
||||||
menu.appendChild(menupopup);
|
|
||||||
|
|
||||||
menupopup.addEventListener("command", clickCb);
|
|
||||||
return this._createMenu(menupopup, excludedContainerCb);
|
|
||||||
},
|
|
||||||
|
|
||||||
_createMenu(target, excludedContainerCb) {
|
|
||||||
while (target.hasChildNodes()) {
|
|
||||||
target.removeChild(target.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
ContainerService.queryIdentities().then(identities => {
|
|
||||||
const fragment = this._window.document.createDocumentFragment();
|
|
||||||
|
|
||||||
const excludedUserContextId = excludedContainerCb ? excludedContainerCb() : 0;
|
|
||||||
if (excludedUserContextId) {
|
|
||||||
const bundle = this._window.document.getElementById("bundle_browser");
|
|
||||||
|
|
||||||
const menuitem = this._window.document.createElementNS(XUL_NS, "menuitem");
|
|
||||||
menuitem.setAttribute("data-usercontextid", "0");
|
|
||||||
menuitem.setAttribute("label", bundle.getString("userContextNone.label"));
|
|
||||||
menuitem.setAttribute("accesskey", bundle.getString("userContextNone.accesskey"));
|
|
||||||
|
|
||||||
fragment.appendChild(menuitem);
|
|
||||||
|
|
||||||
const menuseparator = this._window.document.createElementNS(XUL_NS, "menuseparator");
|
|
||||||
fragment.appendChild(menuseparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
identities.forEach(identity => {
|
|
||||||
if (identity.userContextId === excludedUserContextId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuitem = this._window.document.createElementNS(XUL_NS, "menuitem");
|
|
||||||
menuitem.setAttribute("label", identity.name);
|
|
||||||
menuitem.classList.add("menuitem-iconic");
|
|
||||||
menuitem.setAttribute("data-usercontextid", identity.userContextId);
|
|
||||||
menuitem.setAttribute("data-identity-color", identity.color);
|
|
||||||
menuitem.setAttribute("data-identity-icon", identity.icon);
|
|
||||||
fragment.appendChild(menuitem);
|
|
||||||
});
|
|
||||||
|
|
||||||
target.appendChild(fragment);
|
|
||||||
resolve();
|
|
||||||
}).catch(() => {reject();});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// This timer is used to hide the panel auto-magically if it's not used in
|
|
||||||
// the following X seconds. This is need to avoid the leaking of the panel
|
|
||||||
// when the mouse goes out of of the 'plus' button.
|
|
||||||
_createTimeout(key, callback, timeoutTime) {
|
|
||||||
this._cleanTimeout(key);
|
|
||||||
this._timeoutStore.set(key, this._window.setTimeout(() => {
|
|
||||||
callback();
|
|
||||||
this._timeoutStore.delete(key);
|
|
||||||
}, timeoutTime));
|
|
||||||
},
|
|
||||||
|
|
||||||
_cleanAllTimeouts() {
|
|
||||||
for (let key of this._timeoutStore.keys()) { // eslint-disable-line prefer-const
|
|
||||||
this._cleanTimeout(key);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_cleanTimeout(key) {
|
|
||||||
if (this._timeoutStore.has(key)) {
|
|
||||||
this._window.clearTimeout(this._timeoutStore.get(key));
|
|
||||||
this._timeoutStore.delete(key);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
// CSS must be removed.
|
|
||||||
detachFrom(this._style, this._window);
|
|
||||||
|
|
||||||
this._shutdownFileMenu();
|
|
||||||
this._shutdownAllTabsMenu();
|
|
||||||
this._shutdownContextMenu();
|
|
||||||
},
|
|
||||||
|
|
||||||
_shutDownPlusButtonMenuElement(buttonElement) {
|
|
||||||
if (buttonElement) {
|
|
||||||
this._shutdownElement(buttonElement);
|
|
||||||
buttonElement.setAttribute("tooltip", this._tooltipCache.get(buttonElement));
|
|
||||||
|
|
||||||
buttonElement.removeEventListener("mouseover", this);
|
|
||||||
buttonElement.removeEventListener("click", this);
|
|
||||||
buttonElement.removeEventListener("mouseout", this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_shutdownFileMenu() {
|
|
||||||
this._shutdownMenu("menu_newUserContext");
|
|
||||||
},
|
|
||||||
|
|
||||||
_shutdownAllTabsMenu() {
|
|
||||||
this._shutdownMenu("alltabs_containersTab");
|
|
||||||
},
|
|
||||||
|
|
||||||
_shutdownContextMenu() {
|
|
||||||
this._shutdownMenu("context-openlinkinusercontext-menu");
|
|
||||||
},
|
|
||||||
|
|
||||||
_shutdownMenu(menuId) {
|
|
||||||
const menu = this._window.document.getElementById(menuId);
|
|
||||||
this._shutdownElement(menu);
|
|
||||||
},
|
|
||||||
|
|
||||||
_shutdownElement(element) {
|
|
||||||
// Let's remove our elements.
|
|
||||||
while (element.firstChild) {
|
|
||||||
element.firstChild.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const elementCache = this._elementCache.get(element);
|
|
||||||
if (elementCache) {
|
|
||||||
for (let e of elementCache) { // eslint-disable-line prefer-const
|
|
||||||
element.appendChild(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_disableElement(element) {
|
|
||||||
// Nothing to disable.
|
|
||||||
if (!element || this._elementCache.has(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cacheArray = [];
|
|
||||||
|
|
||||||
// Let's store the previous elements so that we can repopulate it in case
|
|
||||||
// the addon is uninstalled.
|
|
||||||
while (element.firstChild) {
|
|
||||||
cacheArray.push(element.removeChild(element.firstChild));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._elementCache.set(element, cacheArray);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
_resetContainerToCentralIcons() {
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
|
||||||
if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) !== -1 &&
|
|
||||||
IDENTITY_COLORS_STANDARD.indexOf(identity.color) !== -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) === -1) {
|
|
||||||
if (identity.userContextId <= IDENTITY_ICONS_STANDARD.length) {
|
|
||||||
identity.icon = IDENTITY_ICONS_STANDARD[identity.userContextId - 1];
|
|
||||||
} else {
|
|
||||||
identity.icon = IDENTITY_ICONS_STANDARD[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IDENTITY_COLORS_STANDARD.indexOf(identity.color) === -1) {
|
|
||||||
if (identity.userContextId <= IDENTITY_COLORS_STANDARD.length) {
|
|
||||||
identity.color = IDENTITY_COLORS_STANDARD[identity.userContextId - 1];
|
|
||||||
} else {
|
|
||||||
identity.color = IDENTITY_COLORS_STANDARD[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ContextualIdentityService.update(identity.userContextId,
|
|
||||||
identity.name,
|
|
||||||
identity.icon,
|
|
||||||
identity.color);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// uninstall/install events ---------------------------------------------------
|
|
||||||
|
|
||||||
exports.main = function (options) {
|
|
||||||
const installation = options.loadReason === "install" ||
|
|
||||||
options.loadReason === "downgrade" ||
|
|
||||||
options.loadReason === "enable" ||
|
|
||||||
options.loadReason === "upgrade";
|
|
||||||
|
|
||||||
// Let's start :)
|
|
||||||
ContainerService.init(installation, options.loadReason);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.onUnload = function (reason) {
|
|
||||||
if (reason === "disable" ||
|
|
||||||
reason === "downgrade" ||
|
|
||||||
reason === "uninstall" ||
|
|
||||||
reason === "upgrade") {
|
|
||||||
ContainerService.uninstall(reason);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||||
|
<Description about="urn:mozilla:install-manifest">
|
||||||
|
<em:id>@testpilot-containers</em:id>
|
||||||
|
<em:type>2</em:type>
|
||||||
|
<em:bootstrap>true</em:bootstrap>
|
||||||
|
<em:multiprocessCompatible>true</em:multiprocessCompatible>
|
||||||
|
<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
|
||||||
|
<em:name>Firefox Multi-Account Containers</em:name>
|
||||||
|
<em:description>Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.</em:description>
|
||||||
|
<em:targetApplication>
|
||||||
|
<Description>
|
||||||
|
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
|
||||||
|
<em:minVersion>51.0a1</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
</Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
<em:version>4.0.1</em:version>
|
||||||
|
<em:unpack>false</em:unpack>
|
||||||
|
</Description>
|
||||||
|
</RDF>
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/**
|
|
||||||
* Drop-in replacement for {@link external:sdk/event/target.EventTarget} for use
|
|
||||||
* with es6 classes.
|
|
||||||
* @module event-target
|
|
||||||
* @author Martin Giger
|
|
||||||
* @license MPL-2.0
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* An SDK class that add event reqistration methods
|
|
||||||
* @external sdk/event/target
|
|
||||||
* @requires sdk/event/target
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @class EventTarget
|
|
||||||
* @memberof external:sdk/event/target
|
|
||||||
* @see {@link https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/event_target#EventTarget}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// slightly modified from: https://raw.githubusercontent.com/freaktechnik/justintv-stream-notifications/master/lib/event-target.js
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const { on, once, off, setListeners } = require("sdk/event/core");
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
|
||||||
/**
|
|
||||||
* @class
|
|
||||||
*/
|
|
||||||
class EventTarget {
|
|
||||||
constructor(options) {
|
|
||||||
setListeners(this, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
on(...args) {
|
|
||||||
on(this, ...args);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
once(...args) {
|
|
||||||
once(this, ...args);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
off(...args) {
|
|
||||||
off(this, ...args);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeListener(...args) {
|
|
||||||
off(this, ...args);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.EventTarget = EventTarget;
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
// Chrome privileged
|
|
||||||
const {Cu} = require("chrome");
|
|
||||||
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
|
|
||||||
const { TelemetryController } = Cu.import("resource://gre/modules/TelemetryController.jsm");
|
|
||||||
const CID = Cu.import("resource://gre/modules/ClientID.jsm");
|
|
||||||
|
|
||||||
// sdk
|
|
||||||
const { merge } = require("sdk/util/object");
|
|
||||||
const querystring = require("sdk/querystring");
|
|
||||||
const { prefs } = require("sdk/simple-prefs");
|
|
||||||
const prefSvc = require("sdk/preferences/service");
|
|
||||||
const { setInterval } = require("sdk/timers");
|
|
||||||
const tabs = require("sdk/tabs");
|
|
||||||
const { URL } = require("sdk/url");
|
|
||||||
|
|
||||||
const { EventTarget } = require("./event-target");
|
|
||||||
const { emit } = require("sdk/event/core");
|
|
||||||
const self = require("sdk/self");
|
|
||||||
|
|
||||||
const DAY = 86400*1000;
|
|
||||||
|
|
||||||
// ongoing within-addon fuses / timers
|
|
||||||
let lastDailyPing = Date.now();
|
|
||||||
|
|
||||||
/* Functional, self-contained utils */
|
|
||||||
|
|
||||||
// equal probability choices from a list "choices"
|
|
||||||
function chooseVariation(choices,rng=Math.random()) {
|
|
||||||
let l = choices.length;
|
|
||||||
return choices[Math.floor(l*Math.random())];
|
|
||||||
}
|
|
||||||
|
|
||||||
function dateToUTC(date) {
|
|
||||||
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateTelemetryIdIfNeeded() {
|
|
||||||
let id = TelemetryController.clientID;
|
|
||||||
/* istanbul ignore next */
|
|
||||||
if (id == undefined) {
|
|
||||||
return CID.ClientIDImpl._doLoadClientID()
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function userId () {
|
|
||||||
return prefSvc.get("toolkit.telemetry.cachedClientID","unknown");
|
|
||||||
}
|
|
||||||
|
|
||||||
var Reporter = new EventTarget().on("report",
|
|
||||||
(d) => prefSvc.get('shield.debug') && console.log("report",d)
|
|
||||||
);
|
|
||||||
|
|
||||||
function report(data, src="addon", bucket="shield-study") {
|
|
||||||
data = merge({}, data , {
|
|
||||||
study_version: self.version,
|
|
||||||
about: {
|
|
||||||
_src: src,
|
|
||||||
_v: 2
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (prefSvc.get('shield.testing')) data.testing = true
|
|
||||||
|
|
||||||
emit(Reporter, "report", data);
|
|
||||||
let telOptions = {addClientId: true, addEnvironment: true}
|
|
||||||
return TelemetryController.submitExternalPing(bucket, data, telOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
function survey (url, queryArgs={}) {
|
|
||||||
if (! url) return
|
|
||||||
|
|
||||||
let U = new URL(url);
|
|
||||||
let q = U.search;
|
|
||||||
if (q) {
|
|
||||||
url = U.href.split(q)[0];
|
|
||||||
q = querystring.parse(querystring.unescape(q.slice(1)));
|
|
||||||
} else {
|
|
||||||
q = {};
|
|
||||||
}
|
|
||||||
// get user info.
|
|
||||||
let newArgs = merge({},
|
|
||||||
q,
|
|
||||||
queryArgs
|
|
||||||
);
|
|
||||||
let searchstring = querystring.stringify(newArgs);
|
|
||||||
url = url + "?" + searchstring;
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function setOrGetFirstrun () {
|
|
||||||
let firstrun = prefs["shield.firstrun"];
|
|
||||||
if (firstrun === undefined) {
|
|
||||||
firstrun = prefs["shield.firstrun"] = String(dateToUTC(new Date())) // in utc, user set
|
|
||||||
}
|
|
||||||
return Number(firstrun)
|
|
||||||
}
|
|
||||||
|
|
||||||
function reuseVariation (choices) {
|
|
||||||
return prefs["shield.variation"];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVariation (choice) {
|
|
||||||
prefs["shield.variation"] = choice
|
|
||||||
return choice
|
|
||||||
}
|
|
||||||
|
|
||||||
function die (addonId=self.id) {
|
|
||||||
/* istanbul ignore else */
|
|
||||||
if (prefSvc.get("shield.fakedie")) return;
|
|
||||||
/* istanbul ignore next */
|
|
||||||
require("sdk/addon/installer").uninstall(addonId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: GRL vulnerable to clock time issues #1
|
|
||||||
function expired (xconfig, now = Date.now() ) {
|
|
||||||
return ((now - Number(xconfig.firstrun))/ DAY) > xconfig.days;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetShieldPrefs () {
|
|
||||||
delete prefs['shield.firstrun'];
|
|
||||||
delete prefs['shield.variation'];
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanup () {
|
|
||||||
prefSvc.keys(`extensions.${self.preferencesBranch}`).forEach (
|
|
||||||
(p) => {
|
|
||||||
delete prefs[p];
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function telemetrySubset (xconfig) {
|
|
||||||
return {
|
|
||||||
study_name: xconfig.name,
|
|
||||||
branch: xconfig.variation,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Study extends EventTarget {
|
|
||||||
constructor (config) {
|
|
||||||
super();
|
|
||||||
this.config = merge({
|
|
||||||
name: self.addonId,
|
|
||||||
variations: {'observe-only': () => {}},
|
|
||||||
surveyUrls: {},
|
|
||||||
days: 7
|
|
||||||
},config);
|
|
||||||
|
|
||||||
this.config.firstrun = setOrGetFirstrun();
|
|
||||||
|
|
||||||
let variation = reuseVariation();
|
|
||||||
if (variation === undefined) {
|
|
||||||
variation = this.decideVariation();
|
|
||||||
if (!(variation in this.config.variations)) {
|
|
||||||
// chaijs doesn't think this is an instanceof Error
|
|
||||||
// freaktechnik and gregglind debugged for a while.
|
|
||||||
// sdk errors might not be 'Errors' or chai is wack, who knows.
|
|
||||||
// https://dxr.mozilla.org/mozilla-central/search?q=regexp%3AError%5Cs%3F(%3A%7C%3D)+path%3Aaddon-sdk%2Fsource%2F&redirect=false would list
|
|
||||||
throw new Error("Study Error: chosen variation must be in config.variations")
|
|
||||||
}
|
|
||||||
setVariation(variation);
|
|
||||||
}
|
|
||||||
this.config.variation = variation;
|
|
||||||
|
|
||||||
this.flags = {
|
|
||||||
ineligibleDie: undefined
|
|
||||||
};
|
|
||||||
this.states = [];
|
|
||||||
// all these work, but could be cleaner. I hate the `bind` stuff.
|
|
||||||
this.on(
|
|
||||||
"change", (function (newstate) {
|
|
||||||
prefSvc.get('shield.debug') && console.log(newstate, this.states);
|
|
||||||
this.states.push(newstate);
|
|
||||||
emit(this, newstate); // could have checks here.
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"starting", (function () {
|
|
||||||
this.changeState("modifying");
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"maybe-installing", (function () {
|
|
||||||
if (!this.isEligible()) {
|
|
||||||
this.changeState("ineligible-die");
|
|
||||||
} else {
|
|
||||||
this.changeState("installed")
|
|
||||||
}
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"ineligible-die", (function () {
|
|
||||||
try {this.whenIneligible()} catch (err) {/*ok*/} finally { /*ok*/ }
|
|
||||||
this.flags.ineligibleDie = true;
|
|
||||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "ineligible"}), "shield");
|
|
||||||
this.final();
|
|
||||||
die();
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"installed", (function () {
|
|
||||||
try {this.whenInstalled()} catch (err) {/*ok*/} finally { /*ok*/ }
|
|
||||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "install"}), "shield");
|
|
||||||
this.changeState("modifying");
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"modifying", (function () {
|
|
||||||
var mybranchname = this.variation;
|
|
||||||
this.config.variations[mybranchname](); // do the effect
|
|
||||||
this.changeState("running");
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on( // the one 'many'
|
|
||||||
"running", (function () {
|
|
||||||
// report success
|
|
||||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "running"}), "shield");
|
|
||||||
this.final();
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"normal-shutdown", (function () {
|
|
||||||
this.flags.dying = true;
|
|
||||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "shutdown"}), "shield");
|
|
||||||
this.final();
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"end-of-study", (function () {
|
|
||||||
if (this.flags.expired) { // safe to call multiple times
|
|
||||||
this.final();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// first time seen.
|
|
||||||
this.flags.expired = true;
|
|
||||||
try {this.whenComplete()} catch (err) { /*ok*/ } finally { /*ok*/ }
|
|
||||||
this.report(merge({}, telemetrySubset(this.config) ,{study_state: "end-of-study"}), "shield");
|
|
||||||
// survey for end of study
|
|
||||||
let that = this;
|
|
||||||
generateTelemetryIdIfNeeded().then(()=>that.showSurvey("end-of-study"));
|
|
||||||
try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ }
|
|
||||||
this.final();
|
|
||||||
die();
|
|
||||||
}
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
this.on(
|
|
||||||
"user-uninstall-disable", (function () {
|
|
||||||
if (this.flags.dying) {
|
|
||||||
this.final();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.flags.dying = true;
|
|
||||||
this.report(merge({}, telemetrySubset(this.config), {study_state: "user-ended-study"}), "shield");
|
|
||||||
let that = this;
|
|
||||||
generateTelemetryIdIfNeeded().then(()=>that.showSurvey("user-ended-study"));
|
|
||||||
try {this.cleanup()} catch (err) {/*ok*/} finally { /*ok*/ }
|
|
||||||
this.final();
|
|
||||||
die();
|
|
||||||
}).bind(this)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get state () {
|
|
||||||
let n = this.states.length;
|
|
||||||
return n ? this.states[n-1] : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
get variation () {
|
|
||||||
return this.config.variation;
|
|
||||||
}
|
|
||||||
|
|
||||||
get firstrun () {
|
|
||||||
return this.config.firstrun;
|
|
||||||
}
|
|
||||||
|
|
||||||
dieIfExpired () {
|
|
||||||
let xconfig = this.config;
|
|
||||||
if (expired(xconfig)) {
|
|
||||||
emit(this, "change", "end-of-study");
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alivenessPulse (last=lastDailyPing) {
|
|
||||||
// check for new day, phone home if true.
|
|
||||||
let t = Date.now();
|
|
||||||
if ((t - last) >= DAY) {
|
|
||||||
lastDailyPing = t;
|
|
||||||
// phone home
|
|
||||||
emit(this,"change","running");
|
|
||||||
}
|
|
||||||
// check expiration, and die with report if needed
|
|
||||||
return this.dieIfExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
changeState (newstate) {
|
|
||||||
emit(this,'change', newstate);
|
|
||||||
}
|
|
||||||
|
|
||||||
final () {
|
|
||||||
emit(this,'final', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
startup (reason) {
|
|
||||||
// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload
|
|
||||||
|
|
||||||
// check expiry first, before anything, quit and die if so
|
|
||||||
|
|
||||||
// check once, right away, short circuit both install and startup
|
|
||||||
// to prevent modifications from happening.
|
|
||||||
if (this.dieIfExpired()) return this
|
|
||||||
|
|
||||||
switch (reason) {
|
|
||||||
case "install":
|
|
||||||
emit(this, "change", "maybe-installing");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "enable":
|
|
||||||
case "startup":
|
|
||||||
case "upgrade":
|
|
||||||
case "downgrade":
|
|
||||||
emit(this, "change", "starting");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! this._pulseTimer) this._pulseTimer = setInterval(this.alivenessPulse.bind(this), 5*60*1000 /*5 minutes */)
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown (reason) {
|
|
||||||
// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Listening_for_load_and_unload
|
|
||||||
if (this.flags.ineligibleDie ||
|
|
||||||
this.flags.expired ||
|
|
||||||
this.flags.dying
|
|
||||||
) { return this } // special cases.
|
|
||||||
|
|
||||||
switch (reason) {
|
|
||||||
case "uninstall":
|
|
||||||
case "disable":
|
|
||||||
emit(this, "change", "user-uninstall-disable");
|
|
||||||
break;
|
|
||||||
|
|
||||||
// 5. usual end of session.
|
|
||||||
case "shutdown":
|
|
||||||
case "upgrade":
|
|
||||||
case "downgrade":
|
|
||||||
emit(this, "change", "normal-shutdown")
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup () {
|
|
||||||
// do the simple prefs and simplestorage cleanup
|
|
||||||
// extend by extension
|
|
||||||
resetShieldPrefs();
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
isEligible () {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
whenIneligible () {
|
|
||||||
// empty function unless overrided
|
|
||||||
}
|
|
||||||
|
|
||||||
whenInstalled () {
|
|
||||||
// empty unless overrided
|
|
||||||
}
|
|
||||||
|
|
||||||
whenComplete () {
|
|
||||||
// when the study expires
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* equal choice from varations, by default. override to get unequal
|
|
||||||
*/
|
|
||||||
decideVariation (rng=Math.random()) {
|
|
||||||
return chooseVariation(Object.keys(this.config.variations), rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
get surveyQueryArgs () {
|
|
||||||
return {
|
|
||||||
variation: this.variation,
|
|
||||||
xname: this.config.name,
|
|
||||||
who: userId(),
|
|
||||||
updateChannel: Services.appinfo.defaultUpdateChannel,
|
|
||||||
fxVersion: Services.appinfo.version,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showSurvey(reason) {
|
|
||||||
let partial = this.config.surveyUrls[reason];
|
|
||||||
|
|
||||||
let queryArgs = this.surveyQueryArgs;
|
|
||||||
queryArgs.reason = reason;
|
|
||||||
if (partial) {
|
|
||||||
let url = survey(partial, queryArgs);
|
|
||||||
tabs.open(url);
|
|
||||||
return url
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
report () { // convenience only
|
|
||||||
return report.apply(null, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
chooseVariation: chooseVariation,
|
|
||||||
die: die,
|
|
||||||
expired: expired,
|
|
||||||
generateTelemetryIdIfNeeded: generateTelemetryIdIfNeeded,
|
|
||||||
report: report,
|
|
||||||
Reporter: Reporter,
|
|
||||||
resetShieldPrefs: resetShieldPrefs,
|
|
||||||
Study: Study,
|
|
||||||
cleanup: cleanup,
|
|
||||||
survey: survey
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
const { AddonManager } = require('resource://gre/modules/AddonManager.jsm');
|
|
||||||
const { ClientID } = require('resource://gre/modules/ClientID.jsm');
|
|
||||||
const Events = require('sdk/system/events');
|
|
||||||
const { Services } = require('resource://gre/modules/Services.jsm');
|
|
||||||
const { storage } = require('sdk/simple-storage');
|
|
||||||
const {
|
|
||||||
TelemetryController
|
|
||||||
} = require('resource://gre/modules/TelemetryController.jsm');
|
|
||||||
const { Request } = require('sdk/request');
|
|
||||||
|
|
||||||
|
|
||||||
const EVENT_SEND_METRIC = 'testpilot::send-metric';
|
|
||||||
const startTime = (Services.startup.getStartupInfo().process);
|
|
||||||
|
|
||||||
function makeTimestamp(timestamp) {
|
|
||||||
return Math.round((timestamp - startTime) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function experimentPing(event) {
|
|
||||||
const timestamp = new Date();
|
|
||||||
const { subject, data } = event;
|
|
||||||
let parsed;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(data);
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
return console.error(`Dropping bad metrics packet: ${err}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddonManager.getAddonByID(subject, addon => {
|
|
||||||
const payload = {
|
|
||||||
test: subject,
|
|
||||||
version: addon.version,
|
|
||||||
timestamp: makeTimestamp(timestamp),
|
|
||||||
variants: storage.experimentVariants &&
|
|
||||||
subject in storage.experimentVariants
|
|
||||||
? storage.experimentVariants[subject]
|
|
||||||
: null,
|
|
||||||
payload: parsed
|
|
||||||
};
|
|
||||||
TelemetryController.submitExternalPing('testpilottest', payload, {
|
|
||||||
addClientId: true,
|
|
||||||
addEnvironment: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: DRY up this ping centre code here and in lib/Telemetry.
|
|
||||||
const pcPing = TelemetryController.getCurrentPingData();
|
|
||||||
pcPing.type = 'testpilot';
|
|
||||||
pcPing.payload = payload;
|
|
||||||
const pcPayload = {
|
|
||||||
// 'method' is used by testpilot-metrics library.
|
|
||||||
// 'event' was used before that library existed.
|
|
||||||
event_type: parsed.event || parsed.method,
|
|
||||||
client_time: makeTimestamp(parsed.timestamp || timestamp),
|
|
||||||
addon_id: subject,
|
|
||||||
addon_version: addon.version,
|
|
||||||
firefox_version: pcPing.environment.build.version,
|
|
||||||
os_name: pcPing.environment.system.os.name,
|
|
||||||
os_version: pcPing.environment.system.os.version,
|
|
||||||
locale: pcPing.environment.settings.locale,
|
|
||||||
// Note: these two keys are normally inserted by the ping-centre client.
|
|
||||||
client_id: ClientID.getCachedClientID(),
|
|
||||||
topic: 'testpilot'
|
|
||||||
};
|
|
||||||
// Add any other extra top-level keys = require(the payload, possibly including
|
|
||||||
// 'object' or 'category', among others.
|
|
||||||
Object.keys(parsed).forEach(f => {
|
|
||||||
// Ignore the keys we've already added to `pcPayload`.
|
|
||||||
const ignored = ['event', 'method', 'timestamp'];
|
|
||||||
if (!ignored.includes(f)) {
|
|
||||||
pcPayload[f] = parsed[f];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = new Request({
|
|
||||||
url: 'https://tiles.services.mozilla.com/v3/links/ping-centre',
|
|
||||||
contentType: 'application/json',
|
|
||||||
content: JSON.stringify(pcPayload)
|
|
||||||
});
|
|
||||||
req.post();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function Experiment() {
|
|
||||||
// If the user has @testpilot-addon, it already bound
|
|
||||||
// experimentPing to testpilot::send-metric,
|
|
||||||
// so we don't need to bind this one
|
|
||||||
AddonManager.getAddonByID('@testpilot-addon', addon => {
|
|
||||||
if (!addon) {
|
|
||||||
Events.on(EVENT_SEND_METRIC, experimentPing);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Experiment;
|
|
||||||
+5
-20
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "testpilot-containers",
|
"name": "testpilot-containers",
|
||||||
"title": "Containers Experiment",
|
"title": "Firefox Multi-Account Containers",
|
||||||
"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 helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||||
"version": "3.0.0",
|
"version": "4.0.1",
|
||||||
"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"
|
||||||
@@ -18,23 +18,11 @@
|
|||||||
"jpm": "^1.2.2",
|
"jpm": "^1.2.2",
|
||||||
"json": "^9.0.6",
|
"json": "^9.0.6",
|
||||||
"npm-run-all": "^4.0.0",
|
"npm-run-all": "^4.0.0",
|
||||||
"shield-studies-addon-utils": "^2.0.0",
|
|
||||||
"stylelint": "^7.9.0",
|
"stylelint": "^7.9.0",
|
||||||
"stylelint-config-standard": "^16.0.0",
|
"stylelint-config-standard": "^16.0.0",
|
||||||
"stylelint-order": "^0.3.0",
|
"stylelint-order": "^0.3.0"
|
||||||
"testpilot-metrics": "^2.1.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
|
||||||
"firefox": ">=51.0"
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"multiprocess": true
|
|
||||||
},
|
|
||||||
"hasEmbeddedWebExtension": true,
|
|
||||||
"homepage": "https://github.com/mozilla/testpilot-containers#readme",
|
"homepage": "https://github.com/mozilla/testpilot-containers#readme",
|
||||||
"keywords": [
|
|
||||||
"jetpack"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -43,7 +31,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm test && jpm xpi",
|
"build": "npm test && jpm xpi",
|
||||||
"build-shield": "npm test && npm run package-shield",
|
|
||||||
"deploy": "deploy-txp",
|
"deploy": "deploy-txp",
|
||||||
"lint": "npm-run-all lint:*",
|
"lint": "npm-run-all lint:*",
|
||||||
"lint:addon": "addons-linter webextension --self-hosted",
|
"lint:addon": "addons-linter webextension --self-hosted",
|
||||||
@@ -51,8 +38,6 @@
|
|||||||
"lint:html": "htmllint webextension/*.html",
|
"lint:html": "htmllint webextension/*.html",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
||||||
"package-shield": "./node_modules/.bin/json -I -f package.json -e 'this.name=\"shield-study-containers\"' && jpm xpi && ./node_modules/.bin/json -I -f package.json -e 'this.name=\"testpilot-containers\"'",
|
|
||||||
"test": "npm run lint"
|
"test": "npm run lint"
|
||||||
},
|
}
|
||||||
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
const self = require("sdk/self");
|
|
||||||
const { when: unload } = require("sdk/system/unload");
|
|
||||||
|
|
||||||
const shield = require("./lib/shield/index");
|
|
||||||
|
|
||||||
const surveyUrl = "https://www.surveygizmo.com/s3/3621810/shield-txp-containers";
|
|
||||||
|
|
||||||
const studyConfig = {
|
|
||||||
name: self.addonId,
|
|
||||||
days: 28,
|
|
||||||
surveyUrls: {
|
|
||||||
"end-of-study": surveyUrl,
|
|
||||||
"user-ended-study": surveyUrl,
|
|
||||||
ineligible: null,
|
|
||||||
},
|
|
||||||
variations: {
|
|
||||||
"control": () => {},
|
|
||||||
"securityOnboarding": () => {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class ContainersStudy extends shield.Study {
|
|
||||||
isEligible () {
|
|
||||||
// If the user already has testpilot-containers extension, they are in the
|
|
||||||
// Test Pilot experiment, so exclude them.
|
|
||||||
return super.isEligible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const thisStudy = new ContainersStudy(studyConfig);
|
|
||||||
|
|
||||||
if (self.id === "@shield-study-containers") {
|
|
||||||
unload((reason) => thisStudy.shutdown(reason));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.study = thisStudy;
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
const Experiment = require('./lib/testpilot/experiment');
|
|
||||||
|
|
||||||
const experiment = new Experiment();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that represents a metrics event broker. Events are sent to Google
|
|
||||||
* Analytics if the `tid` parameter is set. Events are sent to Mozilla's
|
|
||||||
* data pipeline via the Test Pilot add-on. No metrics code changes are
|
|
||||||
* needed when the experiment is added to or removed from Test Pilot.
|
|
||||||
* @constructor
|
|
||||||
* @param {string} $0.id - addon ID, e.g. '@testpilot-addon'. See https://mdn.io/add_on_id.
|
|
||||||
* @param {string} $0.version - addon version, e.g. '1.0.2'.
|
|
||||||
* @param {string} [$0.uid] - unique identifier for a specific instance of an addon.
|
|
||||||
* Optional, but required to send events to Google Analytics. Sent to Google Analytics
|
|
||||||
* but not Mozilla services.
|
|
||||||
* @param {string} [$0.tid] - Google Analytics tracking ID. Optional, but required
|
|
||||||
* to send events to Google Analytics.
|
|
||||||
* @param {string} [$0.type=webextension] - addon type. one of: 'webextension',
|
|
||||||
* 'sdk', 'bootstrapped'.
|
|
||||||
* @param {boolean} [$0.debug=false] - if true, enables logging. Note that this
|
|
||||||
* value can be changed on a running instance, by modifying its `debug` property.
|
|
||||||
* @throws {SyntaxError} If the required properties are missing, or if the
|
|
||||||
* 'type' property is unrecognized.
|
|
||||||
* @throws {Error} if initializing the transports fails.
|
|
||||||
*/
|
|
||||||
function Metrics({id, version, uid, tid = null, type = 'webextension', debug = false}) {
|
|
||||||
if (!id) {
|
|
||||||
throw new SyntaxError(`'id' property is required.`);
|
|
||||||
} else if (!version) {
|
|
||||||
throw new SyntaxError(`'version' property is required.`);
|
|
||||||
} else if (tid && !uid) {
|
|
||||||
throw new SyntaxError(`'uid' property is required to send events to Google Analytics.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['webextension', 'sdk', 'bootstrapped'].includes(type)) {
|
|
||||||
throw new SyntaxError(`'type' property must be one of: 'webextension', 'sdk', or 'bootstrapped'`);
|
|
||||||
}
|
|
||||||
Object.assign(this, {id, uid, version, tid, type, debug});
|
|
||||||
|
|
||||||
// The test pilot add-on uses its own nsIObserverService topic for sending
|
|
||||||
// pings to Telemetry. Otherwise, the topic is based on add-on type.
|
|
||||||
if (id === '@testpilot-addon') {
|
|
||||||
this.topic = 'testpilot';
|
|
||||||
} else if (type === 'webextension') {
|
|
||||||
this.topic = 'testpilot-telemetry';
|
|
||||||
} else {
|
|
||||||
this.topic = 'testpilottest';
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: order is important here. _initTransports uses console.log, which may
|
|
||||||
// not be available before _initConsole has run.
|
|
||||||
this._initConsole();
|
|
||||||
this._initTransports();
|
|
||||||
|
|
||||||
this.sendEvent = this.sendEvent.bind(this);
|
|
||||||
|
|
||||||
this._log(`Initialized topic to ${this.topic}`);
|
|
||||||
if (!tid) {
|
|
||||||
this._log(`Google Analytics disabled: 'tid' value not passed to constructor.`);
|
|
||||||
} else {
|
|
||||||
this._log(`Google Analytics enabled for Tracking ID ${tid}.`);
|
|
||||||
}
|
|
||||||
this._log('Constructor finished successfully.');
|
|
||||||
}
|
|
||||||
Metrics.prototype = {
|
|
||||||
/**
|
|
||||||
* Sends an event to the Mozilla data pipeline (and Google Analytics, if
|
|
||||||
* a `tid` was passed to the constructor). Note: to avoid breaking callers,
|
|
||||||
* if sending the event fails, no Errors will be thrown. Instead, the message
|
|
||||||
* will be silently dropped, and, if debug mode is enabled, an error will be
|
|
||||||
* logged to the Browser Console.
|
|
||||||
*
|
|
||||||
* If you want to pass extra fields to GA, or use a GA hit type other than
|
|
||||||
* `Event`, you can transform the output data object yourself using the
|
|
||||||
* `transform` parameter. You will need to add Custom Dimensions to GA for any
|
|
||||||
* extra fields: https://support.google.com/analytics/answer/2709828. Note
|
|
||||||
* that, by convention, the `variant` argument is mapped to the first Custom
|
|
||||||
* Dimension (`cd1`) when constructing the GA Event hit.
|
|
||||||
*
|
|
||||||
* Note: the data object format is currently different for each experiment,
|
|
||||||
* and should be defined based on the result of conversations with the Mozilla
|
|
||||||
* data team.
|
|
||||||
*
|
|
||||||
* A suggested default format is:
|
|
||||||
* @param {string} [$0.method] - What is happening? e.g. `click`
|
|
||||||
* @param {string} [$0.object] - What is being affected? e.g. `home-button-1`
|
|
||||||
* @param {string} [$0.category=interactions] - If you want to add a category
|
|
||||||
* for easy reporting later. e.g. `mainmenu`
|
|
||||||
* @param {string} [$0.variant=null] - An identifying string if you're running
|
|
||||||
* different variants. e.g. `cohort-A`
|
|
||||||
* @param {function} [transform] - Transform function used to alter the
|
|
||||||
* parameters sent to GA. The `transform` function signature is
|
|
||||||
* `transform(input, output)`, where `input` is the object passed to
|
|
||||||
* `sendEvent` (excluding `transform`), and `output` is the default GA
|
|
||||||
* object generated by the `_gaTransform` method. The `transform` function
|
|
||||||
* should return an object whose keys are GA Measurement Protocol parameters.
|
|
||||||
* The returned object will be form encoded and sent to GA.
|
|
||||||
*/
|
|
||||||
sendEvent: function(params = {}, transform) {
|
|
||||||
const args = this._clone(params);
|
|
||||||
args.object = params.object || null;
|
|
||||||
args.category = params.category || 'interactions';
|
|
||||||
args.variant = params.variant || null;
|
|
||||||
|
|
||||||
this._log(`sendEvent called with method = ${args.method}, object = ${args.object}, category = ${args.category}, variant = ${args.variant}.`);
|
|
||||||
|
|
||||||
const clientData = this._clone(args);
|
|
||||||
const gaData = this._clone(args);
|
|
||||||
if (!clientData) {
|
|
||||||
this._error(`Unable to process data object. Dropping packet.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._sendToClient(clientData);
|
|
||||||
|
|
||||||
if (this.tid && this.uid) {
|
|
||||||
const defaultEvent = this._gaTransform(gaData);
|
|
||||||
|
|
||||||
let userEvent;
|
|
||||||
if (transform) {
|
|
||||||
userEvent = transform.call(null, gaData, defaultEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._gaSend(userEvent || defaultEvent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone a data object by serializing / deserializing it.
|
|
||||||
* @private
|
|
||||||
* @param {object} o - Object to be cloned.
|
|
||||||
* @returns A clone of the object, or `null` if cloning failed.
|
|
||||||
*/
|
|
||||||
_clone: function(o) {
|
|
||||||
let cloned;
|
|
||||||
try {
|
|
||||||
cloned = JSON.parse(JSON.stringify(o));
|
|
||||||
} catch (ex) {
|
|
||||||
this._error(`Unable to clone object: ${ex}.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return cloned;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an event to the Mozilla data pipeline via the Test Pilot add-on.
|
|
||||||
* Uses BroadcastChannel for WebExtensions, and nsIObserverService for other
|
|
||||||
* add-on types.
|
|
||||||
* @private
|
|
||||||
* @param {object} params - Entire object sent to `sendEvent`.
|
|
||||||
*/
|
|
||||||
_sendToClient: function(params) {
|
|
||||||
if (this.type === 'webextension') {
|
|
||||||
this._channel.postMessage(params);
|
|
||||||
this._log(`Sent client message via postMessage: ${params}`);
|
|
||||||
} else {
|
|
||||||
let stringified;
|
|
||||||
|
|
||||||
try {
|
|
||||||
stringified = JSON.stringify(params);
|
|
||||||
} catch(ex) {
|
|
||||||
this._error(`Unable to serialize metrics event: ${ex}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subject = {
|
|
||||||
wrappedJSObject: {
|
|
||||||
observersModuleSubjectWrapper: true,
|
|
||||||
object: this.id
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
Services.obs.notifyObservers(subject, 'testpilot::send-metric', stringified);
|
|
||||||
this._log(`Sent client message via nsIObserverService: ${stringified}`);
|
|
||||||
} catch (ex) {
|
|
||||||
this._error(`Failed to send nsIObserver client ping: ${ex}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms `sendEvent()` arguments into a Google Analytics `Event` hit.
|
|
||||||
* @private
|
|
||||||
* @param {string} method - see `sendEvent` docs
|
|
||||||
* @param {string} [object] - see `sendEvent` docs
|
|
||||||
* @param {string} category - see `sendEvent` docs. Note that `category` is
|
|
||||||
* required here, assuming the default value was filled in by `sendEvent()`.
|
|
||||||
* @param {string} variant - see `sendEvent` docs. Note that `variant` is
|
|
||||||
* required here, assuming the default value was filled in by `sendEvent()`.
|
|
||||||
*/
|
|
||||||
_gaTransform: function({method, object, category, variant}) {
|
|
||||||
const data = {
|
|
||||||
v: 1,
|
|
||||||
an: this.id,
|
|
||||||
av: this.version,
|
|
||||||
tid: this.tid,
|
|
||||||
uid: this.uid,
|
|
||||||
t: 'event',
|
|
||||||
ec: category,
|
|
||||||
ea: method
|
|
||||||
};
|
|
||||||
if (object) {
|
|
||||||
data.el = object;
|
|
||||||
}
|
|
||||||
if (variant) {
|
|
||||||
data.cd1 = variant;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes and sends an event message to Google Analytics.
|
|
||||||
* @private
|
|
||||||
* @param {object} msg - An object whose keys correspond to parameters in the
|
|
||||||
* Google Analytics Measurement Protocol.
|
|
||||||
*/
|
|
||||||
_gaSend: function(msg) {
|
|
||||||
const encoded = this._formEncode(msg);
|
|
||||||
const GA_URL = 'https://ssl.google-analytics.com/collect';
|
|
||||||
if (this.type === 'webextension') {
|
|
||||||
navigator.sendBeacon(GA_URL, encoded);
|
|
||||||
} else {
|
|
||||||
// SDK and bootstrapped types might not have a window reference, so get
|
|
||||||
// the sendBeacon DOM API from the hidden window.
|
|
||||||
Services.appShell.hiddenDOMWindow.navigator.sendBeacon(GA_URL, encoded);
|
|
||||||
}
|
|
||||||
this._log(`Sent GA message: ${encoded}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* URL encodes an object. Encodes spaces as '%20', not '+', following the
|
|
||||||
* GA docs.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // returns 'a=b&foo=b%20ar'
|
|
||||||
* metrics._formEncode({a: 'b', foo: 'b ar'});
|
|
||||||
* @private
|
|
||||||
* @param {Object} obj - Any JS object
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
_formEncode: function(obj) {
|
|
||||||
const params = [];
|
|
||||||
if (!obj) { return ''; }
|
|
||||||
Object.keys(obj).forEach(item => {
|
|
||||||
const encoded = encodeURIComponent(item) + '=' + encodeURIComponent(obj[item]);
|
|
||||||
params.push(encoded);
|
|
||||||
});
|
|
||||||
return params.join('&');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes transports used for sending messages. For WebExtensions,
|
|
||||||
* creates a `BroadcastChannel` (transport for client pings). WebExtensions
|
|
||||||
* use navigator.sendBeacon for GA transport, and they always have access
|
|
||||||
* to DOM APIs, so there's no setup work required. For other types, loads
|
|
||||||
* `Services.jsm`, which exposes the nsIObserverService (transport for client
|
|
||||||
* pings), and exposes the navigator.sendBeacon API (GA transport) via the
|
|
||||||
* appShell service's hidden window.
|
|
||||||
* @private
|
|
||||||
* @throws {Error} if transport setup unexpectedly fails
|
|
||||||
*/
|
|
||||||
_initTransports: function() {
|
|
||||||
if (this.type === 'webextension') {
|
|
||||||
try {
|
|
||||||
this._channel = new BroadcastChannel(this.topic);
|
|
||||||
} catch(ex) {
|
|
||||||
throw new Error(`Unable to create BroadcastChannel: ${ex}`);
|
|
||||||
}
|
|
||||||
} else if (this.type === 'sdk') {
|
|
||||||
try {
|
|
||||||
const { Cu } = require('chrome');
|
|
||||||
Cu.import('resource://gre/modules/Services.jsm');
|
|
||||||
} catch(ex) {
|
|
||||||
throw new Error(`Unable to load Services.jsm: ${ex}`);
|
|
||||||
}
|
|
||||||
} else { /* this.type === 'bootstrapped' */
|
|
||||||
try {
|
|
||||||
Components.utils.import('resource://gre/modules/Services.jsm');
|
|
||||||
} catch(ex) {
|
|
||||||
throw new Error(`Unable to load Services.jsm: ${ex}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._log('Successfully initialized transports.');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a console for 'bootstrapped' add-ons.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_initConsole: function() {
|
|
||||||
if (this.type === 'bootstrapped') {
|
|
||||||
try {
|
|
||||||
Components.utils.import('resource://gre/modules/Console.jsm');
|
|
||||||
this._log('Successfully initialized console.');
|
|
||||||
} catch(ex) {
|
|
||||||
throw new Error(`Unable to initialize console: ${ex}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs messages to the console. Only enabled if `this.debug` is truthy.
|
|
||||||
* @private
|
|
||||||
* @param {string} msg - A message
|
|
||||||
*/
|
|
||||||
_log: function(msg) {
|
|
||||||
if (this.debug) {
|
|
||||||
console.log(msg); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs errors to the console. Only enabled if `this.debug` is truthy.
|
|
||||||
* @private
|
|
||||||
* @param {string} msg - An error message
|
|
||||||
*/
|
|
||||||
_error: function(msg) {
|
|
||||||
if (this.debug) {
|
|
||||||
console.error(msg); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebExtensions don't support CommonJS module style, so 'module' might not be
|
|
||||||
// defined.
|
|
||||||
if (typeof module !== 'undefined') {
|
|
||||||
module.exports = Metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export the Metrics constructor in Gecko JSM style, for legacy addons
|
|
||||||
// that use the JSM loader. See also: https://mdn.io/jsm/using
|
|
||||||
const EXPORTED_SYMBOLS = ['Metrics']; // eslint-disable-line no-unused-vars
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Containers confirm navigation</title>
|
<title>Firefox Multi-Account Containers Confirm Navigation</title>
|
||||||
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
||||||
<link rel="stylesheet" href="/css/confirm-page.css" />
|
<link rel="stylesheet" href="/css/confirm-page.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="context-fill #4c4c4c" fill-opacity="context-fill-opacity" d="M12.9137931,3.0862069 L12.9137931,1.27586207 C12.9137931,0.84736528 12.5664278,0.5 12.137931,0.5 C11.7094342,0.5 11.362069,0.84736528 11.362069,1.27586207 L11.362069,1.27586207 L11.362069,3.0862069 L9.55172414,3.0862069 C9.12322735,3.0862069 8.77586207,3.43357218 8.77586207,3.86206897 C8.77586207,4.29056575 9.12322735,4.63793103 9.55172414,4.63793103 L11.362069,4.63793103 L11.362069,6.44827586 C11.362069,6.87677265 11.7094342,7.22413793 12.137931,7.22413793 L12.137931,7.22413793 C12.5664278,7.22413793 12.9137931,6.87677265 12.9137931,6.44827586 L12.9137931,6.44827586 L12.9137931,4.63793103 L14.7241379,4.63793103 C15.1526347,4.63793103 15.5,4.29056575 15.5,3.86206897 L15.5,3.86206897 C15.5,3.43357218 15.1526347,3.0862069 14.7241379,3.0862069 L14.7241379,3.0862069 L12.9137931,3.0862069 Z M0.5,9.76803178 C0.5,9.22007158 0.94118947,8.77586207 1.49216971,8.77586207 L6.23196822,8.77586207 C6.77992842,8.77586207 7.22413793,9.21705154 7.22413793,9.76803178 L7.22413793,14.5078303 C7.22413793,15.0557905 6.78294846,15.5 6.23196822,15.5 L1.49216971,15.5 C0.94420951,15.5 0.5,15.0588105 0.5,14.5078303 L0.5,9.76803178 Z M8.77586207,9.76803178 C8.77586207,9.22007158 9.21705154,8.77586207 9.76803178,8.77586207 L14.5078303,8.77586207 C15.0557905,8.77586207 15.5,9.21705154 15.5,9.76803178 L15.5,14.5078303 C15.5,15.0557905 15.0588105,15.5 14.5078303,15.5 L9.76803178,15.5 C9.22007158,15.5 8.77586207,15.0588105 8.77586207,14.5078303 L8.77586207,9.76803178 Z M0.5,1.49216971 C0.5,0.94420951 0.94118947,0.5 1.49216971,0.5 L6.23196822,0.5 C6.77992842,0.5 7.22413793,0.94118947 7.22413793,1.49216971 L7.22413793,6.23196822 C7.22413793,6.77992842 6.78294846,7.22413793 6.23196822,7.22413793 L1.49216971,7.22413793 C0.94420951,7.22413793 0.5,6.78294846 0.5,6.23196822 L0.5,1.49216971 Z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -7,8 +7,6 @@ module.exports = {
|
|||||||
"badge": true,
|
"badge": true,
|
||||||
"backgroundLogic": true,
|
"backgroundLogic": true,
|
||||||
"identityState": true,
|
"identityState": true,
|
||||||
"messageHandler": true,
|
"messageHandler": true
|
||||||
"tabPageCounter": true,
|
|
||||||
"themeManager": true
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
const assignManager = {
|
const assignManager = {
|
||||||
MENU_ASSIGN_ID: "open-in-this-container",
|
MENU_ASSIGN_ID: "open-in-this-container",
|
||||||
MENU_REMOVE_ID: "remove-open-in-this-container",
|
MENU_REMOVE_ID: "remove-open-in-this-container",
|
||||||
|
MENU_SEPARATOR_ID: "separator",
|
||||||
|
MENU_HIDE_ID: "hide-container",
|
||||||
|
MENU_MOVE_ID: "move-to-new-window-container",
|
||||||
|
|
||||||
storageArea: {
|
storageArea: {
|
||||||
area: browser.storage.local,
|
area: browser.storage.local,
|
||||||
exemptedTabs: {},
|
exemptedTabs: {},
|
||||||
@@ -163,15 +167,31 @@ const assignManager = {
|
|||||||
async _onClickedHandler(info, tab) {
|
async _onClickedHandler(info, tab) {
|
||||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||||
|
let remove;
|
||||||
if (userContextId) {
|
if (userContextId) {
|
||||||
// let actionName;
|
switch (info.menuItemId) {
|
||||||
let remove;
|
case this.MENU_ASSIGN_ID:
|
||||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
case this.MENU_REMOVE_ID:
|
||||||
remove = false;
|
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||||
} else {
|
remove = false;
|
||||||
remove = true;
|
} else {
|
||||||
|
remove = true;
|
||||||
|
}
|
||||||
|
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
||||||
|
break;
|
||||||
|
case this.MENU_MOVE_ID:
|
||||||
|
backgroundLogic.moveTabsToWindow({
|
||||||
|
cookieStoreId: tab.cookieStoreId,
|
||||||
|
windowId: tab.windowId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case this.MENU_HIDE_ID:
|
||||||
|
backgroundLogic.hideTabs({
|
||||||
|
cookieStoreId: tab.cookieStoreId,
|
||||||
|
windowId: tab.windowId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -234,10 +254,6 @@ const assignManager = {
|
|||||||
browser.tabs.sendMessage(tabId, {
|
browser.tabs.sendMessage(tabId, {
|
||||||
text: `Successfully ${actionName} site to always open in this container`
|
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);
|
const tab = await browser.tabs.get(tabId);
|
||||||
this.calculateContextMenu(tab);
|
this.calculateContextMenu(tab);
|
||||||
},
|
},
|
||||||
@@ -264,6 +280,9 @@ const assignManager = {
|
|||||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_SEPARATOR_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_HIDE_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_MOVE_ID);
|
||||||
},
|
},
|
||||||
|
|
||||||
async calculateContextMenu(tab) {
|
async calculateContextMenu(tab) {
|
||||||
@@ -274,19 +293,37 @@ const assignManager = {
|
|||||||
if (siteSettings === false) {
|
if (siteSettings === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
|
let checked = false;
|
||||||
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;
|
let menuId = this.MENU_ASSIGN_ID;
|
||||||
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
if (siteSettings &&
|
if (siteSettings &&
|
||||||
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
||||||
prefix = "✓";
|
checked = true;
|
||||||
menuId = this.MENU_REMOVE_ID;
|
menuId = this.MENU_REMOVE_ID;
|
||||||
}
|
}
|
||||||
browser.contextMenus.create({
|
browser.contextMenus.create({
|
||||||
id: menuId,
|
id: menuId,
|
||||||
title: `${prefix} Always Open in This Container`,
|
title: "Always Open in This Container",
|
||||||
checked: true,
|
checked,
|
||||||
|
type: "checkbox",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_SEPARATOR_ID,
|
||||||
|
type: "separator",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_HIDE_ID,
|
||||||
|
title: "Hide This Container",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_MOVE_ID,
|
||||||
|
title: "Move Tabs to a New Window",
|
||||||
contexts: ["all"],
|
contexts: ["all"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -298,15 +335,7 @@ const assignManager = {
|
|||||||
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||||
if (neverAsk) {
|
if (neverAsk) {
|
||||||
browser.tabs.create({url, cookieStoreId, index});
|
browser.tabs.create({url, cookieStoreId, index});
|
||||||
backgroundLogic.sendTelemetryPayload({
|
|
||||||
event: "auto-reload-page-in-container",
|
|
||||||
userContextId: userContextId,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
backgroundLogic.sendTelemetryPayload({
|
|
||||||
event: "prompt-to-reload-page-in-container",
|
|
||||||
userContextId: userContextId,
|
|
||||||
});
|
|
||||||
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
|
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
|
||||||
let currentCookieStoreId;
|
let currentCookieStoreId;
|
||||||
if (currentUserContextId) {
|
if (currentUserContextId) {
|
||||||
|
|||||||
@@ -25,18 +25,12 @@ const backgroundLogic = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteContainer(userContextId) {
|
async deleteContainer(userContextId, removed = false) {
|
||||||
this.sendTelemetryPayload({
|
|
||||||
event: "delete-container",
|
|
||||||
userContextId
|
|
||||||
});
|
|
||||||
|
|
||||||
await this._closeTabs(userContextId);
|
await this._closeTabs(userContextId);
|
||||||
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
if (!removed) {
|
||||||
|
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
||||||
|
}
|
||||||
assignManager.deleteContainer(userContextId);
|
assignManager.deleteContainer(userContextId);
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
method: "forgetIdentityAndRefresh"
|
|
||||||
});
|
|
||||||
return {done: true, userContextId};
|
return {done: true, userContextId};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -47,15 +41,8 @@ const backgroundLogic = {
|
|||||||
this.cookieStoreId(options.userContextId),
|
this.cookieStoreId(options.userContextId),
|
||||||
options.params
|
options.params
|
||||||
);
|
);
|
||||||
this.sendTelemetryPayload({
|
|
||||||
event: "edit-container",
|
|
||||||
userContextId: options.userContextId
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
donePromise = browser.contextualIdentities.create(options.params);
|
donePromise = browser.contextualIdentities.create(options.params);
|
||||||
this.sendTelemetryPayload({
|
|
||||||
event: "add-container"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
await donePromise;
|
await donePromise;
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
@@ -63,22 +50,12 @@ const backgroundLogic = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async openTab(options) {
|
async openNewTab(options) {
|
||||||
let url = options.url || undefined;
|
let url = options.url || undefined;
|
||||||
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
||||||
const active = ("nofocus" in options) ? options.nofocus : true;
|
const active = ("nofocus" in options) ? options.nofocus : true;
|
||||||
const source = ("source" in options) ? options.source : null;
|
|
||||||
|
|
||||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
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
|
// 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
|
// We can't open new tab pages, so open a blank tab. Used in tab un-hide
|
||||||
@@ -86,10 +63,11 @@ const backgroundLogic = {
|
|||||||
url = undefined;
|
url = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unhide all hidden tabs
|
// We can't open these we just have to throw them away
|
||||||
this.showTabs({
|
if (new URL(url).protocol === "about:") {
|
||||||
cookieStoreId
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
return browser.tabs.create({
|
return browser.tabs.create({
|
||||||
url,
|
url,
|
||||||
active,
|
active,
|
||||||
@@ -98,66 +76,61 @@ const backgroundLogic = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTabs(options) {
|
checkArgs(requiredArguments, options, methodName) {
|
||||||
if (!("cookieStoreId" in options)) {
|
requiredArguments.forEach((argument) => {
|
||||||
return new Error("getTabs must be called with cookieStoreId argument.");
|
if (!(argument in options)) {
|
||||||
}
|
return new Error(`${methodName} must be called with ${argument} argument.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
async getTabs(options) {
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
const isKnownContainer = await identityState._isKnownContainer(userContextId);
|
this.checkArgs(requiredArguments, options, "getTabs");
|
||||||
if (!isKnownContainer) {
|
const { cookieStoreId, windowId } = options;
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = [];
|
const list = [];
|
||||||
const tabs = await this._containerTabs(options.cookieStoreId);
|
const tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
list.push(identityState._createTabObject(tab));
|
list.push(identityState._createTabObject(tab));
|
||||||
});
|
});
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
return list.concat(containerState.hiddenTabs);
|
return list.concat(containerState.hiddenTabs);
|
||||||
},
|
},
|
||||||
|
|
||||||
async moveTabsToWindow(options) {
|
async moveTabsToWindow(options) {
|
||||||
if (!("cookieStoreId" in options)) {
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
return new Error("moveTabsToWindow must be called with cookieStoreId argument.");
|
this.checkArgs(requiredArguments, options, "moveTabsToWindow");
|
||||||
}
|
const { cookieStoreId, windowId } = options;
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const list = await browser.tabs.query({
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
cookieStoreId,
|
||||||
if (!identityState._isKnownContainer(userContextId)) {
|
windowId
|
||||||
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(cookieStoreId);
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
if (list.length === 0 &&
|
if (list.length === 0 &&
|
||||||
containerState.hiddenTabs.length === 0) {
|
containerState.hiddenTabs.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const window = await browser.windows.create({
|
const newWindowObj = await browser.windows.create({
|
||||||
tabId: list.shift().id
|
tabId: list.shift().id
|
||||||
});
|
});
|
||||||
browser.tabs.move(list, {
|
browser.tabs.move(list.map((tab) => tab.id), {
|
||||||
windowId: window.id,
|
windowId: newWindowObj.id,
|
||||||
index: -1
|
index: -1
|
||||||
});
|
});
|
||||||
|
|
||||||
// Let's show the hidden tabs.
|
// Let's show the hidden tabs.
|
||||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||||
browser.tabs.create(object.url || DEFAULT_TAB, {
|
browser.tabs.create(object.url || DEFAULT_TAB, {
|
||||||
windowId: window.id,
|
windowId: newWindowObj.id,
|
||||||
cookieStoreId: options.cookieStoreId
|
cookieStoreId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,31 +139,46 @@ const backgroundLogic = {
|
|||||||
// Let's close all the normal tab in the new window. In theory it
|
// 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
|
// should be only the first tab, but maybe there are addons doing
|
||||||
// crazy stuff.
|
// crazy stuff.
|
||||||
const tabs = browser.tabs.query({windowId: window.id});
|
const tabs = browser.tabs.query({windowId: newWindowObj.id});
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||||
if (tabs.cookieStoreId !== options.cookieStoreId) {
|
if (tabs.cookieStoreId !== cookieStoreId) {
|
||||||
browser.tabs.remove(tab.id);
|
browser.tabs.remove(tab.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await identityState.storageArea.set(options.cookieStoreId, containerState);
|
return await identityState.storageArea.set(cookieStoreId, containerState);
|
||||||
},
|
},
|
||||||
|
|
||||||
async _closeTabs(userContextId) {
|
async _closeTabs(userContextId, windowId = false) {
|
||||||
const cookieStoreId = this.cookieStoreId(userContextId);
|
const cookieStoreId = this.cookieStoreId(userContextId);
|
||||||
const tabs = await this._containerTabs(cookieStoreId);
|
let tabs;
|
||||||
|
/* if we have no windowId we are going to close all this container (used for deleting) */
|
||||||
|
if (windowId !== false) {
|
||||||
|
tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId
|
||||||
|
});
|
||||||
|
}
|
||||||
const tabIds = tabs.map((tab) => tab.id);
|
const tabIds = tabs.map((tab) => tab.id);
|
||||||
return browser.tabs.remove(tabIds);
|
return browser.tabs.remove(tabIds);
|
||||||
},
|
},
|
||||||
|
|
||||||
async queryIdentitiesState() {
|
async queryIdentitiesState(windowId) {
|
||||||
const identities = await browser.contextualIdentities.query({});
|
const identities = await browser.contextualIdentities.query({});
|
||||||
const identitiesOutput = {};
|
const identitiesOutput = {};
|
||||||
const identitiesPromise = identities.map(async function (identity) {
|
const identitiesPromise = identities.map(async function (identity) {
|
||||||
await identityState.remapTabsIfMissing(identity.cookieStoreId);
|
const { cookieStoreId } = identity;
|
||||||
const containerState = await identityState.storageArea.get(identity.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
identitiesOutput[identity.cookieStoreId] = {
|
const openTabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
|
identitiesOutput[cookieStoreId] = {
|
||||||
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
||||||
hasOpenTabs: !!containerState.openTabs
|
hasOpenTabs: !!openTabs.length
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
@@ -199,23 +187,16 @@ const backgroundLogic = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async sortTabs() {
|
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();
|
const windows = await browser.windows.getAll();
|
||||||
for (let window of windows) { // eslint-disable-line prefer-const
|
for (let windowObj of windows) { // eslint-disable-line prefer-const
|
||||||
// First the pinned tabs, then the normal ones.
|
// First the pinned tabs, then the normal ones.
|
||||||
await this._sortTabsInternal(window, true);
|
await this._sortTabsInternal(windowObj, true);
|
||||||
await this._sortTabsInternal(window, false);
|
await this._sortTabsInternal(windowObj, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async _sortTabsInternal(window, pinnedTabs) {
|
async _sortTabsInternal(windowObj, pinnedTabs) {
|
||||||
const tabs = await browser.tabs.query({windowId: window.id});
|
const tabs = await browser.tabs.query({windowId: windowObj.id});
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|
||||||
// Let's collect UCIs/tabs for this window.
|
// Let's collect UCIs/tabs for this window.
|
||||||
@@ -247,38 +228,22 @@ const backgroundLogic = {
|
|||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
++pos;
|
++pos;
|
||||||
browser.tabs.move(tab.id, {
|
browser.tabs.move(tab.id, {
|
||||||
windowId: window.id,
|
windowId: windowObj.id,
|
||||||
index: pos
|
index: pos
|
||||||
});
|
});
|
||||||
//xulWindow.gBrowser.moveTabTo(tab, pos++);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async hideTabs(options) {
|
async hideTabs(options) {
|
||||||
if (!("cookieStoreId" in options)) {
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
return new Error("hideTabs must be called with cookieStoreId option.");
|
this.checkArgs(requiredArguments, options, "hideTabs");
|
||||||
}
|
const { cookieStoreId, windowId } = options;
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(cookieStoreId);
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
|
||||||
const isKnownContainer = await identityState._isKnownContainer(userContextId);
|
|
||||||
if (!isKnownContainer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const containersCounts = identityState.containersCounts();
|
const containerState = await identityState.storeHidden(cookieStoreId, windowId);
|
||||||
this.sendTelemetryPayload({
|
await this._closeTabs(userContextId, windowId);
|
||||||
"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;
|
return containerState;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -288,27 +253,12 @@ const backgroundLogic = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
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 promises = [];
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
||||||
|
|
||||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||||
promises.push(this.openTab({
|
promises.push(this.openNewTab({
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
url: object.url,
|
url: object.url,
|
||||||
nofocus: options.nofocus || false,
|
nofocus: options.nofocus || false,
|
||||||
@@ -322,24 +272,8 @@ const backgroundLogic = {
|
|||||||
return await identityState.storageArea.set(options.cookieStoreId, containerState);
|
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) {
|
cookieStoreId(userContextId) {
|
||||||
return `firefox-container-${userContextId}`;
|
return `firefox-container-${userContextId}`;
|
||||||
},
|
}
|
||||||
|
|
||||||
_containerTabs(cookieStoreId) {
|
|
||||||
return browser.tabs.query({
|
|
||||||
cookieStoreId
|
|
||||||
}).catch((e) => {throw e;});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||||
const badge = {
|
const badge = {
|
||||||
init() {
|
async init() {
|
||||||
this.displayBrowserActionBadge();
|
const currentWindow = await browser.windows.getCurrent();
|
||||||
|
this.displayBrowserActionBadge(currentWindow.incognito);
|
||||||
},
|
},
|
||||||
async displayBrowserActionBadge() {
|
async displayBrowserActionBadge(disable) {
|
||||||
|
if (disable) {
|
||||||
|
browser.browserAction.disable();
|
||||||
|
} else {
|
||||||
|
browser.browserAction.enable();
|
||||||
|
}
|
||||||
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
||||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const identityState = {
|
|||||||
if (storageResponse && storeKey in storageResponse) {
|
if (storageResponse && storeKey in storageResponse) {
|
||||||
return storageResponse[storeKey];
|
return storageResponse[storeKey];
|
||||||
}
|
}
|
||||||
return null;
|
const defaultContainerState = identityState._createIdentityState();
|
||||||
|
await this.set(cookieStoreId, defaultContainerState);
|
||||||
|
|
||||||
|
return defaultContainerState;
|
||||||
},
|
},
|
||||||
|
|
||||||
set(cookieStoreId, data) {
|
set(cookieStoreId, data) {
|
||||||
@@ -29,19 +32,13 @@ const identityState = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async _isKnownContainer(userContextId) {
|
|
||||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
|
||||||
const state = await this.storageArea.get(cookieStoreId);
|
|
||||||
return !!state;
|
|
||||||
},
|
|
||||||
|
|
||||||
_createTabObject(tab) {
|
_createTabObject(tab) {
|
||||||
return Object.assign({}, tab);
|
return Object.assign({}, tab);
|
||||||
},
|
},
|
||||||
|
|
||||||
async storeHidden(cookieStoreId) {
|
async storeHidden(cookieStoreId, windowId) {
|
||||||
const containerState = await this.storageArea.get(cookieStoreId);
|
const containerState = await this.storageArea.get(cookieStoreId);
|
||||||
const tabsByContainer = await this._matchTabsByContainer(cookieStoreId);
|
const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId});
|
||||||
tabsByContainer.forEach((tab) => {
|
tabsByContainer.forEach((tab) => {
|
||||||
const tabObject = this._createTabObject(tab);
|
const tabObject = this._createTabObject(tab);
|
||||||
// This tab is going to be closed. Let's mark this tabObject as
|
// This tab is going to be closed. Let's mark this tabObject as
|
||||||
@@ -54,89 +51,9 @@ const identityState = {
|
|||||||
return this.storageArea.set(cookieStoreId, containerState);
|
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() {
|
_createIdentityState() {
|
||||||
return {
|
return {
|
||||||
hiddenTabs: [],
|
hiddenTabs: []
|
||||||
openTabs: 0
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,8 +11,6 @@
|
|||||||
"js/background/badge.js",
|
"js/background/badge.js",
|
||||||
"js/background/identityState.js",
|
"js/background/identityState.js",
|
||||||
"js/background/messageHandler.js",
|
"js/background/messageHandler.js",
|
||||||
"js/background/tabPageCounter.js",
|
|
||||||
"js/background/themeManager.js",
|
|
||||||
"js/backdround/init.js"
|
"js/backdround/init.js"
|
||||||
]
|
]
|
||||||
-->
|
-->
|
||||||
@@ -21,8 +19,6 @@
|
|||||||
<script type="text/javascript" src="badge.js"></script>
|
<script type="text/javascript" src="badge.js"></script>
|
||||||
<script type="text/javascript" src="identityState.js"></script>
|
<script type="text/javascript" src="identityState.js"></script>
|
||||||
<script type="text/javascript" src="messageHandler.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>
|
<script type="text/javascript" src="init.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const messageHandler = {
|
|||||||
// We use this to catch redirected tabs that have just opened
|
// 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
|
// 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,
|
LAST_CREATED_TAB_TIMER: 2000,
|
||||||
|
unhideQueue: [],
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Handles messages from webextension code
|
// Handles messages from webextension code
|
||||||
@@ -16,10 +17,6 @@ const messageHandler = {
|
|||||||
case "createOrUpdateContainer":
|
case "createOrUpdateContainer":
|
||||||
response = backgroundLogic.createOrUpdateContainer(m.message);
|
response = backgroundLogic.createOrUpdateContainer(m.message);
|
||||||
break;
|
break;
|
||||||
case "openTab":
|
|
||||||
// Same as open-tab for index.js
|
|
||||||
response = backgroundLogic.openTab(m.message);
|
|
||||||
break;
|
|
||||||
case "neverAsk":
|
case "neverAsk":
|
||||||
assignManager._neverAsk(m);
|
assignManager._neverAsk(m);
|
||||||
break;
|
break;
|
||||||
@@ -38,9 +35,6 @@ const messageHandler = {
|
|||||||
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "sendTelemetryPayload":
|
|
||||||
// TODO
|
|
||||||
break;
|
|
||||||
case "sortTabs":
|
case "sortTabs":
|
||||||
backgroundLogic.sortTabs();
|
backgroundLogic.sortTabs();
|
||||||
break;
|
break;
|
||||||
@@ -48,26 +42,28 @@ const messageHandler = {
|
|||||||
backgroundLogic.showTabs({cookieStoreId: m.cookieStoreId});
|
backgroundLogic.showTabs({cookieStoreId: m.cookieStoreId});
|
||||||
break;
|
break;
|
||||||
case "hideTabs":
|
case "hideTabs":
|
||||||
backgroundLogic.hideTabs({cookieStoreId: m.cookieStoreId});
|
backgroundLogic.hideTabs({
|
||||||
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "checkIncompatibleAddons":
|
case "checkIncompatibleAddons":
|
||||||
// TODO
|
// TODO
|
||||||
break;
|
break;
|
||||||
case "getShieldStudyVariation":
|
|
||||||
// TODO
|
|
||||||
break;
|
|
||||||
case "moveTabsToWindow":
|
case "moveTabsToWindow":
|
||||||
response = backgroundLogic.moveTabsToWindow({
|
response = backgroundLogic.moveTabsToWindow({
|
||||||
cookieStoreId: m.cookieStoreId
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "getTabs":
|
case "getTabs":
|
||||||
response = backgroundLogic.getTabs({
|
response = backgroundLogic.getTabs({
|
||||||
cookieStoreId: m.cookieStoreId
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "queryIdentitiesState":
|
case "queryIdentitiesState":
|
||||||
response = backgroundLogic.queryIdentitiesState();
|
response = backgroundLogic.queryIdentitiesState(m.message.windowId);
|
||||||
break;
|
break;
|
||||||
case "exemptContainerAssignment":
|
case "exemptContainerAssignment":
|
||||||
response = assignManager._exemptTab(m);
|
response = assignManager._exemptTab(m);
|
||||||
@@ -76,41 +72,16 @@ const messageHandler = {
|
|||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handles messages from sdk code
|
if (browser.contextualIdentities.onRemoved) {
|
||||||
const port = browser.runtime.connect();
|
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
|
||||||
port.onMessage.addListener(m => {
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);
|
||||||
switch (m.type) {
|
backgroundLogic.deleteContainer(userContextId, true);
|
||||||
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) => {
|
browser.tabs.onActivated.addListener((info) => {
|
||||||
assignManager.removeContextMenu();
|
assignManager.removeContextMenu();
|
||||||
browser.tabs.get(info.tabId).then((tab) => {
|
browser.tabs.get(info.tabId).then((tab) => {
|
||||||
tabPageCounter.initTabCounter(tab);
|
|
||||||
assignManager.calculateContextMenu(tab);
|
assignManager.calculateContextMenu(tab);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -118,34 +89,7 @@ const messageHandler = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
browser.windows.onFocusChanged.addListener((windowId) => {
|
||||||
assignManager.removeContextMenu();
|
this.onFocusChangedCallback(windowId);
|
||||||
// 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) => {
|
browser.webRequest.onCompleted.addListener((details) => {
|
||||||
@@ -155,21 +99,50 @@ const messageHandler = {
|
|||||||
assignManager.removeContextMenu();
|
assignManager.removeContextMenu();
|
||||||
|
|
||||||
browser.tabs.get(details.tabId).then((tab) => {
|
browser.tabs.get(details.tabId).then((tab) => {
|
||||||
tabPageCounter.incrementTabCount(tab);
|
|
||||||
assignManager.calculateContextMenu(tab);
|
assignManager.calculateContextMenu(tab);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
}, {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((tab) => {
|
||||||
browser.tabs.onCreated.addListener((details) => {
|
// lets remember the last tab created so we can close it if it looks like a redirect
|
||||||
this.lastCreatedTab = details;
|
this.lastCreatedTab = tab;
|
||||||
|
if (tab.cookieStoreId) {
|
||||||
|
this.unhideContainer(tab.cookieStoreId);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.lastCreatedTab = null;
|
this.lastCreatedTab = null;
|
||||||
}, this.LAST_CREATED_TAB_TIMER);
|
}, this.LAST_CREATED_TAB_TIMER);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async unhideContainer(cookieStoreId) {
|
||||||
|
if (!this.unhideQueue.includes(cookieStoreId)) {
|
||||||
|
this.unhideQueue.push(cookieStoreId);
|
||||||
|
// Unhide all hidden tabs
|
||||||
|
await backgroundLogic.showTabs({
|
||||||
|
cookieStoreId
|
||||||
|
});
|
||||||
|
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onFocusChangedCallback(windowId) {
|
||||||
|
assignManager.removeContextMenu();
|
||||||
|
const currentWindow = await browser.windows.getCurrent();
|
||||||
|
// 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(currentWindow.incognito);
|
||||||
|
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||||
|
if (tabs && tabs[0]) {
|
||||||
|
assignManager.calculateContextMenu(tabs[0]);
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
// 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++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -49,10 +49,6 @@ function confirmSubmit(redirectUrl, cookieStoreId) {
|
|||||||
pageUrl: redirectUrl
|
pageUrl: redirectUrl
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
browser.runtime.sendMessage({
|
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: "click-to-reload-page-in-container",
|
|
||||||
});
|
|
||||||
openInContainer(redirectUrl, cookieStoreId);
|
openInContainer(redirectUrl, cookieStoreId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +66,6 @@ async function denySubmit(redirectUrl) {
|
|||||||
tabId: tab[0].id,
|
tabId: tab[0].id,
|
||||||
pageUrl: redirectUrl
|
pageUrl: redirectUrl
|
||||||
});
|
});
|
||||||
browser.runtime.sendMessage({
|
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: "click-to-reload-page-in-same-container",
|
|
||||||
});
|
|
||||||
document.location.replace(redirectUrl);
|
document.location.replace(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-31
@@ -81,11 +81,9 @@ const Logic = {
|
|||||||
|
|
||||||
// Retrieve the list of identities.
|
// Retrieve the list of identities.
|
||||||
const identitiesPromise = this.refreshIdentities();
|
const identitiesPromise = this.refreshIdentities();
|
||||||
// Get the onboarding variation
|
|
||||||
const variationPromise = this.getShieldStudyVariation();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([identitiesPromise, variationPromise]);
|
await identitiesPromise;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message);
|
throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message);
|
||||||
}
|
}
|
||||||
@@ -150,13 +148,18 @@ const Logic = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async identity(cookieStoreId) {
|
async identity(cookieStoreId) {
|
||||||
const identity = await browser.contextualIdentities.get(cookieStoreId);
|
const defaultContainer = {
|
||||||
return identity || {
|
|
||||||
name: "Default",
|
name: "Default",
|
||||||
cookieStoreId,
|
cookieStoreId,
|
||||||
icon: "default-tab",
|
icon: "default-tab",
|
||||||
color: "default-tab"
|
color: "default-tab"
|
||||||
};
|
};
|
||||||
|
// Handle old style rejection with null and also Promise.reject new style
|
||||||
|
try {
|
||||||
|
return await browser.contextualIdentities.get(cookieStoreId) || defaultContainer;
|
||||||
|
} catch(e) {
|
||||||
|
return defaultContainer;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addEnterHandler(element, handler) {
|
addEnterHandler(element, handler) {
|
||||||
@@ -187,7 +190,10 @@ const Logic = {
|
|||||||
const [identities, state] = await Promise.all([
|
const [identities, state] = await Promise.all([
|
||||||
browser.contextualIdentities.query({}),
|
browser.contextualIdentities.query({}),
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "queryIdentitiesState"
|
method: "queryIdentitiesState",
|
||||||
|
message: {
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
this._identities = identities.map((identity) => {
|
this._identities = identities.map((identity) => {
|
||||||
@@ -269,14 +275,6 @@ const Logic = {
|
|||||||
return identity.cookieStoreId;
|
return identity.cookieStoreId;
|
||||||
},
|
},
|
||||||
|
|
||||||
sendTelemetryPayload(message = {}) {
|
|
||||||
if (!message.event) {
|
|
||||||
throw new Error("Missing event name for telemetry");
|
|
||||||
}
|
|
||||||
message.method = "sendTelemetryPayload";
|
|
||||||
browser.runtime.sendMessage(message);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeIdentity(userContextId) {
|
removeIdentity(userContextId) {
|
||||||
if (!userContextId) {
|
if (!userContextId) {
|
||||||
return Promise.reject("removeIdentity must be called with userContextId argument.");
|
return Promise.reject("removeIdentity must be called with userContextId argument.");
|
||||||
@@ -312,13 +310,6 @@ const Logic = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getShieldStudyVariation() {
|
|
||||||
const variation = await browser.runtime.sendMessage({
|
|
||||||
method: "getShieldStudyVariation"
|
|
||||||
});
|
|
||||||
this._onboardingVariation = variation;
|
|
||||||
},
|
|
||||||
|
|
||||||
generateIdentityName() {
|
generateIdentityName() {
|
||||||
const defaultName = "Container #";
|
const defaultName = "Container #";
|
||||||
const ids = [];
|
const ids = [];
|
||||||
@@ -463,15 +454,12 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
panelSelector: "#container-panel",
|
panelSelector: "#container-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("#container-add-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
|
||||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
|
||||||
Logic.sendTelemetryPayload({
|
|
||||||
event: "edit-containers"
|
|
||||||
});
|
|
||||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -614,12 +602,8 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
|| e.target.parentNode.matches(".open-newtab")
|
|| e.target.parentNode.matches(".open-newtab")
|
||||||
|| e.type === "keydown") {
|
|| e.type === "keydown") {
|
||||||
try {
|
try {
|
||||||
await browser.runtime.sendMessage({
|
browser.tabs.create({
|
||||||
method: "openTab",
|
cookieStoreId: identity.cookieStoreId
|
||||||
message: {
|
|
||||||
userContextId: Logic.userContextId(identity.cookieStoreId),
|
|
||||||
source: "pop-up"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -669,6 +653,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
try {
|
try {
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentCookieStoreId()
|
cookieStoreId: Logic.currentCookieStoreId()
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
@@ -700,6 +685,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
Logic.addEnterHandler(moveTabsEl, async function () {
|
Logic.addEnterHandler(moveTabsEl, async function () {
|
||||||
await browser.runtime.sendMessage({
|
await browser.runtime.sendMessage({
|
||||||
method: "moveTabsToWindow",
|
method: "moveTabsToWindow",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
@@ -741,6 +727,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
// Let's retrieve the list of tabs.
|
// Let's retrieve the list of tabs.
|
||||||
const tabs = await browser.runtime.sendMessage({
|
const tabs = await browser.runtime.sendMessage({
|
||||||
method: "getTabs",
|
method: "getTabs",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentIdentity().cookieStoreId
|
cookieStoreId: Logic.currentIdentity().cookieStoreId
|
||||||
});
|
});
|
||||||
return this.buildInfoTable(tabs);
|
return this.buildInfoTable(tabs);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Containers Experiment",
|
"name": "Firefox Multi-Account Containers",
|
||||||
"version": "3.0.0",
|
"version": "4.0.1",
|
||||||
|
|
||||||
"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": "Firefox Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "img/container-site-d-48.png",
|
"48": "img/container-site-d-48.png",
|
||||||
"96": "img/container-site-d-96.png"
|
"96": "img/container-site-d-96.png"
|
||||||
@@ -44,43 +44,8 @@
|
|||||||
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"browser_style": true,
|
"browser_style": true,
|
||||||
"default_icon": {
|
"default_icon": "img/container-site.svg",
|
||||||
"16": "img/container-site-d-24.png",
|
"default_title": "Firefox Multi-Account Containers",
|
||||||
"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_popup": "popup.html"
|
"default_popup": "popup.html"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Containers browserAction Popup</title>
|
<title>Firefox Multi-Account Containers</title>
|
||||||
<link rel="stylesheet" href="/css/popup.css">
|
<link rel="stylesheet" href="/css/popup.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user