Merge pull request #667 from mozilla/master

Deploy 2.4.0 to Test Pilot users
This commit is contained in:
Jonathan Kingston 2017-07-13 22:53:16 +01:00 committed by GitHub
commit 174c108cb7
29 changed files with 2006 additions and 422 deletions

View file

@ -1 +1,3 @@
testpilot-metrics.js
lib/shield/*.js
lib/testpilot/*.js

View file

@ -9,6 +9,7 @@ module.exports = {
"webextensions": true
},
"globals": {
"Utils": true,
"CustomizableUI": true,
"CustomizableWidgets": true,
"SessionStore": true,

2
.gitignore vendored
View file

@ -1,8 +1,10 @@
.DS_Store
package-lock.json
node_modules
README.html
*.xpi
*.swp
*.swo
.vimrc
.env
addon.env

View file

@ -11,7 +11,7 @@
"declaration-block-no-duplicate-properties": true,
"order/declaration-block-properties-alphabetical-order": true,
"property-blacklist": [
"/height/",
"/(min[-]|max[-])height/",
"/width/",
"/top/",
"/bottom/",

3
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,3 @@
# Code Of Conduct
This add-on follows the [Mozilla Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/) for our code of conduct.

35
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,35 @@
# Contributing
Everyone is welcome to contribute to containers. Reach out to team members if you have questions:
- IRC: #containers on irc.mozilla.org
- Email: containers@mozilla.com
## Filing bugs
If you find a bug with containers, please file a issue.
Check first if the bug might already exist: https://github.com/mozilla/testpilot-containers/issues
[Open an issue](https://github.com/mozilla/testpilot-containers/issues/new)
1. Visit about:support
2. Click "Copy raw data to clipboard" and paste into the bug. Alternatively copy the following sections into the issue:
- Application Basics
- Nightly Features (if you are in nightly)
- Extensions
- Experimental Features
3. Include clear steps to reproduce the issue you have experienced.
4. Include screenshots if possible.
## Sending Pull Requests
Patches should be submitted as pull requests. When submitting patches as PRs:
- You agree to license your code under the project's open source license (MPL 2.0).
- Base your branch off the current master (see below for an example workflow).
- Add both your code and new tests if relevant.
- Run npm test to make sure all tests still pass.
- Please do not include merge commits in pull requests; include only commits with the new relevant code.
See the main [README](./README.md) for information on prerequisites, installing, running and testing.

View file

@ -1,25 +1,22 @@
# Containers: Test Pilot Experiment
# Containers Add-on
[![Available on Test Pilot](https://img.shields.io/badge/available_on-Test_Pilot-0996F8.svg)](https://testpilot.firefox.com/experiments/containers)
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to experiment with [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) in [Firefox Test Pilot](https://testpilot.firefox.com/) to learn:
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to build [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) as a Firefox [Test Pilot](https://testpilot.firefox.com/) Experiment and [Shield Study](https://wiki.mozilla.org/Firefox/Shield/Shield_Studies) to learn:
* Will a general Firefox audience understand the Containers feature?
* Is the UI as currently implemented in Nightly clear or discoverable?
See [the Product Hypothesis Document for more
details](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit?ts=5824ba12#).
For more info, see:
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
* [Shield Product Hypothesis Document](https://docs.google.com/document/d/1vMD-fH_5hGDDqNvpRZk12_RhCN2WAe4_yaBamaNdtik/edit#)
## Requirements
* node 7+ (for jpm)
* Firefox 51+
## Run it
See Development
* Firefox 53+
## Development
@ -27,28 +24,23 @@ See Development
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.
1. Make a new profile by running `/path/to/firefox -P`, which launches the profile editor. "Create Profile" -- name it whatever you wish (e.g. 'addon_dev') and store it in the default location. It's probably best to deselect the option to "Use without asking," since you probably don't want to use this as your default profile.
2. Once you've created your profile, click "Start Firefox". A new instance of Firefox should launch. Go to Tools->Add-ons and search for "DevPrefs". Install it. Quit Firefox.
3. Now you have a new, vanilla Firefox profile with the DevPrefs add-on installed. You can use your new profile with the code in _this_ repository like so:
**Beta building**
#### Run the `.xpi` file in an unbranded build
Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. So, you must run the add-on in an [unbranded build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds):
To build this for 51 beta just using the downloaded version of beta will not work as XPI signature checking is disabled fully.
1. Download and install an un-branded build of Firefox
2. Download the latest `.xpi` from this repository's releases
3. Run the un-branded build of Firefox with your DevPrefs profile
4. Go to `about:addons`
5. Click the gear, and select "Install Add-on From File..."
6. Select the `.xpi` file
The only way to run the experiment is using an [unbranded version build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds) or to build beta yourself:
1. [Download the mozilla-beta repo](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Source_Code/Mercurial#mozilla-beta_(prerelease_development_tree))
2. [Create a mozconfig file](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Configuring_Build_Options) - probably optional
3. `cd <reponame>`
3. `./mach bootstrap`
4. `./mach build`
5. Follow the above instructions by creating the new profile via: `~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox -P` (Where "obj-x86_64-pc-linux-gnu" may be different depending on platform obj-...)
### Run with jpm
#### Run the TxP experiment with `jpm`
1. `git clone git@github.com:mozilla/testpilot-containers.git`
2. `cd testpilot-containers`
@ -57,11 +49,22 @@ The only way to run the experiment is using an [unbranded version build](https:/
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
To build a local .xpi, use the plain [`jpm
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command.
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,
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
@ -75,6 +78,11 @@ add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/
### Testing
TBD
### Distributing
TBD
### Links
- [Licence](./LICENSE.txt)
- [Contributing](./CONTRIBUTING.md)
- [Code Of Conduct](./CODE_OF_CONDUCT.md)

View file

@ -52,55 +52,55 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
[data-identity-icon="fingerprint"],
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fingerprint");
--identity-icon: url("/data/usercontext.svg#fingerprint");
}
[data-identity-icon="briefcase"],
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#briefcase");
--identity-icon: url("/data/usercontext.svg#briefcase");
}
[data-identity-icon="dollar"],
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#dollar");
--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("resource://testpilot-containers/data/usercontext.svg#cart");
--identity-icon: url("/data/usercontext.svg#cart");
}
[data-identity-icon="circle"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#circle");
--identity-icon: url("/data/usercontext.svg#circle");
}
[data-identity-icon="gift"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#gift");
--identity-icon: url("/data/usercontext.svg#gift");
}
[data-identity-icon="vacation"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#vacation");
--identity-icon: url("/data/usercontext.svg#vacation");
}
[data-identity-icon="food"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#food");
--identity-icon: url("/data/usercontext.svg#food");
}
[data-identity-icon="fruit"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fruit");
--identity-icon: url("/data/usercontext.svg#fruit");
}
[data-identity-icon="pet"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#pet");
--identity-icon: url("/data/usercontext.svg#pet");
}
[data-identity-icon="tree"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#tree");
--identity-icon: url("/data/usercontext.svg#tree");
}
[data-identity-icon="chill"] {
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#chill");
--identity-icon: url("/data/usercontext.svg#chill");
}
#userContext-indicator {
@ -139,7 +139,7 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
background-size: contain;
fill: var(--identity-icon-color) !important;
filter: url(/img/filters.svg#fill);
filter: url(resource://testpilot-containers/data/filters.svg#fill);
filter: url(/data/filters.svg#fill);
}
/* containers experiment */
@ -200,7 +200,7 @@ special cases are addressed below */
}
#new-tab-overlay {
--icon-size: 26px;
--icon-size: 16px;
-moz-appearance: none;
background: transparent;
font-style: -moz-use-system-font;
@ -252,8 +252,8 @@ special cases are addressed below */
}
#new-tab-overlay .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon {
block-height: var(--icon-size);
block-width: var(--icon-size);
block-size: var(--icon-size);
inline-size: var(--icon-size);
}
.menuitem-iconic[data-usercontextid] > .menu-iconic-left {

View file

@ -68,6 +68,16 @@ 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
@ -76,7 +86,7 @@ of a `testpilottest` telemetry ping for each scenario.
"userContextId": <userContextId>,
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
"event": "open-tab",
"eventSource": ["tab-bar"|"pop-up"|"file-menu"]
"eventSource": ["tab-bar"|"pop-up"|"file-menu"|"alltabs-menu"|"plus-button"]
}
```
@ -220,7 +230,7 @@ of a `testpilottest` telemetry ping for each scenario.
}
```
* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this 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
{
@ -229,6 +239,15 @@ of a `testpilottest` telemetry ping for each scenario.
}
```
* 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
@ -260,7 +279,7 @@ local schema = {
### Valid data should be enforced on the server side:
* `eventSource` should be one of `tab-bar`, `pop-up`, or `file-menu`.
* `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.

217
index.js
View file

