Add files via upload

This commit is contained in:
BPower0036 2022-05-04 16:16:34 +00:00 committed by GitHub
parent 99ea5decfe
commit 63837b8fb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 6481 additions and 0 deletions

35
src/confirm-page.html Normal file
View file

@ -0,0 +1,35 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title data-i18n-message-id="confirmNavigationTitle"></title>
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
<script type="text/javascript" src="./js/i18n.js"></script>
<link rel="stylesheet" href="/css/confirm-page.css" />
</head>
<body>
<main>
<div class="title">
<h1 class="title-text" data-i18n-message-id="openThisSiteConfirmation"></h1>
</div>
<form id="redirect-form">
<p data-message-id="youAskedFireFox" data-message-arg="container-name"></p>
<div id="redirect-url"></div>
<p data-i18n-message-id="wouldYouStillLikeToOpenConfirmation"></p>
<br />
<br />
<label for="never-ask" class="check-label">
<input id="never-ask" type="checkbox" />
<span data-i18n-message-id="rememberMyDecision"></span>
</label>
<br />
<div class="button-container">
<button id="deny" class="button" data-message-id="openInContainer" data-message-arg="current-container-name"></button>
<button id="confirm" class="button primary" autofocus data-message-id="openInContainer" data-message-arg="container-name"></button>
</div>
</form>
</main>
<script src="js/utils.js"></script>
<script src="js/confirm-page.js"></script>
</body>
</html>

View file

@ -0,0 +1,866 @@
window.assignManager = {
MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container",
MENU_SEPARATOR_ID: "separator",
MENU_HIDE_ID: "hide-container",
MENU_MOVE_ID: "move-to-new-window-container",
OPEN_IN_CONTAINER: "open-bookmark-in-container-tab",
storageArea: {
area: browser.storage.local,
exemptedTabs: {},
getSiteStoreKey(pageUrlorUrlKey) {
if (pageUrlorUrlKey.includes("siteContainerMap@@_")) return pageUrlorUrlKey;
const url = new window.URL(pageUrlorUrlKey);
const storagePrefix = "siteContainerMap@@_";
if (url.port === "80" || url.port === "443") {
return `${storagePrefix}${url.hostname}`;
} else {
return `${storagePrefix}${url.hostname}${url.port}`;
}
},
getWildcardStoreKey(wildcardHostname) {
return `wildcardMap@@_${wildcardHostname}`;
},
setExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) {
this.exemptedTabs[siteStoreKey] = [];
}
this.exemptedTabs[siteStoreKey].push(tabId);
},
removeExempted(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
this.exemptedTabs[siteStoreKey] = [];
},
isExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) {
return false;
}
return this.exemptedTabs[siteStoreKey].includes(tabId);
},
get(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
return this.getByUrlKey(siteStoreKey);
},
async getOrWildcardMatch(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
const siteSettings = await this.getByUrlKey(siteStoreKey);
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}
return this.getByWildcardMatch(siteStoreKey);
},
async getSyncEnabled() {
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
return !!syncEnabled;
},
async getReplaceTabEnabled() {
const { replaceTabEnabled } = await browser.storage.local.get("replaceTabEnabled");
return !!replaceTabEnabled;
},
getByUrlKey(siteStoreKey) {
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);
});
});
},
async getByWildcardMatch(siteStoreKey) {
// Keep stripping subdomains off site hostname until match a wildcard hostname
let remainingHostname = siteStoreKey.replace(/^siteContainerMap@@_/, "");
while (remainingHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(remainingHostname);
siteStoreKey = await this.getByUrlKey(wildcardStoreKey);
if (siteStoreKey) {
const siteSettings = await this.getByUrlKey(siteStoreKey);
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}
}
const indexOfDot = remainingHostname.indexOf(".");
remainingHostname = indexOfDot < 0 ? null : remainingHostname.substring(indexOfDot + 1);
}
},
async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (exemptedTabIds) {
exemptedTabIds.forEach((tabId) => {
this.setExempted(pageUrlorUrlKey, tabId);
});
}
await this.removeWildcardLookup(siteStoreKey);
// eslint-disable-next-line require-atomic-updates
data.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(data.userContextId);
await this.area.set({
[siteStoreKey]: data
});
if (data.wildcardHostname) {
await this.setWildcardLookup(siteStoreKey, data.wildcardHostname);
}
const syncEnabled = await this.getSyncEnabled();
if (backup && syncEnabled) {
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
}
return;
},
async setWildcardLookup(siteStoreKey, wildcardHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
return this.area.set({
[wildcardStoreKey]: siteStoreKey
});
},
async remove(pageUrlorUrlKey, shouldSync = true) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
// When we remove an assignment we should clear all the exemptions
this.removeExempted(pageUrlorUrlKey);
// When we remove an assignment we should clear the wildcard lookup
await this.removeWildcardLookup(siteStoreKey);
await this.area.remove([siteStoreKey]);
const syncEnabled = await this.getSyncEnabled();
if (shouldSync && syncEnabled) await sync.storageArea.backup({siteStoreKey});
return;
},
async removeWildcardLookup(siteStoreKey) {
const siteSettings = await this.getByUrlKey(siteStoreKey);
const wildcardHostname = siteSettings && siteSettings.wildcardHostname;
if (wildcardHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
await this.area.remove([wildcardStoreKey]);
}
},
async deleteContainer(userContextId) {
const sitesByContainer = await this.getAssignedSites(userContextId);
this.area.remove(Object.keys(sitesByContainer));
// Delete wildcard lookups
const wildcardStoreKeys = Object.values(sitesByContainer)
.map((site) => {
if (site && site.wildcardHostname) {
return this.getWildcardStoreKey(site.wildcardHostname);
}
})
.filter((wildcardStoreKey) => { return !!wildcardStoreKey; });
this.area.remove(wildcardStoreKeys);
},
async getAssignedSites(userContextId = null) {
const sites = {};
const siteConfigs = await this.area.get();
for(const urlKey of Object.keys(siteConfigs)) {
if (urlKey.includes("siteContainerMap@@_")) {
// For some reason this is stored as string... lets check
// them both as that
if (!!userContextId &&
String(siteConfigs[urlKey].userContextId)
!== String(userContextId)) {
continue;
}
const site = siteConfigs[urlKey];
// In hindsight we should have stored this
// TODO file a follow up to clean the storage onLoad
site.hostname = urlKey.replace(/^siteContainerMap@@_/, "");
sites[urlKey] = site;
}
}
return sites;
},
/*
* Looks for abandoned site assignments. If there is no identity with
* the site assignment's userContextId (cookieStoreId), then the assignment
* is removed.
*/
async upgradeData() {
const identitiesList = await browser.contextualIdentities.query({});
const macConfigs = await this.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("siteContainerMap@@_")) {
const cookieStoreId =
"firefox-container-" + macConfigs[configKey].userContextId;
const match = identitiesList.find(
localIdentity => localIdentity.cookieStoreId === cookieStoreId
);
if (!match) {
await this.remove(configKey);
continue;
}
const updatedSiteAssignment = macConfigs[configKey];
updatedSiteAssignment.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(match.cookieStoreId);
await this.set(
configKey,
updatedSiteAssignment,
false,
false
);
}
}
}
},
_neverAsk(m) {
const pageUrl = m.pageUrl;
if (m.neverAsk === true) {
// If we have existing data and for some reason it hasn't been
// deleted etc lets update it
this.storageArea.getOrWildcardMatch(pageUrl).then((siteMatchResult) => {
if (siteMatchResult) {
siteMatchResult.siteSettings.neverAsk = true;
this.storageArea.set(siteMatchResult.siteStoreKey, siteMatchResult.siteSettings);
}
}).catch((e) => {
throw e;
});
}
},
// We return here so the confirm page can load the tab when exempted
async _exemptTab(m) {
const pageUrl = m.pageUrl;
await this.storageArea.setExempted(pageUrl, m.tabId);
return true;
},
async handleProxifiedRequest(requestInfo) {
// The following blocks potentially dangerous requests for privacy that come without a tabId
if(requestInfo.tabId === -1) {
return {};
}
const tab = await browser.tabs.get(requestInfo.tabId);
const result = await proxifiedContainers.retrieve(tab.cookieStoreId);
if (!result || !result.proxy) {
return {};
}
// proxyDNS only works for SOCKS proxies
if (["socks", "socks4"].includes(result.proxy.type)) {
result.proxy.proxyDNS = true;
}
if (!result.proxy.mozProxyEnabled) {
return result.proxy;
}
// Let's add the isolation key.
return [{ ...result.proxy, connectionIsolationKey: "" + MozillaVPN_Background.isolationKey }];
},
// Before a request is handled by the browser we decide if we should
// route through a different container
async onBeforeRequest(options) {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
}
this.removeContextMenu();
const [tab, siteMatchResult] = await Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.getOrWildcardMatch(options.url)
]);
const siteSettings = siteMatchResult && siteMatchResult.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 (siteSettings && !container) {
this.deleteContainer(siteSettings.userContextId);
return {};
}
const userContextId = this.getUserContextIdFromCookieStore(tab);
// https://github.com/mozilla/multi-account-containers/issues/847
//
// Handle the case where this request's URL is not assigned to any particular
// container. We must do the following check:
//
// If the current tab's container is "unlocked", we can just go ahead
// and open the URL in the current tab, since an "unlocked" container accepts
// any-and-all sites.
//
// But if the current tab's container has been "locked" by the user, then we must
// re-open the page in the default container, because the user doesn't want random
// sites polluting their locked container.
//
// For example:
// - the current tab's container is locked and only allows "www.google.com"
// - the incoming request is for "www.amazon.com", which has no specific container assignment
// - in this case, we must re-open "www.amazon.com" in a new tab in the default container
const siteIsolatedReloadInDefault =
await this._maybeSiteIsolatedReloadInDefault(siteSettings, tab);
if (!siteIsolatedReloadInDefault) {
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| this.storageArea.isExempted(options.url, tab.id)) {
return {};
}
}
const replaceTabEnabled = await this.storageArea.getReplaceTabEnabled();
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|| (messageHandler.lastCreatedTab
&& messageHandler.lastCreatedTab.id === tab.id)
|| replaceTabEnabled;
const openTabId = removeTab ? tab.openerTabId : tab.id;
if (!this.canceledRequests[tab.id]) {
// we decided to cancel the request at this point, register
// canceled request
this.canceledRequests[tab.id] = {
requestIds: {
[options.requestId]: true
},
urls: {
[options.url]: true
}
};
// since webRequest onCompleted and onErrorOccurred are not 100%
// reliable (see #1120)
// we register a timer here to cleanup canceled requests, just to
// make sure we don't
// end up in a situation where certain urls in a tab.id stay canceled
setTimeout(() => {
if (this.canceledRequests[tab.id]) {
delete this.canceledRequests[tab.id];
}
}, 2000);
} else {
let cancelEarly = false;
if (this.canceledRequests[tab.id].requestIds[options.requestId] ||
this.canceledRequests[tab.id].urls[options.url]) {
// same requestId or url from the same tab
// this is a redirect that we have to cancel early to prevent
// opening two tabs
cancelEarly = true;
}
// we decided to cancel the request at this point, register canceled
// request
this.canceledRequests[tab.id].requestIds[options.requestId] = true;
this.canceledRequests[tab.id].urls[options.url] = true;
if (cancelEarly) {
return {
cancel: true
};
}
}
if (siteIsolatedReloadInDefault) {
this.reloadPageInDefaultContainer(
options.url,
tab.index + 1,
tab.active,
openTabId
);
} else {
this.reloadPageInContainer(
options.url,
userContextId,
siteSettings.userContextId,
tab.index + 1,
tab.active,
siteSettings.neverAsk,
openTabId
);
}
this.calculateContextMenu(tab);
/* Removal of existing tabs:
We aim to open the new assigned container tab / warning prompt in
it's own tab:
- As the history won't span from one container to another it
seems most sane to not try and reopen a tab on history.back()
- When users open a new tab themselves we want to make sure we
don't end up with three tabs as per:
https://github.com/mozilla/testpilot-containers/issues/421
If we are coming from an internal url that are used for the new
tab page (NEW_TAB_PAGES), we can safely close as user is unlikely
losing history
Detecting redirects on "new tab" opening actions is pretty hard
as we don't get tab history:
- Redirects happen from Short URLs and tracking links that act as
a gateway
- Extensions don't provide a way to history crawl for tabs, we
could inject content scripts to do this
however they don't run on about:blank so this would likely be
just as hacky.
We capture the time the tab was created and close if it was within
the timeout to try to capture pages which haven't had user
interaction or history.
*/
if (removeTab) {
browser.tabs.remove(tab.id);
}
return {
cancel: true,
};
},
async _maybeSiteIsolatedReloadInDefault(siteSettings, tab) {
// Tab doesn't support cookies, so containers not supported either.
if (!("cookieStoreId" in tab)) {
return false;
}
// Requested page has been assigned to a specific container.
// I.e. it will be opened in that container anyway, so we don't need to check if the
// current tab's container is locked or not.
if (siteSettings) {
return false;
}
//tab is alredy reopening in the default container
if (tab.cookieStoreId === "firefox-default") {
return false;
}
// Requested page is not assigned to a specific container. If the current tab's container
// is locked, then the page must be reloaded in the default container.
const currentContainerState = await identityState.storageArea.get(tab.cookieStoreId);
return currentContainerState && currentContainerState.isIsolated;
},
maybeAddProxyListeners() {
if (browser.proxy) {
browser.proxy.onRequest.addListener(this.handleProxifiedRequest, {urls: ["<all_urls>"]});
}
},
init() {
browser.contextMenus.onClicked.addListener((info, tab) => {
info.bookmarkId ?
this._onClickedBookmark(info) :
this._onClickedHandler(info, tab);
});
// Before anything happens we decide if the request should be proxified
this.maybeAddProxyListeners();
// Before a request is handled by the browser we decide if we should
// route through a different container
this.canceledRequests = {};
browser.webRequest.onBeforeRequest.addListener((options) => {
return this.onBeforeRequest(options);
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
// Clean up canceled requests
browser.webRequest.onCompleted.addListener((options) => {
if (this.canceledRequests[options.tabId]) {
delete this.canceledRequests[options.tabId];
}
},{urls: ["<all_urls>"], types: ["main_frame"]});
browser.webRequest.onErrorOccurred.addListener((options) => {
if (this.canceledRequests[options.tabId]) {
delete this.canceledRequests[options.tabId];
}
},{urls: ["<all_urls>"], types: ["main_frame"]});
this.resetBookmarksMenuItem();
},
async resetBookmarksMenuItem() {
const hasPermission = await browser.permissions.contains({
permissions: ["bookmarks"]
});
if (this.hadBookmark === hasPermission) {
return;
}
this.hadBookmark = hasPermission;
if (hasPermission) {
this.initBookmarksMenu();
browser.contextualIdentities.onCreated
.addListener(this.contextualIdentityCreated);
browser.contextualIdentities.onUpdated
.addListener(this.contextualIdentityUpdated);
browser.contextualIdentities.onRemoved
.addListener(this.contextualIdentityRemoved);
} else {
this.removeBookmarksMenu();
browser.contextualIdentities.onCreated
.removeListener(this.contextualIdentityCreated);
browser.contextualIdentities.onUpdated
.removeListener(this.contextualIdentityUpdated);
browser.contextualIdentities.onRemoved
.removeListener(this.contextualIdentityRemoved);
}
},
contextualIdentityCreated(changeInfo) {
browser.contextMenus.create({
parentId: assignManager.OPEN_IN_CONTAINER,
id: changeInfo.contextualIdentity.cookieStoreId,
title: changeInfo.contextualIdentity.name,
icons: { "16": `img/usercontext.svg#${
changeInfo.contextualIdentity.icon
}` }
});
},
contextualIdentityUpdated(changeInfo) {
browser.contextMenus.update(
changeInfo.contextualIdentity.cookieStoreId, {
title: changeInfo.contextualIdentity.name,
icons: { "16": `img/usercontext.svg#${
changeInfo.contextualIdentity.icon}` }
});
},
contextualIdentityRemoved(changeInfo) {
browser.contextMenus.remove(
changeInfo.contextualIdentity.cookieStoreId
);
},
async _onClickedHandler(info, tab) {
const userContextId = this.getUserContextIdFromCookieStore(tab);
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
let remove;
if (userContextId) {
switch (info.menuItemId) {
case this.MENU_ASSIGN_ID:
case this.MENU_REMOVE_ID:
if (info.menuItemId === this.MENU_ASSIGN_ID) {
remove = false;
} else {
remove = true;
}
await this._setOrRemoveAssignment(
tab.id, info.pageUrl, userContextId, remove
);
break;
case this.MENU_MOVE_ID:
backgroundLogic.moveTabsToWindow({
cookieStoreId: tab.cookieStoreId,
windowId: tab.windowId,
});
break;
case this.MENU_HIDE_ID:
backgroundLogic.hideTabs({
cookieStoreId: tab.cookieStoreId,
windowId: tab.windowId,
});
break;
}
}
},
async _onClickedBookmark(info) {
async function _getBookmarksFromInfo(info) {
const [bookmarkTreeNode] =
await browser.bookmarks.get(info.bookmarkId);
if (bookmarkTreeNode.type === "folder") {
return browser.bookmarks.getChildren(bookmarkTreeNode.id);
}
return [bookmarkTreeNode];
}
const bookmarks = await _getBookmarksFromInfo(info);
for (const bookmark of bookmarks) {
// Some checks on the urls from
// https://github.com/Rob--W/bookmark-container-tab/ thanks!
if ( !/^(javascript|place):/i.test(bookmark.url) &&
bookmark.type !== "folder") {
const openInReaderMode = bookmark.url.startsWith("about:reader");
if(openInReaderMode) {
try {
const parsed = new URL(bookmark.url);
bookmark.url = parsed.searchParams.get("url") + parsed.hash;
} catch (err) {
return err.message;
}
}
browser.tabs.create({
cookieStoreId: info.menuItemId,
url: bookmark.url,
openInReaderMode: openInReaderMode
});
}
}
},
deleteContainer(userContextId) {
this.storageArea.deleteContainer(userContextId);
},
getUserContextIdFromCookieStore(tab) {
if (!("cookieStoreId" in tab)) {
return false;
}
return backgroundLogic.getUserContextIdFromCookieStoreId(
tab.cookieStoreId
);
},
isTabPermittedAssign(tab) {
// Ensure we are not an important about url
const url = new URL(tab.url);
if (url.protocol === "about:"
|| url.protocol === "moz-extension:") {
return false;
}
return true;
},
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
let actionName;
// https://github.com/mozilla/testpilot-containers/issues/626
// Context menu has stored context IDs as strings, so we need to coerce
// the value to a string for accurate checking
userContextId = String(userContextId);
if (!remove) {
const tabs = await browser.tabs.query({});
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
const exemptedTabIds = tabs.filter((tab) => {
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
if (tabStoreKey === assignmentStoreKey &&
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
return true;
}
return false;
}).map((tab) => {
return tab.id;
});
await this.storageArea.set(pageUrl, {
userContextId,
neverAsk: false
}, exemptedTabIds);
actionName = "assigned site to always open in this container";
} else {
// Remove assignment
await this.storageArea.remove(pageUrl);
actionName = "removed from assigned sites list";
// remove site isolation if now empty
await this._maybeRemoveSiteIsolation(userContextId);
}
if (tabId) {
const tab = await browser.tabs.get(tabId);
setTimeout(function(){
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName}`
});
}, 1000);
this.calculateContextMenu(tab);
}
},
async _setWildcardHostnameForAssignment(pageUrl, wildcardHostname) {
const siteSettings = await this.storageArea.get(pageUrl);
if (siteSettings) {
siteSettings.wildcardHostname = wildcardHostname;
await this.storageArea.set(pageUrl, siteSettings);
}
},
async _maybeRemoveSiteIsolation(userContextId) {
const assignments = await this.storageArea.getByContainer(userContextId);
const hasAssignments = assignments && Object.keys(assignments).length > 0;
if (hasAssignments) {
return;
}
await backgroundLogic.addRemoveSiteIsolation(
backgroundLogic.cookieStoreId(userContextId),
true
);
},
async _getAssignment(tab) {
const cookieStore = this.getUserContextIdFromCookieStore(tab);
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
return this.storageArea.get(tab.url);
}
return false;
},
_getByContainer(userContextId) {
return this.storageArea.getAssignedSites(userContextId);
},
removeContextMenu() {
// There is a focus issue in this menu where if you change window with a context menu click
// you get the wrong menu display because of async
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
// We also can't change for always private mode
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
browser.contextMenus.remove(this.MENU_REMOVE_ID);
browser.contextMenus.remove(this.MENU_SEPARATOR_ID);
browser.contextMenus.remove(this.MENU_HIDE_ID);
browser.contextMenus.remove(this.MENU_MOVE_ID);
},
async calculateContextMenu(tab) {
this.removeContextMenu();
const siteSettings = await this._getAssignment(tab);
// Return early and not add an item if we have false
// False represents assignment is not permitted
if (siteSettings === false) {
return false;
}
let checked = false;
let menuId = this.MENU_ASSIGN_ID;
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
if (siteSettings &&
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
checked = true;
menuId = this.MENU_REMOVE_ID;
}
browser.contextMenus.create({
id: menuId,
title: "Always Open in This Container",
checked,
type: "checkbox",
contexts: ["all"],
});
browser.contextMenus.create({
id: this.MENU_SEPARATOR_ID,
type: "separator",
contexts: ["all"],
});
browser.contextMenus.create({
id: this.MENU_HIDE_ID,
title: "Hide This Container",
contexts: ["all"],
});
browser.contextMenus.create({
id: this.MENU_MOVE_ID,
title: "Move Tabs to a New Window",
contexts: ["all"],
});
},
encodeURLProperty(url) {
return encodeURIComponent(url).replace(/[!'()*]/g, (c) => {
const charCode = c.charCodeAt(0).toString(16);
return `%${charCode}`;
});
},
reloadPageInDefaultContainer(url, index, active, openerTabId) {
// To create a new tab in the default container, it is easiest just to omit the
// cookieStoreId entirely.
//
// Unfortunately, if you create a new tab WITHOUT a cookieStoreId but WITH an openerTabId,
// then the new tab automatically inherits the opener tab's cookieStoreId.
// I.e. it opens in the wrong container!
//
// So we have to explicitly pass in a cookieStoreId when creating the tab, since we
// are specifying the openerTabId. There doesn't seem to be any way
// to look up the default container's cookieStoreId programatically, so sadly
// we have to hardcode it here as "firefox-default". This is potentially
// not cross-browser compatible.
//
// Note that we could have just omitted BOTH cookieStoreId and openerTabId. But the
// drawback then is that if the user later closes the newly-created tab, the browser
// does not automatically return to the original opener tab. To get this desired behaviour,
// we MUST specify the openerTabId when creating the new tab.
const cookieStoreId = "firefox-default";
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
},
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false, openerTabId = null) {
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
const loadPage = browser.runtime.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) {
return browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
} else {
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
let currentCookieStoreId;
if (currentUserContextId) {
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
confirmUrl += `&currentCookieStoreId=${currentCookieStoreId}`;
}
return browser.tabs.create({
url: confirmUrl,
cookieStoreId: currentCookieStoreId,
openerTabId,
index,
active
}).then(() => {
// We don't want to sync this URL ever nor clutter the users history
browser.history.deleteUrl({url: confirmUrl});
}).catch((e) => {
throw e;
});
}
},
async initBookmarksMenu() {
browser.contextMenus.create({
id: this.OPEN_IN_CONTAINER,
title: "Open Bookmark in Container Tab",
contexts: ["bookmark"],
});
const identities = await browser.contextualIdentities.query({});
for (const identity of identities) {
browser.contextMenus.create({
parentId: this.OPEN_IN_CONTAINER,
id: identity.cookieStoreId,
title: identity.name,
icons: { "16": `img/usercontext.svg#${identity.icon}` }
});
}
},
async removeBookmarksMenu() {
browser.contextMenus.remove(this.OPEN_IN_CONTAINER);
const identities = await browser.contextualIdentities.query({});
for (const identity of identities) {
browser.contextMenus.remove(identity.cookieStoreId);
}
},
};
assignManager.init();

