Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adadb98482 | |||
| 25e760cd64 | |||
| 0ff8e17005 | |||
| c433c6b39e | |||
| 1c09c29104 | |||
| c1e9cc3c56 | |||
| 27296d24c5 | |||
| 030e635417 | |||
| 07711aaecc | |||
| 16ed8992e2 | |||
| 88e6dc7a05 | |||
| 28e8d46743 | |||
| 77ba1b723f | |||
| b0cc6e7c2f | |||
| e84e482130 | |||
| b5ae20b874 | |||
| 3ec81e3d1f | |||
| fb5436c287 | |||
| 01a628822b | |||
| 66e2c8e297 | |||
| 80661d68f2 | |||
| ef8aa3be75 | |||
| 6bc056e019 | |||
| 75deab139b | |||
| ae79f0a303 | |||
| 9b83068234 | |||
| fec2be9429 | |||
| 9f1b06ddd3 | |||
| ad2198e8b5 | |||
| 1791fdf0ef |
@@ -3,6 +3,7 @@ docs/
|
||||
test/
|
||||
.npm/
|
||||
node_modules/
|
||||
bin/
|
||||
|
||||
.env
|
||||
.eslintrc.js
|
||||
@@ -14,6 +15,7 @@ node_modules/
|
||||
.stylelintrc
|
||||
.travis.yml
|
||||
*.xpi
|
||||
*.md
|
||||
.vimrc
|
||||
.DS_Store
|
||||
.gdb_history
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Firefox Multi-Account Containers
|
||||
# Multi-Account Containers
|
||||
|
||||
[](https://testpilot.firefox.com/experiments/containers)
|
||||
|
||||
[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:
|
||||
The Firefox Multi-Account Containers extension lets you carve out a separate box for each of your online lives – no more opening a different browser just to check your work email! [Learn More Here](https://blog.mozilla.org/firefox/introducing-firefox-multi-account-containers/)
|
||||
|
||||
* Will a general Firefox audience understand the Containers feature?
|
||||
* Is the UI as currently implemented in Nightly clear or discoverable?
|
||||
[Available on addons.mozilla.org](https://addons.mozilla.org/en-GB/firefox/addon/multi-account-containers/)
|
||||
|
||||
**Note:** Firefox 57 + 58 users should Install from our [latest GitHub Release](https://github.com/mozilla/testpilot-containers/releases/latest)
|
||||
|
||||
For more info, see:
|
||||
|
||||
|
||||
+3
-3
@@ -7,16 +7,16 @@
|
||||
<em:bootstrap>true</em:bootstrap>
|
||||
<em:multiprocessCompatible>true</em:multiprocessCompatible>
|
||||
<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
|
||||
<em:name>Firefox Multi-Account Containers</em:name>
|
||||
<em:name>Multi-Account Containers</em:name>
|
||||
<em:description>Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.</em:description>
|
||||
<em:targetApplication>
|
||||
<Description>
|
||||
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
|
||||
<em:minVersion>51.0a1</em:minVersion>
|
||||
<em:minVersion>53.0</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
</Description>
|
||||
</em:targetApplication>
|
||||
<em:version>4.0.1</em:version>
|
||||
<em:version>4.0.3</em:version>
|
||||
<em:unpack>false</em:unpack>
|
||||
</Description>
|
||||
</RDF>
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "testpilot-containers",
|
||||
"title": "Firefox Multi-Account Containers",
|
||||
"title": "Multi-Account Containers",
|
||||
"description": "Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.3",
|
||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Firefox Multi-Account Containers Confirm Navigation</title>
|
||||
<title>Multi-Account Containers Confirm Navigation</title>
|
||||
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
||||
<link rel="stylesheet" href="/css/confirm-page.css" />
|
||||
</head>
|
||||
|
||||
@@ -113,21 +113,29 @@ const assignManager = {
|
||||
return true;
|
||||
},
|
||||
|
||||
init() {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
this._onClickedHandler(info, tab);
|
||||
});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
browser.webRequest.onBeforeRequest.addListener((options) => {
|
||||
async onBeforeRequest(options) {
|
||||
if (options.frameId !== 0 || options.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
this.removeContextMenu();
|
||||
return Promise.all([
|
||||
const [tab, siteSettings] = await Promise.all([
|
||||
browser.tabs.get(options.tabId),
|
||||
this.storageArea.get(options.url)
|
||||
]).then(([tab, siteSettings]) => {
|
||||
]);
|
||||
let container;
|
||||
try {
|
||||
container = await browser.contextualIdentities.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
|
||||
} catch (e) {
|
||||
container = false;
|
||||
}
|
||||
|
||||
// The container we have in the assignment map isn't present any more so lets remove it
|
||||
// then continue the existing load
|
||||
if (!container) {
|
||||
this.deleteContainer(siteSettings.userContextId);
|
||||
return {};
|
||||
}
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (!siteSettings
|
||||
|| userContextId === siteSettings.userContextId
|
||||
@@ -158,9 +166,16 @@ const assignManager = {
|
||||
return {
|
||||
cancel: true,
|
||||
};
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
},
|
||||
|
||||
init() {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
this._onClickedHandler(info, tab);
|
||||
});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
browser.webRequest.onBeforeRequest.addListener((options) => {
|
||||
return this.onBeforeRequest(options);
|
||||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
||||
},
|
||||
|
||||
@@ -328,6 +343,13 @@ const assignManager = {
|
||||
});
|
||||
},
|
||||
|
||||
encodeURLProperty(url) {
|
||||
return encodeURIComponent(url).replace(/[!'()*]/g, (c) => {
|
||||
const charCode = c.charCodeAt(0).toString(16);
|
||||
return `%${charCode}`;
|
||||
});
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) {
|
||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||
@@ -336,7 +358,7 @@ const assignManager = {
|
||||
if (neverAsk) {
|
||||
browser.tabs.create({url, cookieStoreId, index});
|
||||
} else {
|
||||
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
|
||||
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
|
||||
let currentCookieStoreId;
|
||||
if (currentUserContextId) {
|
||||
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
|
||||
|
||||
@@ -63,8 +63,7 @@ const backgroundLogic = {
|
||||
url = undefined;
|
||||
}
|
||||
|
||||
// We can't open these we just have to throw them away
|
||||
if (new URL(url).protocol === "about:") {
|
||||
if (!this.isPermissibleURL(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,6 +75,17 @@ const backgroundLogic = {
|
||||
});
|
||||
},
|
||||
|
||||
isPermissibleURL(url) {
|
||||
const protocol = new URL(url).protocol;
|
||||
// We can't open these we just have to throw them away
|
||||
if (protocol === "about:"
|
||||
|| protocol === "chrome:"
|
||||
|| protocol === "moz-extension:") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
checkArgs(requiredArguments, options, methodName) {
|
||||
requiredArguments.forEach((argument) => {
|
||||
if (!(argument in options)) {
|
||||
@@ -118,20 +128,37 @@ const backgroundLogic = {
|
||||
containerState.hiddenTabs.length === 0) {
|
||||
return;
|
||||
}
|
||||
const newWindowObj = await browser.windows.create({
|
||||
let newWindowObj;
|
||||
let hiddenDefaultTabToClose;
|
||||
if (list.length) {
|
||||
newWindowObj = await browser.windows.create({
|
||||
tabId: list.shift().id
|
||||
});
|
||||
browser.tabs.move(list.map((tab) => tab.id), {
|
||||
windowId: newWindowObj.id,
|
||||
index: -1
|
||||
});
|
||||
} else {
|
||||
//As we get a blank tab here we will need to await the tabs creation
|
||||
newWindowObj = await browser.windows.create({
|
||||
});
|
||||
hiddenDefaultTabToClose = true;
|
||||
}
|
||||
|
||||
const showHiddenPromises = [];
|
||||
|
||||
// Let's show the hidden tabs.
|
||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||
browser.tabs.create(object.url || DEFAULT_TAB, {
|
||||
showHiddenPromises.push(browser.tabs.create({
|
||||
url: object.url || DEFAULT_TAB,
|
||||
windowId: newWindowObj.id,
|
||||
cookieStoreId
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
if (hiddenDefaultTabToClose) {
|
||||
// Lets wait for hidden tabs to show before closing the others
|
||||
await showHiddenPromises;
|
||||
}
|
||||
|
||||
containerState.hiddenTabs = [];
|
||||
@@ -139,9 +166,9 @@ const backgroundLogic = {
|
||||
// Let's close all the normal tab in the new window. In theory it
|
||||
// should be only the first tab, but maybe there are addons doing
|
||||
// crazy stuff.
|
||||
const tabs = browser.tabs.query({windowId: newWindowObj.id});
|
||||
const tabs = await browser.tabs.query({windowId: newWindowObj.id});
|
||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||
if (tabs.cookieStoreId !== cookieStoreId) {
|
||||
if (tab.cookieStoreId !== cookieStoreId) {
|
||||
browser.tabs.remove(tab.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ const badge = {
|
||||
const currentWindow = await browser.windows.getCurrent();
|
||||
this.displayBrowserActionBadge(currentWindow.incognito);
|
||||
},
|
||||
async displayBrowserActionBadge(disable) {
|
||||
if (disable) {
|
||||
browser.browserAction.disable();
|
||||
} else {
|
||||
browser.browserAction.enable();
|
||||
}
|
||||
|
||||
disableAddon(tabId) {
|
||||
browser.browserAction.disable(tabId);
|
||||
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
||||
},
|
||||
|
||||
async displayBrowserActionBadge() {
|
||||
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ const identityState = {
|
||||
const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId});
|
||||
tabsByContainer.forEach((tab) => {
|
||||
const tabObject = this._createTabObject(tab);
|
||||
if (!backgroundLogic.isPermissibleURL(tab.url)) {
|
||||
return;
|
||||
}
|
||||
// This tab is going to be closed. Let's mark this tabObject as
|
||||
// non-active.
|
||||
tabObject.active = false;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"js/background/badge.js",
|
||||
"js/background/identityState.js",
|
||||
"js/background/messageHandler.js",
|
||||
"js/backdround/init.js"
|
||||
]
|
||||
-->
|
||||
<script type="text/javascript" src="backgroundLogic.js"></script>
|
||||
@@ -19,6 +18,5 @@
|
||||
<script type="text/javascript" src="badge.js"></script>
|
||||
<script type="text/javascript" src="identityState.js"></script>
|
||||
<script type="text/javascript" src="messageHandler.js"></script>
|
||||
<script type="text/javascript" src="init.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
browser.runtime.sendMessage({
|
||||
method: "getPreference",
|
||||
pref: "browser.privatebrowsing.autostart"
|
||||
}).then(pbAutoStart => {
|
||||
|
||||
// We don't want to disable the addon if we are in auto private-browsing.
|
||||
if (!pbAutoStart) {
|
||||
browser.tabs.onCreated.addListener(tab => {
|
||||
if (tab.incognito) {
|
||||
disableAddon(tab.id);
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.query({}).then(tabs => {
|
||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||
if (tab.incognito) {
|
||||
disableAddon(tab.id);
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
function disableAddon(tabId) {
|
||||
browser.browserAction.disable(tabId);
|
||||
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
||||
}
|
||||
@@ -39,7 +39,7 @@ const messageHandler = {
|
||||
backgroundLogic.sortTabs();
|
||||
break;
|
||||
case "showTabs":
|
||||
backgroundLogic.showTabs({cookieStoreId: m.cookieStoreId});
|
||||
this.unhideContainer(m.cookieStoreId);
|
||||
break;
|
||||
case "hideTabs":
|
||||
backgroundLogic.hideTabs({
|
||||
@@ -106,6 +106,9 @@ const messageHandler = {
|
||||
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
||||
|
||||
browser.tabs.onCreated.addListener((tab) => {
|
||||
if (tab.incognito) {
|
||||
badge.disableAddon(tab.id);
|
||||
}
|
||||
// lets remember the last tab created so we can close it if it looks like a redirect
|
||||
this.lastCreatedTab = tab;
|
||||
if (tab.cookieStoreId) {
|
||||
@@ -130,12 +133,11 @@ const messageHandler = {
|
||||
|
||||
async onFocusChangedCallback(windowId) {
|
||||
assignManager.removeContextMenu();
|
||||
const currentWindow = await browser.windows.getCurrent();
|
||||
// browserAction loses background color in new windows ...
|
||||
// https://bugzil.la/1314674
|
||||
// https://github.com/mozilla/testpilot-containers/issues/608
|
||||
// ... so re-call displayBrowserActionBadge on window changes
|
||||
badge.displayBrowserActionBadge(currentWindow.incognito);
|
||||
badge.displayBrowserActionBadge();
|
||||
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||
if (tabs && tabs[0]) {
|
||||
assignManager.calculateContextMenu(tabs[0]);
|
||||
|
||||
@@ -20,11 +20,15 @@ async function doAnimation(element, 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
|
||||
// Ideally we would use https://bugzilla.mozilla.org/show_bug.cgi?id=1340930 when this is available
|
||||
divElement.innerText = message.text;
|
||||
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.src = browser.extension.getURL("/img/container-site-d-24.png");
|
||||
const imagePath = browser.extension.getURL("/img/container-site-d-24.png");
|
||||
const response = await fetch(imagePath);
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
imageElement.src = objectUrl;
|
||||
divElement.prepend(imageElement);
|
||||
|
||||
document.body.appendChild(divElement);
|
||||
|
||||
@@ -93,18 +93,7 @@ const Logic = {
|
||||
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")) {
|
||||
onboarded = 3;
|
||||
} else if (localStorage.getItem("onboarded2")) {
|
||||
onboarded = 2;
|
||||
} else if (localStorage.getItem("onboarded1")) {
|
||||
onboarded = 1;
|
||||
} else {
|
||||
onboarded = 0;
|
||||
}
|
||||
this.setOnboardingStage(onboarded);
|
||||
}
|
||||
|
||||
@@ -144,7 +133,11 @@ const Logic = {
|
||||
browser.browserAction.setBadgeBackgroundColor({color: ""});
|
||||
browser.browserAction.setBadgeText({text: ""});
|
||||
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
||||
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
|
||||
// use set and spread to create a unique array
|
||||
const browserActionBadgesClicked = [...new Set(storage.browserActionBadgesClicked)];
|
||||
browser.storage.local.set({
|
||||
browserActionBadgesClicked
|
||||
});
|
||||
},
|
||||
|
||||
async identity(cookieStoreId) {
|
||||
@@ -168,6 +161,7 @@ const Logic = {
|
||||
});
|
||||
element.addEventListener("keydown", (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
handler(e);
|
||||
}
|
||||
});
|
||||
@@ -802,7 +796,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||
</td>`;
|
||||
tr.querySelector(".container-name").textContent = identity.name;
|
||||
tr.querySelector(".edit-container").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".remove-container").setAttribute("title", `Delete ${identity.name} container`);
|
||||
tr.querySelector(".remove-container").setAttribute("title", `Remove ${identity.name} container`);
|
||||
|
||||
|
||||
Logic.addEnterHandler(tr, e => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Firefox Multi-Account Containers",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.3",
|
||||
|
||||
"description": "Firefox Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||
"description": "Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||
"icons": {
|
||||
"48": "img/container-site-d-48.png",
|
||||
"96": "img/container-site-d-96.png"
|
||||
@@ -11,8 +11,7 @@
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "51.0",
|
||||
"update_url": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
||||
"strict_min_version": "53.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,7 +44,7 @@
|
||||
"browser_action": {
|
||||
"browser_style": true,
|
||||
"default_icon": "img/container-site.svg",
|
||||
"default_title": "Firefox Multi-Account Containers",
|
||||
"default_title": "Multi-Account Containers",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Firefox Multi-Account Containers</title>
|
||||
<title>Multi-Account Containers</title>
|
||||
<link rel="stylesheet" href="/css/popup.css">
|
||||
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user