@ -6,9 +6,6 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const DEFAULT_TAB = "about:newtab";
const LOOKUP_KEY = "$ref";
const SHOW_MENU_TIMEOUT = 100;
const HIDE_MENU_TIMEOUT = 300;
const INCOMPATIBLE_ADDON_IDS = [
"pulse@mozilla.com",
"snoozetabs@mozilla.com",
@ -42,8 +39,17 @@ const IDENTITY_ICONS = [
{ 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 ],
];
@ -60,6 +66,7 @@ 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 tabsUtils = require("sdk/tabs/utils");
@ -213,6 +220,7 @@ const ContainerService = {
"getPreference",
"sendTelemetryPayload",
"getTheme",
"getShieldStudyVariation",
"refreshNeeded",
"forgetIdentityAndRefresh",
"checkIncompatibleAddons"
@ -236,18 +244,15 @@ const ContainerService = {
}
tabs.on("open", tab => {
this._hideAllPanels();
this._restyleTab(tab);
this._remapTab(tab);
});
tabs.on("close", tab => {
this._hideAllPanels();
this._remapTab(tab);
});
tabs.on("activate", tab => {
this._hideAllPanels();
this._restyleActiveTab(tab).catch(() => {});
this._configureActiveWindows();
this._remapTab(tab);
@ -320,6 +325,11 @@ const ContainerService = {
// End-Of-Hack
Services.obs.addObserver(this, "lightweight-theme-changed", false);
if (self.id === "@shield-study-containers") {
study.startup(reason);
this.shieldStudyVariation = study.variation;
}
},
registerBackgroundConnection(api) {
@ -362,6 +372,10 @@ const ContainerService = {
});
},
getShieldStudyVariation() {
return this.shieldStudyVariation;
},
// utility methods
_containerTabCount(userContextId) {
@ -922,12 +936,6 @@ const ContainerService = {
return this._configureWindows();
},
_hideAllPanels() {
for (let windowObject of this._windowMap.values()) { // eslint-disable-line prefer-const
windowObject.hidePanel();
}
},
_restyleActiveTab(tab) {
if (!tab) {
return Promise.resolve(null);
@ -1086,22 +1094,50 @@ ContainerWindow.prototype = {
_timeoutStore: new Map(),
_elementCache: new Map(),
_tooltipCache: new Map(),
_plusButton: null,
_overflowPlusButton: null,
_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");
this._style = Style({ uri: self.data.url("usercontext.css") });
// Only hack the normal plus button as the alltabs is done elsewhere
this.attachMenuEvent("plus-button", this._plusButton);
attachTo(this._style, this._window);
},
attachMenuEvent(source, button) {
const popup = button.querySelector(".new-tab-popup");
popup.addEventListener("popupshown", () => {
ContainerService.sendTelemetryPayload({
"event": "show-plus-button-menu",
"eventSource": source
});
popup.querySelector("menuseparator").remove();
const popupMenuItems = [...popup.querySelectorAll("menuitem")];
popupMenuItems.forEach((item) => {
const userContextId = item.getAttribute("data-usercontextid");
if (!userContextId) {
item.remove();
}
item.setAttribute("command", "");
item.addEventListener("command", (e) => {
e.stopPropagation();
e.preventDefault();
ContainerService.openTab({
userContextId: userContextId,
source: source
});
});
});
});
},
configure() {
return Promise.all([
this._configurePlusButtonMenu(),
this._configureActiveTab(),
this._configureFileMenu(),
this._configureAllTabsMenu(),
@ -1114,112 +1150,6 @@ ContainerWindow.prototype = {
return this._configureContextMenu();
},
handleEvent(e) {
let el = e.target;
switch (e.type) {
case "mouseover":
this._createTimeout("show", () => {
this.showPopup(el);
}, SHOW_MENU_TIMEOUT);
break;
case "click":
this.hidePanel();
break;
case "mouseout":
while(el) {
if (el === this._panelElement ||
el === this._plusButton ||
el === this._overflowPlusButton) {
// Clear show timeout so we don't hide and reshow
this._cleanTimeout("show");
this._createTimeout("hidden", () => {
this.hidePanel();
}, HIDE_MENU_TIMEOUT);
return;
}
el = el.parentElement;
}
break;
}
},
showPopup(buttonElement) {
this._cleanAllTimeouts();
this._panelElement.openPopup(buttonElement);
},
_configurePlusButtonMenuElement(buttonElement) {
if (buttonElement) {
// Let's remove the tooltip because it can go over our panel.
this._tooltipCache.set(buttonElement, buttonElement.getAttribute("tooltip"));
buttonElement.setAttribute("tooltip", "");
this._disableElement(buttonElement);
buttonElement.addEventListener("mouseover", this);
buttonElement.addEventListener("click", this);
buttonElement.addEventListener("mouseout", this);
}
},
async _configurePlusButtonMenu() {
const mainPopupSetElement = this._window.document.getElementById("mainPopupSet");
// Let's remove all the previous panels.
if (this._panelElement) {
this._panelElement.remove();
}
this._panelElement = this._window.document.createElementNS(XUL_NS, "panel");
this._panelElement.setAttribute("id", "new-tab-overlay");
this._panelElement.setAttribute("position", "bottomcenter topleft");
this._panelElement.setAttribute("side", "top");
this._panelElement.setAttribute("flip", "side");
this._panelElement.setAttribute("type", "arrow");
this._panelElement.setAttribute("animate", "open");
this._panelElement.setAttribute("consumeoutsideclicks", "never");
mainPopupSetElement.appendChild(this._panelElement);
this._configurePlusButtonMenuElement(this._plusButton);
this._configurePlusButtonMenuElement(this._overflowPlusButton);
this._panelElement.addEventListener("mouseout", this);
this._panelElement.addEventListener("mouseover", () => {
this._cleanAllTimeouts();
});
try {
const identities = await ContainerService.queryIdentities();
identities.forEach(identity => {
const menuItemElement = this._window.document.createElementNS(XUL_NS, "menuitem");
this._panelElement.appendChild(menuItemElement);
menuItemElement.className = "menuitem-iconic";
menuItemElement.setAttribute("label", identity.name);
menuItemElement.setAttribute("data-usercontextid", identity.userContextId);
menuItemElement.setAttribute("data-identity-icon", identity.icon);
menuItemElement.setAttribute("data-identity-color", identity.color);
menuItemElement.addEventListener("command", (e) => {
ContainerService.openTab({
userContextId: identity.userContextId,
source: "tab-bar"
});
e.stopPropagation();
});
menuItemElement.addEventListener("mouseover", () => {
this._cleanAllTimeouts();
});
menuItemElement.addEventListener("mouseout", this);
this._panelElement.appendChild(menuItemElement);
});
} catch (e) {
this.hidePanel();
}
},
_configureTabStyle() {
const promises = [];
for (let tab of modelFor(this._window).tabs) { // eslint-disable-line prefer-const
@ -1389,19 +1319,15 @@ ContainerWindow.prototype = {
}
},
hidePanel() {
this._cleanAllTimeouts();
this._panelElement.hidePopup();
},
shutdown() {
// CSS must be removed.
detachFrom(this._style, this._window);
this._shutdownPlusButtonMenu();
this._shutdownFileMenu();
this._shutdownAllTabsMenu();
this._shutdownContextMenu();
this._shutdownContainers();
},
_shutDownPlusButtonMenuElement(buttonElement) {
@ -1415,11 +1341,6 @@ ContainerWindow.prototype = {
}
},
_shutdownPlusButtonMenu() {
this._shutDownPlusButtonMenuElement(this._plusButton);
this._shutDownPlusButtonMenuElement(this._overflowPlusButton);
},
_shutdownFileMenu() {
this._shutdownMenu("menu_newUserContext");
},
@ -1468,6 +1389,36 @@ ContainerWindow.prototype = {
return true;
},
_shutdownContainers() {
ContextualIdentityProxy.getIdentities().forEach(identity => {
if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) !== -1 &&
IDENTITY_COLORS_STANDARD.indexOf(identity.color) !== -1) {
return;
}
if (IDENTITY_ICONS_STANDARD.indexOf(identity.icon) === -1) {
if (identity.userContextId <= IDENTITY_ICONS_STANDARD.length) {
identity.icon = IDENTITY_ICONS_STANDARD[identity.userContextId - 1];
} else {
identity.icon = IDENTITY_ICONS_STANDARD[0];
}
}
if (IDENTITY_COLORS_STANDARD.indexOf(identity.color) === -1) {
if (identity.userContextId <= IDENTITY_COLORS_STANDARD.length) {
identity.color = IDENTITY_COLORS_STANDARD[identity.userContextId - 1];
} else {
identity.color = IDENTITY_COLORS_STANDARD[0];
}
}
ContextualIdentityService.update(identity.userContextId,
identity.name,
identity.icon,
identity.color);
});
}
};
// uninstall/install events ---------------------------------------------------

View file

@ -0,0 +1,55 @@
/**
* 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;

428
lib/shield/index.js Normal file
View file

@ -0,0 +1,428 @@
"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
}

View file

@ -0,0 +1,95 @@
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;

View file

@ -2,7 +2,7 @@
"name": "testpilot-containers",
"title": "Containers Experiment",
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored Container Tabs. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
"version": "2.3.0",
"version": "2.4.0",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": {
"url": "https://github.com/mozilla/testpilot-containers/issues"
@ -16,7 +16,9 @@
"eslint-plugin-promise": "^3.4.0",
"htmllint-cli": "^0.0.5",
"jpm": "^1.2.2",
"json": "^9.0.6",
"npm-run-all": "^4.0.0",
"shield-studies-addon-utils": "^2.0.0",
"stylelint": "^7.9.0",
"stylelint-config-standard": "^16.0.0",
"stylelint-order": "^0.3.0",
@ -41,6 +43,7 @@
},
"scripts": {
"build": "npm test && jpm xpi",
"build-shield": "npm test && npm run package-shield",
"deploy": "deploy-txp",
"lint": "npm-run-all lint:*",
"lint:addon": "addons-linter webextension --self-hosted",
@ -48,6 +51,7 @@
"lint:html": "htmllint webextension/*.html",
"lint:js": "eslint .",
"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"
},
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"

40
study.js Normal file
View file

@ -0,0 +1,40 @@
/* 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;

View file

@ -1,6 +1,9 @@
// 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

View file

@ -1,4 +1,4 @@
const MAJOR_VERSIONS = ["2.3.0"];
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
const LOOKUP_KEY = "$ref";
const assignManager = {
@ -6,6 +6,7 @@ const assignManager = {
MENU_REMOVE_ID: "remove-open-in-this-container",
storageArea: {
area: browser.storage.local,
exemptedTabs: {},
getSiteStoreKey(pageUrl) {
const url = new window.URL(pageUrl);
@ -13,6 +14,27 @@ const assignManager = {
return `${storagePrefix}${url.hostname}`;
},
setExempted(pageUrl, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
if (!(siteStoreKey in this.exemptedTabs)) {
this.exemptedTabs[siteStoreKey] = [];
}
this.exemptedTabs[siteStoreKey].push(tabId);
},
removeExempted(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
this.exemptedTabs[siteStoreKey] = [];
},
isExempted(pageUrl, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
if (!(siteStoreKey in this.exemptedTabs)) {
return false;
}
return this.exemptedTabs[siteStoreKey].includes(tabId);
},
get(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return new Promise((resolve, reject) => {
@ -27,8 +49,13 @@ const assignManager = {
});
},
set(pageUrl, data) {
set(pageUrl, data, exemptedTabIds) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
if (exemptedTabIds) {
exemptedTabIds.forEach((tabId) => {
this.setExempted(pageUrl, tabId);
});
}
return this.area.set({
[siteStoreKey]: data
});
@ -36,22 +63,30 @@ const assignManager = {
remove(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
// When we remove an assignment we should clear all the exemptions
this.removeExempted(pageUrl);
return this.area.remove([siteStoreKey]);
},
deleteContainer(userContextId) {
const removeKeys = [];
this.area.get().then((siteConfigs) => {
async deleteContainer(userContextId) {
const sitesByContainer = await this.getByContainer(userContextId);
this.area.remove(Object.keys(sitesByContainer));
},
async getByContainer(userContextId) {
const sites = {};
const siteConfigs = await this.area.get();
Object.keys(siteConfigs).forEach((key) => {
// For some reason this is stored as string... lets check them both as that
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
removeKeys.push(key);
const site = siteConfigs[key];
// In hindsight we should have stored this
// TODO file a follow up to clean the storage onLoad
site.hostname = key.replace(/^siteContainerMap@@_/, "");
sites[key] = site;
}
});
this.area.remove(removeKeys);
}).catch((e) => {
throw e;
});
return sites;
}
},
@ -70,39 +105,16 @@ const assignManager = {
}
},
// We return here so the confirm page can load the tab when exempted
async _exemptTab(m) {
const pageUrl = m.pageUrl;
this.storageArea.setExempted(pageUrl, m.tabId);
return true;
},
init() {
browser.contextMenus.onClicked.addListener((info, tab) => {
const userContextId = this.getUserContextIdFromCookieStore(tab);
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
if (userContextId) {
let actionName;
let storageAction;
if (info.menuItemId === this.MENU_ASSIGN_ID) {
actionName = "added";
storageAction = this.storageArea.set(info.pageUrl, {
userContextId,
neverAsk: false
});
} else {
actionName = "removed";
storageAction = this.storageArea.remove(info.pageUrl);
}
storageAction.then(() => {
browser.notifications.create({
type: "basic",
title: "Containers",
message: `Successfully ${actionName} site to always open in this container`,
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
});
backgroundLogic.sendTelemetryPayload({
event: `${actionName}-container-assignment`,
userContextId: userContextId,
});
this.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}
this._onClickedHandler(info, tab);
});
// Before a request is handled by the browser we decide if we should route through a different container
@ -110,6 +122,7 @@ const assignManager = {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
}
this.removeContextMenu();
return Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.get(options.url)
@ -117,11 +130,12 @@ const assignManager = {
const userContextId = this.getUserContextIdFromCookieStore(tab);
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| tab.incognito) {
|| tab.incognito
|| this.storageArea.isExempted(options.url, tab.id)) {
return {};
}
this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
this.calculateContextMenu(tab);
/* Removal of existing tabs:
@ -149,6 +163,21 @@ const assignManager = {
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
},
async _onClickedHandler(info, tab) {
const userContextId = this.getUserContextIdFromCookieStore(tab);
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
if (userContextId) {
// let actionName;
let remove;
if (info.menuItemId === this.MENU_ASSIGN_ID) {
remove = false;
} else {
remove = true;
}
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
}
},
deleteContainer(userContextId) {
this.storageArea.deleteContainer(userContextId);
@ -171,29 +200,94 @@ const assignManager = {
// Ensure we are not in incognito mode
const url = new URL(tab.url);
if (url.protocol === "about:"
|| url.protocol === "moz-extension:"
|| tab.incognito) {
return false;
}
return true;
},
calculateContextMenu(tab) {
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
let actionName;
// https://github.com/mozilla/testpilot-containers/issues/626
// Context menu has stored context IDs as strings, so we need to coerce
// the value to a string for accurate checking
userContextId = String(userContextId);
if (!remove) {
const tabs = await browser.tabs.query({});
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
const exemptedTabIds = tabs.filter((tab) => {
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
if (tabStoreKey === assignmentStoreKey &&
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
return true;
}
return false;
}).map((tab) => {
return tab.id;
});
await this.storageArea.set(pageUrl, {
userContextId,
neverAsk: false
}, exemptedTabIds);
actionName = "added";
} else {
await this.storageArea.remove(pageUrl);
actionName = "removed";
}
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName} site to always open in this container`
});
backgroundLogic.sendTelemetryPayload({
event: `${actionName}-container-assignment`,
userContextId: userContextId,
});
const tab = await browser.tabs.get(tabId);
this.calculateContextMenu(tab);
},
async _getAssignment(tab) {
const cookieStore = this.getUserContextIdFromCookieStore(tab);
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
return await this.storageArea.get(tab.url);
}
return false;
},
_getByContainer(userContextId) {
return this.storageArea.getByContainer(userContextId);
},
removeContextMenu() {
// There is a focus issue in this menu where if you change window with a context menu click
// you get the wrong menu display because of async
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
// We also can't change for always private mode
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
const cookieStore = this.getUserContextIdFromCookieStore(tab);
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
browser.contextMenus.remove(this.MENU_REMOVE_ID);
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
this.storageArea.get(tab.url).then((siteSettings) => {
},
async calculateContextMenu(tab) {
this.removeContextMenu();
const siteSettings = await this._getAssignment(tab);
// Return early and not add an item if we have false
// False represents assignment is not permitted
if (siteSettings === false) {
return false;
}
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
let menuId = this.MENU_ASSIGN_ID;
if (siteSettings) {
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
if (siteSettings &&
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
prefix = "✓";
menuId = this.MENU_REMOVE_ID;
}
@ -203,17 +297,15 @@ const assignManager = {
checked: true,
contexts: ["all"],
});
}).catch((e) => {
throw e;
});
}
},
reloadPageInContainer(url, userContextId, index, neverAsk = false) {
reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) {
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
const loadPage = browser.extension.getURL("confirm-page.html");
// False represents assignment is not permitted
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
if (neverAsk) {
browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index});
browser.tabs.create({url, cookieStoreId, index});
backgroundLogic.sendTelemetryPayload({
event: "auto-reload-page-in-container",
userContextId: userContextId,
@ -223,8 +315,17 @@ const assignManager = {
event: "prompt-to-reload-page-in-container",
userContextId: userContextId,
});
const confirmUrl = `${loadPage}?url=${url}`;
browser.tabs.create({url: confirmUrl, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}).then(() => {
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
let currentCookieStoreId;
if (currentUserContextId) {
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
confirmUrl += `&currentCookieStoreId=${currentCookieStoreId}`;
}
browser.tabs.create({
url: confirmUrl,
cookieStoreId: currentCookieStoreId,
index
}).then(() => {
// We don't want to sync this URL ever nor clutter the users history
browser.history.deleteUrl({url: confirmUrl});
}).catch((e) => {
@ -271,7 +372,7 @@ const backgroundLogic = {
createOrUpdateContainer(options) {
let donePromise;
if (options.userContextId) {
if (options.userContextId !== "new") {
donePromise = browser.contextualIdentities.update(
this.cookieStoreId(options.userContextId),
options.params
@ -354,7 +455,7 @@ const messageHandler = {
LAST_CREATED_TAB_TIMER: 2000,
init() {
// Handles messages from webextension/js/popup.js
// Handles messages from webextension code
browser.runtime.onMessage.addListener((m) => {
let response;
@ -372,11 +473,29 @@ const messageHandler = {
case "neverAsk":
assignManager._neverAsk(m);
break;
case "getAssignment":
response = browser.tabs.get(m.tabId).then((tab) => {
return assignManager._getAssignment(tab);
});
break;
case "getAssignmentObjectByContainer":
response = assignManager._getByContainer(m.message.userContextId);
break;
case "setOrRemoveAssignment":
// m.tabId is used for where to place the in content message
// m.url is the assignment to be removed/added
response = browser.tabs.get(m.tabId).then((tab) => {
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
});
break;
case "exemptContainerAssignment":
response = assignManager._exemptTab(m);
break;
}
return response;
});
// Handles messages from index.js
// Handles messages from sdk code
const port = browser.runtime.connect();
port.onMessage.addListener(m => {
switch (m.type) {
@ -408,6 +527,7 @@ const messageHandler = {
});
browser.tabs.onActivated.addListener((info) => {
assignManager.removeContextMenu();
browser.tabs.get(info.tabId).then((tab) => {
tabPageCounter.initTabCounter(tab);
assignManager.calculateContextMenu(tab);
@ -417,6 +537,12 @@ const messageHandler = {
});
browser.windows.onFocusChanged.addListener((windowId) => {
assignManager.removeContextMenu();
// browserAction loses background color in new windows ...
// https://bugzil.la/1314674
// https://github.com/mozilla/testpilot-containers/issues/608
// ... so re-call displayBrowserActionBadge on window changes
displayBrowserActionBadge();
browser.tabs.query({active: true, windowId}).then((tabs) => {
if (tabs && tabs[0]) {
tabPageCounter.initTabCounter(tabs[0]);
@ -445,6 +571,7 @@ const messageHandler = {
if (details.frameId !== 0 || details.tabId === -1) {
return {};
}
assignManager.removeContextMenu();
browser.tabs.get(details.tabId).then((tab) => {
tabPageCounter.incrementTabCount(tab);

View file

@ -8,26 +8,29 @@
<body>
<main>
<div class="title">
<h1 class="title-text">Should we open this in your container?</h1>
<h1 class="title-text">Open this site in your assigned container?</h1>
</div>
<form id="redirect-form">
<p>
Looks like you requested:
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn class="container-name"></dfn> for this site:<br />
</p>
<div id="redirect-url"></div>
<p>
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn id="redirect-site"></dfn> in <dfn>this</dfn> type of container. Would you like to proceed?<br />
</p>
<p>Would you still like to open in this current container?</p>
<br />
<br />
<input id="never-ask" type="checkbox" /><label for="never-ask">Remember my decision for this site</label>
<label for="never-ask" class="check-label">
<input id="never-ask" type="checkbox" />
Remember my decision for this site
</label>
<br />
<div class="button-container">
<button id="confirm" class="button primary" autofocus>Take me there</button>
<button id="deny" class="button">Open in <dfn id="current-container-name">Current</dfn> Container</button>
<button id="confirm" class="button primary" autofocus>Open in <dfn class="container-name"></dfn> Container</button>
</div>
</form>
</main>
<script src="js/utils.js"></script>
<script src="js/confirm-page.js"></script>
</body>
</html>

View file

@ -4,11 +4,21 @@
}
main {
background: url(/img/onboarding-1.png) no-repeat;
background: url(/img/onboarding-4.png) no-repeat;
background-position: -10px -15px;
background-size: 285px;
margin-inline-start: -285px;
padding-inline-start: 285px;
background-size: 300px;
margin-inline-start: -350px;
padding-inline-start: 350px;
}
.container-name {
font-weight: bold;
}
button .container-name,
#current-container-name {
font-weight: bold;
text-transform: capitalize;
}
@media only screen and (max-width: 1300px) {
@ -36,6 +46,33 @@ html {
word-break: break-all;
}
#redirect-url {
background: #efefef;
border-radius: 2px;
line-height: 1.5;
padding-block-end: 0.5rem;
padding-block-start: 0.5rem;
padding-inline-end: 0.5rem;
padding-inline-start: 0.5rem;
}
#redirect-url img {
block-size: 16px;
inline-size: 16px;
margin-inline-end: 6px;
offset-block-start: 3px;
position: relative;
}
dfn {
font-style: normal;
}
.button-container > button {
min-inline-size: 240px;
}
.check-label {
align-items: center;
display: flex;
}

View file

@ -0,0 +1,27 @@
.container-notification {
align-items: center;
background: #efefef;
color: #003f07;
display: flex;
font: 12px sans-serif;
inline-size: 100vw;
justify-content: start;
offset-block-start: 0;
offset-inline-start: 0;
padding-block-end: 8px;
padding-block-start: 8px;
padding-inline-end: 8px;
padding-inline-start: 8px;
position: fixed;
text-align: start;
transform: translateY(-100%);
transition: transform 0.3s cubic-bezier(0.07, 0.95, 0, 1) 0.3s;
z-index: 999999999999;
}
.container-notification img {
block-size: 16px;
display: inline-block;
inline-size: 16px;
margin-inline-end: 3px;
}

View file

@ -1,11 +1,55 @@
/* General Rules and Resets */
body {
inline-size: 300px;
max-inline-size: 300px;
* {
font-size: inherit;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
html {
box-sizing: border-box;
font-size: 12px;
}
body {
font-family: Roboto, Noto, "San Francisco", Ubuntu, "Segoe UI", "Fira Sans", message-box, Arial, sans-serif;
inline-size: 300px;
max-inline-size: 300px;
}
:root {
--primary-action-color: #248aeb;
--title-text-color: #000;
--text-normal-color: #4a4a4a;
--text-heading-color: #000;
/* calculated from 12px */
--font-size-heading: 1.33rem; /* 16px */
--block-line-space-size: 0.5rem; /* 6px */
--inline-item-space-size: 0.5rem; /* 6px */
--block-line-separation-size: 0.33rem; /* 10px */
--inline-icon-space-size: 0.833rem; /* 10px */
/* Use for url and icon size */
--block-url-label-size: 2rem; /* 24px */
--inline-start-size: 1.66rem; /* 20px */
--inline-button-size: 5.833rem; /* 70px */
--icon-size: 1.166rem; /* 14px */
--small-text-size: 0.833rem; /* 10px */
--small-radius: 3px;
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
}
@media (min-resolution: 1dppx) {
html {
font-size: 14px;
}
}
*,
@ -14,6 +58,13 @@ html {
box-sizing: inherit;
}
form {
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
table {
border: 0;
border-spacing: 0;
@ -30,11 +81,27 @@ table {
}
.scrollable {
border-block-start: 1px solid #f1f1f1;
inline-size: 100%;
max-block-size: 400px;
overflow: auto;
}
.offpage {
opacity: 0;
}
[hidden] {
display: none !important;
}
/* Effect borrowed from tabs in Firefox, ensure that the element flexes to the full width */
.truncate-text {
mask-image: linear-gradient(to left, transparent, black 1em);
overflow: hidden;
white-space: nowrap;
}
/* Color and icon helpers */
[data-identity-color="blue"] {
--identity-tab-color: #37adff;
@ -51,6 +118,11 @@ table {
--identity-icon-color: #51cd00;
}
[data-identity-color="grey"] {
/* Only used for the edit panel */
--identity-icon-color: #616161;
}
[data-identity-color="yellow"] {
--identity-tab-color: #ffcb00;
--identity-icon-color: #ffcb00;
@ -124,7 +196,16 @@ table {
--identity-icon: url("/img/usercontext.svg#chill");
}
#current-tab [data-identity-icon="default-tab"] {
background: center center no-repeat url("/img/blank-tab.svg");
fill: currentColor;
}
/* Buttons */
.button {
color: black;
}
.button.primary {
background-color: #0996f8;
color: white;
@ -140,6 +221,18 @@ table {
background-color: rgba(0, 0, 0, 0.05);
}
/* Text links with actions */
.action-link:link {
color: var(--primary-action-color);
text-decoration: none;
}
.action-link:active,
.action-link:hover {
text-decoration: underline;
}
/* Panels keep everything togethert */
.panel {
display: flex;
@ -183,7 +276,7 @@ table {
.column-panel-content .button,
.panel-footer .button {
align-items: center;
block-size: 54px;
block-size: 100%;
display: flex;
flex: 1;
justify-content: center;
@ -223,7 +316,7 @@ table {
.onboarding-title {
color: #43484e;
font-size: 16px;
font-size: var(--font-size-heading);
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
@ -232,7 +325,7 @@ table {
}
.onboarding p {
color: #4a4a4a;
color: var(--text-normal-color);
font-size: 14px;
margin-block-end: 16px;
max-inline-size: 84%;
@ -261,9 +354,10 @@ table {
manage things like container crud */
.pop-button {
align-items: center;
block-size: 48px;
block-size: var(--icon-button-size);
cursor: pointer;
display: flex;
flex: 0 0 48px;
flex: 0 0 var(--icon-button-size);
justify-content: center;
}
@ -288,6 +382,10 @@ manage things like container crud */
.pop-button-image {
block-size: 20px;
flex: 0 0 20px;
margin-block-end: auto;
margin-block-start: auto;
margin-inline-end: auto;
margin-inline-start: auto;
}
.pop-button-image-small {
@ -299,20 +397,23 @@ manage things like container crud */
.panel-header {
align-items: center;
block-size: 48px;
border-block-end: 1px solid #ebebeb;
display: flex;
justify-content: space-between;
}
.panel-header .usercontext-icon {
inline-size: var(--icon-button-size);
}
.column-panel-content .panel-header {
flex: 0 0 48px;
inline-size: 100%;
}
.panel-header-text {
color: #4a4a4a;
color: var(--text-normal-color);
flex: 1;
font-size: 16px;
font-size: var(--font-size-heading);
font-weight: normal;
margin-block-end: 0;
margin-block-start: 0;
@ -324,6 +425,47 @@ manage things like container crud */
padding-inline-start: 16px;
}
#container-panel .panel-header {
background-color: #efefef;
block-size: 26px;
font-size: 14px;
}
#container-panel .panel-header-text {
color: #727272;
font-size: 14px;
padding-block-end: 0;
padding-block-start: 0;
text-transform: uppercase;
}
.container-panel-controls {
display: flex;
justify-content: flex-end;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-space-size);
margin-inline-end: var(--inline-item-space-size);
margin-inline-start: var(--inline-item-space-size);
}
#container-panel #sort-containers-link {
align-items: center;
block-size: var(--block-url-label-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
color: var(--title-text-color);
display: flex;
font-size: var(--small-text-size);
inline-size: var(--inline-button-size);
justify-content: center;
text-decoration: none;
}
#container-panel #sort-containers-link:hover,
#container-panel #sort-containers-link:focus {
background: #f2f2f2;
}
span ~ .panel-header-text {
padding-block-end: 0;
padding-block-start: 0;
@ -331,11 +473,92 @@ span ~ .panel-header-text {
padding-inline-start: 0;
}
#current-tab {
align-items: center;
color: var(--text-normal-color);
display: grid;
font-size: var(--small-text-size);
grid-column-gap: var(--inline-item-space-size);
grid-row-gap: var(--block-line-space-size);
grid-template-columns: var(--icon-size) var(--icon-size) 1fr;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-separation-size);
margin-inline-end: var(--inline-start-size);
margin-inline-start: var(--inline-start-size);
max-inline-size: 100%;
}
#current-tab img {
max-block-size: var(--icon-size);
}
#current-tab > h3 {
color: var(--text-heading-color);
font-weight: normal;
grid-column: span 3;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-page {
display: contents;
}
#current-tab .page-title {
font-size: var(--font-size-heading);
grid-column: 2 / 4;
}
#current-tab > label {
display: contents;
font-size: var(--small-text-size);
}
#current-tab > label > input {
-moz-appearance: none;
block-size: var(--icon-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
display: block;
grid-column-start: 2;
inline-size: var(--icon-size);
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-tab > label > input[disabled] {
background-color: #efefef;
}
#current-tab > label > input:checked {
background-image: url("chrome://global/skin/in-content/check.svg#check-native");
background-position: -1px -1px;
background-size: var(--icon-size);
}
#current-container {
color: var(--identity-tab-color);
flex: 1;
}
#current-tab > label > .usercontext-icon {
background-size: 16px;
block-size: 16px;
display: block;
flex: 0 0 20px;
inline-size: 20px;
margin-inline-end: 3px;
margin-inline-start: 3px;
}
/* Rows used when iterating over panels */
.container-panel-row {
align-items: center;
background-color: #fefefe !important;
block-size: 48px;
border-block-end: 1px solid #f1f1f1;
box-sizing: border-box;
display: flex;
@ -343,12 +566,10 @@ span ~ .panel-header-text {
}
.container-panel-row .container-name {
flex: 1;
max-inline-size: 160px;
overflow: hidden;
padding-inline-end: 4px;
padding-inline-start: 4px;
text-overflow: ellipsis;
white-space: nowrap;
}
.edit-containers-panel .userContext-wrapper {
@ -368,8 +589,9 @@ span ~ .panel-header-text {
}
.userContext-icon-wrapper {
block-size: 48px;
flex: 0 0 48px;
block-size: var(--icon-button-size);
flex: 0 0 var(--icon-button-size);
margin-inline-start: var(--inline-icon-space-size);
}
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */
@ -378,24 +600,29 @@ span ~ .panel-header-text {
background-position: center center;
background-repeat: no-repeat;
background-size: 20px 20px;
block-size: 48px;
block-size: 100%;
fill: var(--identity-icon-color);
filter: url('/img/filters.svg#fill');
flex: 0 0 48px;
}
.container-panel-row:hover .clickable .usercontext-icon,
.container-panel-row:focus .clickable .usercontext-icon {
.container-panel-row:focus .clickable .usercontext-icon,
.container-panel-row .clickable:focus .usercontext-icon {
background-image: url('/img/container-newtab.svg');
fill: 'gray';
fill: #979797;
filter: url('/img/filters.svg#fill');
}
.container-panel-row .clickable:hover .usercontext-icon,
.container-panel-row .clickable:focus .usercontext-icon {
fill: #0094fb;
}
/* Panel Footer */
.panel-footer {
align-items: center;
background: #efefef;
block-size: 54px;
block-size: var(--icon-button-size);
border-block-end: 1px solid #d8d8d8;
color: #000;
display: flex;
@ -404,14 +631,9 @@ span ~ .panel-header-text {
justify-content: space-between;
}
.panel-footer .pop-button {
block-size: 54px;
flex: 0 0 54px;
}
.edit-containers-text {
align-items: center;
block-size: 54px;
block-size: 100%;
border-inline-end: solid 1px #d8d8d8;
display: flex;
flex: 1;
@ -420,7 +642,7 @@ span ~ .panel-header-text {
.edit-containers-text a {
align-items: center;
block-size: 54px;
block-size: 100%;
color: #0a0a0a;
display: flex;
flex: 1;
@ -428,11 +650,8 @@ span ~ .panel-header-text {
}
/* Container info list */
#container-info-name {
margin-inline-end: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.container-info-tab-title {
flex: 1;
}
#container-info-hideorshow {
@ -477,19 +696,19 @@ span ~ .panel-header-text {
.container-info-tab-row td {
max-inline-size: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.container-info-list {
border-block-start: 1px solid #ebebeb;
display: flex;
flex-direction: column;
margin-block-start: 4px;
padding-block-start: 4px;
}
.container-info-list tbody {
display: contents;
}
.clickable {
cursor: pointer;
}
@ -501,7 +720,7 @@ span ~ .panel-header-text {
.edit-containers-exit-text {
align-items: center;
background: #248aeb;
background: var(--primary-action-color);
block-size: 100%;
color: #fff;
display: flex;
@ -528,7 +747,7 @@ span ~ .panel-header-text {
.delete-container-confirm-title {
color: #000;
font-size: 16px;
font-size: var(--font-size-heading);
}
/* Form info */
@ -540,36 +759,95 @@ span ~ .panel-header-text {
padding-inline-start: 16px;
}
.column-panel-content form span {
align-items: center;
block-size: 44px;
display: flex;
flex: 0 0 25%;
justify-content: center;
#edit-sites-assigned {
flex: 1;
}
.edit-container-panel label {
#edit-sites-assigned h3 {
font-size: 14px;
font-weight: normal;
padding-block-end: 6px;
padding-block-start: 6px;
padding-inline-end: 16px;
padding-inline-start: 16px;
}
.assigned-sites-list > div {
display: flex;
padding-block-end: 6px;
padding-block-start: 6px;
}
.assigned-sites-list > div > .icon {
margin-inline-end: 10px;
}
.assigned-sites-list > div > .delete-assignment {
display: none;
}
.assigned-sites-list > div:hover > .delete-assignment {
display: block;
}
.assigned-sites-list > div > .hostname {
flex: 1;
}
.radio-choice > .radio-container {
align-items: center;
block-size: 29px;
display: flex;
flex: 0 0 calc(100% / 8);
}
.radio-choice > .radio-container > label {
background: none;
block-size: 23px;
border: 0;
filter: none;
inline-size: 23px;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
.radio-choice > .radio-container > label::before {
background-color: unset;
background-image: var(--identity-icon);
background-size: 26px 26px;
block-size: 34px;
background-position: center;
background-repeat: no-repeat;
background-size: 16px;
block-size: 23px;
border: none;
content: "";
display: block;
fill: var(--identity-icon-color);
filter: url('/img/filters.svg#fill');
flex: 0 0 34px;
inline-size: 23px;
position: relative;
}
.edit-container-panel label::before {
opacity: 0 !important;
}
.edit-container-panel [type="radio"] {
.radio-choice > .radio-container > [type="radio"] {
-moz-appearance: none;
display: inline;
opacity: 0;
}
.edit-container-panel [type="radio"]:checked + label {
outline: 2px solid grey;
-moz-outline-radius: 50px;
.radio-choice > .radio-container > [type="radio"]:checked + label {
background: #d3d3d3;
border-radius: 100%;
}
/* When focusing the element add a thin blue highlight to match input fields. This gives a distinction to other selected radio items */
.radio-choice > .radio-container > [type="radio"]:focus + label {
outline: 1px solid #1f9ffc;
-moz-outline-radius: 100%;
}
.edit-container-panel fieldset {
@ -588,6 +866,10 @@ span ~ .panel-header-text {
padding-inline-start: 0;
}
.edit-container-panel fieldset:last-of-type {
margin-block-end: 0;
}
.edit-container-panel input[type="text"] {
block-size: 36px;
border-radius: 3px;
@ -602,5 +884,5 @@ span ~ .panel-header-text {
.edit-container-panel legend {
flex: 1 0;
font-size: 14px !important;
padding-block-end: 5px;
padding-block-end: 6px;
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<path d="M17,12v2a1,1,0,0,1-1,1H2a1,1,0,0,1-1-1V12a1,1,0,0,1,1-1H1.142c2.3,0,2.536-1.773,2.874-4,0.351-2.316.083-4,3.13-4h3.707C13.917,3,13.647,4.684,14,7c0.34,2.228.582,4,2.89,4H16A1,1,0,0,1,17,12Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,10 +1,45 @@
const redirectUrl = new URL(window.location).searchParams.get("url");
document.getElementById("redirect-url").textContent = redirectUrl;
const redirectSite = new URL(redirectUrl).hostname;
document.getElementById("redirect-site").textContent = redirectSite;
async function load() {
const searchParams = new URL(window.location).searchParams;
const redirectUrl = decodeURIComponent(searchParams.get("url"));
const cookieStoreId = searchParams.get("cookieStoreId");
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
const redirectUrlElement = document.getElementById("redirect-url");
redirectUrlElement.textContent = redirectUrl;
appendFavicon(redirectUrl, redirectUrlElement);
document.getElementById("redirect-form").addEventListener("submit", (e) => {
const container = await browser.contextualIdentities.get(cookieStoreId);
[...document.querySelectorAll(".container-name")].forEach((containerNameElement) => {
containerNameElement.textContent = container.name;
});
// If default container, button will default to normal HTML content
if (currentCookieStoreId) {
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
document.getElementById("current-container-name").textContent = currentContainer.name;
}
document.getElementById("redirect-form").addEventListener("submit", (e) => {
e.preventDefault();
const buttonTarget = e.explicitOriginalTarget;
switch (buttonTarget.id) {
case "confirm":
confirmSubmit(redirectUrl, cookieStoreId);
break;
case "deny":
denySubmit(redirectUrl);
break;
}
});
}
function appendFavicon(pageUrl, redirectUrlElement) {
const origin = new URL(pageUrl).origin;
const favIconElement = Utils.createFavIconElement(`${origin}/favicon.ico`);
redirectUrlElement.prepend(favIconElement);
}
function confirmSubmit(redirectUrl, cookieStoreId) {
const neverAsk = document.getElementById("never-ask").checked;
// Sending neverAsk message to background to store for next time we see this process
if (neverAsk) {
@ -12,20 +47,45 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => {
method: "neverAsk",
neverAsk: true,
pageUrl: redirectUrl
}).then(() => {
redirect();
}).catch(() => {
// Can't really do much here user will have to click it again
});
}
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "click-to-reload-page-in-container",
});
redirect();
});
openInContainer(redirectUrl, cookieStoreId);
}
function redirect() {
const redirectUrl = document.getElementById("redirect-url").textContent;
function getCurrentTab() {
return browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT
});
}
async function denySubmit(redirectUrl) {
const tab = await getCurrentTab();
await browser.runtime.sendMessage({
method: "exemptContainerAssignment",
tabId: tab[0].id,
pageUrl: redirectUrl
});
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "click-to-reload-page-in-same-container",
});
document.location.replace(redirectUrl);
}
load();
async function openInContainer(redirectUrl, cookieStoreId) {
const tab = await getCurrentTab();
await browser.tabs.create({
cookieStoreId,
url: redirectUrl
});
if (tab.length > 0) {
browser.tabs.remove(tab[0].id);
}
}

View file

@ -0,0 +1,42 @@
async function delayAnimation(delay = 350) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
async function doAnimation(element, property, value) {
return new Promise((resolve) => {
const handler = () => {
resolve();
element.removeEventListener("transitionend", handler);
};
element.addEventListener("transitionend", handler);
window.requestAnimationFrame(() => {
element.style[property] = value;
});
});
}
async function addMessage(message) {
const divElement = document.createElement("div");
divElement.classList.add("container-notification");
// For the eager eyed, this is an experiment. It is however likely that a website will know it is "contained" anyway
divElement.innerText = message.text;
const imageElement = document.createElement("img");
imageElement.src = browser.extension.getURL("/img/container-site-d-24.png");
divElement.prepend(imageElement);
document.body.appendChild(divElement);
await delayAnimation(100);
await doAnimation(divElement, "transform", "translateY(0)");
await delayAnimation(3000);
await doAnimation(divElement, "transform", "translateY(-100%)");
divElement.remove();
}
browser.runtime.onMessage.addListener((message) => {
addMessage(message);
});

View file

@ -7,12 +7,16 @@ const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg";
const DEFAULT_COLOR = "blue";
const DEFAULT_ICON = "circle";
const NEW_CONTAINER_ID = "new";
const ONBOARDING_STORAGE_KEY = "onboarding-stage";
// List of panels
const P_ONBOARDING_1 = "onboarding1";
const P_ONBOARDING_2 = "onboarding2";
const P_ONBOARDING_3 = "onboarding3";
const P_ONBOARDING_4 = "onboarding4";
const P_ONBOARDING_5 = "onboarding5";
const P_CONTAINERS_LIST = "containersList";
const P_CONTAINERS_EDIT = "containersEdit";
const P_CONTAINER_INFO = "containerInfo";
@ -69,32 +73,70 @@ const Logic = {
_currentPanel: null,
_previousPanel: null,
_panels: {},
_onboardingVariation: null,
init() {
async init() {
// Remove browserAction "upgraded" badge when opening panel
this.clearBrowserActionBadge();
// Retrieve the list of identities.
this.refreshIdentities()
const identitiesPromise = this.refreshIdentities();
// Get the onboarding variation
const variationPromise = this.getShieldStudyVariation();
try {
await Promise.all([identitiesPromise, variationPromise]);
} catch(e) {
throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message);
}
// Routing to the correct panel.
.then(() => {
// If localStorage is disabled, we don't show the onboarding.
if (!localStorage || localStorage.getItem("onboarded4")) {
this.showPanel(P_CONTAINERS_LIST);
const data = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
let onboarded = data[ONBOARDING_STORAGE_KEY];
if (!onboarded) {
// Legacy local storage used before panel 5
if (localStorage.getItem("onboarded4")) {
onboarded = 4;
} else if (localStorage.getItem("onboarded3")) {
this.showPanel(P_ONBOARDING_4);
onboarded = 3;
} else if (localStorage.getItem("onboarded2")) {
this.showPanel(P_ONBOARDING_3);
onboarded = 2;
} else if (localStorage.getItem("onboarded1")) {
this.showPanel(P_ONBOARDING_2);
onboarded = 1;
} else {
this.showPanel(P_ONBOARDING_1);
onboarded = 0;
}
this.setOnboardingStage(onboarded);
}
})
.catch(() => {
throw new Error("Failed to retrieve the identities. We cannot continue.");
switch (onboarded) {
case 5:
this.showPanel(P_CONTAINERS_LIST);
break;
case 4:
this.showPanel(P_ONBOARDING_5);
break;
case 3:
this.showPanel(P_ONBOARDING_4);
break;
case 2:
this.showPanel(P_ONBOARDING_3);
break;
case 1:
this.showPanel(P_ONBOARDING_2);
break;
case 0:
default:
this.showPanel(P_ONBOARDING_1);
break;
}
},
setOnboardingStage(stage) {
return browser.storage.local.set({
[ONBOARDING_STORAGE_KEY]: stage
});
},
@ -107,8 +149,20 @@ const Logic = {
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
},
async identity(cookieStoreId) {
const identity = await browser.contextualIdentities.get(cookieStoreId);
return identity || {
name: "Default",
cookieStoreId,
icon: "default-tab",
color: "default-tab"
};
},
addEnterHandler(element, handler) {
element.addEventListener("click", handler);
element.addEventListener("click", (e) => {
handler(e);
});
element.addEventListener("keydown", (e) => {
if (e.keyCode === 13) {
handler(e);
@ -121,6 +175,14 @@ const Logic = {
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
},
async currentTab() {
const activeTabs = await browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT});
if (activeTabs.length > 0) {
return activeTabs[0];
}
return false;
},
refreshIdentities() {
return Promise.all([
browser.contextualIdentities.query({}),
@ -139,7 +201,16 @@ const Logic = {
}).catch((e) => {throw e;});
},
showPanel(panel, currentIdentity = null) {
getPanelSelector(panel) {
if (this._onboardingVariation === "securityOnboarding" &&
panel.hasOwnProperty("securityPanelSelector")) {
return panel.securityPanelSelector;
} else {
return panel.panelSelector;
}
},
async showPanel(panel, currentIdentity = null) {
// Invalid panel... ?!?
if (!(panel in this._panels)) {
throw new Error("Something really bad happened. Unknown panel: " + panel);
@ -151,15 +222,18 @@ const Logic = {
this._currentIdentity = currentIdentity;
// Initialize the panel before showing it.
this._panels[panel].prepare().then(() => {
for (let panelElement of document.querySelectorAll(".panel")) { // eslint-disable-line prefer-const
await this._panels[panel].prepare();
Object.keys(this._panels).forEach((panelKey) => {
const panelItem = this._panels[panelKey];
const panelElement = document.querySelector(this.getPanelSelector(panelItem));
if (!panelElement.classList.contains("hide")) {
panelElement.classList.add("hide");
if ("unregister" in panelItem) {
panelItem.unregister();
}
}
document.querySelector(this._panels[panel].panelSelector).classList.remove("hide");
})
.catch(() => {
throw new Error("Failed to show panel " + panel);
});
document.querySelector(this.getPanelSelector(this._panels[panel])).classList.remove("hide");
},
showPreviousPanel() {
@ -186,6 +260,11 @@ const Logic = {
return this._currentIdentity;
},
currentUserContextId() {
const identity = Logic.currentIdentity();
return Logic.userContextId(identity.cookieStoreId);
},
sendTelemetryPayload(message = {}) {
if (!message.event) {
throw new Error("Missing event name for telemetry");
@ -205,6 +284,38 @@ const Logic = {
});
},
getAssignment(tab) {
return browser.runtime.sendMessage({
method: "getAssignment",
tabId: tab.id
});
},
getAssignmentObjectByContainer(userContextId) {
return browser.runtime.sendMessage({
method: "getAssignmentObjectByContainer",
message: {userContextId}
});
},
setOrRemoveAssignment(tabId, url, userContextId, value) {
return browser.runtime.sendMessage({
method: "setOrRemoveAssignment",
tabId,
url,
userContextId,
value
});
},
getShieldStudyVariation() {
return browser.runtime.sendMessage({
method: "getShieldStudyVariation"
}).then(variation => {
this._onboardingVariation = variation;
});
},
generateIdentityName() {
const defaultName = "Container #";
const ids = [];
@ -233,14 +344,17 @@ const Logic = {
Logic.registerPanel(P_ONBOARDING_1, {
panelSelector: ".onboarding-panel-1",
securityPanelSelector: ".security-onboarding-panel-1",
// This method is called when the object is registered.
initialize() {
// Let's move to the next panel.
Logic.addEnterHandler(document.querySelector("#onboarding-start-button"), () => {
localStorage.setItem("onboarded1", true);
[...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => {
Logic.addEnterHandler(startElement, async function () {
await Logic.setOnboardingStage(1);
Logic.showPanel(P_ONBOARDING_2);
});
});
},
// This method is called when the panel is shown.
@ -254,14 +368,17 @@ Logic.registerPanel(P_ONBOARDING_1, {
Logic.registerPanel(P_ONBOARDING_2, {
panelSelector: ".onboarding-panel-2",
securityPanelSelector: ".security-onboarding-panel-2",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Logic.addEnterHandler(document.querySelector("#onboarding-next-button"), () => {
localStorage.setItem("onboarded2", true);
[...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => {
Logic.addEnterHandler(nextElement, async function () {
await Logic.setOnboardingStage(2);
Logic.showPanel(P_ONBOARDING_3);
});
});
},
// This method is called when the panel is shown.
@ -275,14 +392,17 @@ Logic.registerPanel(P_ONBOARDING_2, {
Logic.registerPanel(P_ONBOARDING_3, {
panelSelector: ".onboarding-panel-3",
securityPanelSelector: ".security-onboarding-panel-3",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Logic.addEnterHandler(document.querySelector("#onboarding-almost-done-button"), () => {
localStorage.setItem("onboarded3", true);
[...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => {
Logic.addEnterHandler(almostElement, async function () {
await Logic.setOnboardingStage(3);
Logic.showPanel(P_ONBOARDING_4);
});
});
},
// This method is called when the panel is shown.
@ -300,8 +420,29 @@ Logic.registerPanel(P_ONBOARDING_4, {
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
document.querySelector("#onboarding-done-button").addEventListener("click", () => {
localStorage.setItem("onboarded4", true);
Logic.addEnterHandler(document.querySelector("#onboarding-done-button"), async function () {
await Logic.setOnboardingStage(4);
Logic.showPanel(P_ONBOARDING_5);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_ONBOARDING_5: Fifth page for Onboarding: new tab long-press behavior
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_5, {
panelSelector: ".onboarding-panel-5",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Logic.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async function () {
await Logic.setOnboardingStage(5);
Logic.showPanel(P_CONTAINERS_LIST);
});
},
@ -346,13 +487,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
function next() {
const nextElement = element.nextElementSibling;
if (nextElement) {
nextElement.focus();
nextElement.querySelector("td[tabindex=0]").focus();
}
}
function previous() {
const previousElement = element.previousElementSibling;
if (previousElement) {
previousElement.focus();
previousElement.querySelector("td[tabindex=0]").focus();
}
}
switch (e.keyCode) {
@ -364,12 +505,72 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
break;
}
});
// When the popup is open sometimes the tab will still be updating it's state
this.tabUpdateHandler = (tabId, changeInfo) => {
const propertiesToUpdate = ["title", "favIconUrl"];
const hasChanged = Object.keys(changeInfo).find((changeInfoKey) => {
if (propertiesToUpdate.includes(changeInfoKey)) {
return true;
}
});
if (hasChanged) {
this.prepareCurrentTabHeader();
}
};
browser.tabs.onUpdated.addListener(this.tabUpdateHandler);
},
unregister() {
browser.tabs.onUpdated.removeListener(this.tabUpdateHandler);
},
setupAssignmentCheckbox(siteSettings, currentUserContextId) {
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
let checked = false;
if (siteSettings && Number(siteSettings.userContextId) === currentUserContextId) {
checked = true;
}
assignmentCheckboxElement.checked = checked;
let disabled = false;
if (siteSettings === false) {
disabled = true;
}
assignmentCheckboxElement.disabled = disabled;
},
async prepareCurrentTabHeader() {
const currentTab = await Logic.currentTab();
const currentTabElement = document.getElementById("current-tab");
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
const currentTabUserContextId = Logic.userContextId(currentTab.cookieStoreId);
assignmentCheckboxElement.addEventListener("change", () => {
Logic.setOrRemoveAssignment(currentTab.id, currentTab.url, currentTabUserContextId, !assignmentCheckboxElement.checked);
});
currentTabElement.hidden = !currentTab;
this.setupAssignmentCheckbox(false, currentTabUserContextId);
if (currentTab) {
const identity = await Logic.identity(currentTab.cookieStoreId);
const siteSettings = await Logic.getAssignment(currentTab);
this.setupAssignmentCheckbox(siteSettings, currentTabUserContextId);
const currentPage = document.getElementById("current-page");
currentPage.innerHTML = escaped`<span class="page-title truncate-text">${currentTab.title}</span>`;
const favIconElement = Utils.createFavIconElement(currentTab.favIconUrl || "");
currentPage.prepend(favIconElement);
const currentContainer = document.getElementById("current-container");
currentContainer.innerText = identity.name;
currentContainer.setAttribute("data-identity-color", identity.color);
}
},
// This method is called when the panel is shown.
prepare() {
async prepare() {
const fragment = document.createDocumentFragment();
this.prepareCurrentTabHeader();
Logic.identities().forEach(identity => {
const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs);
const tr = document.createElement("tr");
@ -378,10 +579,11 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
tr.classList.add("container-panel-row");
tr.setAttribute("tabindex", "0");
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
manage.classList.add("show-tabs", "pop-button");
manage.title = escaped`View ${identity.name} container`;
context.setAttribute("tabindex", "0");
context.title = escaped`Create ${identity.name} tab`;
context.innerHTML = escaped`
<div class="userContext-icon-wrapper open-newtab">
<div class="usercontext-icon"
@ -389,7 +591,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
data-identity-color="${identity.color}">
</div>
</div>
<div class="container-name"></div>`;
<div class="container-name truncate-text"></div>`;
context.querySelector(".container-name").textContent = identity.name;
manage.innerHTML = "<img src='/img/container-arrow.svg' class='show-tabs pop-button-image-small' />";
@ -422,15 +624,21 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
});
});
const list = document.querySelector(".identities-list");
const list = document.querySelector(".identities-list tbody");
list.innerHTML = "";
list.appendChild(fragment);
/* Not sure why extensions require a focus for the doorhanger,
however it allows us to have a tabindex before the first selected item
*/
document.addEventListener("focus", () => {
list.querySelector("tr").focus();
const focusHandler = () => {
list.querySelector("tr .clickable").focus();
document.removeEventListener("focus", focusHandler);
};
document.addEventListener("focus", focusHandler);
/* If the user mousedown's first then remove the focus handler */
document.addEventListener("mousedown", () => {
document.removeEventListener("focus", focusHandler);
});
return Promise.resolve();
@ -453,7 +661,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
const identity = Logic.currentIdentity();
browser.runtime.sendMessage({
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
userContextId: Logic.userContextId(identity.cookieStoreId)
userContextId: Logic.currentUserContextId()
}).then(() => {
window.close();
}).catch(() => {
@ -525,7 +733,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
// Let's retrieve the list of tabs.
return browser.runtime.sendMessage({
method: "getTabs",
userContextId: Logic.userContextId(identity.cookieStoreId),
userContextId: Logic.currentUserContextId(),
}).then(this.buildInfoTable);
},
@ -537,8 +745,9 @@ Logic.registerPanel(P_CONTAINER_INFO, {
fragment.appendChild(tr);
tr.classList.add("container-info-tab-row");
tr.innerHTML = escaped`
<td><img class="icon" src="${tab.favicon}" /></td>
<td class="container-info-tab-title">${tab.title}</td>`;
<td></td>
<td class="container-info-tab-title truncate-text" title="${tab.url}" >${tab.title}</td>`;
tr.querySelector("td").appendChild(Utils.createFavIconElement(tab.favicon));
// On click, we activate this tab. But only if this tab is active.
if (tab.active) {
@ -588,22 +797,22 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
data-identity-color="${identity.color}">
</div>
</div>
<div class="container-name"></div>
<div class="container-name truncate-text"></div>
</td>
<td class="edit-container pop-button edit-container-icon">
<img
src="/img/container-edit.svg"
class="pop-button-image" />
</td>
<td class="remove-container pop-button delete-container-icon" >
<td class="remove-container pop-button delete-container-icon">
<img
class="pop-button-image"
src="/img/container-delete.svg"
/>
</td>`;
tr.querySelector(".container-name").textContent = identity.name;
tr.querySelector(".edit-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
tr.querySelector(".edit-container").setAttribute("title", `Edit ${identity.name} container`);
tr.querySelector(".remove-container").setAttribute("title", `Delete ${identity.name} container`);
Logic.addEnterHandler(tr, e => {
@ -635,7 +844,12 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
this.initializeRadioButtons();
Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => {
const formValues = new FormData(this._editForm);
if (formValues.get("container-id") !== NEW_CONTAINER_ID) {
this._submitForm();
} else {
Logic.showPreviousPanel();
}
});
Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => {
@ -644,18 +858,25 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
this._editForm = document.getElementById("edit-container-panel-form");
const editLink = document.querySelector("#edit-container-ok-link");
Logic.addEnterHandler(editLink, this._submitForm.bind(this));
editLink.addEventListener("submit", this._submitForm.bind(this));
this._editForm.addEventListener("submit", this._submitForm.bind(this));
Logic.addEnterHandler(editLink, () => {
this._submitForm();
});
editLink.addEventListener("submit", () => {
this._submitForm();
});
this._editForm.addEventListener("submit", () => {
this._submitForm();
});
},
_submitForm() {
const identity = Logic.currentIdentity();
const formValues = new FormData(this._editForm);
return browser.runtime.sendMessage({
method: "createOrUpdateContainer",
message: {
userContextId: Logic.userContextId(identity.cookieStoreId) || false,
userContextId: formValues.get("container-id") || NEW_CONTAINER_ID,
params: {
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
icon: formValues.get("container-icon") || DEFAULT_ICON,
@ -671,6 +892,51 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
});
},
showAssignedContainers(assignments) {
const assignmentPanel = document.getElementById("edit-sites-assigned");
const assignmentKeys = Object.keys(assignments);
assignmentPanel.hidden = !(assignmentKeys.length > 0);
if (assignments) {
const tableElement = assignmentPanel.querySelector(".assigned-sites-list");
/* Remove previous assignment list,
after removing one we rerender the list */
while (tableElement.firstChild) {
tableElement.firstChild.remove();
}
assignmentKeys.forEach((siteKey) => {
const site = assignments[siteKey];
const trElement = document.createElement("div");
/* As we don't have the full or correct path the best we can assume is the path is HTTPS and then replace with a broken icon later if it doesn't load.
This is pending a better solution for favicons from web extensions */
const assumedUrl = `https://${site.hostname}`;
trElement.innerHTML = escaped`
<img class="icon" src="${assumedUrl}/favicon.ico">
<div title="${site.hostname}" class="truncate-text hostname">
${site.hostname}
</div>
<img
class="pop-button-image delete-assignment"
src="/img/container-delete.svg"
/>`;
const deleteButton = trElement.querySelector(".delete-assignment");
Logic.addEnterHandler(deleteButton, () => {
const userContextId = Logic.currentUserContextId();
// Lets show the message to the current tab
// TODO remove then when firefox supports arrow fn async
Logic.currentTab().then((currentTab) => {
Logic.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true);
delete assignments[siteKey];
this.showAssignedContainers(assignments);
}).catch((e) => {
throw e;
});
});
trElement.classList.add("container-info-tab-row", "clickable");
tableElement.appendChild(trElement);
});
}
},
initializeRadioButtons() {
const colorRadioTemplate = (containerColor) => {
return escaped`<input type="radio" value="${containerColor}" name="container-color" id="edit-container-panel-choose-color-${containerColor}" />
@ -679,7 +945,8 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
const colors = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple" ];
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
colors.forEach((containerColor) => {
const templateInstance = document.createElement("span");
const templateInstance = document.createElement("div");
templateInstance.classList.add("radio-container");
// eslint-disable-next-line no-unsanitized/property
templateInstance.innerHTML = colorRadioTemplate(containerColor);
colorRadioFieldset.appendChild(templateInstance);
@ -692,7 +959,8 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
const icons = ["fingerprint", "briefcase", "dollar", "cart", "vacation", "gift", "food", "fruit", "pet", "tree", "chill", "circle"];
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
icons.forEach((containerIcon) => {
const templateInstance = document.createElement("span");
const templateInstance = document.createElement("div");
templateInstance.classList.add("radio-container");
// eslint-disable-next-line no-unsanitized/property
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
iconRadioFieldset.appendChild(templateInstance);
@ -700,9 +968,16 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
},
// This method is called when the panel is shown.
prepare() {
async prepare() {
const identity = Logic.currentIdentity();
const userContextId = Logic.currentUserContextId();
const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
this.showAssignedContainers(assignments);
document.querySelector("#edit-container-panel .panel-footer").hidden = !!userContextId;
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
colorInput.checked = colorInput.value === identity.color;
});

23
webextension/js/utils.js Normal file
View file

@ -0,0 +1,23 @@
const DEFAULT_FAVICON = "moz-icon://goat?size=16";
// TODO use export here instead of globals
window.Utils = {
createFavIconElement(url) {
const imageElement = document.createElement("img");
imageElement.classList.add("icon", "offpage");
imageElement.src = url;
const loadListener = (e) => {
e.target.classList.remove("offpage");
e.target.removeEventListener("load", loadListener);
e.target.removeEventListener("error", errorListener);
};
const errorListener = (e) => {
e.target.src = DEFAULT_FAVICON;
};
imageElement.addEventListener("error", errorListener);
imageElement.addEventListener("load", loadListener);
return imageElement;
}
};

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Containers Experiment",
"version": "2.3.0",
"version": "2.4.0",
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored Container Tabs. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
"icons": {
@ -26,7 +26,6 @@
"contextualIdentities",
"history",
"idle",
"notifications",
"storage",
"tabs",
"webRequestBlocking",
@ -36,7 +35,8 @@
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Ctrl+Y"
"default": "Ctrl+Period",
"mac": "MacCtrl+Period"
},
"description": "Open containers panel"
}
@ -54,5 +54,18 @@
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/content-script.js"],
"css": ["css/content.css"],
"run_at": "document_start"
}
],
"web_accessible_resources": [
"/img/container-site-d-24.png"
]
}

View file

@ -3,56 +3,96 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Containers browserAction Popup</title>
<link rel="stylesheet" href="/css/popup.css">
</head>
<body>
<div class="hide panel onboarding onboarding-panel-1" id="onboarding-panel-1">
<div class="hide panel onboarding onboarding-panel-1">
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
<h3 class="onboarding-title">A better way to manage all the things you do online</h3>
<p>
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
</p>
<a href="#" id="onboarding-start-button" class="onboarding-button">Get Started</a>
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
</div>
<div class="hide panel onboarding security-onboarding-panel-1">
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
<h3 class="onboarding-title">A simple and secure way to manage your online life</h3>
<p>
Use containers to organize tasks, manage accounts, and store sensitive data.
</p>
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
</div>
<div class="panel onboarding onboarding-panel-2 hide" id="onboarding-panel-2">
<div class="panel onboarding onboarding-panel-2 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
<h3 class="onboarding-title">Put containers to work for you.</h3>
<p>Features like color-coding and separate container tabs help you find things easily, focus your attention, and minimize distractions.</p>
<a href="#" id="onboarding-next-button" class="onboarding-button">Next</a>
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-3 hide" id="onboarding-panel-3">
<div class="panel onboarding security-onboarding-panel-2 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
<h3 class="onboarding-title">Put containers to work for you.</h3>
<p>Color-coding helps you categorize your online life, find things easily, and minimize distractions.</p>
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-3 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
<p>Start with the containers we've created, or create your own.</p>
<a href="#" id="onboarding-almost-done-button" class="onboarding-button">Next</a>
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
</div>
<div class="panel onboarding security-onboarding-panel-3 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3-security.png" />
<h3 class="onboarding-title">Set boundaries for your browsing.</h3>
<p>Cookies are stored within a container, so you can segment sensitive data and browsing history to stay organized and to limit the impact of online trackers.</p>
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
<img class="onboarding-img" alt="How to assign sites to containers" src="/img/onboarding-4.png" />
<h3 class="onboarding-title">Always open sites in the containers you want.</h3>
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
<a href="#" id="onboarding-done-button" class="onboarding-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-5 hide" id="onboarding-panel-5">
<img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" />
<h3 class="onboarding-title">Container tabs when you need them.</h3>
<p>Long-press the New Tab button to create a new container tab.</p>
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
</div>
<div class="panel container-panel hide" id="container-panel">
<div class="panel-header">
<h3 class="panel-header-text">Containers</h3>
<a href="#" class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a>
<div id="current-tab">
<h3>Current Tab</h3>
<div id="current-page"></div>
<label for="container-page-assigned">
<input type="checkbox" id="container-page-assigned" />
<span class="truncate-text">
Always open in
<span id="current-container"></span>
</span>
</label>
</div>
<div class="container-panel-controls">
<a href="#" class="action-link" id="sort-containers-link" title="Sort tabs into container order">Sort Tabs</a>
</div>
<div class="scrollable panel-content" tabindex="-1">
<table>
<tbody class="identities-list"></tbody>
<table class="identities-list">
<tbody></tbody>
</table>
</div>
<div class="panel-footer edit-identities">
<div class="edit-containers-text panel-footer-secondary">
<a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
</div>
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link">
<img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" />
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link" title="Create new container">
<img class="pop-button-image-small icon" alt="Create new container icon" src="/img/container-add.svg" />
</a>
</div>
</div>
@ -66,7 +106,7 @@
<div class="column-panel-content">
<div class="panel-header container-info-panel-header">
<span class="usercontext-icon" id="container-info-icon"></span>
<h3 id="container-info-name" class="panel-header-text container-name"></h3>
<h3 id="container-info-name" class="panel-header-text container-name truncate-text"></h3>
</div>
<div class="select-row clickable container-info-panel-hide container-info-has-tabs" id="container-info-hideorshow">
<img id="container-info-hideorshow-icon" alt="Hide Container icon" src="/img/container-hide.svg" class="icon container-info-panel-hideorshow-icon"/>
@ -104,17 +144,23 @@
</div>
<div class="column-panel-content">
<form id="edit-container-panel-form">
<input type="hidden" name="container-id" id="edit-container-panel-usercontext-input" />
<fieldset>
<legend>Name</legend>
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
</fieldset>
<fieldset id="edit-container-panel-choose-color">
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
<legend>Choose a color</legend>
</fieldset>
<fieldset id="edit-container-panel-choose-icon">
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
<legend>Choose an icon</legend>
</fieldset>
</form>
<div id="edit-sites-assigned" class="scrollable" hidden>
<h3>Sites assigned to this container</h3>
<div class="assigned-sites-list">
</div>
</div>
<div class="panel-footer">
<a href="#" class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
@ -138,7 +184,7 @@
</div>
</div>
<script src="js/utils.js"></script>
<script src="js/popup.js"></script>
</body>
</html>