View file

@ -0,0 +1,406 @@
/* 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 DEFAULT_TAB = "about:newtab";
const backgroundLogic = {
NEW_TAB_PAGES: new Set([
"about:startpage",
"about:newtab",
"about:home",
"about:blank"
]),
NUMBER_OF_KEYBOARD_SHORTCUTS: 10,
unhideQueue: [],
init() {
browser.commands.onCommand.addListener(function (command) {
for (let i=0; i < backgroundLogic.NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const key = "open_container_" + i;
const cookieStoreId = identityState.keyboardShortcut[key];
if (command === key) {
if (cookieStoreId === "none") return;
browser.tabs.create({cookieStoreId});
}
}
});
browser.permissions.onAdded.addListener(permissions => this.resetPermissions(permissions));
browser.permissions.onRemoved.addListener(permissions => this.resetPermissions(permissions));
},
resetPermissions(permissions) {
permissions.permissions.forEach(async permission => {
switch (permission) {
case "bookmarks":
assignManager.resetBookmarksMenuItem();
break;
case "nativeMessaging":
await MozillaVPN_Background.removeMozillaVpnProxies();
await browser.runtime.reload();
break;
case "proxy":
assignManager.maybeAddProxyListeners();
break;
}
});
},
async getExtensionInfo() {
const manifestPath = browser.runtime.getURL("manifest.json");
const response = await fetch(manifestPath);
const extensionInfo = await response.json();
return extensionInfo;
},
getUserContextIdFromCookieStoreId(cookieStoreId) {
if (!cookieStoreId) {
return false;
}
const container = cookieStoreId.replace("firefox-container-", "");
if (container !== cookieStoreId) {
return container;
}
return false;
},
async deleteContainer(userContextId, removed = false) {
await this._closeTabs(userContextId);
if (!removed) {
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
}
assignManager.deleteContainer(userContextId);
// Now remove the identity->proxy association in proxifiedContainers also
proxifiedContainers.delete(this.cookieStoreId(userContextId));
return {done: true, userContextId};
},
async createOrUpdateContainer(options) {
if (options.userContextId !== "new") {
return await browser.contextualIdentities.update(
this.cookieStoreId(options.userContextId),
options.params
);
}
return await browser.contextualIdentities.create(options.params);
},
async openNewTab(options) {
let url = options.url || undefined;
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
const active = ("nofocus" in options) ? options.nofocus : true;
const discarded = ("noload" in options) ? options.noload : false;
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
// Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072
// We can't open new tab pages, so open a blank tab. Used in tab un-hide
if (this.NEW_TAB_PAGES.has(url)) {
url = undefined;
}
if (!this.isPermissibleURL(url)) {
return;
}
return browser.tabs.create({
url,
active,
discarded,
pinned: options.pinned || false,
cookieStoreId
});
},
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)) {
return new Error(`${methodName} must be called with ${argument} argument.`);
}
});
},
async getTabs(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "getTabs");
const { cookieStoreId, windowId } = options;
const list = [];
const tabs = await browser.tabs.query({
cookieStoreId,
windowId
});
tabs.forEach((tab) => {
list.push(identityState._createTabObject(tab));
});
const containerState = await identityState.storageArea.get(cookieStoreId);
return list.concat(containerState.hiddenTabs);
},
async unhideContainer(cookieStoreId, alreadyShowingUrl) {
if (!this.unhideQueue.includes(cookieStoreId)) {
this.unhideQueue.push(cookieStoreId);
await this.showTabs({
cookieStoreId,
alreadyShowingUrl
});
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
}
},
// https://github.com/mozilla/multi-account-containers/issues/847
async addRemoveSiteIsolation(cookieStoreId, remove = false) {
const containerState = await identityState.storageArea.get(cookieStoreId);
try {
if ("isIsolated" in containerState || remove) {
delete containerState.isIsolated;
} else {
containerState.isIsolated = "locked";
}
return await identityState.storageArea.set(cookieStoreId, containerState);
} catch (error) {
// console.error(`No container: ${cookieStoreId}`);
}
},
async moveTabsToWindow(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "moveTabsToWindow");
const { cookieStoreId, windowId } = options;
const list = await browser.tabs.query({
cookieStoreId,
windowId
});
const containerState = await identityState.storageArea.get(cookieStoreId);
// Nothing to do
if (list.length === 0 &&
containerState.hiddenTabs.length === 0) {
return;
}
let newWindowObj;
let hiddenDefaultTabToClose;
if (list.length) {
newWindowObj = await browser.windows.create();
// Pin the default tab in the new window so existing pinned tabs can be moved after it.
// From the docs (https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/move):
// Note that you can't move pinned tabs to a position after any unpinned tabs in a window, or move any unpinned tabs to a position before any pinned tabs.
await browser.tabs.update(newWindowObj.tabs[0].id, { pinned: true });
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.
if (!this.unhideQueue.includes(cookieStoreId)) {
this.unhideQueue.push(cookieStoreId);
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
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 = [];
// 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 = await browser.tabs.query({windowId: newWindowObj.id});
for (let tab of tabs) { // eslint-disable-line prefer-const
if (tab.cookieStoreId !== cookieStoreId) {
browser.tabs.remove(tab.id);
}
}
const rv = await identityState.storageArea.set(cookieStoreId, containerState);
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
return rv;
},
async _closeTabs(userContextId, windowId = false) {
const cookieStoreId = this.cookieStoreId(userContextId);
let tabs;
/* if we have no windowId we are going to close all this container (used for deleting) */
if (windowId !== false) {
tabs = await browser.tabs.query({
cookieStoreId,
windowId
});
} else {
tabs = await browser.tabs.query({
cookieStoreId
});
}
const tabIds = tabs.map((tab) => tab.id);
return browser.tabs.remove(tabIds);
},
async queryIdentitiesState(windowId) {
const identities = await browser.contextualIdentities.query({});
const identitiesOutput = {};
const identitiesPromise = identities.map(async (identity) => {
const { cookieStoreId } = identity;
const containerState = await identityState.storageArea.get(cookieStoreId);
const openTabs = await browser.tabs.query({
cookieStoreId,
windowId
});
identitiesOutput[cookieStoreId] = {
hasHiddenTabs: !!containerState.hiddenTabs.length,
hasOpenTabs: !!openTabs.length,
numberOfHiddenTabs: containerState.hiddenTabs.length,
numberOfOpenTabs: openTabs.length,
isIsolated: !!containerState.isIsolated
};
return;
});
await Promise.all(identitiesPromise);
return identitiesOutput;
},
async sortTabs() {
const windows = await browser.windows.getAll();
for (let windowObj of windows) { // eslint-disable-line prefer-const
// First the pinned tabs, then the normal ones.
await this._sortTabsInternal(windowObj, true);
await this._sortTabsInternal(windowObj, false);
}
},
async _sortTabsInternal(windowObj, pinnedTabs) {
const tabs = await browser.tabs.query({windowId: windowObj.id});
let pos = 0;
// Let's collect UCIs/tabs for this window.
const map = new Map;
for (const tab of tabs) {
if (pinnedTabs && !tab.pinned) {
// We don't have, or we already handled all the pinned tabs.
break;
}
if (!pinnedTabs && tab.pinned) {
// pinned tabs must be consider as taken positions.
++pos;
continue;
}
if (!map.has(tab.cookieStoreId)) {
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
map.set(tab.cookieStoreId, { order: userContextId, tabs: [] });
}
map.get(tab.cookieStoreId).tabs.push(tab);
}
const containerOrderStorage = await browser.storage.local.get([CONTAINER_ORDER_STORAGE_KEY]);
const containerOrder =
containerOrderStorage && containerOrderStorage[CONTAINER_ORDER_STORAGE_KEY];
if (containerOrder) {
map.forEach((obj, key) => {
obj.order = (key in containerOrder) ? containerOrder[key] : -1;
});
}
// Let's sort the map.
const sortMap = new Map([...map.entries()].sort((a, b) => a[1].order > b[1].order));
// Let's move tabs.
sortMap.forEach(obj => {
for (const tab of obj.tabs) {
++pos;
browser.tabs.move(tab.id, {
windowId: windowObj.id,
index: pos
});
}
});
},
async hideTabs(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "hideTabs");
const { cookieStoreId, windowId } = options;
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(cookieStoreId);
const containerState = await identityState.storeHidden(cookieStoreId, windowId);
await this._closeTabs(userContextId, windowId);
return containerState;
},
async showTabs(options) {
if (!("cookieStoreId" in options)) {
return Promise.reject("showTabs must be called with cookieStoreId argument.");
}
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
const promises = [];
const containerState = await identityState.storageArea.get(options.cookieStoreId);
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
// do not show already opened url
const noload = !object.pinned;
if (object.url !== options.alreadyShowingUrl) {
promises.push(this.openNewTab({
userContextId: userContextId,
url: object.url,
nofocus: options.nofocus || false,
noload: noload,
pinned: object.pinned,
}));
}
}
containerState.hiddenTabs = [];
await Promise.all(promises);
return identityState.storageArea.set(options.cookieStoreId, containerState);
},
cookieStoreId(userContextId) {
if(userContextId === 0) return "firefox-default";
return `firefox-container-${userContextId}`;
}
};
backgroundLogic.init();

