Add files via upload
This commit is contained in:
parent
99ea5decfe
commit
63837b8fb4
22 changed files with 6481 additions and 0 deletions
35
src/confirm-page.html
Normal file
35
src/confirm-page.html
Normal 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>
|
866
src/js/background/assignManager.js
Normal file
866
src/js/background/assignManager.js
Normal 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 += `¤tCookieStoreId=${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();
|
406
src/js/background/backgroundLogic.js
Normal file
406
src/js/background/backgroundLogic.js
Normal 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();
|
21
src/js/background/badge.js
Normal file
21
src/js/background/badge.js
Normal 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();
|
194
src/js/background/identityState.js
Normal file
194
src/js/background/identityState.js
Normal 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)
|
||||
);
|
||||
}
|
26
src/js/background/index.html
Normal file
26
src/js/background/index.html
Normal 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>
|
272
src/js/background/messageHandler.js
Normal file
272
src/js/background/messageHandler.js
Normal 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();
|
118
src/js/background/mozillaVpnBackground.js
Normal file
118
src/js/background/mozillaVpnBackground.js
Normal 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
580
src/js/background/sync.js
Normal 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
80
src/js/confirm-page.js
Normal 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
46
src/js/content-script.js
Normal 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
9
src/js/i18n.js
Normal 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
260
src/js/mozillaVpn.js
Normal 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
137
src/js/options.js
Normal 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
37
src/js/pageAction.js
Normal 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
2363
src/js/popup.js
Normal file
File diff suppressed because it is too large
Load diff
76
src/js/proxified-containers.js
Normal file
76
src/js/proxified-containers.js
Normal 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
182
src/js/utils.js
Normal 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 = { "&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/" };
|
||||
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
155
src/manifest.json
Normal 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
123
src/options.html
Normal 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
35
src/pageActionPopup.html
Normal 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
460
src/popup.html
Normal 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>
|
Loading…
Add table
Reference in a new issue