Add allowlist for isolated containers

This implements a per-container allow-list that is applied to isolated
containers. This can allow multiple containers to have an overlapping set of
sites that they are isolated to.

For example: Container A and Container B could both be isolated and have
calendar.example.com in their allow list. Both Container A and Container B
would then be able to visit calendar.example.com.

See: https://github.com/mozilla/multi-account-containers/issues/1892 and
https://github.com/mozilla/multi-account-containers/issues/1887 for additional
motivation.
This commit is contained in:
Noah Callaway 2021-04-25 12:36:10 -07:00
parent 4b56a2f0bb
commit 5eab4b7a2c
8 changed files with 409 additions and 79 deletions

View file

@ -585,6 +585,11 @@ manage things like container crud */
padding-inline-start: 0; padding-inline-start: 0;
} }
.edit-allowed-sites-panel fieldset {
background: none;
border: none;
}
.edit-container-panel fieldset:last-of-type { .edit-container-panel fieldset:last-of-type {
margin-block-end: 0; margin-block-end: 0;
} }
@ -729,6 +734,7 @@ h3.title {
} }
/* Maintain 1:1 square ratio for Favicons of websites added to a specific container */ /* Maintain 1:1 square ratio for Favicons of websites added to a specific container */
.edit-allowed-sites-panel .menu-icon,
#edit-sites-assigned .menu-icon, #edit-sites-assigned .menu-icon,
#container-info-table .menu-icon { #container-info-table .menu-icon {
inline-size: 16px; inline-size: 16px;
@ -952,6 +958,16 @@ tr:hover > td > .trash-button {
height: 16px; height: 16px;
} }
#add-allowed-site-form {
align-items: end;
display: flex;
flex-direction: row;
}
#add-allowed-site-form fieldset {
flex: 1;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--title-text-color: #fff; --title-text-color: #fff;

View file