View file

@ -0,0 +1,21 @@
const MAJOR_VERSIONS = ["2.3.0", "2.4.0", "6.2.0", "8.0.2"];
const badge = {
async init() {
const currentWindow = await browser.windows.getCurrent();
this.displayBrowserActionBadge(currentWindow);
},
async displayBrowserActionBadge() {
const extensionInfo = await backgroundLogic.getExtensionInfo();
const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] });
if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 &&
storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) {
browser.browserAction.setBadgeBackgroundColor({ color: "rgb(255, 79, 94)" });
browser.browserAction.setBadgeText({ text: "!" });
browser.browserAction.setBadgeTextColor({ color: "rgb(255, 255, 255)" });
}
}
};
badge.init();

View file

@ -0,0 +1,194 @@
window.identityState = {
keyboardShortcut: {},
storageArea: {
area: browser.storage.local,
getContainerStoreKey(cookieStoreId) {
const storagePrefix = "identitiesState@@_";
return `${storagePrefix}${cookieStoreId}`;
},
async get(cookieStoreId) {
const storeKey = this.getContainerStoreKey(cookieStoreId);
const storageResponse = await this.area.get([storeKey]);
if (storageResponse && storeKey in storageResponse) {
if (!storageResponse[storeKey].macAddonUUID){
storageResponse[storeKey].macAddonUUID = uuidv4();
await this.set(cookieStoreId, storageResponse[storeKey]);
}
return storageResponse[storeKey];
}
// If local storage doesn't have an entry, look it up to make sure it's
// an in-use identity.
const identities = await browser.contextualIdentities.query({});
const match = identities.find(
(identity) => identity.cookieStoreId === cookieStoreId);
if (match) {
const defaultContainerState = identityState._createIdentityState();
await this.set(cookieStoreId, defaultContainerState);
return defaultContainerState;
}
return false;
},
set(cookieStoreId, data) {
const storeKey = this.getContainerStoreKey(cookieStoreId);
return this.area.set({
[storeKey]: data
});
},
async remove(cookieStoreId) {
const storeKey = this.getContainerStoreKey(cookieStoreId);
return this.area.remove([storeKey]);
},
async setKeyboardShortcut(shortcutId, cookieStoreId) {
identityState.keyboardShortcut[shortcutId] = cookieStoreId;
return this.area.set({[shortcutId]: cookieStoreId});
},
async loadKeyboardShortcuts () {
const identities = await browser.contextualIdentities.query({});
for (let i=0; i < backgroundLogic.NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const key = "open_container_" + i;
const storageObject = await this.area.get(key);
if (storageObject[key]){
identityState.keyboardShortcut[key] = storageObject[key];
continue;
}
if (identities[i]) {
identityState.keyboardShortcut[key] = identities[i].cookieStoreId;
continue;
}
identityState.keyboardShortcut[key] = "none";
}
return identityState.keyboardShortcut;
},
/*
* Looks for abandoned identity keys in local storage, and makes sure all
* identities registered in the browser are also in local storage. (this
* appears to not always be the case based on how this.get() is written)
*/
async upgradeData() {
const identitiesList = await browser.contextualIdentities.query({});
for (const identity of identitiesList) {
// ensure all identities have an entry in local storage
await identityState.addUUID(identity.cookieStoreId);
}
const macConfigs = await this.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("identitiesState@@_")) {
const cookieStoreId = String(configKey).replace(/^identitiesState@@_/, "");
const match = identitiesList.find(
localIdentity => localIdentity.cookieStoreId === cookieStoreId
);
if (cookieStoreId === "firefox-default") continue;
if (!match) {
await this.remove(cookieStoreId);
continue;
}
if (!macConfigs[configKey].macAddonUUID) {
await identityState.storageArea.get(cookieStoreId);
}
}
}
},
},
_createTabObject(tab) {
return Object.assign({}, tab);
},
async getCookieStoreIDuuidMap() {
const containers = {};
const identities = await browser.contextualIdentities.query({});
for(const identity of identities) {
const containerInfo = await this.storageArea.get(identity.cookieStoreId);
containers[identity.cookieStoreId] = containerInfo.macAddonUUID;
}
return containers;
},
async storeHidden(cookieStoreId, windowId) {
const containerState = await this.storageArea.get(cookieStoreId);
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;
tabObject.hiddenState = true;
containerState.hiddenTabs.push(tabObject);
});
return this.storageArea.set(cookieStoreId, containerState);
},
async updateUUID(cookieStoreId, uuid) {
if (!cookieStoreId || !uuid) {
throw new Error ("cookieStoreId or uuid missing");
}
const containerState = await this.storageArea.get(cookieStoreId);
containerState.macAddonUUID = uuid;
await this.storageArea.set(cookieStoreId, containerState);
return uuid;
},
async addUUID(cookieStoreId) {
await this.storageArea.get(cookieStoreId);
},
async lookupMACaddonUUID(cookieStoreId) {
// This stays a lookup, because if the cookieStoreId doesn't
// exist, this.get() will create it, which is not what we want.
const cookieStoreIdKey = cookieStoreId.includes("firefox-container-") ?
cookieStoreId : "firefox-container-" + cookieStoreId;
const macConfigs = await this.storageArea.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey === this.storageArea.getContainerStoreKey(cookieStoreIdKey)) {
return macConfigs[configKey].macAddonUUID;
}
}
return false;
},
async lookupCookieStoreId(macAddonUUID) {
const macConfigs = await this.storageArea.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("identitiesState@@_")) {
if(macConfigs[configKey].macAddonUUID === macAddonUUID) {
return String(configKey).replace(/^identitiesState@@_/, "");
}
}
}
return false;
},
_createIdentityState() {
return {
hiddenTabs: [],
macAddonUUID: uuidv4()
};
},
init() {
this.storageArea.loadKeyboardShortcuts();
}
};
identityState.init();
function uuidv4() {
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}

View file

@ -0,0 +1,26 @@
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<!--
This didn't work for debugging in the manifest.
"scripts": [
"js/background/backgroundLogic.js",
"js/background/assignManager.js",
"js/background/badge.js",
"js/background/identityState.js",
"js/background/messageHandler.js",
]
-->
<script type="text/javascript" src="../utils.js"></script>
<script type="text/javascript" src="../proxified-containers.js"></script>
<script type="text/javascript" src="backgroundLogic.js"></script>
<script type="text/javascript" src="mozillaVpnBackground.js"></script>
<script type="text/javascript" src="assignManager.js"></script>
<script type="text/javascript" src="badge.js"></script>
<script type="text/javascript" src="identityState.js"></script>
<script type="text/javascript" src="messageHandler.js"></script>
<script type="text/javascript" src="sync.js"></script>
</body>
</html>

View file

