Merge pull request #420 from mozilla/master

Release 2.1.2 to production
This commit is contained in:
luke crouch 2017-04-04 13:39:14 -05:00 committed by GitHub
commit 13fba94dc5
10 changed files with 549 additions and 60 deletions

View file

@ -66,7 +66,8 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
}
[data-identity-icon="cart"],
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"] {
[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");
}
@ -190,18 +191,6 @@ special cases are addressed below */
width: 400%;
}
/* this fixes containers tab center */
#verticaltabs-box .tabbrowser-tab[usercontextid] .tab-content::after {
background-color: var(--identity-tab-color);
content: '';
height: 100% !important;
left: 0;
position: absolute;
top: 0;
width: 3px !important;
z-index: 999;
}
.tabs-newtab-button .toolbarbutton-icon[type="menu"] {
margin-inline-end: 0;
}

View file

@ -49,7 +49,7 @@ whether they are actively interacting with it.
* Average number of container tabs when new window was clicked
* How many containers do users have hidden at the same time? (when should we measure this? each time a container is hidden?)
* Do users pin container tabs? (do we have existing Telemetry for pinning?)
* Do users change URLs in a container tab? (sounds like it could be a flood unless we only record the first URL change?)
* Do users visit more pages in container tabs than non-container tabs?
### Follow-up Questions
@ -178,6 +178,56 @@ of a `testpilottest` telemetry ping for each scenario.
}
```
* The user closes a tab
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "page-requests-completed-per-tab",
"pageRequestCount": <pageRequestCount>
}
```
* The user chooses "Always Open in this Container" context menu option. (Note: We send two separate event names: one for assigning a site to a container, one for removing a site from a container.)
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "[added|removed]-container-assignment"
}
```
* Firefox prompts the user to reload a site into a container after the user picked "Always Open in this Container".
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "prompt-reload-page-in-container"
}
```
* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this Container".
```js
{
"uuid": <uuid>,
"event": "click-to-reload-page-in-container"
}
```
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
```js
{
"uuid": <uuid>,
"userContextId": <userContextId>,
"event": "auto-reload-page-in-container"
}
```
### A Redshift schema for the payload:
```lua
@ -188,6 +238,7 @@ local schema = {
{"clickedContainerTabCount", "INTEGER", 255, nil, "Fields[payload.clickedContainerTabCount]"},
{"eventSource", "VARCHAR", 255, nil, "Fields[payload.eventSource]"},
{"event", "VARCHAR", 255, nil, "Fields[payload.event]"},
{"pageRequestCount", "INTEGER", 255, nil, "Fields[payload.pageRequestCount]"}
{"hiddenContainersCount", "INTEGER", 255, nil, "Fields[payload.hiddenContainersCount]"},
{"shownContainersCount", "INTEGER", 255, nil, "Fields[payload.shownContainersCount]"},
{"totalContainersCount", "INTEGER", 255, nil, "Fields[payload.totalContainersCount]"},

View file

@ -117,7 +117,7 @@ const ContainerService = {
_identitiesState: {},
_windowMap: new Map(),
_containerWasEnabled: false,
_onThemeChangedCallback: null,
_onBackgroundConnectCallback: null,
init(installation, reason) {
// If we are just been installed, we must store some information for the
@ -260,7 +260,7 @@ const ContainerService = {
}
});
this.registerThemeConnection(api);
this.registerBackgroundConnection(api);
}).catch(() => {
throw new Error("WebExtension startup failed. Unable to continue.");
});
@ -307,28 +307,28 @@ const ContainerService = {
Services.obs.addObserver(this, "lightweight-theme-changed", false);
},
registerThemeConnection(api) {
// This is only used for theme notifications
registerBackgroundConnection(api) {
// This is only used for theme and container deletion notifications
api.browser.runtime.onConnect.addListener((port) => {
this.onThemeChanged((theme, topic) => {
this._onBackgroundConnectCallback = (message, topic) => {
port.postMessage({
type: topic,
theme
});
message
});
};
});
},
triggerThemeChanged(theme, topic) {
if (this._onThemeChangedCallback) {
this._onThemeChangedCallback(theme, topic);
triggerBackgroundCallback(message, topic) {
if (this._onBackgroundConnectCallback) {
this._onBackgroundConnectCallback(message, topic);
}
},
observe(subject, topic) {
if (topic === "lightweight-theme-changed") {
this.getTheme().then((theme) => {
this.triggerThemeChanged(theme, topic);
this.triggerBackgroundCallback(theme, topic);
}).catch(() => {
throw new Error("Unable to get theme");
});
@ -346,10 +346,6 @@ const ContainerService = {
});
},
onThemeChanged(callback) {
this._onThemeChangedCallback = callback;
},
// utility methods
_containerTabCount(userContextId) {
@ -960,12 +956,13 @@ const ContainerService = {
},
removeIdentity(args) {
const eventName = "delete-container";
if (!("userContextId" in args)) {
return Promise.reject("removeIdentity must be called with userContextId argument.");
}
this.sendTelemetryPayload({
"event": "delete-container",
"event": eventName,
"userContextId": args.userContextId
});
@ -976,6 +973,7 @@ const ContainerService = {
return this._closeTabs(tabsToClose).then(() => {
const removed = ContextualIdentityProxy.remove(args.userContextId);
this.triggerBackgroundCallback({userContextId: args.userContextId}, eventName);
this._forgetIdentity(args.userContextId);
return this._refreshNeeded().then(() => removed );
});
@ -1370,7 +1368,8 @@ ContainerWindow.prototype = {
},
_configureContextMenu() {
return this._configureMenu("context-openlinkinusercontext-menu",
return Promise.all([
this._configureMenu("context-openlinkinusercontext-menu",
() => {
// This userContextId is what we want to exclude.
const tab = modelFor(this._window).tabs.activeTab;
@ -1388,7 +1387,29 @@ ContainerWindow.prototype = {
window: this._window,
});
}
);
),
this._configureContextMenuOpenLink(),
]);
},
_configureContextMenuOpenLink() {
return new Promise(resolve => {
const self = this;
this._window.gSetUserContextIdAndClick = function(event) {
const tab = modelFor(self._window).tabs.activeTab;
const userContextId = ContainerService._getUserContextIdFromTab(tab);
event.target.setAttribute("data-usercontextid", userContextId);
self._window.gContextMenu.openLinkInTab(event);
};
let item = this._window.document.getElementById("context-openlinkincontainertab");
item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)");
item = this._window.document.getElementById("context-openlinkintab");
item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)");
resolve();
});
},
// Generic menu configuration.

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.0.0",
"version": "2.1.2",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": {
"url": "https://github.com/mozilla/testpilot-containers/issues"

View file

@ -1,15 +1,304 @@
const assignManager = {
CLOSEABLE_WINDOWS: new Set([
"about:startpage",
"about:newtab",
"about:home",
"about:blank"
]),
MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container",
storageArea: {
area: browser.storage.local,
getSiteStoreKey(pageUrl) {
const url = new window.URL(pageUrl);
const storagePrefix = "siteContainerMap@@_";
return `${storagePrefix}${url.hostname}`;
},
get(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return new Promise((resolve, reject) => {
this.area.get([siteStoreKey]).then((storageResponse) => {
if (storageResponse && siteStoreKey in storageResponse) {
resolve(storageResponse[siteStoreKey]);
}
resolve(null);
}).catch((e) => {
reject(e);
});
});
},
set(pageUrl, data) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return this.area.set({
[siteStoreKey]: data
});
},
remove(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return this.area.remove([siteStoreKey]);
},
deleteContainer(userContextId) {
const removeKeys = [];
this.area.get().then((siteConfigs) => {
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);
}
});
this.area.remove(removeKeys);
}).catch((e) => {
throw e;
});
}
},
init() {
browser.runtime.onMessage.addListener((neverAskMessage) => {
const pageUrl = neverAskMessage.pageUrl;
if (neverAskMessage.neverAsk === true) {
// If we have existing data and for some reason it hasn't been deleted etc lets update it
this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) {
siteSettings.neverAsk = true;
this.storageArea.set(pageUrl, siteSettings);
}
}).catch((e) => {
throw e;
});
}
});
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")
});
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: `${actionName}-container-assignment`,
userContextId: userContextId,
});
this.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}
});
// Before a request is handled by the browser we decide if we should route through a different container
browser.webRequest.onBeforeRequest.addListener((options) => {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
}
return Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.get(options.url)
]).then(([tab, siteSettings]) => {
const userContextId = this.getUserContextIdFromCookieStore(tab);
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| tab.incognito) {
return {};
}
this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
this.calculateContextMenu(tab);
// If the user just opened the tab, we can auto close it
if (this.CLOSEABLE_WINDOWS.has(tab.url)) {
browser.tabs.remove(tab.id);
}
return {
cancel: true,
};
}).catch((e) => {
throw e;
});
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
},
deleteContainer(userContextId) {
this.storageArea.deleteContainer(userContextId);
},
getUserContextIdFromCookieStore(tab) {
if (!("cookieStoreId" in tab)) {
return false;
}
const cookieStore = tab.cookieStoreId;
const container = cookieStore.replace("firefox-container-", "");
if (container !== cookieStore) {
return container;
}
return false;
},
isTabPermittedAssign(tab) {
// Ensure we are not an important about url
// Ensure we are not in incognito mode
const url = new URL(tab.url);
if (url.protocol === "about:"
|| tab.incognito) {
return false;
}
return true;
},
calculateContextMenu(tab) {
// 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) => {
// ✓ 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) {
prefix = "✓";
menuId = this.MENU_REMOVE_ID;
}
browser.contextMenus.create({
id: menuId,
title: `${prefix} Always Open in This Container`,
checked: true,
contexts: ["all"],
});
}).catch((e) => {
throw e;
});
}
},
reloadPageInContainer(url, userContextId, index, neverAsk = false) {
const loadPage = browser.extension.getURL("confirm-page.html");
// 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: `firefox-container-${userContextId}`, index});
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "auto-reload-page-in-container",
userContextId: userContextId,
});
} else {
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "prompt-to-reload-page-in-container",
userContextId: userContextId,
});
const confirmUrl = `${loadPage}?url=${url}`;
browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => {
// We don't want to sync this URL ever nor clutter the users history
browser.history.deleteUrl({url: confirmUrl});
}).catch((e) => {
throw e;
});
}
}
};
const messageHandler = {
init() {
// Handles messages from index.js
const port = browser.runtime.connect();
port.onMessage.addListener(m => {
switch (m.type) {
case "lightweight-theme-changed":
themeManager.update(m.message);
break;
case "delete-container":
assignManager.deleteContainer(m.message.userContextId);
break;
default:
throw new Error(`Unhandled message type: ${m.message}`);
}
});
browser.tabs.onCreated.addListener((tab) => {
// This works at capturing the tabs as they are created
// However we need onFocusChanged and onActivated to capture the initial tab
if (tab.id === -1) {
return {};
}
tabPageCounter.initTabCounter(tab);
});
browser.tabs.onRemoved.addListener((tabId) => {
if (tabId === -1) {
return {};
}
tabPageCounter.sendTabCountAndDelete(tabId);
});
browser.tabs.onActivated.addListener((info) => {
browser.tabs.get(info.tabId).then((tab) => {
tabPageCounter.initTabCounter(tab);
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
});
browser.windows.onFocusChanged.addListener((windowId) => {
browser.tabs.query({active: true, windowId}).then((tabs) => {
if (tabs && tabs[0]) {
tabPageCounter.initTabCounter(tabs[0]);
assignManager.calculateContextMenu(tabs[0]);
}
}).catch((e) => {
throw e;
});
});
browser.webRequest.onCompleted.addListener((details) => {
if (details.frameId !== 0 || details.tabId === -1) {
return {};
}
browser.tabs.get(details.tabId).then((tab) => {
tabPageCounter.incrementTabCount(tab);
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}, {urls: ["<all_urls>"], types: ["main_frame"]});
}
};
const themeManager = {
existingTheme: null,
init() {
this.check();
const port = browser.runtime.connect();
port.onMessage.addListener(m => {
if (m.type === "lightweight-theme-changed") {
this.update(m.theme);
}
});
},
setPopupIcon(theme) {
let icons = {
@ -43,7 +332,38 @@ const themeManager = {
}
};
const tabPageCounter = {
counter: {},
initTabCounter(tab) {
if (tab.id in this.counter) {
return;
}
this.counter[tab.id] = {
"cookieStoreId": tab.cookieStoreId,
"pageRequests": 0
};
},
sendTabCountAndDelete(tabId) {
browser.runtime.sendMessage({
method: "sendTelemetryPayload",
event: "page-requests-completed-per-tab",
userContextId: this.counter[tabId].cookieStoreId,
pageRequestCount: this.counter[tabId].pageRequests
});
delete this.counter[tabId];
},
incrementTabCount(tab) {
this.counter[tab.id].pageRequests++;
}
};
assignManager.init();
themeManager.init();
// Lets do this last as theme manager did a check before connecting before
messageHandler.init();
browser.runtime.sendMessage({
method: "getPreference",

View file

@ -0,0 +1,33 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Containers confirm navigation</title>
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
<link rel="stylesheet" href="/css/confirm-page.css" />
</head>
<body>
<main>
<div class="title">
<h1 class="title-text">Should we open this in your container?</h1>
</div>
<form id="redirect-form">
<p>
Looks like you requested:
</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>
<br />
<br />
<input id="never-ask" type="checkbox" /><label for="never-ask">Remember my decision for this site</label>
<br />
<div class="button-container">
<button id="confirm" class="button primary">Take me there</button>
</div>
</form>
</main>
<script src="js/confirm-page.js"></script>
</body>
</html>

View file

@ -0,0 +1,37 @@
/* General Rules and Resets */
.title {
background-image: none;
}
main {
background: url(/img/onboarding-1.png) no-repeat;
background-position: -10px -15px;
background-size: 285px;
margin-inline-start: -285px;
padding-inline-start: 285px;
}
@media only screen and (max-width: 1300px) {
main {
background: none;
}
/* for a mid sized window we have enough for this but not our image */
.title {
background-image: url("chrome://global/skin/icons/info.svg");
}
}
html {
box-sizing: border-box;
font: message-box;
}
#redirect-url,
#redirect-origin {
font-weight: bold;
}
dfn {
font-style: normal;
}

View file

@ -383,7 +383,7 @@ span ~ .panel-header-text {
.panel-footer {
align-items: center;
background: #efefef;
block-size: 55px;
block-size: 54px;
border-block-end: 1px solid #d8d8d8;
color: #000;
display: flex;

View file

@ -0,0 +1,30 @@
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;
document.getElementById("redirect-form").addEventListener("submit", (e) => {
e.preventDefault();
const neverAsk = document.getElementById("never-ask").checked;
// Sending neverAsk message to background to store for next time we see this process
if (neverAsk) {
browser.runtime.sendMessage({
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();
});
function redirect() {
const redirectUrl = document.getElementById("redirect-url").textContent;
document.location.replace(redirectUrl);
}

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Containers Experiment",
"version": "2.0.0",
"version": "2.1.2",
"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": {
@ -19,8 +19,16 @@
"homepage_url": "https://testpilot.firefox.com/",
"permissions": [
"<all_urls>",
"activeTab",
"cookies",
"tabs"
"contextMenus",
"history",
"notifications",
"storage",
"tabs",
"webRequestBlocking",
"webRequest"
],
"browser_action": {