@ -109,10 +109,10 @@ window.assignManager = {
const siteConfigs = await this.area.get(); const siteConfigs = await this.area.get();
for(const urlKey of Object.keys(siteConfigs)) { for(const urlKey of Object.keys(siteConfigs)) {
if (urlKey.includes("siteContainerMap@@_")) { if (urlKey.includes("siteContainerMap@@_")) {
// For some reason this is stored as string... lets check // For some reason this is stored as string... lets check
// them both as that // them both as that
if (!!userContextId && if (!!userContextId &&
String(siteConfigs[urlKey].userContextId) String(siteConfigs[urlKey].userContextId)
!== String(userContextId)) { !== String(userContextId)) {
continue; continue;
} }
@ -127,7 +127,7 @@ window.assignManager = {
}, },
/* /*
* Looks for abandoned site assignments. If there is no identity with * Looks for abandoned site assignments. If there is no identity with
* the site assignment's userContextId (cookieStoreId), then the assignment * the site assignment's userContextId (cookieStoreId), then the assignment
* is removed. * is removed.
*/ */
@ -136,8 +136,8 @@ window.assignManager = {
const macConfigs = await this.area.get(); const macConfigs = await this.area.get();
for(const configKey of Object.keys(macConfigs)) { for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("siteContainerMap@@_")) { if (configKey.includes("siteContainerMap@@_")) {
const cookieStoreId = const cookieStoreId =
"firefox-container-" + macConfigs[configKey].userContextId; "firefox-container-" + macConfigs[configKey].userContextId;
const match = identitiesList.find( const match = identitiesList.find(
localIdentity => localIdentity.cookieStoreId === cookieStoreId localIdentity => localIdentity.cookieStoreId === cookieStoreId
); );
@ -146,7 +146,7 @@ window.assignManager = {
continue; continue;
} }
const updatedSiteAssignment = macConfigs[configKey]; const updatedSiteAssignment = macConfigs[configKey];
updatedSiteAssignment.identityMacAddonUUID = updatedSiteAssignment.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(match.cookieStoreId); await identityState.lookupMACaddonUUID(match.cookieStoreId);
await this.set( await this.set(
configKey, configKey,
@ -164,7 +164,7 @@ window.assignManager = {
_neverAsk(m) { _neverAsk(m) {
const pageUrl = m.pageUrl; const pageUrl = m.pageUrl;
if (m.neverAsk === true) { if (m.neverAsk === true) {
// If we have existing data and for some reason it hasn't been // If we have existing data and for some reason it hasn't been
// deleted etc lets update it // deleted etc lets update it
this.storageArea.get(pageUrl).then((siteSettings) => { this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) { if (siteSettings) {
@ -210,9 +210,10 @@ window.assignManager = {
return {}; return {};
} }
const userContextId = this.getUserContextIdFromCookieStore(tab); const userContextId = this.getUserContextIdFromCookieStore(tab);
const url = options.url;
// https://github.com/mozilla/multi-account-containers/issues/847 // https://github.com/mozilla/multi-account-containers/issues/847
// //
// Handle the case where this request's URL is not assigned to any particular // Handle the case where this request's URL is not assigned to any particular
// container. We must do the following check: // container. We must do the following check:
// //
@ -228,8 +229,11 @@ window.assignManager = {
// - the current tab's container is locked and only allows "www.google.com" // - 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 // - 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 // - in this case, we must re-open "www.amazon.com" in a new tab in the default container
const siteIsolatedReloadInDefault = const siteIsolatedReloadInDefault = await this._maybeSiteIsolatedReloadInDefault(
await this._maybeSiteIsolatedReloadInDefault(siteSettings, tab); siteSettings,
tab,
url
);
if (!siteIsolatedReloadInDefault) { if (!siteIsolatedReloadInDefault) {
if (!siteSettings if (!siteSettings
@ -246,7 +250,7 @@ window.assignManager = {
const openTabId = removeTab ? tab.openerTabId : tab.id; const openTabId = removeTab ? tab.openerTabId : tab.id;
if (!this.canceledRequests[tab.id]) { if (!this.canceledRequests[tab.id]) {
// we decided to cancel the request at this point, register // we decided to cancel the request at this point, register
// canceled request // canceled request
this.canceledRequests[tab.id] = { this.canceledRequests[tab.id] = {
requestIds: { requestIds: {
@ -313,7 +317,7 @@ window.assignManager = {
- As the history won't span from one container to another it - 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() 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 - When users open a new tab themselves we want to make sure we
don't end up with three tabs as per: don't end up with three tabs as per:
https://github.com/mozilla/testpilot-containers/issues/421 https://github.com/mozilla/testpilot-containers/issues/421
If we are coming from an internal url that are used for the new 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 tab page (NEW_TAB_PAGES), we can safely close as user is unlikely
@ -338,7 +342,7 @@ window.assignManager = {
}; };
}, },
async _maybeSiteIsolatedReloadInDefault(siteSettings, tab) { async _maybeSiteIsolatedReloadInDefault(siteSettings, tab, url) {
// Tab doesn't support cookies, so containers not supported either. // Tab doesn't support cookies, so containers not supported either.
if (!("cookieStoreId" in tab)) { if (!("cookieStoreId" in tab)) {
return false; return false;
@ -348,7 +352,7 @@ window.assignManager = {
// I.e. it will be opened in that container anyway, so we don't need to check if the // 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. // current tab's container is locked or not.
if (siteSettings) { if (siteSettings) {
return false; return false;
} }
//tab is alredy reopening in the default container //tab is alredy reopening in the default container
@ -358,13 +362,27 @@ window.assignManager = {
// Requested page is not assigned to a specific container. If the current tab's container // 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. // is locked, then the page must be reloaded in the default container.
const currentContainerState = await identityState.storageArea.get(tab.cookieStoreId); const currentContainerState = await identityState.storageArea.get(tab.cookieStoreId);
return currentContainerState && currentContainerState.isIsolated;
// the container is not isolated, so any site can be opened
const isIsolated = currentContainerState && currentContainerState.isIsolated;
if (!isIsolated) {
return false;
}
// the site is isolated, and it's *not* an assigned site, so check if it's in the allowed
// sites array. If it is we can open the site in the container, otherwise we should reload
// in the default container
const allowedSites =
(currentContainerState && currentContainerState.allowedSites) || [];
const allowedKey = Utils.getAllowedSiteKeyFor(url);
return !allowedSites.includes(allowedKey);
}, },
init() { init() {
browser.contextMenus.onClicked.addListener((info, tab) => { browser.contextMenus.onClicked.addListener((info, tab) => {
info.bookmarkId ? info.bookmarkId ?
this._onClickedBookmark(info) : this._onClickedBookmark(info) :
this._onClickedHandler(info, tab); this._onClickedHandler(info, tab);
}); });
@ -479,7 +497,7 @@ window.assignManager = {
async _onClickedBookmark(info) { async _onClickedBookmark(info) {
async function _getBookmarksFromInfo(info) { async function _getBookmarksFromInfo(info) {
const [bookmarkTreeNode] = const [bookmarkTreeNode] =
await browser.bookmarks.get(info.bookmarkId); await browser.bookmarks.get(info.bookmarkId);
if (bookmarkTreeNode.type === "folder") { if (bookmarkTreeNode.type === "folder") {
return browser.bookmarks.getChildren(bookmarkTreeNode.id); return browser.bookmarks.getChildren(bookmarkTreeNode.id);
@ -489,9 +507,9 @@ window.assignManager = {
const bookmarks = await _getBookmarksFromInfo(info); const bookmarks = await _getBookmarksFromInfo(info);
for (const bookmark of bookmarks) { for (const bookmark of bookmarks) {
// Some checks on the urls from // Some checks on the urls from
// https://github.com/Rob--W/bookmark-container-tab/ thanks! // https://github.com/Rob--W/bookmark-container-tab/ thanks!
if ( !/^(javascript|place):/i.test(bookmark.url) && if ( !/^(javascript|place):/i.test(bookmark.url) &&
bookmark.type !== "folder") { bookmark.type !== "folder") {
const openInReaderMode = bookmark.url.startsWith("about:reader"); const openInReaderMode = bookmark.url.startsWith("about:reader");
if(openInReaderMode) { if(openInReaderMode) {
@ -569,12 +587,12 @@ window.assignManager = {
actionName = "removed from assigned sites list"; actionName = "removed from assigned sites list";
// remove site isolation if now empty // remove site isolation if now empty
await this._maybeRemoveSiteIsolation(userContextId); await this._maybeRemoveSiteIsolation(userContextId);
} }
if (tabId) { if (tabId) {
const tab = await browser.tabs.get(tabId); const tab = await browser.tabs.get(tabId);
setTimeout(function(){ setTimeout(function(){
browser.tabs.sendMessage(tabId, { browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName}` text: `Successfully ${actionName}`
}); });
@ -677,17 +695,17 @@ window.assignManager = {
reloadPageInDefaultContainer(url, index, active, openerTabId) { reloadPageInDefaultContainer(url, index, active, openerTabId) {
// To create a new tab in the default container, it is easiest just to omit the // To create a new tab in the default container, it is easiest just to omit the
// cookieStoreId entirely. // cookieStoreId entirely.
// //
// Unfortunately, if you create a new tab WITHOUT a cookieStoreId but WITH an openerTabId, // 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. // then the new tab automatically inherits the opener tab's cookieStoreId.
// I.e. it opens in the wrong container! // I.e. it opens in the wrong container!
// //
// So we have to explicitly pass in a cookieStoreId when creating the tab, since we // 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 // are specifying the openerTabId. There doesn't seem to be any way
// to look up the default container's cookieStoreId programatically, so sadly // to look up the default container's cookieStoreId programatically, so sadly
// we have to hardcode it here as "firefox-default". This is potentially // we have to hardcode it here as "firefox-default". This is potentially
// not cross-browser compatible. // not cross-browser compatible.
// //
// Note that we could have just omitted BOTH cookieStoreId and openerTabId. But the // 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 // 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, // does not automatically return to the original opener tab. To get this desired behaviour,

View file

@ -143,7 +143,7 @@ const backgroundLogic = {
if ("isIsolated" in containerState || remove) { if ("isIsolated" in containerState || remove) {
delete containerState.isIsolated; delete containerState.isIsolated;
} else { } else {
containerState.isIsolated = "locked"; containerState.isIsolated = "locked";
} }
return await identityState.storageArea.set(cookieStoreId, containerState); return await identityState.storageArea.set(cookieStoreId, containerState);
} catch (error) { } catch (error) {
@ -151,6 +151,42 @@ const backgroundLogic = {
} }
}, },
async addRemoveAllowedSite(cookieStoreId, allowedSiteUrl, remove = false) {
try {
const containerState = await identityState.storageArea.get(cookieStoreId);
const allowedSiteKey = Utils.getAllowedSiteKeyFor(allowedSiteUrl);
const allowedSites = containerState.allowedSites || [];
const allowedSiteIdx = allowedSites.indexOf(allowedSiteKey);
if (!remove) {
if (allowedSiteIdx === -1) {
// only add the site if it's not already in the list.
allowedSites.push(allowedSiteKey);
containerState.allowedSites = allowedSites;
}
} else {
// remove
if (allowedSiteIdx >= 0) {
allowedSites.splice(allowedSiteIdx, 1);
}
}
containerState.allowedSites = allowedSites;
return await identityState.storageArea.set(cookieStoreId, containerState);
} catch (error) {
console.error(`No container: ${cookieStoreId}`);
}
},
async clearAllowedSites(cookieStoreId) {
try {
const containerState = await identityState.storageArea.get(cookieStoreId);
containerState.allowedSites = [];
return await identityState.storageArea.set(cookieStoreId, containerState);
} catch (error) {
console.error(`No container: ${cookieStoreId}`);
}
},
async moveTabsToWindow(options) { async moveTabsToWindow(options) {
const requiredArguments = ["cookieStoreId", "windowId"]; const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "moveTabsToWindow"); this.checkArgs(requiredArguments, options, "moveTabsToWindow");
@ -257,7 +293,8 @@ const backgroundLogic = {
hasOpenTabs: !!openTabs.length, hasOpenTabs: !!openTabs.length,
numberOfHiddenTabs: containerState.hiddenTabs.length, numberOfHiddenTabs: containerState.hiddenTabs.length,
numberOfOpenTabs: openTabs.length, numberOfOpenTabs: openTabs.length,
isIsolated: !!containerState.isIsolated isIsolated: !!containerState.isIsolated,
allowedSites: containerState.allowedSites || []
}; };
return; return;
}); });

View file

@ -19,5 +19,6 @@
<script type="text/javascript" src="identityState.js"></script> <script type="text/javascript" src="identityState.js"></script>
<script type="text/javascript" src="messageHandler.js"></script> <script type="text/javascript" src="messageHandler.js"></script>
<script type="text/javascript" src="sync.js"></script> <script type="text/javascript" src="sync.js"></script>
<script type="text/javascript" src="../utils.js"></script>
</body> </body>
</html> </html>

View file

@ -35,6 +35,16 @@ const messageHandler = {
case "addRemoveSiteIsolation": case "addRemoveSiteIsolation":
response = backgroundLogic.addRemoveSiteIsolation(m.cookieStoreId); response = backgroundLogic.addRemoveSiteIsolation(m.cookieStoreId);
break; break;
case "addRemoveAllowedSite":
response = backgroundLogic.addRemoveAllowedSite(
m.cookieStoreId,
m.allowedSiteUrl,
m.remove
);
break;
case "clearAllowedSites":
response = backgroundLogic.clearAllowedSites(m.cookieStoreId);
break;
case "getAssignment": case "getAssignment":
response = browser.tabs.get(m.tabId).then((tab) => { response = browser.tabs.get(m.tabId).then((tab) => {
return assignManager._getAssignment(tab); return assignManager._getAssignment(tab);

View file

@ -26,6 +26,7 @@ const OPEN_NEW_CONTAINER_PICKER = "new-tab";
const MANAGE_CONTAINERS_PICKER = "manage"; const MANAGE_CONTAINERS_PICKER = "manage";
const REOPEN_IN_CONTAINER_PICKER = "reopen-in"; const REOPEN_IN_CONTAINER_PICKER = "reopen-in";
const ALWAYS_OPEN_IN_PICKER = "always-open-in"; const ALWAYS_OPEN_IN_PICKER = "always-open-in";
const ALLOW_OPEN_IN_PICKER = "allow-open-in";
const P_CONTAINER_INFO = "containerInfo"; const P_CONTAINER_INFO = "containerInfo";
const P_CONTAINER_EDIT = "containerEdit"; const P_CONTAINER_EDIT = "containerEdit";
const P_CONTAINER_DELETE = "containerDelete"; const P_CONTAINER_DELETE = "containerDelete";
@ -40,6 +41,15 @@ function addRemoveSiteIsolation() {
}); });
} }
function addRemoveAllowedSite(cookieStoreId, allowedSiteUrl, remove = false) {
return browser.runtime.sendMessage({
method: "addRemoveAllowedSite",
cookieStoreId: cookieStoreId,
allowedSiteUrl: allowedSiteUrl,
remove: remove
});
}
async function getExtensionInfo() { async function getExtensionInfo() {
const manifestPath = browser.extension.getURL("manifest.json"); const manifestPath = browser.extension.getURL("manifest.json");
const response = await fetch(manifestPath); const response = await fetch(manifestPath);
@ -225,6 +235,7 @@ const Logic = {
identity.numberOfHiddenTabs = stateObject.numberOfHiddenTabs; identity.numberOfHiddenTabs = stateObject.numberOfHiddenTabs;
identity.numberOfOpenTabs = stateObject.numberOfOpenTabs; identity.numberOfOpenTabs = stateObject.numberOfOpenTabs;
identity.isIsolated = stateObject.isIsolated; identity.isIsolated = stateObject.isIsolated;
identity.allowedSites = stateObject.allowedSites;
} }
if (containerOrder) { if (containerOrder) {
identity.order = containerOrder[identity.cookieStoreId]; identity.order = containerOrder[identity.cookieStoreId];
@ -302,6 +313,14 @@ const Logic = {
return this._currentIdentity; return this._currentIdentity;
}, },
async refreshCurrentIdentity() {
const current = this.currentIdentity();
await this.refreshIdentities();
this._currentIdentity = this.identities().find(
identity => identity.cookieStoreId === current.cookieStoreId
);
},
currentUserContextId() { currentUserContextId() {
const identity = Logic.currentIdentity(); const identity = Logic.currentIdentity();
return Utils.userContextId(identity.cookieStoreId); return Utils.userContextId(identity.cookieStoreId);
@ -645,6 +664,9 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
Utils.addEnterHandler(document.querySelector("#always-open-in"), () => { Utils.addEnterHandler(document.querySelector("#always-open-in"), () => {
Logic.showPanel(ALWAYS_OPEN_IN_PICKER); Logic.showPanel(ALWAYS_OPEN_IN_PICKER);
}); });
Utils.addEnterHandler(document.querySelector("#allow-open-in"), () => {
Logic.showPanel(ALLOW_OPEN_IN_PICKER);
});
Utils.addEnterHandler(document.querySelector("#info-icon"), () => { Utils.addEnterHandler(document.querySelector("#info-icon"), () => {
browser.runtime.openOptionsPage(); browser.runtime.openOptionsPage();
}); });
@ -668,6 +690,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
async prepare() { async prepare() {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const anyIsolatedContainers = Logic.identities().some(
identity => identity.isIsolated
);
const allowOpenIn = document.querySelector("#allow-open-in");
allowOpenIn.hidden = !anyIsolatedContainers;
Logic.identities().forEach(identity => { Logic.identities().forEach(identity => {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
tr.classList.add("menu-item", "hover-highlight", "keyboard-nav", "keyboard-right-arrow-override"); tr.classList.add("menu-item", "hover-highlight", "keyboard-nav", "keyboard-right-arrow-override");
@ -803,8 +832,8 @@ Logic.registerPanel(P_CONTAINER_INFO, {
}); });
// Populating the panel: name and icon // Populating the panel: name and icon
document.getElementById("container-info-title").textContent = identity.name; document.getElementById("container-info-title").textContent = identity.name;
const alwaysOpen = document.querySelector("#always-open-in-info-panel"); const alwaysOpen = document.querySelector("#always-open-in-info-panel");
Utils.addEnterHandler(alwaysOpen, async () => { Utils.addEnterHandler(alwaysOpen, async () => {
Utils.alwaysOpenInContainer(identity); Utils.alwaysOpenInContainer(identity);
window.close(); window.close();
@ -941,7 +970,7 @@ Logic.registerPanel(OPEN_NEW_CONTAINER_PICKER, {
tr.setAttribute("tabindex", "0"); tr.setAttribute("tabindex", "0");
const td = document.createElement("td"); const td = document.createElement("td");
td.innerHTML = Utils.escaped` td.innerHTML = Utils.escaped`
<div class="menu-icon"> <div class="menu-icon">
<div class="usercontext-icon" <div class="usercontext-icon"
data-identity-icon="${identity.icon}" data-identity-icon="${identity.icon}"
@ -1017,7 +1046,7 @@ Logic.registerPanel(MANAGE_CONTAINERS_PICKER, {
tr.setAttribute("tabindex", "0"); tr.setAttribute("tabindex", "0");
const td = document.createElement("td"); const td = document.createElement("td");
td.innerHTML = Utils.escaped` td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight"> <div class="menu-icon hover-highlight">
<div class="usercontext-icon" <div class="usercontext-icon"
data-identity-icon="${identity.icon}" data-identity-icon="${identity.icon}"
@ -1110,10 +1139,10 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
const pickedFunction = function (identity) { const pickedFunction = function (identity) {
const newUserContextId = Utils.userContextId(identity.cookieStoreId); const newUserContextId = Utils.userContextId(identity.cookieStoreId);
Utils.reloadInContainer( Utils.reloadInContainer(
currentTab.url, currentTab.url,
false, false,
newUserContextId, newUserContextId,
currentTab.index + 1, currentTab.index + 1,
currentTab.active currentTab.active
); );
window.close(); window.close();
@ -1126,7 +1155,7 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
tr.classList.add("menu-item", "hover-highlight", "keyboard-nav"); tr.classList.add("menu-item", "hover-highlight", "keyboard-nav");
const td = document.createElement("td"); const td = document.createElement("td");
td.innerHTML = Utils.escaped` td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight"> <div class="menu-icon hover-highlight">
<div class="mac-icon"> <div class="mac-icon">
</div> </div>
@ -1139,10 +1168,10 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
Utils.addEnterHandler(tr, () => { Utils.addEnterHandler(tr, () => {
Utils.reloadInContainer( Utils.reloadInContainer(
currentTab.url, currentTab.url,
false, false,
0, 0,
currentTab.index + 1, currentTab.index + 1,
currentTab.active currentTab.active
); );
window.close(); window.close();
@ -1156,7 +1185,7 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
tr.setAttribute("tabindex", "0"); tr.setAttribute("tabindex", "0");
const td = document.createElement("td"); const td = document.createElement("td");
td.innerHTML = Utils.escaped` td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight"> <div class="menu-icon hover-highlight">
<div class="usercontext-icon" <div class="usercontext-icon"
data-identity-icon="${identity.icon}" data-identity-icon="${identity.icon}"
@ -1208,7 +1237,7 @@ Logic.registerPanel(ALWAYS_OPEN_IN_PICKER, {
tr.setAttribute("tabindex", "0"); tr.setAttribute("tabindex", "0");
const td = document.createElement("td"); const td = document.createElement("td");
td.innerHTML = Utils.escaped` td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight"> <div class="menu-icon hover-highlight">
<div class="usercontext-icon" <div class="usercontext-icon"
data-identity-icon="${identity.icon}" data-identity-icon="${identity.icon}"
@ -1236,6 +1265,65 @@ Logic.registerPanel(ALWAYS_OPEN_IN_PICKER, {
} }
}); });
// ALLOW_OPEN_IN_PICKER: Makes the list editable.
// ----------------------------------------------------------------------------
Logic.registerPanel(ALLOW_OPEN_IN_PICKER, {
panelSelector: "#container-picker-panel",
// This method is called when the object is registered.
initialize() {},
// This method is called when the panel is shown.
prepare() {
Logic.listenToPickerBackButton();
document.getElementById("picker-title").textContent =
"Allow opening this site in";
const fragment = document.createDocumentFragment();
document.getElementById("new-container-div").innerHTML = Utils.escaped`
<div class="sub-header">Only showing isolated containers</div>
`;
Logic.identities()
.filter(identity => identity.isIsolated)
.forEach(identity => {
const tr = document.createElement("tr");
tr.classList.add("menu-item", "hover-highlight", "keyboard-nav");
tr.setAttribute("tabindex", "0");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}">
</div>
</div>
<span class="menu-text">${identity.name}</span>`;
fragment.appendChild(tr);
tr.appendChild(td);
Utils.addEnterHandler(tr, async () => {
// const currentTab = await this.currentTab();
const currentTab = await Utils.currentTab();
const url = currentTab.url;
addRemoveAllowedSite(identity.cookieStoreId, url);
window.close();
});
});
const list = document.querySelector("#picker-identities-list");
list.innerHTML = "";
list.appendChild(fragment);
return Promise.resolve(null);
}
});
// P_CONTAINER_ASSIGNMENTS: Shows Site Assignments and allows editing. // P_CONTAINER_ASSIGNMENTS: Shows Site Assignments and allows editing.
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -1259,7 +1347,15 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
const userContextId = Logic.currentUserContextId(); const userContextId = Logic.currentUserContextId();
const assignments = await Logic.getAssignmentObjectByContainer(userContextId); const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
this._addAllowedForm = document.getElementById("add-allowed-site-form");
this._addAllowedForm.addEventListener("submit", e => {
e.preventDefault();
this._submitAddAllowedSiteForm();
});
this.showAssignedContainers(assignments); this.showAssignedContainers(assignments);
this.showAllowedContainers(identity.isIsolated, identity.allowedSites);
return Promise.resolve(null); return Promise.resolve(null);
}, },
@ -1277,18 +1373,7 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
} }
assignmentKeys.forEach((siteKey) => { assignmentKeys.forEach((siteKey) => {
const site = assignments[siteKey]; const site = assignments[siteKey];
const trElement = document.createElement("tr"); const { trElement, deleteButton, assumedUrl } = this._createContainerRow(site.hostname);
/* As we don't have the full or correct path the best we can assume is the path is HTTPS and then replace with a broken icon later if it doesn't load.
This is pending a better solution for favicons from web extensions */
const assumedUrl = `https://${site.hostname}/favicon.ico`;
trElement.innerHTML = Utils.escaped`
<td>
<div class="favicon"></div>
<span title="${site.hostname}" class="menu-text">${site.hostname}</span>
<img class="trash-button delete-assignment" src="/img/container-delete.svg" />
</td>`;
trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
const deleteButton = trElement.querySelector(".trash-button");
Utils.addEnterHandler(deleteButton, async () => { Utils.addEnterHandler(deleteButton, async () => {
const userContextId = Logic.currentUserContextId(); const userContextId = Logic.currentUserContextId();
// Lets show the message to the current tab // Lets show the message to the current tab
@ -1297,11 +1382,67 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
delete assignments[siteKey]; delete assignments[siteKey];
this.showAssignedContainers(assignments); this.showAssignedContainers(assignments);
}); });
trElement.classList.add("menu-item", "hover-highlight", "keyboard-nav");
tableElement.appendChild(trElement); tableElement.appendChild(trElement);
}); });
} }
}, },
showAllowedContainers(isIsolated, allowed) {
const allowedSitesPanel = document.querySelector(".edit-allowed-sites-panel");
allowedSitesPanel.hidden = !isIsolated;
const tableElement = document.getElementById("edit-sites-allowed");
// Clear the previous list list
while (tableElement.firstChild) {
tableElement.firstChild.remove();
}
allowed.forEach((allowedKey, idx) => {
const hostname = Utils.getLabelForAllowedSiteKey(allowedKey);
const { trElement, deleteButton } = this._createContainerRow(hostname);
Utils.addEnterHandler(deleteButton, async () => {
const currentCookieStoreId = Logic.currentCookieStoreId();
addRemoveAllowedSite(currentCookieStoreId, hostname, true);
allowed.splice(idx, 1);
this.showAllowedContainers(isIsolated, allowed);
});
tableElement.appendChild(trElement);
});
},
_createContainerRow(hostname) {
const trElement = document.createElement("tr");
/* As we don't have the full or correct path the best we can assume is the path is HTTPS and then replace with a broken icon later if it doesn't load.
This is pending a better solution for favicons from web extensions */
const assumedUrl = `https://${hostname}/favicon.ico`;
trElement.innerHTML = Utils.escaped`
<td>
<div class="favicon"></div>
<span title="${hostname}" class="menu-text truncate-text">${hostname}</span>
<img class="trash-button delete-assignment" src="/img/container-delete.svg" />
</td>`;
trElement
.getElementsByClassName("favicon")[0]
.appendChild(Utils.createFavIconElement(assumedUrl));
const deleteButton = trElement.querySelector(".trash-button");
trElement.classList.add("menu-item", "hover-highlight", "keyboard-nav");
return { trElement, deleteButton, assumedUrl };
},
async _submitAddAllowedSiteForm() {
const formValues = new FormData(this._addAllowedForm);
const currentCookieStoreId = Logic.currentCookieStoreId();
const allowedSite = formValues.get("add-allowed-site-name");
await addRemoveAllowedSite(currentCookieStoreId, allowedSite);
await Logic.refreshCurrentIdentity();
const identity = Logic.currentIdentity();
this.showAllowedContainers(identity.isIsolated, identity.allowedSites);
this._addAllowedForm.reset();
},
}); });
// P_CONTAINER_EDIT: Editor for a container. // P_CONTAINER_EDIT: Editor for a container.

View file

@ -62,7 +62,7 @@ const Utils = {
} }
return false; return false;
}, },
addEnterHandler(element, handler) { addEnterHandler(element, handler) {
element.addEventListener("click", (e) => { element.addEventListener("click", (e) => {
handler(e); handler(e);
@ -82,7 +82,7 @@ const Utils = {
handler(e); handler(e);
} }
}); });
}, },
userContextId(cookieStoreId = "") { userContextId(cookieStoreId = "") {
const userContextId = cookieStoreId.replace("firefox-container-", ""); const userContextId = cookieStoreId.replace("firefox-container-", "");
@ -102,10 +102,10 @@ const Utils = {
async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) { async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
return await browser.runtime.sendMessage({ return await browser.runtime.sendMessage({
method: "reloadInContainer", method: "reloadInContainer",
url, url,
currentUserContextId, currentUserContextId,
newUserContextId, newUserContextId,
tabIndex, tabIndex,
active active
}); });
}, },
@ -116,20 +116,94 @@ const Utils = {
if (currentTab.cookieStoreId !== identity.cookieStoreId) { if (currentTab.cookieStoreId !== identity.cookieStoreId) {
return await browser.runtime.sendMessage({ return await browser.runtime.sendMessage({
method: "assignAndReloadInContainer", method: "assignAndReloadInContainer",
url: currentTab.url, url: currentTab.url,
currentUserContextId: false, currentUserContextId: false,
newUserContextId: assignedUserContextId, newUserContextId: assignedUserContextId,
tabIndex: currentTab.index +1, tabIndex: currentTab.index +1,
active:currentTab.active active:currentTab.active
}); });
} }
await Utils.setOrRemoveAssignment( await Utils.setOrRemoveAssignment(
currentTab.id, currentTab.id,
currentTab.url, currentTab.url,
assignedUserContextId, assignedUserContextId,
false false
); );
} },
/**
* Get the allowed site key for a given url, hostname, or hostname:port
* @param {string} pageUrl
* @returns the allowed site key for the given url
*/
getAllowedSiteKeyFor(pageUrl) {
if (!pageUrl) {
throw new Error("pageUrl cannot be empty");
}
if (pageUrl.startsWith("allowedSiteKey@@_")) {
// we trust that you're a key already
return pageUrl;
}
// attempt to parse the attribute as a naked hostname
if (this._isValidHostname(pageUrl)) {
return this._allowedSiteKeyForHostPort(pageUrl);
}
// attempt to parse the attribute as a hostname:port
if (pageUrl.includes(":")) {
const parts = pageUrl.split(":");
if (parts.length === 2) {
const potentialHost = parts[0];
const potentialPort = parts[1];
if (this._isValidHostname(potentialHost) && this._isValidPort(potentialPort)) {
return this._allowedSiteKeyForHostPort(potentialHost, potentialPort);
}
}
}
// try parsing the attribute as a page url
try {
const url = new window.URL(pageUrl);
return this._allowedSiteKeyForHostPort(url.hostname, url.port);
} catch (err) {
console.log(`paramter ${pageUrl} was not parsed as a url`);
}
throw new Error("pageUrl could not be parsed");
},
getLabelForAllowedSiteKey(allowedSiteKey) {
if (!allowedSiteKey) {
throw new Error("pageUrl cannot be empty");
}
if (allowedSiteKey.startsWith("allowedSiteKey@@_")) {
return allowedSiteKey.replace("allowedSiteKey@@_", "");
}
return allowedSiteKey;
},
_isValidPort(potentialPort) {
return potentialPort > 0 && potentialPort <= 65535;
},
_isValidHostname(potentialHostname) {
// From @bkr https://stackoverflow.com/a/20204811
return /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/.test(
potentialHostname
);
},
_allowedSiteKeyForHostPort(hostname, port) {
if (port === undefined || port === "" || port === "80" || port === "443") {
return `allowedSiteKey@@_${hostname}`;
} else {
return `allowedSiteKey@@_${hostname}:${port}`;
}
},
}; };

View file

@ -162,6 +162,15 @@
</span> </span>
</td> </td>
</tr> </tr>
<tr class="menu-item hover-highlight keyboard-nav" id="allow-open-in" tabindex="0">
<td>
<img class="menu-icon" alt="Open in New Tab" src="/img/container-openin-16.svg" />
<span class="menu-text">Allow Opening This Site in...</span>
<span class="menu-arrow">
<img alt="Container Info" src="/img/arrow-icon-right.svg" />
</span>
</td>
</tr>
</table> </table>
<hr> <hr>
<div class="sub-header"> <div class="sub-header">
@ -325,17 +334,41 @@
</h3> </h3>
<button class="btn-return arrow-left" id="close-container-assignment-panel"></button> <button class="btn-return arrow-left" id="close-container-assignment-panel"></button>
<hr> <hr>
<div class="scrollable edit-sites-assigned"> <div class="scrollable">
<div class="sub-header">Sites assigned to this container</div> <div class="edit-sites-assigned">
<table class="menu scrollable" id="edit-sites-assigned"> <div class="sub-header">Sites assigned to this container</div>
<tr class="menu-item hover-highlight" tabindex="0"> <table class="menu scrollable" id="edit-sites-assigned">
<td> <tr class="menu-item hover-highlight" tabindex="0">
<div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div> <td>
<span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllla.org</span> <div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div>
<img class="trash-button" src="/img/container-delete.svg" /> <span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllla.org</span>
</td> <img class="trash-button" src="/img/container-delete.svg" />
</tr> </td>
</table> </tr>
</table>
<div class="edit-allowed-sites-panel">
<hr>
<div class="sub-header">Allowed sites for this container</div>
<table class="menu scrollable" id="edit-sites-allowed">
<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 class="edit-form">
<form id="add-allowed-site-form">
<fieldset>
<legend class="form-header" id="add-allowed-site-name-label">Add Allowed Site</legend>
<input type="text" name="add-allowed-site-name" id="add-allowed-site-name-input" class="edit-container-panel-name-input" aria-labelledby="add-allowed-site-name-label"/>
</fieldset>
<button type="submit">Add</button>
</form>
</div>
</div>
</div>
</div> </div>
</div> </div>