@ -0,0 +1,272 @@
const messageHandler = {
// After the timer completes we assume it's a tab the user meant to keep open
// We use this to catch redirected tabs that have just opened
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
LAST_CREATED_TAB_TIMER: 2000,
init() {
// Handles messages from webextension code
browser.runtime.onMessage.addListener(async (m) => {
let response;
let tab;
switch (m.method) {
case "getShortcuts":
response = identityState.storageArea.loadKeyboardShortcuts();
break;
case "setShortcut":
identityState.storageArea.setKeyboardShortcut(m.shortcut, m.cookieStoreId);
break;
case "resetSync":
response = sync.resetSync();
break;
case "deleteContainer":
response = backgroundLogic.deleteContainer(m.message.userContextId);
break;
case "createOrUpdateContainer":
response = backgroundLogic.createOrUpdateContainer(m.message);
break;
case "neverAsk":
assignManager._neverAsk(m);
break;
case "addRemoveSiteIsolation":
response = backgroundLogic.addRemoveSiteIsolation(m.cookieStoreId);
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 = assignManager._setOrRemoveAssignment(m.tabId, m.url, m.userContextId, m.value);
break;
case "setWildcardHostnameForAssignment":
response = assignManager._setWildcardHostnameForAssignment(m.url, m.wildcardHostname);
break;
case "sortTabs":
backgroundLogic.sortTabs();
break;
case "showTabs":
backgroundLogic.unhideContainer(m.cookieStoreId);
break;
case "hideTabs":
backgroundLogic.hideTabs({
cookieStoreId: m.cookieStoreId,
windowId: m.windowId
});
break;
case "checkIncompatibleAddons":
// TODO
break;
case "moveTabsToWindow":
response = backgroundLogic.moveTabsToWindow({
cookieStoreId: m.cookieStoreId,
windowId: m.windowId
});
break;
case "getTabs":
response = backgroundLogic.getTabs({
cookieStoreId: m.cookieStoreId,
windowId: m.windowId
});
break;
case "queryIdentitiesState":
response = backgroundLogic.queryIdentitiesState(m.message.windowId);
break;
case "exemptContainerAssignment":
response = assignManager._exemptTab(m);
break;
case "reloadInContainer":
response = assignManager.reloadPageInContainer(
m.url,
m.currentUserContextId,
m.newUserContextId,
m.tabIndex,
m.active,
true
);
break;
case "assignAndReloadInContainer":
tab = await assignManager.reloadPageInContainer(
m.url,
m.currentUserContextId,
m.newUserContextId,
m.tabIndex,
m.active,
true
);
// 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(tab.id).then((tab) => {
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.newUserContextId, m.value);
});
break;
case "MozillaVPN_attemptPort":
MozillaVPN_Background.maybeInitPort();
break;
case "MozillaVPN_queryServers":
MozillaVPN_Background.postToApp("servers");
break;
case "MozillaVPN_queryStatus":
response = MozillaVPN_Background.postToApp("status");
break;
case "MozillaVPN_getConnectionStatus":
response = MozillaVPN_Background.getConnectionStatus();
break;
case "MozillaVPN_getInstallationStatus":
response = MozillaVPN_Background.getInstallationStatus();
break;
}
return response;
});
// Handles external messages from webextensions
const externalExtensionAllowed = {};
browser.runtime.onMessageExternal.addListener(async (message, sender) => {
if (!externalExtensionAllowed[sender.id]) {
const extensionInfo = await browser.management.get(sender.id);
if (!extensionInfo.permissions.includes("contextualIdentities")) {
throw new Error("Missing contextualIdentities permission");
}
// eslint-disable-next-line require-atomic-updates
externalExtensionAllowed[sender.id] = true;
}
let response;
switch (message.method) {
case "getAssignment":
if (typeof message.url === "undefined") {
throw new Error("Missing message.url");
}
response = assignManager.storageArea.get(message.url);
break;
default:
throw new Error("Unknown message.method");
}
return response;
});
// Delete externalExtensionAllowed if add-on installs/updates; permissions might change
browser.management.onInstalled.addListener(extensionInfo => {
if (externalExtensionAllowed[extensionInfo.id]) {
delete externalExtensionAllowed[extensionInfo.id];
}
});
// Delete externalExtensionAllowed if add-on uninstalls; not needed anymore
browser.management.onUninstalled.addListener(extensionInfo => {
if (externalExtensionAllowed[extensionInfo.id]) {
delete externalExtensionAllowed[extensionInfo.id];
}
});
if (browser.contextualIdentities.onRemoved) {
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);
backgroundLogic.deleteContainer(userContextId, true);
});
}
browser.tabs.onActivated.addListener((info) => {
assignManager.removeContextMenu();
browser.tabs.get(info.tabId).then((tab) => {
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
});
browser.windows.onFocusChanged.addListener((windowId) => {
this.onFocusChangedCallback(windowId);
});
browser.webRequest.onCompleted.addListener((details) => {
if (details.frameId !== 0 || details.tabId === -1) {
return {};
}
assignManager.removeContextMenu();
browser.tabs.get(details.tabId).then((tab) => {
assignManager.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}, {urls: ["<all_urls>"], types: ["main_frame"]});
browser.tabs.onCreated.addListener((tab) => {
// lets remember the last tab created so we can close it if it looks like a redirect
this.lastCreatedTab = tab;
if (tab.cookieStoreId) {
// Don't count firefox-default, firefox-private, nor our own confirm page loads
if (tab.cookieStoreId !== "firefox-default" &&
tab.cookieStoreId !== "firefox-private" &&
!tab.url.startsWith("moz-extension")) {
// increment the counter of container tabs opened
this.incrementCountOfContainerTabsOpened();
this.tabUpdateHandler = (tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.status === "complete") {
// get current tab's url to not open the same one from hidden tabs
browser.tabs.get(tabId).then(loadedTab => {
backgroundLogic.unhideContainer(tab.cookieStoreId, loadedTab.url);
}).catch((e) => {
throw e;
});
browser.tabs.onUpdated.removeListener(this.tabUpdateHandler);
}
};
// if it's a container tab wait for it to complete and
// unhide other tabs from this container
if (tab.cookieStoreId.startsWith("firefox-container")) {
browser.tabs.onUpdated.addListener(this.tabUpdateHandler);
}
}
}
setTimeout(() => {
this.lastCreatedTab = null;
}, this.LAST_CREATED_TAB_TIMER);
});
},
async incrementCountOfContainerTabsOpened() {
const key = "containerTabsOpened";
const count = await browser.storage.local.get({[key]: 0});
const countOfContainerTabsOpened = ++count[key];
browser.storage.local.set({[key]: countOfContainerTabsOpened});
// When the user opens their _ tab, give them the achievement
if (countOfContainerTabsOpened === 100) {
const storage = await browser.storage.local.get({achievements: []});
storage.achievements.push({"name": "manyContainersOpened", "done": false});
// use set and spread to create a unique array
const achievements = [...new Set(storage.achievements)];
browser.storage.local.set({achievements});
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
browser.browserAction.setBadgeText({text: "NEW"});
}
},
async onFocusChangedCallback(windowId) {
assignManager.removeContextMenu();
// browserAction loses background color in new windows ...
// https://bugzil.la/1314674
// https://github.com/mozilla/testpilot-containers/issues/608
// ... so re-call displayBrowserActionBadge on window changes
badge.displayBrowserActionBadge();
browser.tabs.query({active: true, windowId}).then((tabs) => {
if (tabs && tabs[0]) {
assignManager.calculateContextMenu(tabs[0]);
}
}).catch((e) => {
throw e;
});
}
};
// Lets do this last as theme manager did a check before connecting before
messageHandler.init();

View file

@ -0,0 +1,118 @@
const MozillaVPN_Background = {
MOZILLA_VPN_SERVERS_KEY: "mozillaVpnServers",
MOZILLA_VPN_HIDDEN_TOUTS_LIST_KEY: "mozillaVpnHiddenToutsList",
_isolationKey: 0,
async maybeInitPort() {
if (this.port && this.port.error === null) {
return;
}
try {
/*
Find a way to not spam the console when MozillaVPN client is not installed
File at path ".../../MozillaVPN/..." is not executable.` thrown by resource://gre/modules/Subprocess.jsm:152`
Which does is not caught by this try/catch
*/
this.port = await browser.runtime.connectNative("mozillavpn");
this.port.onMessage.addListener(response => this.handleResponse(response));
this.port.onMessage.addListener(this.handleResponse);
this.postToApp("status");
this.postToApp("servers");
// When the mozillavpn dies or the VPN disconnects, we need to increase
// the isolation key in order to create new proxy connections. Otherwise
// we could see random timeout when the browser tries to connect to an
// invalid proxy connection.
this.port.onDisconnect.addListener(() => this.increaseIsolationKey());
} catch(e) {
this._installed = false;
this._connected = false;
}
},
async init() {
const { mozillaVpnServers } = await browser.storage.local.get(this.MOZILLA_VPN_SERVERS_KEY);
if (typeof(mozillaVpnServers) === "undefined") {
await browser.storage.local.set({ [this.MOZILLA_VPN_SERVERS_KEY]:[] });
await browser.storage.local.set({ [this.MOZILLA_VPN_HIDDEN_TOUTS_LIST_KEY]:[] });
this._installed = false;
this._connected = false;
}
this.maybeInitPort();
},
getConnectionStatus() {
return this._connected;
},
getInstallationStatus() {
return this._installed;
},
// Post messages to MozillaVPN client
postToApp(message) {
try {
this.port.postMessage({t: message});
} catch(e) {
if (e.message === "Attempt to postMessage on disconnected port") {
this._installed = false;
this._connected = false;
}
}
},
// Handle responses from MozillaVPN client
async handleResponse(response) {
MozillaVPN_Background._installed = true;
if (response.error && response.error === "vpn-client-down") {
MozillaVPN_Background._connected = false;
return;
}
if (response.servers) {
const servers = response.servers.countries;
browser.storage.local.set({ [MozillaVPN_Background.MOZILLA_VPN_SERVERS_KEY]: servers});
return;
}
if ((response.status && response.status.vpn) || response.t === "status") {
const status = response.status ? response.status.vpn : response.vpn;
if (status === "StateOn") {
MozillaVPN_Background._connected = true;
}
if (status === "StateOff" || status === "StateDisconnecting") {
MozillaVPN_Background._connected = false;
}
// Let's increase the network key isolation at any vpn status change.
MozillaVPN_Background.increaseIsolationKey();
}
},
increaseIsolationKey() {
++this._isolationKey;
},
get isolationKey() {
return this._isolationKey;
},
async removeMozillaVpnProxies() {
const proxies = await proxifiedContainers.retrieveAll();
if (!proxies) {
return;
}
for (const proxyObj of proxies) {
const { proxy } = proxyObj;
if (proxy.countryCode !== undefined) {
await proxifiedContainers.delete(proxyObj.cookieStoreId);
}
}
},
};
MozillaVPN_Background.init();

580
src/js/background/sync.js Normal file
View file

@ -0,0 +1,580 @@
const SYNC_DEBUG = false;
const sync = {
storageArea: {
area: browser.storage.sync,
async get(){
return this.area.get();
},
async set(options) {
return this.area.set(options);
},
async deleteIdentity(deletedIdentityUUID) {
const deletedIdentityList =
await sync.storageArea.getDeletedIdentityList();
if (
! deletedIdentityList.find(element => element === deletedIdentityUUID)
) {
deletedIdentityList.push(deletedIdentityUUID);
await sync.storageArea.set({ deletedIdentityList });
}
await this.removeIdentityKeyFromSync(deletedIdentityUUID);
},
async removeIdentityKeyFromSync(deletedIdentityUUID) {
await sync.storageArea.area.remove( "identity@@_" + deletedIdentityUUID);
},
async deleteSite(siteStoreKey) {
const deletedSiteList =
await sync.storageArea.getDeletedSiteList();
if (deletedSiteList.find(element => element === siteStoreKey)) return;
deletedSiteList.push(siteStoreKey);
await sync.storageArea.set({ deletedSiteList });
await sync.storageArea.area.remove(siteStoreKey);
},
async getDeletedIdentityList() {
const storedArray = await this.getStoredItem("deletedIdentityList");
return storedArray || [];
},
async getIdentities() {
const allSyncStorage = await this.get();
const identities = [];
for (const storageKey of Object.keys(allSyncStorage)) {
if (storageKey.includes("identity@@_")) {
identities.push(allSyncStorage[storageKey]);
}
}
return identities;
},
async getDeletedSiteList() {
const storedArray = await this.getStoredItem("deletedSiteList");
return (storedArray) ? storedArray : [];
},
async getAssignedSites() {
const allSyncStorage = await this.get();
const sites = {};
for (const storageKey of Object.keys(allSyncStorage)) {
if (storageKey.includes("siteContainerMap@@_")) {
sites[storageKey] = allSyncStorage[storageKey];
}
}
return sites;
},
async getStoredItem(objectKey) {
const outputObject = await this.get(objectKey);
if (outputObject && outputObject[objectKey])
return outputObject[objectKey];
return false;
},
async getAllInstanceInfo() {
const instanceList = {};
const allSyncInfo = await this.get();
for (const objectKey of Object.keys(allSyncInfo)) {
if (objectKey.includes("MACinstance")) {
instanceList[objectKey] = allSyncInfo[objectKey]; }
}
return instanceList;
},
getInstanceKey() {
return browser.runtime.getURL("")
.replace(/moz-extension:\/\//, "MACinstance:")
.replace(/\//, "");
},
async removeInstance(installUUID) {
if (SYNC_DEBUG) console.log("removing", installUUID);
await this.area.remove(installUUID);
return;
},
async removeThisInstanceFromSync() {
const installUUID = this.getInstanceKey();
await this.removeInstance(installUUID);
return;
},
async hasSyncStorage(){
const inSync = await this.get();
return !(Object.entries(inSync).length === 0);
},
async backup(options) {
// remove listeners to avoid an infinite loop!
await sync.checkForListenersMaybeRemove();
const identities = await updateSyncIdentities();
const siteAssignments = await updateSyncSiteAssignments();
await updateInstanceInfo(identities, siteAssignments);
if (options && options.uuid)
await this.deleteIdentity(options.uuid);
if (options && options.undeleteUUID)
await removeFromDeletedIdentityList(options.undeleteUUID);
if (options && options.siteStoreKey)
await this.deleteSite(options.siteStoreKey);
if (options && options.undeleteSiteStoreKey)
await removeFromDeletedSitesList(options.undeleteSiteStoreKey);
if (SYNC_DEBUG) console.log("Backed up!");
await sync.checkForListenersMaybeAdd();
async function updateSyncIdentities() {
const identities = await browser.contextualIdentities.query({});
for (const identity of identities) {
delete identity.colorCode;
delete identity.iconUrl;
identity.macAddonUUID = await identityState.lookupMACaddonUUID(identity.cookieStoreId);
if(identity.macAddonUUID) {
const storageKey = "identity@@_" + identity.macAddonUUID;
await sync.storageArea.set({ [storageKey]: identity });
}
}
//await sync.storageArea.set({ identities });
return identities;
}
async function updateSyncSiteAssignments() {
const assignedSites =
await assignManager.storageArea.getAssignedSites();
for (const siteKey of Object.keys(assignedSites)) {
await sync.storageArea.set({ [siteKey]: assignedSites[siteKey] });
}
return assignedSites;
}
async function updateInstanceInfo(identitiesInput, siteAssignmentsInput) {
const date = new Date();
const timestamp = date.getTime();
const installUUID = sync.storageArea.getInstanceKey();
if (SYNC_DEBUG) console.log("adding", installUUID);
const identities = [];
const siteAssignments = [];
for (const identity of identitiesInput) {
identities.push(identity.macAddonUUID);
}
for (const siteAssignmentKey of Object.keys(siteAssignmentsInput)) {
siteAssignments.push(siteAssignmentKey);
}
await sync.storageArea.set({ [installUUID]: { timestamp, identities, siteAssignments } });
}
async function removeFromDeletedIdentityList(identityUUID) {
const deletedIdentityList =
await sync.storageArea.getDeletedIdentityList();
const newDeletedIdentityList = deletedIdentityList
.filter(element => element !== identityUUID);
await sync.storageArea.set({ deletedIdentityList: newDeletedIdentityList });
}
async function removeFromDeletedSitesList(siteStoreKey) {
const deletedSiteList =
await sync.storageArea.getDeletedSiteList();
const newDeletedSiteList = deletedSiteList
.filter(element => element !== siteStoreKey);
await sync.storageArea.set({ deletedSiteList: newDeletedSiteList });
}
},
onChangedListener(changes, areaName) {
if (areaName === "sync") sync.errorHandledRunSync();
},
async addToDeletedList(changeInfo) {
const identity = changeInfo.contextualIdentity;
const deletedUUID =
await identityState.lookupMACaddonUUID(identity.cookieStoreId);
await identityState.storageArea.remove(identity.cookieStoreId);
sync.storageArea.backup({uuid: deletedUUID});
}
},
async init() {
const syncEnabled = await assignManager.storageArea.getSyncEnabled();
if (syncEnabled) {
// Add listener to sync storage and containers.
// Works for all installs that have any sync storage.
// Waits for sync storage change before kicking off the restore/backup
// initial sync must be kicked off by user.
this.checkForListenersMaybeAdd();
return;
}
this.checkForListenersMaybeRemove();
},
async errorHandledRunSync () {
await sync.runSync().catch( async (error)=> {
if (SYNC_DEBUG) console.error("Error from runSync", error);
await sync.checkForListenersMaybeAdd();
});
},
async checkForListenersMaybeAdd() {
const hasStorageListener =
await browser.storage.onChanged.hasListener(
sync.storageArea.onChangedListener
);
const hasCIListener = await sync.hasContextualIdentityListeners();
if (!hasCIListener) {
await sync.addContextualIdentityListeners();
}
if (!hasStorageListener) {
await browser.storage.onChanged.addListener(
sync.storageArea.onChangedListener);
}
},
async checkForListenersMaybeRemove() {
const hasStorageListener =
await browser.storage.onChanged.hasListener(
sync.storageArea.onChangedListener
);
const hasCIListener = await sync.hasContextualIdentityListeners();
if (hasCIListener) {
await sync.removeContextualIdentityListeners();
}
if (hasStorageListener) {
await browser.storage.onChanged.removeListener(
sync.storageArea.onChangedListener);
}
},
async runSync() {
if (SYNC_DEBUG) {
const syncInfo = await sync.storageArea.get();
const localInfo = await browser.storage.local.get();
const idents = await browser.contextualIdentities.query({});
console.log("Initial State:", {syncInfo, localInfo, idents});
}
await sync.checkForListenersMaybeRemove();
if (SYNC_DEBUG) console.log("runSync");
await identityState.storageArea.upgradeData();
await assignManager.storageArea.upgradeData();
const hasSyncStorage = await sync.storageArea.hasSyncStorage();
if (hasSyncStorage) await restore();
await sync.storageArea.backup();
await removeOldDeletedItems();
return;
},
async addContextualIdentityListeners() {
await browser.contextualIdentities.onCreated.addListener(sync.storageArea.backup);
await browser.contextualIdentities.onRemoved.addListener(sync.storageArea.addToDeletedList);
await browser.contextualIdentities.onUpdated.addListener(sync.storageArea.backup);
},
async removeContextualIdentityListeners() {
await browser.contextualIdentities.onCreated.removeListener(sync.storageArea.backup);
await browser.contextualIdentities.onRemoved.removeListener(sync.storageArea.addToDeletedList);
await browser.contextualIdentities.onUpdated.removeListener(sync.storageArea.backup);
},
async hasContextualIdentityListeners() {
return (
await browser.contextualIdentities.onCreated.hasListener(sync.storageArea.backup) &&
await browser.contextualIdentities.onRemoved.hasListener(sync.storageArea.addToDeletedList) &&
await browser.contextualIdentities.onUpdated.hasListener(sync.storageArea.backup)
);
},
async resetSync() {
const syncEnabled = await assignManager.storageArea.getSyncEnabled();
if (syncEnabled) {
this.errorHandledRunSync();
return;
}
await this.checkForListenersMaybeRemove();
await this.storageArea.removeThisInstanceFromSync();
}
};
// attaching to window for use in mocha tests
window.sync = sync;
sync.init();
async function restore() {
if (SYNC_DEBUG) console.log("restore");
await reconcileIdentities();
await reconcileSiteAssignments();
return;
}
/*
* Checks for the container name. If it exists, they are assumed to be the
* same container, and the color and icon are overwritten from sync, if
* different.
*/
async function reconcileIdentities(){
if (SYNC_DEBUG) console.log("reconcileIdentities");
// first delete any from the deleted list
const deletedIdentityList =
await sync.storageArea.getDeletedIdentityList();
// first remove any deleted identities
for (const deletedUUID of deletedIdentityList) {
const deletedCookieStoreId =
await identityState.lookupCookieStoreId(deletedUUID);
if (deletedCookieStoreId){
try{
await browser.contextualIdentities.remove(deletedCookieStoreId);
} catch (error) {
// if the identity we are deleting is not there, that's fine.
console.error("Error deleting contextualIdentity", deletedCookieStoreId);
continue;
}
}
}
const localIdentities = await browser.contextualIdentities.query({});
const syncIdentitiesRemoveDupes =
await sync.storageArea.getIdentities();
// find any local dupes created on sync storage and delete from sync storage
for (const localIdentity of localIdentities) {
const syncIdentitiesOfName = syncIdentitiesRemoveDupes
.filter(identity => identity.name === localIdentity.name);
if (syncIdentitiesOfName.length > 1) {
const identityMatchingContextId = syncIdentitiesOfName
.find(identity => identity.cookieStoreId === localIdentity.cookieStoreId);
if (identityMatchingContextId)
await sync.storageArea.removeIdentityKeyFromSync(identityMatchingContextId.macAddonUUID);
}
}
const syncIdentities =
await sync.storageArea.getIdentities();
// now compare all containers for matching names.
for (const syncIdentity of syncIdentities) {
if (syncIdentity.macAddonUUID){
const localMatch = localIdentities.find(
localIdentity => localIdentity.name === syncIdentity.name
);
if (!localMatch) {
// if there's no name match found, check on uuid,
const localCookieStoreID =
await identityState.lookupCookieStoreId(syncIdentity.macAddonUUID);
if (localCookieStoreID) {
await ifUUIDMatch(syncIdentity, localCookieStoreID);
continue;
}
await ifNoMatch(syncIdentity);
continue;
}
// Names match, so use the info from Sync
await updateIdentityWithSyncInfo(syncIdentity, localMatch);
continue;
}
// if no macAddonUUID, there is a problem with the sync info and it needs to be ignored.
}
await updateSiteAssignmentUUIDs();
async function updateSiteAssignmentUUIDs(){
const sites = assignManager.storageArea.getAssignedSites();
for (const siteKey of Object.keys(sites)) {
await assignManager.storageArea.set(siteKey, sites[siteKey]);
}
}
}
async function updateIdentityWithSyncInfo(syncIdentity, localMatch) {
// Sync is truth. if there is a match, compare data and update as needed
if (syncIdentity.color !== localMatch.color
|| syncIdentity.icon !== localMatch.icon) {
await browser.contextualIdentities.update(
localMatch.cookieStoreId, {
name: syncIdentity.name,
color: syncIdentity.color,
icon: syncIdentity.icon
});
if (SYNC_DEBUG) {
if (localMatch.color !== syncIdentity.color) {
console.log(localMatch.name, "Change color: ", syncIdentity.color);
}
if (localMatch.icon !== syncIdentity.icon) {
console.log(localMatch.name, "Change icon: ", syncIdentity.icon);
}
}
}
// Sync is truth. If all is the same, update the local uuid to match sync
if (localMatch.macAddonUUID !== syncIdentity.macAddonUUID) {
await identityState.updateUUID(
localMatch.cookieStoreId,
syncIdentity.macAddonUUID
);
}
// TODOkmw: update any site assignment UUIDs
}
async function ifUUIDMatch(syncIdentity, localCookieStoreID) {
// if there's an identical local uuid, it's the same container. Sync is truth
const identityInfo = {
name: syncIdentity.name,
color: syncIdentity.color,
icon: syncIdentity.icon
};
if (SYNC_DEBUG) {
try {
const getIdent =
await browser.contextualIdentities.get(localCookieStoreID);
if (getIdent.name !== identityInfo.name) {
console.log(getIdent.name, "Change name: ", identityInfo.name);
}
if (getIdent.color !== identityInfo.color) {
console.log(getIdent.name, "Change color: ", identityInfo.color);
}
if (getIdent.icon !== identityInfo.icon) {
console.log(getIdent.name, "Change icon: ", identityInfo.icon);
}
} catch (error) {
//if this fails, there is probably differing sync info.
console.error("Error getting info on CI", error);
}
}
try {
// update the local container with the sync data
await browser.contextualIdentities
.update(localCookieStoreID, identityInfo);
return;
} catch (error) {
// If this fails, sync info is off.
console.error("Error udpating CI", error);
}
}
async function ifNoMatch(syncIdentity){
// if no uuid match either, make new identity
if (SYNC_DEBUG) console.log("create new ident: ", syncIdentity.name);
const newIdentity =
await browser.contextualIdentities.create({
name: syncIdentity.name,
color: syncIdentity.color,
icon: syncIdentity.icon
});
await identityState.updateUUID(
newIdentity.cookieStoreId,
syncIdentity.macAddonUUID
);
return;
}
/*
* Checks for site previously assigned. If it exists, and has the same
* container assignment, the assignment is kept. If it exists, but has
* a different assignment, the user is prompted (not yet implemented).
* If it does not exist, it is created.
*/
async function reconcileSiteAssignments() {
if (SYNC_DEBUG) console.log("reconcileSiteAssignments");
const assignedSitesLocal =
await assignManager.storageArea.getAssignedSites();
const assignedSitesFromSync =
await sync.storageArea.getAssignedSites();
const deletedSiteList =
await sync.storageArea.getDeletedSiteList();
for(const siteStoreKey of deletedSiteList) {
if (Object.prototype.hasOwnProperty.call(assignedSitesLocal,siteStoreKey)) {
await assignManager
.storageArea
.remove(siteStoreKey, false);
}
}
for(const urlKey of Object.keys(assignedSitesFromSync)) {
const assignedSite = assignedSitesFromSync[urlKey];
try{
if (assignedSite.identityMacAddonUUID) {
// Sync is truth.
// Not even looking it up. Just overwrite
if (SYNC_DEBUG){
const isInStorage = await assignManager.storageArea.getByUrlKey(urlKey);
if (!isInStorage)
console.log("new assignment ", assignedSite);
}
await setAssignmentWithUUID(assignedSite, urlKey);
continue;
}
} catch (error) {
// this is probably old or incorrect site info in Sync
// skip and move on.
}
}
}
const MILISECONDS_IN_THIRTY_DAYS = 2592000000;
async function removeOldDeletedItems() {
const instanceList = await sync.storageArea.getAllInstanceInfo();
const deletedSiteList = await sync.storageArea.getDeletedSiteList();
const deletedIdentityList = await sync.storageArea.getDeletedIdentityList();
for (const instanceKey of Object.keys(instanceList)) {
const date = new Date();
const currentTimestamp = date.getTime();
if (instanceList[instanceKey].timestamp < currentTimestamp - MILISECONDS_IN_THIRTY_DAYS) {
delete instanceList[instanceKey];
sync.storageArea.removeInstance(instanceKey);
continue;
}
}
for (const siteStoreKey of deletedSiteList) {
let hasMatch = false;
for (const instance of Object.values(instanceList)) {
const match = instance.siteAssignments.find(element => element === siteStoreKey);
if (!match) continue;
hasMatch = true;
}
if (!hasMatch) {
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
}
}
for (const identityUUID of deletedIdentityList) {
let hasMatch = false;
for (const instance of Object.values(instanceList)) {
const match = instance.identities.find(element => element === identityUUID);
if (!match) continue;
hasMatch = true;
}
if (!hasMatch) {
await sync.storageArea.backup({undeleteUUID: identityUUID});
}
}
}
async function setAssignmentWithUUID(assignedSite, urlKey) {
const uuid = assignedSite.identityMacAddonUUID;
const cookieStoreId = await identityState.lookupCookieStoreId(uuid);
if (cookieStoreId) {
// eslint-disable-next-line require-atomic-updates
assignedSite.userContextId = cookieStoreId
.replace(/^firefox-container-/, "");
await assignManager.storageArea.set(
urlKey,
assignedSite,
false,
false
);
return;
}
throw new Error (`No cookieStoreId found for: ${uuid}, ${urlKey}`);
}

80
src/js/confirm-page.js Normal file
View file

@ -0,0 +1,80 @@
async function load() {
const searchParams = new URL(window.location).searchParams;
const redirectUrl = 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("deny").addEventListener("click", (e) => {
e.preventDefault();
denySubmit(redirectUrl);
});
const container = await browser.contextualIdentities.get(cookieStoreId);
const currentContainer = currentCookieStoreId ? await browser.contextualIdentities.get(currentCookieStoreId) : null;
const currentContainerName = currentContainer ? currentContainer.name : "";
document.querySelectorAll("[data-message-id]").forEach(el => {
const elementData = el.dataset;
const containerName = elementData.messageArg === "container-name" ? container.name : currentContainerName;
el.textContent = browser.i18n.getMessage(elementData.messageId, containerName);
});
document.getElementById("confirm").addEventListener("click", (e) => {
e.preventDefault();
confirmSubmit(redirectUrl, cookieStoreId);
});
}
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) {
browser.runtime.sendMessage({
method: "neverAsk",
neverAsk: true,
pageUrl: redirectUrl
});
}
openInContainer(redirectUrl, cookieStoreId);
}
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
});
document.location.replace(redirectUrl);
}
load();
async function openInContainer(redirectUrl, cookieStoreId) {
const tab = await getCurrentTab();
await browser.tabs.create({
index: tab[0].index + 1,
cookieStoreId,
url: redirectUrl
});
if (tab.length > 0) {
browser.tabs.remove(tab[0].id);
}
}

