Merge branch 'master' into proxy-support
This commit is contained in:
+3
-1
@@ -7,6 +7,8 @@ module.exports = {
|
||||
"badge": true,
|
||||
"backgroundLogic": true,
|
||||
"identityState": true,
|
||||
"messageHandler": true
|
||||
"messageHandler": true,
|
||||
"sync": true,
|
||||
"Utils": true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
const assignManager = {
|
||||
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(pageUrl) {
|
||||
const url = new window.URL(pageUrl);
|
||||
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}`;
|
||||
@@ -19,29 +20,43 @@ const assignManager = {
|
||||
}
|
||||
},
|
||||
|
||||
setExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
setExempted(pageUrlorUrlKey, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
}
|
||||
this.exemptedTabs[siteStoreKey].push(tabId);
|
||||
},
|
||||
|
||||
removeExempted(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
removeExempted(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
},
|
||||
|
||||
isExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
isExempted(pageUrlorUrlKey, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
return false;
|
||||
}
|
||||
return this.exemptedTabs[siteStoreKey].includes(tabId);
|
||||
},
|
||||
|
||||
get(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
get(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
return this.getByUrlKey(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) {
|
||||
@@ -54,51 +69,103 @@ const assignManager = {
|
||||
});
|
||||
},
|
||||
|
||||
set(pageUrl, data, exemptedTabIds) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (exemptedTabIds) {
|
||||
exemptedTabIds.forEach((tabId) => {
|
||||
this.setExempted(pageUrl, tabId);
|
||||
this.setExempted(pageUrlorUrlKey, tabId);
|
||||
});
|
||||
}
|
||||
return this.area.set({
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
data.identityMacAddonUUID =
|
||||
await identityState.lookupMACaddonUUID(data.userContextId);
|
||||
await this.area.set({
|
||||
[siteStoreKey]: data
|
||||
});
|
||||
const syncEnabled = await this.getSyncEnabled();
|
||||
if (backup && syncEnabled) {
|
||||
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
remove(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
async remove(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
// When we remove an assignment we should clear all the exemptions
|
||||
this.removeExempted(pageUrl);
|
||||
return this.area.remove([siteStoreKey]);
|
||||
this.removeExempted(pageUrlorUrlKey);
|
||||
await this.area.remove([siteStoreKey]);
|
||||
const syncEnabled = await this.getSyncEnabled();
|
||||
if (syncEnabled) await sync.storageArea.backup({siteStoreKey});
|
||||
return;
|
||||
},
|
||||
|
||||
async deleteContainer(userContextId) {
|
||||
const sitesByContainer = await this.getByContainer(userContextId);
|
||||
const sitesByContainer = await this.getAssignedSites(userContextId);
|
||||
this.area.remove(Object.keys(sitesByContainer));
|
||||
},
|
||||
|
||||
async getByContainer(userContextId) {
|
||||
async getAssignedSites(userContextId = null) {
|
||||
const sites = {};
|
||||
const siteConfigs = await this.area.get();
|
||||
Object.keys(siteConfigs).forEach((key) => {
|
||||
// For some reason this is stored as string... lets check them both as that
|
||||
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
|
||||
const site = siteConfigs[key];
|
||||
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 = key.replace(/^siteContainerMap@@_/, "");
|
||||
sites[key] = site;
|
||||
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
|
||||
// If we have existing data and for some reason it hasn't been
|
||||
// deleted etc lets update it
|
||||
this.storageArea.get(pageUrl).then((siteSettings) => {
|
||||
if (siteSettings) {
|
||||
siteSettings.neverAsk = true;
|
||||
@@ -113,12 +180,11 @@ const assignManager = {
|
||||
// We return here so the confirm page can load the tab when exempted
|
||||
async _exemptTab(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
this.storageArea.setExempted(pageUrl, m.tabId);
|
||||
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 Utils.getBogusProxy();
|
||||
@@ -129,7 +195,8 @@ const assignManager = {
|
||||
return proxy;
|
||||
},
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
// 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 {};
|
||||
@@ -141,31 +208,57 @@ const assignManager = {
|
||||
]);
|
||||
let container;
|
||||
try {
|
||||
container = await browser.contextualIdentities.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
|
||||
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
|
||||
// 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);
|
||||
if (!siteSettings
|
||||
|| userContextId === siteSettings.userContextId
|
||||
|| tab.incognito
|
||||
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||
return {};
|
||||
|
||||
// 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);
|
||||
&& 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
|
||||
// we decided to cancel the request at this point, register
|
||||
// canceled request
|
||||
this.canceledRequests[tab.id] = {
|
||||
requestIds: {
|
||||
[options.requestId]: true
|
||||
@@ -175,8 +268,10 @@ const assignManager = {
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
// 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]) {
|
||||
@@ -188,10 +283,12 @@ const assignManager = {
|
||||
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
|
||||
// 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
|
||||
// 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) {
|
||||
@@ -201,27 +298,48 @@ const assignManager = {
|
||||
}
|
||||
}
|
||||
|
||||
this.reloadPageInContainer(
|
||||
options.url,
|
||||
userContextId,
|
||||
siteSettings.userContextId,
|
||||
tab.index + 1,
|
||||
tab.active,
|
||||
siteSettings.neverAsk,
|
||||
openTabId
|
||||
);
|
||||
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.
|
||||
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);
|
||||
@@ -231,15 +349,41 @@ const assignManager = {
|
||||
};
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
init() {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
this._onClickedHandler(info, tab);
|
||||
info.bookmarkId ?
|
||||
this._onClickedBookmark(info) :
|
||||
this._onClickedHandler(info, tab);
|
||||
});
|
||||
|
||||
// Before anything happens we decide if the request should be proxified
|
||||
browser.proxy.onRequest.addListener(this.handleProxifiedRequest, {urls: ["<all_urls>"]});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
// 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);
|
||||
@@ -257,6 +401,60 @@ const assignManager = {
|
||||
}
|
||||
},{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) {
|
||||
@@ -272,7 +470,9 @@ const assignManager = {
|
||||
} else {
|
||||
remove = true;
|
||||
}
|
||||
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
||||
await this._setOrRemoveAssignment(
|
||||
tab.id, info.pageUrl, userContextId, remove
|
||||
);
|
||||
break;
|
||||
case this.MENU_MOVE_ID:
|
||||
backgroundLogic.moveTabsToWindow({
|
||||
@@ -290,6 +490,41 @@ const assignManager = {
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
@@ -299,16 +534,16 @@ const assignManager = {
|
||||
if (!("cookieStoreId" in tab)) {
|
||||
return false;
|
||||
}
|
||||
return backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
|
||||
return backgroundLogic.getUserContextIdFromCookieStoreId(
|
||||
tab.cookieStoreId
|
||||
);
|
||||
},
|
||||
|
||||
isTabPermittedAssign(tab) {
|
||||
// Ensure we are not an important about url
|
||||
// Ensure we are not in incognito mode
|
||||
const url = new URL(tab.url);
|
||||
if (url.protocol === "about:"
|
||||
|| url.protocol === "moz-extension:"
|
||||
|| tab.incognito) {
|
||||
|| url.protocol === "moz-extension:") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -316,7 +551,6 @@ const assignManager = {
|
||||
|
||||
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
|
||||
@@ -341,16 +575,40 @@ const assignManager = {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
}, exemptedTabIds);
|
||||
actionName = "added";
|
||||
actionName = "assigned site to always open in this container";
|
||||
} else {
|
||||
// Remove assignment
|
||||
await this.storageArea.remove(pageUrl);
|
||||
actionName = "removed";
|
||||
|
||||
actionName = "removed from assigned sites list";
|
||||
|
||||
// remove site isolation if now empty
|
||||
await this._maybeRemoveSiteIsolation(userContextId);
|
||||
}
|
||||
browser.tabs.sendMessage(tabId, {
|
||||
text: `Successfully ${actionName} site to always open in this container`
|
||||
});
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
this.calculateContextMenu(tab);
|
||||
|
||||
if (tabId) {
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
setTimeout(function(){
|
||||
browser.tabs.sendMessage(tabId, {
|
||||
text: `Successfully ${actionName}`
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
|
||||
this.calculateContextMenu(tab);
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
@@ -358,13 +616,13 @@ const assignManager = {
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
return await this.storageArea.get(tab.url);
|
||||
return this.storageArea.get(tab.url);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_getByContainer(userContextId) {
|
||||
return this.storageArea.getByContainer(userContextId);
|
||||
return this.storageArea.getAssignedSites(userContextId);
|
||||
},
|
||||
|
||||
removeContextMenu() {
|
||||
@@ -430,13 +688,35 @@ const assignManager = {
|
||||
});
|
||||
},
|
||||
|
||||
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.extension.getURL("confirm-page.html");
|
||||
// False represents assignment is not permitted
|
||||
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||
if (neverAsk) {
|
||||
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
|
||||
return browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
|
||||
} else {
|
||||
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
|
||||
let currentCookieStoreId;
|
||||
@@ -444,7 +724,7 @@ const assignManager = {
|
||||
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
|
||||
confirmUrl += `¤tCookieStoreId=${currentCookieStoreId}`;
|
||||
}
|
||||
browser.tabs.create({
|
||||
return browser.tabs.create({
|
||||
url: confirmUrl,
|
||||
cookieStoreId: currentCookieStoreId,
|
||||
openerTabId,
|
||||
@@ -457,7 +737,33 @@ const assignManager = {
|
||||
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();
|
||||
|
||||
@@ -6,7 +6,20 @@ const backgroundLogic = {
|
||||
"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});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async getExtensionInfo() {
|
||||
const manifestPath = browser.extension.getURL("manifest.json");
|
||||
@@ -59,15 +72,13 @@ const backgroundLogic = {
|
||||
});
|
||||
}
|
||||
await donePromise;
|
||||
browser.runtime.sendMessage({
|
||||
method: "refreshNeeded"
|
||||
});
|
||||
},
|
||||
|
||||
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
|
||||
@@ -84,6 +95,7 @@ const backgroundLogic = {
|
||||
return browser.tabs.create({
|
||||
url,
|
||||
active,
|
||||
discarded,
|
||||
pinned: options.pinned || false,
|
||||
cookieStoreId
|
||||
});
|
||||
@@ -126,16 +138,31 @@ const backgroundLogic = {
|
||||
return list.concat(containerState.hiddenTabs);
|
||||
},
|
||||
|
||||
async unhideContainer(cookieStoreId) {
|
||||
async unhideContainer(cookieStoreId, alreadyShowingUrl) {
|
||||
if (!this.unhideQueue.includes(cookieStoreId)) {
|
||||
this.unhideQueue.push(cookieStoreId);
|
||||
await this.showTabs({
|
||||
cookieStoreId
|
||||
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"];
|
||||
@@ -242,7 +269,8 @@ const backgroundLogic = {
|
||||
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
||||
hasOpenTabs: !!openTabs.length,
|
||||
numberOfHiddenTabs: containerState.hiddenTabs.length,
|
||||
numberOfOpenTabs: openTabs.length
|
||||
numberOfOpenTabs: openTabs.length,
|
||||
isIsolated: !!containerState.isIsolated
|
||||
};
|
||||
return;
|
||||
});
|
||||
@@ -322,21 +350,30 @@ const backgroundLogic = {
|
||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
||||
|
||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||
promises.push(this.openNewTab({
|
||||
userContextId: userContextId,
|
||||
url: object.url,
|
||||
nofocus: options.nofocus || false,
|
||||
pinned: object.pinned,
|
||||
}));
|
||||
// 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 await identityState.storageArea.set(options.cookieStoreId, containerState);
|
||||
return identityState.storageArea.set(options.cookieStoreId, containerState);
|
||||
},
|
||||
|
||||
cookieStoreId(userContextId) {
|
||||
if(userContextId === 0) return "firefox-default";
|
||||
return `firefox-container-${userContextId}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
backgroundLogic.init();
|
||||
@@ -1,23 +1,18 @@
|
||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0", "6.2.0"];
|
||||
const badge = {
|
||||
async init() {
|
||||
const currentWindow = await browser.windows.getCurrent();
|
||||
this.displayBrowserActionBadge(currentWindow.incognito);
|
||||
},
|
||||
|
||||
disableAddon(tabId) {
|
||||
browser.browserAction.disable(tabId);
|
||||
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
||||
this.displayBrowserActionBadge(currentWindow);
|
||||
},
|
||||
|
||||
async displayBrowserActionBadge() {
|
||||
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||
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: "rgba(0,217,0,255)"});
|
||||
browser.browserAction.setBadgeText({text: "NEW"});
|
||||
storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) {
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: "rgba(0,217,0,255)" });
|
||||
browser.browserAction.setBadgeText({ text: "NEW" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const identityState = {
|
||||
window.identityState = {
|
||||
keyboardShortcut: {},
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
|
||||
@@ -11,12 +12,23 @@ const identityState = {
|
||||
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];
|
||||
}
|
||||
const defaultContainerState = identityState._createIdentityState();
|
||||
await this.set(cookieStoreId, defaultContainerState);
|
||||
|
||||
return defaultContainerState;
|
||||
// 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) {
|
||||
@@ -26,16 +38,82 @@ const identityState = {
|
||||
});
|
||||
},
|
||||
|
||||
remove(cookieStoreId) {
|
||||
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});
|
||||
@@ -54,9 +132,63 @@ const identityState = {
|
||||
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: []
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
<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>
|
||||
|
||||
@@ -6,10 +6,23 @@ const messageHandler = {
|
||||
|
||||
init() {
|
||||
// Handles messages from webextension code
|
||||
browser.runtime.onMessage.addListener((m) => {
|
||||
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 "resetBookmarksContext":
|
||||
response = assignManager.resetBookmarksMenuItem();
|
||||
break;
|
||||
case "deleteContainer":
|
||||
response = backgroundLogic.deleteContainer(m.message.userContextId);
|
||||
break;
|
||||
@@ -19,6 +32,9 @@ const messageHandler = {
|
||||
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);
|
||||
@@ -30,9 +46,7 @@ const messageHandler = {
|
||||
case "setOrRemoveAssignment":
|
||||
// m.tabId is used for where to place the in content message
|
||||
// m.url is the assignment to be removed/added
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
||||
});
|
||||
response = assignManager._setOrRemoveAssignment(m.tabId, m.url, m.userContextId, m.value);
|
||||
break;
|
||||
case "sortTabs":
|
||||
backgroundLogic.sortTabs();
|
||||
@@ -67,6 +81,31 @@ const messageHandler = {
|
||||
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;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
@@ -79,6 +118,7 @@ const messageHandler = {
|
||||
if (!extensionInfo.permissions.includes("contextualIdentities")) {
|
||||
throw new Error("Missing contextualIdentities permission");
|
||||
}
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
externalExtensionAllowed[sender.id] = true;
|
||||
}
|
||||
let response;
|
||||
@@ -141,9 +181,6 @@ const messageHandler = {
|
||||
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
||||
|
||||
browser.tabs.onCreated.addListener((tab) => {
|
||||
if (tab.incognito) {
|
||||
badge.disableAddon(tab.id);
|
||||
}
|
||||
// lets remember the last tab created so we can close it if it looks like a redirect
|
||||
this.lastCreatedTab = tab;
|
||||
if (tab.cookieStoreId) {
|
||||
@@ -153,9 +190,26 @@ const messageHandler = {
|
||||
!tab.url.startsWith("moz-extension")) {
|
||||
// increment the counter of container tabs opened
|
||||
this.incrementCountOfContainerTabsOpened();
|
||||
}
|
||||
|
||||
backgroundLogic.unhideContainer(tab.cookieStoreId);
|
||||
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;
|
||||
|
||||
@@ -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)) {
|
||||
assignManager
|
||||
.storageArea
|
||||
.remove(siteStoreKey);
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
+8
-19
@@ -1,6 +1,6 @@
|
||||
async function load() {
|
||||
const searchParams = new URL(window.location).searchParams;
|
||||
const redirectUrl = decodeURIComponent(searchParams.get("url"));
|
||||
const redirectUrl = searchParams.get("url");
|
||||
const cookieStoreId = searchParams.get("cookieStoreId");
|
||||
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
|
||||
const redirectUrlElement = document.getElementById("redirect-url");
|
||||
@@ -17,25 +17,14 @@ async function load() {
|
||||
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
|
||||
document.getElementById("current-container-name").textContent = currentContainer.name;
|
||||
}
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
document.getElementById("deny").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
let button = "confirm"; // Confirm is the form default.
|
||||
let buttonTarget = e.explicitOriginalTarget;
|
||||
if (buttonTarget.tagName !== "BUTTON") {
|
||||
buttonTarget = buttonTarget.closest("button");
|
||||
}
|
||||
if (buttonTarget && buttonTarget.id) {
|
||||
button = buttonTarget.id;
|
||||
}
|
||||
switch (button) {
|
||||
case "deny":
|
||||
denySubmit(redirectUrl);
|
||||
break;
|
||||
case "confirm":
|
||||
confirmSubmit(redirectUrl, cookieStoreId);
|
||||
break;
|
||||
}
|
||||
denySubmit(redirectUrl);
|
||||
});
|
||||
|
||||
document.getElementById("confirm").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
confirmSubmit(redirectUrl, cookieStoreId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
const NUMBER_OF_KEYBOARD_SHORTCUTS = 10;
|
||||
|
||||
async function requestPermissions() {
|
||||
const checkbox = document.querySelector("#bookmarksPermissions");
|
||||
if (checkbox.checked) {
|
||||
const granted = await browser.permissions.request({permissions: ["bookmarks"]});
|
||||
if (!granted) {
|
||||
checkbox.checked = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await browser.permissions.remove({permissions: ["bookmarks"]});
|
||||
}
|
||||
browser.runtime.sendMessage({ method: "resetBookmarksContext" });
|
||||
}
|
||||
|
||||
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 hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]});
|
||||
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
|
||||
const { replaceTabEnabled } = await browser.storage.local.get("replaceTabEnabled");
|
||||
if (hasPermission) {
|
||||
document.querySelector("#bookmarksPermissions").checked = true;
|
||||
}
|
||||
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});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", setupOptions);
|
||||
document.querySelector("#bookmarksPermissions").addEventListener( "change", requestPermissions);
|
||||
document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync);
|
||||
document.querySelector("#replaceTabCheck").addEventListener( "change", enableDisableReplaceTab);
|
||||
document.querySelector("button").addEventListener("click", resetOnboarding);
|
||||
|
||||
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
|
||||
document.querySelector("#open_container_"+i)
|
||||
.addEventListener("change", storeShortcutChoice);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
async function init() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
|
||||
identities.forEach(identity => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.classList.add("menu-item", "hover-highlight");
|
||||
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>`;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
init();
|
||||
+818
-496
File diff suppressed because it is too large
Load Diff
+115
-2
@@ -1,11 +1,11 @@
|
||||
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
|
||||
|
||||
// TODO use export here instead of globals
|
||||
window.Utils = {
|
||||
const Utils = {
|
||||
|
||||
createFavIconElement(url) {
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.classList.add("icon", "offpage");
|
||||
imageElement.classList.add("icon", "offpage", "menu-icon");
|
||||
imageElement.src = url;
|
||||
const loadListener = (e) => {
|
||||
e.target.classList.remove("offpage");
|
||||
@@ -54,6 +54,117 @@ window.Utils = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
},
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// The following creates a fake (but convincing) constant Utils.DEFAULT_PROXY
|
||||
@@ -65,3 +176,5 @@ Object.defineProperty(window.Utils, "DEFAULT_PROXY", {
|
||||
// Setting configurable to false avoids deletion of Utils.DEFAULT_PROXY
|
||||
configurable: false
|
||||
});
|
||||
|
||||
window.Utils = Utils;
|
||||
|
||||
Reference in New Issue
Block a user