46
src/js/content-script.js Normal file
View file

@ -0,0 +1,46 @@
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");
// 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");
const imagePath = browser.runtime.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);
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);
});

9
src/js/i18n.js Normal file
View file

@ -0,0 +1,9 @@
document.addEventListener("DOMContentLoaded", async () => {
document.querySelectorAll("[data-i18n-message-id]").forEach(el => {
const messageArgs = el.dataset.i18nPlaceholder ? el.dataset.i18nPlaceholder : null;
el.textContent = browser.i18n.getMessage(el.dataset.i18nMessageId, [messageArgs]);
});
document.querySelectorAll("[data-i18n-attribute]").forEach(el => {
el.setAttribute(el.dataset.i18nAttribute, browser.i18n.getMessage(el.dataset.i18nAttributeMessageId));
});
});

260
src/js/mozillaVpn.js Normal file
View file

@ -0,0 +1,260 @@
const MozillaVPN = {
async handleContainerList(identities) {
const mozillaVpnConnected = await browser.runtime.sendMessage({ method: "MozillaVPN_getConnectionStatus" });
const mozillaVpnInstalled = await browser.runtime.sendMessage({ method: "MozillaVPN_getInstallationStatus" });
this.handleStatusIndicatorsInContainerLists(mozillaVpnInstalled);
const permissionsEnabled = await this.bothPermissionsEnabled();
if (!permissionsEnabled) {
return;
}
const proxies = await this.getProxies(identities);
if (Object.keys(proxies).length === 0) {
return;
}
const tooltipProxyWarning = browser.i18n.getMessage("tooltipWarning");
for (const el of document.querySelectorAll("[data-cookie-store-id]")) {
const cookieStoreId = el.dataset.cookieStoreId;
if (!proxies[cookieStoreId]) {
continue;
}
const { proxy } = proxies[cookieStoreId];
if (typeof(proxy) !== "undefined") {
const flag = el.querySelector(".flag-img");
if (proxy.countryCode) {
flag.src = `/img/flags/${proxy.countryCode.toUpperCase()}.png`;
}
if (typeof(proxy.mozProxyEnabled) === "undefined" && typeof(proxy.countryCode) !== "undefined") {
flag.classList.add("proxy-disabled");
}
if (!mozillaVpnConnected && proxy.mozProxyEnabled) {
flag.classList.add("proxy-unavailable");
const tooltip = el.querySelector(".tooltip.proxy-unavailable");
if (tooltip) {
tooltip.textContent = tooltipProxyWarning;
}
const menuItemName = el.querySelector(".menu-item-name");
if (menuItemName) {
el.querySelector(".menu-item-name").dataset.mozProxyWarning = "proxy-unavailable";
}
}
}
}
},
async setStatusIndicatorIcons(mozillaVpnInstalled) {
const statusIconEls = document.querySelectorAll(".moz-vpn-connection-status-indicator");
if (!mozillaVpnInstalled) {
statusIconEls.forEach(el => {
el.style.backgroundImage = "none";
if (el.querySelector(".tooltip")) {
el.querySelector(".tooltip").textContent = "";
}
el.textContent = "";
});
return;
}
const connectedIndicatorSrc = "url(./img/moz-vpn-connected.svg)";
const disconnectedIndicatorSrc = "url(./img/moz-vpn-disconnected.svg)";
const mozillaVpnConnected = await browser.runtime.sendMessage({ method: "MozillaVPN_getConnectionStatus" });
const connectionStatusStringId = mozillaVpnConnected ? "moz-vpn-connected" : "moz-vpn-disconnected";
const connectionStatusLocalizedString = browser.i18n.getMessage(connectionStatusStringId);
statusIconEls.forEach(el => {
el.style.backgroundImage = mozillaVpnConnected ? connectedIndicatorSrc : disconnectedIndicatorSrc;
if (el.querySelector(".tooltip")) {
el.querySelector(".tooltip").textContent = connectionStatusLocalizedString;
} else {
el.textContent = connectionStatusLocalizedString;
}
});
},
async handleStatusIndicatorsInContainerLists(mozillaVpnInstalled) {
const mozVpnLogotypes = document.querySelectorAll(".moz-vpn-logotype.vpn-status-container-list");
try {
if (!mozillaVpnInstalled) {
mozVpnLogotypes.forEach(el => {
el.style.display = "none";
});
return;
}
mozVpnLogotypes.forEach(el => {
el.style.display = "flex";
el.classList.remove("display-none");
});
this.setStatusIndicatorIcons(mozillaVpnInstalled);
} catch (e) {
mozVpnLogotypes.forEach(el => {
el.style.display = "none";
});
return;
}
},
handleMozillaCtaClick(buttonIdentifier) {
browser.tabs.create({
url: MozillaVPN.attachUtmParameters("https://www.mozilla.org/products/vpn", buttonIdentifier),
});
},
getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
proxyIsDisabled(proxy) {
return (
// Mozilla VPN proxy is disabled, last location data is stored
(proxy.mozProxyEnabled === undefined && proxy.countryCode !== undefined && proxy.cityName !== undefined) ||
// Mozilla VPN proxy is enabled but Mozilla VPN is not connected
proxy.mozProxyEnabled !== undefined
);
},
attachUtmParameters(baseUrl, utmContent) {
const url = new URL(baseUrl);
const utmParameters = {
utm_source: "multi.account.containers",
utm_medium: "mac-browser-addon",
utm_content: utmContent,
utm_campaign: "vpn-better-together",
};
for (const param in utmParameters) {
url.searchParams.append(param, utmParameters[param]);
}
return url.href;
},
async getProxies(identities) {
const proxies = {};
const mozillaVpnInstalled = await browser.runtime.sendMessage({ method: "MozillaVPN_getInstallationStatus" });
if (mozillaVpnInstalled) {
for (const identity of identities) {
try {
const proxy = await proxifiedContainers.retrieve(identity.cookieStoreId);
proxies[identity.cookieStoreId] = proxy;
} catch (e) {
proxies[identity.cookieStoreId] = {};
}
}
}
return proxies;
},
getMozillaProxyInfoObj() {
return {
countryCode: undefined,
cityName: undefined,
mozProxyEnabled: undefined
};
},
async bothPermissionsEnabled() {
return await browser.permissions.contains({ permissions: ["proxy", "nativeMessaging"] });
},
async getProxyWarnings(proxyObj) {
if (!proxyObj) {
return "";
}
const { proxy } = proxyObj;
if (typeof(proxy) === "undefined") {
return "";
}
const mozillaVpnConnected = await browser.runtime.sendMessage({ method: "MozillaVPN_getConnectionStatus" });
if (typeof(proxy.mozProxyEnabled) !== "undefined" && !mozillaVpnConnected) {
return "proxy-unavailable";
}
},
async getFlag(proxyObj) {
const flag = {
imgCode: "default",
elemClasses: "display-none",
imgAlt: "",
};
if (!proxyObj) {
return flag;
}
const { proxy } = proxyObj;
const mozillaVpnInstalled = await browser.runtime.sendMessage({ method: "MozillaVPN_getInstallationStatus" });
if (typeof(proxy) === "undefined" || !mozillaVpnInstalled) {
return flag;
}
const mozillaVpnConnected = await browser.runtime.sendMessage({ method: "MozillaVPN_getConnectionStatus" });
if (mozillaVpnInstalled && typeof(proxy.cityName) !== "undefined") {
flag.imgCode = proxy.countryCode.toUpperCase();
flag.imgAlt = proxy.cityName;
flag.elemClasses = typeof(proxy.mozProxyEnabled) === "undefined" || !mozillaVpnConnected ? "proxy-disabled" : "";
}
return flag;
},
getProxy(countryCode, cityName, mozProxyEnabled, mozillaVpnServers) {
const selectedServerCountry = mozillaVpnServers.find(({code}) => code === countryCode);
const selectedServerCity = selectedServerCountry.cities.find(({name}) => name === cityName);
const proxyServer = this.pickServerBasedOnWeight(selectedServerCity.servers);
return proxifiedContainers.parseProxy(
this.makeProxyString(proxyServer.socksName),
{
countryCode: countryCode,
cityName: cityName,
mozProxyEnabled,
}
);
},
makeProxyString(socksName) {
return `socks://${socksName}.mullvad.net:1080`;
},
async pickRandomLocation() {
const { mozillaVpnServers } = await browser.storage.local.get("mozillaVpnServers");
const randomInteger = this.getRandomInteger(0, mozillaVpnServers.length - 1);
const randomServerCountry = mozillaVpnServers[randomInteger];
return {
randomServerCountryCode: randomServerCountry.code,
randomServerCityName: randomServerCountry.cities[0].name,
};
},
pickServerBasedOnWeight(serverList) {
const filteredServerList = serverList.filter(server => typeof(server.socksName) !== "undefined" && server.socksName !== "");
const sumWeight = filteredServerList.reduce((sum, { weight }) => sum + weight, 0);
let randomInteger = this.getRandomInteger(0, sumWeight);
let nextServer = {};
for (const server of filteredServerList) {
if (server.weight >= randomInteger) {
return nextServer = server;
}
randomInteger = (randomInteger - server.weight);
}
return nextServer;
},
};
window.MozillaVPN = MozillaVPN;

137
src/js/options.js Normal file
View file

@ -0,0 +1,137 @@
const NUMBER_OF_KEYBOARD_SHORTCUTS = 10;
async function setUpCheckBoxes() {
document.querySelectorAll("[data-permission-id]").forEach(async(el) => {
const permissionId = el.dataset.permissionId;
const permissionEnabled = await browser.permissions.contains({ permissions: [permissionId] });
el.checked = !!permissionEnabled;
});
}
function disablePermissionsInputs() {
document.querySelectorAll("[data-permission-id").forEach(el => {
el.disabled = true;
});
}
function enablePermissionsInputs() {
document.querySelectorAll("[data-permission-id").forEach(el => {
el.disabled = false;
});
}
document.querySelectorAll("[data-permission-id").forEach(async(el) => {
const permissionId = el.dataset.permissionId;
el.addEventListener("change", async() => {
if (el.checked) {
disablePermissionsInputs();
const granted = await browser.permissions.request({ permissions: [permissionId] });
if (!granted) {
el.checked = false;
enablePermissionsInputs();
}
return;
}
await browser.permissions.remove({ permissions: [permissionId] });
});
});
async function maybeShowPermissionsWarningIcon() {
const bothMozillaVpnPermissionsEnabled = await MozillaVPN.bothPermissionsEnabled();
const permissionsWarningEl = document.querySelector(".warning-icon");
permissionsWarningEl.classList.toggle("show-warning", !bothMozillaVpnPermissionsEnabled);
}
async function enableDisableSync() {
const checkbox = document.querySelector("#syncCheck");
await browser.storage.local.set({syncEnabled: !!checkbox.checked});
browser.runtime.sendMessage({ method: "resetSync" });
}
async function enableDisableReplaceTab() {
const checkbox = document.querySelector("#replaceTabCheck");
await browser.storage.local.set({replaceTabEnabled: !!checkbox.checked});
}
async function setupOptions() {
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
const { replaceTabEnabled } = await browser.storage.local.get("replaceTabEnabled");
document.querySelector("#syncCheck").checked = !!syncEnabled;
document.querySelector("#replaceTabCheck").checked = !!replaceTabEnabled;
setupContainerShortcutSelects();
}
async function setupContainerShortcutSelects () {
const keyboardShortcut = await browser.runtime.sendMessage({method: "getShortcuts"});
const identities = await browser.contextualIdentities.query({});
const fragment = document.createDocumentFragment();
const noneOption = document.createElement("option");
noneOption.value = "none";
noneOption.id = "none";
noneOption.textContent = "None";
fragment.append(noneOption);
for (const identity of identities) {
const option = document.createElement("option");
option.value = identity.cookieStoreId;
option.id = identity.cookieStoreId;
option.textContent = identity.name;
fragment.append(option);
}
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const shortcutKey = "open_container_"+i;
const shortcutSelect = document.getElementById(shortcutKey);
shortcutSelect.appendChild(fragment.cloneNode(true));
if (keyboardShortcut && keyboardShortcut[shortcutKey]) {
const cookieStoreId = keyboardShortcut[shortcutKey];
shortcutSelect.querySelector("#" + cookieStoreId).selected = true;
}
}
}
function storeShortcutChoice (event) {
browser.runtime.sendMessage({
method: "setShortcut",
shortcut: event.target.id,
cookieStoreId: event.target.value
});
}
function resetOnboarding() {
browser.storage.local.set({"onboarding-stage": 0});
}
async function resetPermissionsUi() {
await maybeShowPermissionsWarningIcon();
await setUpCheckBoxes();
enablePermissionsInputs();
}
browser.permissions.onAdded.addListener(resetPermissionsUi);
browser.permissions.onRemoved.addListener(resetPermissionsUi);
document.addEventListener("DOMContentLoaded", setupOptions);
document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync);
document.querySelector("#replaceTabCheck").addEventListener( "change", enableDisableReplaceTab);
maybeShowPermissionsWarningIcon();
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
document.querySelector("#open_container_"+i)
.addEventListener("change", storeShortcutChoice);
}
document.querySelectorAll("[data-btn-id]").forEach(btn => {
btn.addEventListener("click", () => {
switch (btn.dataset.btnId) {
case "reset-onboarding":
resetOnboarding();
break;
case "moz-vpn-learn-more":
browser.tabs.create({
url: MozillaVPN.attachUtmParameters("https://support.mozilla.org/kb/protect-your-container-tabs-mozilla-vpn", "options-learn-more")
});
break;
}
});
});
resetPermissionsUi();

37
src/js/pageAction.js Normal file
View file

@ -0,0 +1,37 @@
async function init() {
const fragment = document.createDocumentFragment();
const identities = await browser.contextualIdentities.query({});
for (const identity of identities) {
const tr = document.createElement("tr");
tr.classList.add("menu-item", "hover-highlight");
tr.setAttribute("data-cookie-store-id", identity.cookieStoreId);
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}">
</div>
</div>
<span class="menu-text">${identity.name}</span>
<img alt="" class="page-action-flag flag-img" src="/img/flags/.png"/>
`;
tr.appendChild(td);
fragment.appendChild(tr);
Utils.addEnterHandler(tr, async () => {
Utils.alwaysOpenInContainer(identity);
window.close();
});
}
const list = document.querySelector("#picker-identities-list");
list.innerHTML = "";
list.appendChild(fragment);
MozillaVPN.handleContainerList(identities);
}
init();

2363
src/js/popup.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,76 @@
// This object allows other scripts to access the list mapping containers to their proxies
proxifiedContainers = {
async retrieveAll() {
const result = await browser.storage.local.get("proxifiedContainersKey");
if(!result || !result["proxifiedContainersKey"]) {
return null;
}
return result["proxifiedContainersKey"];
},
async retrieve(cookieStoreId) {
const result = await this.retrieveAll();
if(!result) {
return null;
}
return result.find(o => o.cookieStoreId === cookieStoreId);
},
async set(cookieStoreId, proxy) {
// Assumes proxy is a properly formatted object
let proxifiedContainersStore = await proxifiedContainers.retrieveAll();
if (!proxifiedContainersStore) proxifiedContainersStore = [];
const index = proxifiedContainersStore.findIndex(i => i.cookieStoreId === cookieStoreId);
if (index === -1) {
proxifiedContainersStore.push({
cookieStoreId: cookieStoreId,
proxy: proxy
});
} else {
proxifiedContainersStore[index] = {
cookieStoreId: cookieStoreId,
proxy: proxy
};
}
await browser.storage.local.set({
proxifiedContainersKey: proxifiedContainersStore
});
},
// Parses a proxy description string of the format type://host[:port] or type://username:password@host[:port] (port is optional)
parseProxy(proxy_str, mozillaVpnData = null) {
const proxyRegexp = /(?<type>(https?)|(socks4?)):\/\/(\b(?<username>\w+):(?<password>\w+)@)?(?<host>((?:\d{1,3}\.){3}\d{1,3}\b)|(\b([\w.-]+)+))(:(?<port>\d+))?/;
const matches = proxyRegexp.exec(proxy_str);
if (!matches) {
return false;
}
if (mozillaVpnData && mozillaVpnData.mozProxyEnabled === undefined) {
matches.groups.type = null;
}
if (!mozillaVpnData) {
mozillaVpnData = MozillaVPN.getMozillaProxyInfoObj();
}
return {...matches.groups,...mozillaVpnData};
},
// Deletes the proxy information object for a specified cookieStoreId [useful for cleaning]
async delete(cookieStoreId) {
// Assumes proxy is a properly formatted object
const proxifiedContainersStore = await proxifiedContainers.retrieveAll();
const index = proxifiedContainersStore.findIndex(i => i.cookieStoreId === cookieStoreId);
if (index !== -1) {
proxifiedContainersStore.splice(index, 1);
}
await browser.storage.local.set({
proxifiedContainersKey: proxifiedContainersStore
});
}
};

182
src/js/utils.js Normal file
View file

@ -0,0 +1,182 @@
/*global getBogusProxy */
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
// eslint-disable-next-line
const CONTAINER_ORDER_STORAGE_KEY = "container-order";
// TODO use export here instead of globals
const Utils = {
createFavIconElement(url) {
const imageElement = document.createElement("img");
imageElement.classList.add("icon", "offpage", "menu-icon");
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;
},
// See comment in PR #313 - so far the (hacky) method being used to block proxies is to produce a sufficiently long random address
getBogusProxy() {
const bogusFailover = 1;
const bogusType = "socks4";
const bogusPort = 9999;
const bogusUsername = "foo";
if(typeof window.Utils.pregeneratedString !== "undefined")
{
return {type:bogusType, host:`w.${window.Utils.pregeneratedString}.coo`, port:bogusPort, username:bogusUsername, failoverTimeout:bogusFailover};
}
else
{
// Initialize Utils.pregeneratedString
window.Utils.pregeneratedString = "";
// We generate a cryptographically random string (of length specified in bogusLength), but we only do so once - thus negating any time delay caused
const bogusLength = 8;
const array = new Uint8Array(bogusLength);
window.crypto.getRandomValues(array);
for(let i = 0; i < bogusLength; i++)
{
const s = array[i].toString(16);
if(s.length === 1)
window.Utils.pregeneratedString += `0${s}`;
else
window.Utils.pregeneratedString += s;
}
// The only issue with this approach is that if (for some unknown reason) pregeneratedString is not saved, it will result in an infinite loop - but better than a privacy leak!
return getBogusProxy();
}
},
/**
* Escapes any occurances of &, ", <, > or / with XML entities.
*
* @param {string} str
* The string to escape.
* @return {string} The escaped string.
*/
escapeXML(str) {
const replacements = { "&": "&amp;", "\"": "&quot;", "'": "&apos;", "<": "&lt;", ">": "&gt;", "/": "&#x2F;" };
return String(str).replace(/[&"'<>/]/g, m => replacements[m]);
},
/**
* A tagged template function which escapes any XML metacharacters in
* interpolated values.
*
* @param {Array<string>} strings
* An array of literal strings extracted from the templates.
* @param {Array} values
* An array of interpolated values extracted from the template.
* @returns {string}
* The result of the escaped values interpolated with the literal
* strings.
*/
escaped(strings, ...values) {
const result = [];
for (const [i, string] of strings.entries()) {
result.push(string);
if (i < values.length)
result.push(this.escapeXML(values[i]));
}
return result.join("");
},
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;
},
addEnterHandler(element, handler) {
element.addEventListener("click", (e) => {
handler(e);
});
element.addEventListener("keydown", (e) => {
if (e.keyCode === 13) {
e.preventDefault();
handler(e);
}
});
},
addEnterOnlyHandler(element, handler) {
element.addEventListener("keydown", (e) => {
if (e.keyCode === 13) {
e.preventDefault();
handler(e);
}
});
},
userContextId(cookieStoreId = "") {
const userContextId = cookieStoreId.replace("firefox-container-", "");
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
},
setOrRemoveAssignment(tabId, url, userContextId, value) {
return browser.runtime.sendMessage({
method: "setOrRemoveAssignment",
tabId,
url,
userContextId,
value
});
},
setWildcardHostnameForAssignment(url, wildcardHostname) {
return browser.runtime.sendMessage({
method: "setWildcardHostnameForAssignment",
url,
wildcardHostname
});
},
async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
return await browser.runtime.sendMessage({
method: "reloadInContainer",
url,
currentUserContextId,
newUserContextId,
tabIndex,
active
});
},
async alwaysOpenInContainer(identity) {
const currentTab = await this.currentTab();
const assignedUserContextId = this.userContextId(identity.cookieStoreId);
if (currentTab.cookieStoreId !== identity.cookieStoreId) {
return await browser.runtime.sendMessage({
method: "assignAndReloadInContainer",
url: currentTab.url,
currentUserContextId: false,
newUserContextId: assignedUserContextId,
tabIndex: currentTab.index +1,
active:currentTab.active
});
}
await Utils.setOrRemoveAssignment(
currentTab.id,
currentTab.url,
assignedUserContextId,
false
);
},
};
window.Utils = Utils;

155
src/manifest.json Normal file
View file

@ -0,0 +1,155 @@
{
"manifest_version": 2,
"name": "Multi-Account Containers (Wildcard)",
"version": "8.0.7.1",
"incognito": "not_allowed",
"description": "__MSG_extensionDescription__",
"icons": {
"48": "img/container-site-d-48.png",
"96": "img/container-site-d-96.png"
},
"homepage_url": "https://github.com/mckenfra/multi-account-containers/tree/8.0.7.1-WildcardSubdomains",
"permissions": [
"<all_urls>",
"activeTab",
"cookies",
"contextMenus",
"contextualIdentities",
"history",
"idle",
"management",
"storage",
"unlimitedStorage",
"tabs",
"webRequestBlocking",
"webRequest"
],
"optional_permissions": [
"bookmarks",
"nativeMessaging",
"proxy"
],
"browser_specific_settings": {
"gecko": {
"id": "@testpilot-containers-wildcard",
"strict_min_version": "91.1.0"
}
},
"commands": {
"_execute_browser_action": {
"suggested_key": {
"default": "Ctrl+Period",
"mac": "MacCtrl+Period"
},
"description": "Open containers panel"
},
"open_container_0": {
"suggested_key": {
"default": "Ctrl+Shift+1"
},
"description": "Container Shortcut 1"
},
"open_container_1": {
"suggested_key": {
"default": "Ctrl+Shift+2"
},
"description": "Container Shortcut 2"
},
"open_container_2": {
"suggested_key": {
"default": "Ctrl+Shift+3"
},
"description": "Container Shortcut 3"
},
"open_container_3": {
"suggested_key": {
"default": "Ctrl+Shift+4"
},
"description": "Container Shortcut 4"
},
"open_container_4": {
"suggested_key": {
"default": "Ctrl+Shift+5"
},
"description": "Container Shortcut 5"
},
"open_container_5": {
"suggested_key": {
"default": "Ctrl+Shift+6"
},
"description": "Container Shortcut 6"
},
"open_container_6": {
"suggested_key": {
"default": "Ctrl+Shift+7"
},
"description": "Container Shortcut 7"
},
"open_container_7": {
"suggested_key": {
"default": "Ctrl+Shift+8"
},
"description": "Container Shortcut 8"
},
"open_container_8": {
"suggested_key": {
"default": "Ctrl+Shift+9"
},
"description": "Container Shortcut 9"
},
"open_container_9": {
"suggested_key": {
"default": "Ctrl+Shift+0"
},
"description": "Container Shortcut 10"
}
},
"browser_action": {
"browser_style": true,
"default_icon": "img/multiaccountcontainer-16.svg",
"default_title": "Multi-Account Containers",
"default_popup": "popup.html",
"theme_icons": [
{
"light": "img/multiaccountcontainer-16-dark.svg",
"dark": "img/multiaccountcontainer-16.svg",
"size": 32
}
]
},
"page_action": {
"browser_style": true,
"default_icon": "img/container-openin-16.svg",
"default_title": "Always open this in a Container",
"default_popup": "pageActionPopup.html",
"pinned": false,
"show_matches": [
"*://*/*"
]
},
"background": {
"page": "js/background/index.html"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"js/content-script.js"
],
"css": [
"css/content.css"
],
"run_at": "document_start"
}
],
"default_locale": "en",
"web_accessible_resources": [
"/img/container-site-d-24.png"
],
"options_ui": {
"page": "options.html",
"browser_style": true
}
}

123
src/options.html Normal file
View file

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="./js/i18n.js"></script>
<script type="text/javascript" src="./js/mozillaVpn.js"></script>
<script type="text/javascript" src="./js/proxified-containers.js"></script>
<link rel="stylesheet" href="css/options.css">
</head>
<body>
<form>
<h3 data-i18n-message-id="optionalPermissions"></h3>
<div class="settings-group">
<label class="permission">
<input type="checkbox" data-permission-id="bookmarks" id="bookmarksPermissions">
<span class="bold" data-i18n-message-id="enableBookMarkMenus"></span>
</label>
<p><em data-i18n-message-id="enableBookMarkMenusDescription"></em></p>
</div>
<div id="moz-vpn-proxy-permissions" class="moz-vpn-proxy-permissions">
<h3 class="moz-vpn-proxy-permissions-title">
<span data-i18n-message-id="mozillaVpnAndProxyPermissionsTitle" class="warning-icon"></span>
</h3>
<div class="moz-vpn-proxy-permissions-content">
<div class="settings-group">
<label class="permission">
<input type="checkbox" data-permission-id="nativeMessaging">
<span class="bold" data-i18n-message-id="nativeMessagingPermissionTitle"></span>
</label>
<p><em data-i18n-message-id="nativeMessagingPermissionDescription"></em></p>
</div>
<div class="settings-group">
<label class="permission">
<input type="checkbox" data-permission-id="proxy">
<span class="bold" data-i18n-message-id="proxyPermissionTitle"></span>
</label>
<p><em data-i18n-message-id="proxyPermissionDescription"></em></p>
</div>
</div>
</div>
<h3 data-i18n-message-id="firefoxAccountsSync"></h3>
<div class="settings-group">
<label>
<input type="checkbox" id="syncCheck">
<span class="bold" data-i18n-message-id="enableSync"></span>
</label>
<p><em data-i18n-message-id="enableSyncDescription"></em></p>
</div>
<h3 data-i18n-message-id="tabBehavior"></h3>
<div class="settings-group">
<label>
<input type="checkbox" id="replaceTabCheck">
<span class="bold" data-i18n-message-id="replaceTab"></span>
</label>
<p><em data-i18n-message-id="replaceTabDescription"></em></p>
</div>
<h3 data-i18n-message-id="keyboardShortCuts"></h3>
<p><em data-i18n-message-id="editWhichContainer"></em></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="1"></span>
<select id="open_container_0">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="2"></span>
<select id="open_container_1">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="3"></span>
<select id="open_container_2">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="4"></span>
<select id="open_container_3">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="5"></span>
<select id="open_container_4">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="6"></span>
<select id="open_container_5">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="7"></span>
<select id="open_container_6">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="8"></span>
<select id="open_container_7">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="9"></span>
<select id="open_container_8">
</select>
</label></p>
<p><label class="keyboard-shortcut">
<span data-i18n-message-id="keyboardShortCut" data-i18n-placeholder="10"></span>
<select id="open_container_9">
</select>
</label></p>
<h3 data-i18n-message-id="onboarding"></h3>
<button data-btn-id="reset-onboarding" data-i18n-message-id="resetOnboardingPanels"></button>
<p><em data-i18n-message-id="onboardingToggle"></em></p>
<h3>Mozilla VPN</h3>
<button data-btn-id="moz-vpn-learn-more" data-i18n-message-id="learnMore"></button>
</form>
<script src="js/options.js"></script>
</body>
</html>

35
src/pageActionPopup.html Normal file
View file

@ -0,0 +1,35 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Multi-Account Containers</title>
<script type="text/javascript" src="./js/i18n.js"></script>
<link rel="stylesheet" type="text/css" href="css/popup.css">
</head>
<body>
<div class="page-action-container-picker" id="container-picker-panel">
<h3 class="title" data-i18n-message-id="alwaysOpenThisSiteIn"></h3>
<hr>
<div class="scrollable identities-list">
<table class="menu" id="picker-identities-list">
<tr class="menu-item hover-highlight">
<td>
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="pet"
data-identity-color="blue">
</div>
</div>
<span class="menu-text" data-i18n-message-id="default"></span>
</td>
</tr>
</table>
</div>
</div>
<script src="js/mozillaVpn.js"></script>
<script src="js/utils.js"></script>
<script src="js/pageAction.js"></script>
<script src="js/proxified-containers.js"></script>
</body>
</html>

460
src/popup.html Normal file
View file

@ -0,0 +1,460 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Multi-Account Containers</title>
<script type="text/javascript" src="./js/i18n.js"></script>
<link rel="stylesheet" href="./css/popup.css">
</head>
<body>
<div class="hide panel onboarding onboarding-panel-1">
<img class="onboarding-img" alt="" src="/img/onboarding-1.png" />
<h3 class="onboarding-title" data-i18n-message-id="onboarding-1-header"></h3>
<p data-i18n-message-id="onboarding-1-description"></p>
<a href="#" class="onboarding-button onboarding-start-button keyboard-nav" tabindex="0" data-i18n-message-id="getStarted"></a>
</div>
<div class="panel onboarding onboarding-panel-2 hide">
<img class="onboarding-img" alt="" src="/img/onboarding-2.png" />
<h3 class="onboarding-title" data-i18n-message-id="onboarding-2-header"></h3>
<p data-i18n-message-id="onboarding-2-description"></p>
<a href="#" class="onboarding-button onboarding-next-button keyboard-nav" tabindex="0" data-i18n-message-id="next"></a>
</div>
<div class="panel onboarding onboarding-panel-3 hide">
<img class="onboarding-img" alt="" src="/img/onboarding-3.png" />
<h3 class="onboarding-title" data-i18n-message-id="onboarding-3-header"></h3>
<p data-i18n-message-id="onboarding-3-description"></p>
<a href="#" class="onboarding-button onboarding-almost-done-button keyboard-nav" tabindex="0" data-i18n-message-id="next"></a>
</div>
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
<img class="onboarding-img" alt="" src="/img/onboarding-4.png" />
<h3 class="onboarding-title" data-i18n-message-id="onboarding-4-header"></h3>
<p data-i18n-message-id="onboarding-4-description"></p>
<a href="#" id="onboarding-done-button" class="onboarding-button keyboard-nav" tabindex="0" data-i18n-message-id="next"></a>
</div>
<div class="panel onboarding onboarding-panel-5 hide" id="onboarding-panel-5">
<img class="onboarding-img" alt="" src="/img/onboarding-3.png" />
<h3 class="onboarding-title" data-i18n-message-id="onboarding-5-header"></h3>
<p data-i18n-message-id="onboarding-5-description"></p>
<a href="#" id="onboarding-longpress-button" class="onboarding-button keyboard-nav" tabindex="0" data-i18n-message-id="next"></a>
</div>
<div class="panel onboarding onboarding-panel-6 hide" id="onboarding-panel-6">
<img class="onboarding-img" alt="" src="/img/Sync.svg" />
<h3 class="onboarding-title" data-i18n-message-id="onboarding-6-header"></h3>
<p data-i18n-message-id="onboarding-6-description"></p>
<div class="half-button-wrapper">
<a href="#" id="no-sync" class="half-onboarding-button grey-button keyboard-nav" tabindex="0" data-i18n-message-id="notNow"></a>
<a href="#" id="start-sync-button" class="half-onboarding-button keyboard-nav" tabindex="0" data-i18n-message-id="startSyncing"></a>
</div>
</div>
<div class="panel onboarding onboarding-panel-7 hide" id="onboarding-panel-7">
<img class="onboarding-img" alt="" src="/img/Account.svg" />
<h3 class="onboarding-title" data-i18n-message-id="onboarding-7-header"></h3>
<p data-i18n-message-id="onboarding-7-description"></p>
<div class="half-button-wrapper">
<a href="#" id="no-sign-in" class="half-onboarding-button grey-button keyboard-nav" tabindex="0" data-i18n-message-id="notNow"></a>
<a href="#" id="sign-in" class="half-onboarding-button keyboard-nav" tabindex="0" data-i18n-message-id="signIn"></a>
</div>
</div>
<div class="panel onboarding onboarding-panel-8 hide" id="onboarding-panel-8">
<div class="moz-vpn-onboarding-content">
<img class="onboarding-img" alt="" src="/img/moz-vpn-onboarding.svg" />
<h3 class="onboarding-title" data-i18n-message-id="proxyNowAvailable"></h3>
<p data-i18n-message-id="onboarding-8-description"></p>
</div>
<div id="moz-vpn-fw-onboarding-done" class="half-button-wrapper">
<a id="moz-vpn-fw-onboarding-done" href="#" class="half-onboarding-button keyboard-nav onboarding-done" tabindex="0" data-i18n-message-id="done"></a>
</div>
<div class="moz-vpn-permissions">
<div class="moz-vpn-permissions-copy">
<span data-i18n-message-id="mozillaVpnRequiresAdditionalPermissions"></span>
</div>
<div class="half-button-wrapper">
<a href="#" id="permissions-not-now" class="half-onboarding-button grey-button keyboard-nav onboarding-done" tabindex="0" data-i18n-message-id="notNow"></a>
<a href="#" id="onboarding-enable-permissions" class="half-onboarding-button keyboard-nav" tabindex="0" data-i18n-message-id="enable"></a>
</div>
</div>
</div>
<div class="panel achievement-panel hide" id="achievement-panel">
<img class="onboarding-img" alt="" src="/img/onboarding-3.png" />
<h3 class="onboarding-title" data-i18n-message-id="oneHundredTabsHeader"></h3>
<p data-i18n-message-id="youHaveOpened"></p>
<p data-i18n-message-id="spreadTheWord"></p>
<p class="share-ctas">
<a class="cta-link" href="https://mzl.la/2gJtIZ4" id="achievement-rate-button" target="_blank">
<span class="cta amo-rate-cta" data-i18n-message-id="rate">
<img src="/img/amo-icon.svg" class="cta-icon" alt="">
</span>
</a>
<a class="cta-link" href="https://bit.ly/fb-share-mac-addon" target="_blank">
<span class="cta fb-share-cta" data-i18n-message-id="share">
<img src="/img/webicon-facebook.svg" class="cta-icon" alt="">
</span>
</a>
<a class="cta-link" href="http://bit.ly/tweet-100-tabs-mac-addon" target="_blank">
<span class="cta tweet-cta" data-i18n-message-id="tweet">
<img src="/img/webicon-twitter.svg" class="cta-icon" alt="">
</span>
</a>
</p>
<a href="#" id="achievement-done-button" class="onboarding-button keyboard-nav" data-i18n-message-id="done"></a>
</div>
<div class="panel menu-panel container-panel hide" id="container-panel">
<h3 class="title">Multi-Account Containers</h3>
<a href="#" class="info-icon" id="info-icon" tabindex="10">
<img data-i18n-attribute-message-id="info" data-i18n-attribute="alt" alt="" src="/img/info.svg" / >
</a>
<hr>
<table class="menu">
<tr class="menu-item hover-highlight keyboard-nav" id="open-new-tab-in" tabindex="0">
<td>
<img class="menu-icon" alt="" src="/img/tab-new-16.svg" />
<span class="menu-text" data-i18n-message-id="openInNewTabTitle"></span>
<span class="menu-arrow">
<img alt="" src="/img/arrow-icon-right.svg" />
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="reopen-site-in" tabindex="0">
<td>
<img class="menu-icon" alt="" src="/img/refresh-16.svg" />
<span data-i18n-message-id="reopenThisSiteIn" class="menu-text"></span>
<span class="menu-arrow">
<img alt="" src="/img/arrow-icon-right.svg" />
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="sort-containers-link" tabindex="0">
<td>
<img class="menu-icon" alt="" src="/img/sort-16_1.svg" />
<span class="menu-text" data-i18n-message-id="sortTabsByContainer"></span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="always-open-in" tabindex="0">
<td>
<img class="menu-icon" alt="" src="/img/container-openin-16.svg" />
<span class="menu-text" data-i18n-message-id="alwaysOpenThisSiteIn"></span>
<span class="menu-arrow">
<img alt="" src="/img/arrow-icon-right.svg" />
</span>
</td>
</tr>
</table>
<hr>
<div class="sub-header-wrapper flx-row flx-space-between">
<div class="sub-header" data-i18n-message-id="containers"></div>
<h4 class="moz-vpn-logotype vpn-status-container-list display-none">Mozilla VPN
<span class="moz-vpn-connection-status-indicator container-list-status-icon">
<span class="tooltip"></span>
</span>
</h4>
</div>
<div class="scrollable identities-list">
<table class="menu" id="identities-list">
<tr class="menu-item hover-highlight">
<td>
<div class="menu-item-name">
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="pet"
data-identity-color="blue"></div>
</div>
<span class="menu-text" data-i18n-message-id="default"></span>
</div>
<span class="menu-right-float">
<span class="container-count">22</span>
<span class="menu-arrow">
<img alt="" src="/img/arrow-icon-right.svg" />
</span>
</span>
</td>
</tr>
</table>
</div>
<div id="moz-vpn-tout" class="moz-vpn-content expanded">
<div class="flx-row button-wrapper">
<h4 class="moz-vpn-logo">Mozilla VPN</h4>
<button class="controller dismiss-moz-vpn-tout" tab-index="0"></button>
</div>
<div class="collapsible-content flx-col controller-collapsible-content">
<div class="flx-row flx-space-between">
<span class="moz-vpn-subtitle" data-i18n-message-id="integrateContainers"></span>
</div>
<button id="moz-vpn-learn-more" class="moz-vpn-cta primary-cta" data-i18n-message-id="getMozillaVpn"></button>
</div>
</div>
<v-padding-hack-footer></v-padding-hack-footer> <!--prevents last container from getting covered up by the 'manage containers button' when list is long-->
<div class="bottom-btn keyboard-nav controller" id="manage-containers-link" tabindex="0" data-i18n-message-id="manageContainers"></div>
</div>
<div class="hide panel menu-panel container-info-panel" id="container-info-panel" tabindex="-1">
<h3 class="title" id="container-info-title" data-i18n-attribute-message-id="personal"></h3>
<button class="btn-return arrow-left controller keyboard-nav-back" id="close-container-info-panel" tabindex="0"></button>
<hr>
<table class="menu">
<tr class="menu-item hover-highlight keyboard-nav" id="open-new-tab-in-info" tabindex="0">
<td>
<img class="menu-icon" alt="" src="/img/tab-new-16.svg" />
<span class="menu-text" data-i18n-message-id="openNewTabInThisContainer"></span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="hideorshow-container" tabindex="0">
<td>
<img id="container-info-hideorshow-icon" class="menu-icon" alt="" src="img/password-hide.svg" />
<span id="container-info-hideorshow-label" class="menu-text" data-i18n-message-id="hideThisContainer"></span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="move-to-new-window" tabindex="0">
<td>
<img class="menu-icon" alt="" src="/img/movetowindow-16.svg" />
<span class="menu-text" data-i18n-message-id="moveTabsToANewWindow"></span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="always-open" tabindex="0">
<td>
<img class="menu-icon" alt="" src="/img/container-openin-16.svg" />
<span class="menu-text" id="always-open-in-info-panel" data-i18n-message-id="alwaysOpenSiteInContainer"></span>
<span class="menu-arrow">
</span>
</td>
</tr>
</table>
<hr>
<div class="sub-header-wrapper">
<div class="sub-header" data-i18n-message-id="openTabs"></div>
</div>
<div class="scrollable">
<table class="menu" id="container-info-table">
<tr class="menu-item hover-highlight keyboard-nav" tabindex="0">
<td>
<div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div>
<span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllllllllllla.org</span>
<img class="trash-button" src="/img/close.svg" />
</td>
</tr>
</table>
</div>
<v-padding-hack-footer></v-padding-hack-footer>
<div class="bottom-btn keyboard-nav hover-highlight" id="manage-container-link" tabindex="0" data-i18n-message-id="manageThisContainer"></div>
</div>
<div class="panel menu-panel container-picker-panel hide" id="container-picker-panel">
<h3 class="title" id="picker-title">
Multi-Account Containers
</h3>
<button class="btn-return arrow-left controller keyboard-nav-back" id="close-container-picker-panel" tabindex="0"></button>
<hr>
<div id="new-container-div"></div>
<div class="scrollable identities-list">
<table class="menu" id="picker-identities-list">
<tr class="menu-item hover-highlight keyboard-nav">
<td>
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="pet"
data-identity-color="blue">
</div>
</div>
<span class="menu-text" data-i18n-message-id="default"></span>
</td>
</tr>
</table>
</div>
</div>
<div class="panel menu-panel edit-container-panel hide" id="edit-container-panel">
<h3 class="title" id="container-edit-title" data-i18n-message-id="default"></h3>
<button class="btn-return arrow-left controller" id="close-container-edit-panel"></button>
<hr>
<div class="edit-form">
<form id="edit-container-panel-form">
<input type="hidden" name="container-id" id="edit-container-panel-usercontext-input" />
<fieldset>
<legend class="form-header" data-i18n-message-id="name"></legend>
<v-padding-hack-4></v-padding-hack-4>
<input type="text" name="container-name" id="edit-container-panel-name-input" class="edit-container-panel-name-input" maxlength="25"/>
</fieldset>
<legend class="form-header" data-i18n-message-id="color"></legend>
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
</fieldset>
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
<legend class="form-header" data-i18n-message-id="icon"></legend>
</fieldset>
<fieldset class="proxies"> <!---- PROXIES -->
<input type="text" class="proxies" name="container-proxy" id="edit-container-panel-proxy" placeholder="type://host:port" hidden/>
<input type="text" class="proxies" name="moz-proxy-enabled" id="moz-proxy-enabled" maxlength="5" hidden/>
<input type="text" class="proxies" name="country-code" id="country-code-input" maxlength="5" hidden/>
<input type="text" class="proxies" name="city-name" id="city-name-input" maxlength="5" hidden/>
</fieldset>
</form>
<div id="edit-container-options">
<div class="options-header" data-i18n-message-id="options"></div>
<div class="container-options">
<label for="site-isolation" class="options-label" data-i18n-message-id="limitToDesignatedSites"></label>
<label class="switch">
<input id="site-isolation" class="switch-input" name="site-isolation" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<button class="container-options blue-link" id="manage-assigned-sites-list" tabindex="0" data-i18n-message-id="manageSiteList"></button>
</div>
</div>
<moz-vpn-container-ui class="moz-vpn-controller-content expanded">
<div class="flx-row button-wrapper">
<h4 class="moz-vpn-logotype">Mozilla VPN
<span class="moz-vpn-connection-status-indicator"></span>
</h4>
<button class="expand-collapse blue-link" tab-index="0">
<span data-i18n-message-id="hide" class="hide-label hide-show-label"></span>
<span data-i18n-message-id="show" class="show-label hide-show-label"></span>
</button>
</div>
<div class="collapsible-content flx-col controller-collapsible-conten">
<div class="flx-row flx-space-between add-bg-color">
<span class="moz-vpn-subtitle"></span>
<label class="switch">
<input id="moz-vpn-switch" class="moz-vpn-switch switch-input" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<button id="get-mozilla-vpn" class="moz-vpn-cta primary-cta" data-i18n-message-id="getMozillaVpn"></button>
<button id="moz-vpn-current-server" class="controller">
<span class="current-country-flag"></span>
<span class="current-city-name"></span>
</button>
</div>
</moz-vpn-container-ui>
<button id="advanced-proxy-settings-btn" class="proxy-section advanced-proxy-settings-btn controller">
<span class="advanced-proxy-settings-btn-label" data-i18n-message-id="advancedProxySettings"></span>
<span id="advanced-proxy-address"></span>
</button>
<button class="delete-container delete-btn alert-text" id="delete-container-button" data-i18n-message-id="deleteThisContainer"></button>
<!-- TODO get UX / CONTENT on how to message about unavailable proxies -->
<!-- Prevent users from opening containers where proxies are unavailable and which will result in timeouts -->
<!-- Provide a way for users to disable Mozilla proxies if they cancel their subscription or somehow lose access -->
<!-- <div class="modal-warning">
<div class="modal-content">
<button id="close-proxy-warning" class="x-close modal-clickable">Close</button>
<p>This container has been configured to use a Mozilla VPN proxy, but the Mozilla VPN app is off. To access the web via this container, turn Mozilla VPN on or disable the proxy for this container.</p>
<button class="disable-proxy modal-clickable">Disable proxy for this container</button>
<button id="close-modal" class="modal-clickable" class="disable-proxy">Close</button>
</div>
</div> -->
<div class="panel-footer">
<a href="#" class="button expanded secondary footer-button cancel-button" id="create-container-cancel-link" data-i18n-message-id="cancel"></a>
<a href="#" class="button expanded primary footer-button" id="create-container-ok-link" data-i18n-message-id="ok"></a>
</div>
</div>
<div class="panel menu-panel edit-container-assignments hide" id="edit-container-assignments">
<h3 class="title" id="edit-assignments-title" data-i18n-message-id="default"></h3>
<button class="btn-return arrow-left controller" id="close-container-assignment-panel"></button>
<hr>
<div class="scrollable edit-sites-assigned">
<div class="sub-header" data-i18n-attribute-message-id="sitesAssignedToThisContainer"></div>
<table class="menu scrollable" id="edit-sites-assigned">
<tr class="menu-item hover-highlight" tabindex="0">
<td>
<div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div>
<span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllla.org</span>
<img class="trash-button" src="/img/container-delete.svg" />
</td>
</tr>
</table>
</div>
</div>
<div class="hide panel delete-container-panel" id="delete-container-panel">
<h3 class="title" id="container-delete-title" data-i18n-message-id="default">
</h3>
<button class="btn-return arrow-left controller" id="close-container-delete-panel"></button>
<hr>
<div class="panel-content delete-container-confirm">
<h4 class="delete-container-confirm-title" data-i18n-message-id="removeThisContainer"></h4>
<p class="delete-warning" data-i18n-message-id="removeThisContainerConfirmation"></p>
</div>
<div class="panel-footer">
<a href="#" class="button expanded secondary footer-button cancel-button" data-i18n-message-id="cancel" id="delete-container-cancel-link"></a>
<a href="#" class="button expanded primary footer-button" data-i18n-message-id="ok" id="delete-container-ok-link"></a>
</div>
</div>
<div class="hide panel moz-vpn-server-list-panel" id="moz-vpn-server-list-panel">
<h3 class="title proxy-panel-title" id="vpn-server-list-title" data-i18n-message-id="chooseLocation"></h3>
<button class="btn-return arrow-left controller moz-vpn-return" id="moz-vpn-return"></button>
<ul id="moz-vpn-server-list" class="moz-vpn-server-list">
<template id="server-list-item">
<li class="server-list-item" data-country-code="">
<button class="flx-row server-city-list-visibility-btn controller">
<div class="toggle"></div>
<img class="server-country-flag" src="" alt="" />
<p class="server-country-name"></p>
</button>
<ul class="server-city-list"></ul>
</li>
</template>
<template id="server-city-list-items">
<li>
<label class="server-city-list-item" tabindex="0">
<input class="server-radio-btn" type="radio" data-country-code="" data-city-name="" checked=""/>
<div class="server-radio-control"></div>
<span class="server-city-name"></span>
</label>
</li>
</template>
</ul>
</div>
<div class="hide panel advanced-proxy-settings-panel" id="advanced-proxy-settings-panel">
<h3 class="title proxy-panel-title" id="advanced-proxy-settings-title">
<span data-i18n-message-id="advancedProxySettings"></span>
<div class="flx-row">
<p data-identity-color="" class="proxy-title-container-color"></p>
<span id="proxy-title-container-name"></span>
</div>
</h3>
<button class="btn-return arrow-left controller moz-vpn-return" id="advanced-proxy-settings-return"></button>
<form class="advanced-proxy-panel-content">
<label class="advanced-proxy-input-label" for="container-proxy" data-i18n-message-id="advancedProxySettings"></label>
<div class="advanced-proxy-input-wrapper">
<input id="edit-advanced-proxy-input" class="proxy-host primary-input" name="container-proxy" type="text" placeholder="type://host:port" />
<button id="clear-advanced-proxy-input" class="controller" data-i18n-attribute="value" data-i18n-attribute-message-id="clearproxylabel"></button>
<span class="proxy-validity" data-i18n-message-id="invalidProxyAlert"></span>
</div>
<button id="submit-advanced-proxy" class="primary-cta apply-to-container" data-i18n-message-id="applyToContainer"></button>
<a id="advanced-proxy-settings-learn-more" href="" class="blue-link" data-i18n-message-id="learnMore"></a>
</form>
<div id="permissions-overlay" class="permissions-overlay" data-tab-group="proxy-disabled">
<p data-tab-group="proxy-disabled" data-i18n-message-id="additionalPermissionNeeded"></p>
<button id="enable-proxy-permissions" class="primary-cta" data-tab-group="proxy-disabled" data-i18n-message-id="enable"></button>
</div>
</div>
<script src="js/utils.js"></script>
<script src="js/popup.js"></script>
<script src="js/mozillaVpn.js"></script>
<script src="js/proxified-containers.js"></script>
</body>
</html>