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;
}
.edit-allowed-sites-panel fieldset {
background: none;
border: none;
}
.edit-container-panel fieldset:last-of-type {
margin-block-end: 0;
}
@ -729,6 +734,7 @@ h3.title {
}
/* 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,
#container-info-table .menu-icon {
inline-size: 16px;
@ -952,6 +958,16 @@ tr:hover > td > .trash-button {
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) {
:root {
--title-text-color: #fff;

View file

@ -109,10 +109,10 @@ window.assignManager = {
const siteConfigs = await this.area.get();
for(const urlKey of Object.keys(siteConfigs)) {
if (urlKey.includes("siteContainerMap@@_")) {
// For some reason this is stored as string... lets check
// For some reason this is stored as string... lets check
// them both as that
if (!!userContextId &&
String(siteConfigs[urlKey].userContextId)
if (!!userContextId &&
String(siteConfigs[urlKey].userContextId)
!== String(userContextId)) {
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
* is removed.
*/
@ -136,8 +136,8 @@ window.assignManager = {
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 cookieStoreId =
"firefox-container-" + macConfigs[configKey].userContextId;
const match = identitiesList.find(
localIdentity => localIdentity.cookieStoreId === cookieStoreId
);
@ -146,7 +146,7 @@ window.assignManager = {
continue;
}
const updatedSiteAssignment = macConfigs[configKey];
updatedSiteAssignment.identityMacAddonUUID =
updatedSiteAssignment.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(match.cookieStoreId);
await this.set(
configKey,
@ -164,7 +164,7 @@ window.assignManager = {
_neverAsk(m) {
const pageUrl = m.pageUrl;
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
this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) {
@ -210,9 +210,10 @@ window.assignManager = {
return {};
}
const userContextId = this.getUserContextIdFromCookieStore(tab);
const url = options.url;
// 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:
//
@ -228,8 +229,11 @@ window.assignManager = {
// - 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);
const siteIsolatedReloadInDefault = await this._maybeSiteIsolatedReloadInDefault(
siteSettings,
tab,
url
);
if (!siteIsolatedReloadInDefault) {
if (!siteSettings
@ -246,7 +250,7 @@ window.assignManager = {
const openTabId = removeTab ? tab.openerTabId : 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
this.canceledRequests[tab.id] = {
requestIds: {
@ -313,7 +317,7 @@ window.assignManager = {
- 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:
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
@ -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.
if (!("cookieStoreId" in tab)) {
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
// current tab's container is locked or not.
if (siteSettings) {
return false;
return false;
}
//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
// is locked, then the page must be reloaded in the default container.
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() {
browser.contextMenus.onClicked.addListener((info, tab) => {
info.bookmarkId ?
this._onClickedBookmark(info) :
info.bookmarkId ?
this._onClickedBookmark(info) :
this._onClickedHandler(info, tab);
});
@ -479,7 +497,7 @@ window.assignManager = {
async _onClickedBookmark(info) {
async function _getBookmarksFromInfo(info) {
const [bookmarkTreeNode] =
const [bookmarkTreeNode] =
await browser.bookmarks.get(info.bookmarkId);
if (bookmarkTreeNode.type === "folder") {
return browser.bookmarks.getChildren(bookmarkTreeNode.id);
@ -489,9 +507,9 @@ window.assignManager = {
const bookmarks = await _getBookmarksFromInfo(info);
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!
if ( !/^(javascript|place):/i.test(bookmark.url) &&
if ( !/^(javascript|place):/i.test(bookmark.url) &&
bookmark.type !== "folder") {
const openInReaderMode = bookmark.url.startsWith("about:reader");
if(openInReaderMode) {
@ -569,12 +587,12 @@ window.assignManager = {
actionName = "removed from assigned sites list";
// remove site isolation if now empty
await this._maybeRemoveSiteIsolation(userContextId);
await this._maybeRemoveSiteIsolation(userContextId);
}
if (tabId) {
const tab = await browser.tabs.get(tabId);
setTimeout(function(){
setTimeout(function(){
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName}`
});
@ -677,17 +695,17 @@ window.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,

View file

@ -143,7 +143,7 @@ const backgroundLogic = {
if ("isIsolated" in containerState || remove) {
delete containerState.isIsolated;
} else {
containerState.isIsolated = "locked";
containerState.isIsolated = "locked";
}
return await identityState.storageArea.set(cookieStoreId, containerState);
} 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) {
const requiredArguments = ["cookieStoreId", "windowId"];
this.checkArgs(requiredArguments, options, "moveTabsToWindow");
@ -257,7 +293,8 @@ const backgroundLogic = {
hasOpenTabs: !!openTabs.length,
numberOfHiddenTabs: containerState.hiddenTabs.length,
numberOfOpenTabs: openTabs.length,
isIsolated: !!containerState.isIsolated
isIsolated: !!containerState.isIsolated,
allowedSites: containerState.allowedSites || []
};
return;
});

View file

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

View file

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

View file

@ -26,6 +26,7 @@ const OPEN_NEW_CONTAINER_PICKER = "new-tab";
const MANAGE_CONTAINERS_PICKER = "manage";
const REOPEN_IN_CONTAINER_PICKER = "reopen-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_EDIT = "containerEdit";
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() {
const manifestPath = browser.extension.getURL("manifest.json");
const response = await fetch(manifestPath);
@ -225,6 +235,7 @@ const Logic = {
identity.numberOfHiddenTabs = stateObject.numberOfHiddenTabs;
identity.numberOfOpenTabs = stateObject.numberOfOpenTabs;
identity.isIsolated = stateObject.isIsolated;
identity.allowedSites = stateObject.allowedSites;
}
if (containerOrder) {
identity.order = containerOrder[identity.cookieStoreId];
@ -302,6 +313,14 @@ const Logic = {
return this._currentIdentity;
},
async refreshCurrentIdentity() {
const current = this.currentIdentity();
await this.refreshIdentities();
this._currentIdentity = this.identities().find(
identity => identity.cookieStoreId === current.cookieStoreId
);
},
currentUserContextId() {
const identity = Logic.currentIdentity();
return Utils.userContextId(identity.cookieStoreId);
@ -645,6 +664,9 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
Utils.addEnterHandler(document.querySelector("#always-open-in"), () => {
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"), () => {
browser.runtime.openOptionsPage();
});
@ -668,6 +690,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
async prepare() {
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 => {
const tr = document.createElement("tr");
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
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.alwaysOpenInContainer(identity);
window.close();
@ -941,7 +970,7 @@ Logic.registerPanel(OPEN_NEW_CONTAINER_PICKER, {
tr.setAttribute("tabindex", "0");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
td.innerHTML = Utils.escaped`
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
@ -1017,7 +1046,7 @@ Logic.registerPanel(MANAGE_CONTAINERS_PICKER, {
tr.setAttribute("tabindex", "0");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
@ -1110,10 +1139,10 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
const pickedFunction = function (identity) {
const newUserContextId = Utils.userContextId(identity.cookieStoreId);
Utils.reloadInContainer(
currentTab.url,
false,
currentTab.url,
false,
newUserContextId,
currentTab.index + 1,
currentTab.index + 1,
currentTab.active
);
window.close();
@ -1126,7 +1155,7 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
tr.classList.add("menu-item", "hover-highlight", "keyboard-nav");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight">
<div class="mac-icon">
</div>
@ -1139,10 +1168,10 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
Utils.addEnterHandler(tr, () => {
Utils.reloadInContainer(
currentTab.url,
false,
currentTab.url,
false,
0,
currentTab.index + 1,
currentTab.index + 1,
currentTab.active
);
window.close();
@ -1156,7 +1185,7 @@ Logic.registerPanel(REOPEN_IN_CONTAINER_PICKER, {
tr.setAttribute("tabindex", "0");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
@ -1208,7 +1237,7 @@ Logic.registerPanel(ALWAYS_OPEN_IN_PICKER, {
tr.setAttribute("tabindex", "0");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight">
<div class="usercontext-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.
// ----------------------------------------------------------------------------
@ -1259,7 +1347,15 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
const userContextId = Logic.currentUserContextId();
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.showAllowedContainers(identity.isIsolated, identity.allowedSites);
return Promise.resolve(null);
},
@ -1277,18 +1373,7 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
}
assignmentKeys.forEach((siteKey) => {
const site = assignments[siteKey];
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://${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");
const { trElement, deleteButton, assumedUrl } = this._createContainerRow(site.hostname);
Utils.addEnterHandler(deleteButton, async () => {
const userContextId = Logic.currentUserContextId();
// Lets show the message to the current tab
@ -1297,11 +1382,67 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
delete assignments[siteKey];
this.showAssignedContainers(assignments);
});
trElement.classList.add("menu-item", "hover-highlight", "keyboard-nav");
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.

View file

@ -62,7 +62,7 @@ const Utils = {
}
return false;
},
addEnterHandler(element, handler) {
element.addEventListener("click", (e) => {
handler(e);
@ -82,7 +82,7 @@ const Utils = {
handler(e);
}
});
},
},
userContextId(cookieStoreId = "") {
const userContextId = cookieStoreId.replace("firefox-container-", "");
@ -102,10 +102,10 @@ const Utils = {
async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
return await browser.runtime.sendMessage({
method: "reloadInContainer",
url,
currentUserContextId,
newUserContextId,
tabIndex,
url,
currentUserContextId,
newUserContextId,
tabIndex,
active
});
},
@ -116,20 +116,94 @@ const Utils = {
if (currentTab.cookieStoreId !== identity.cookieStoreId) {
return await browser.runtime.sendMessage({
method: "assignAndReloadInContainer",
url: currentTab.url,
currentUserContextId: false,
newUserContextId: assignedUserContextId,
tabIndex: currentTab.index +1,
url: currentTab.url,
currentUserContextId: false,
newUserContextId: assignedUserContextId,
tabIndex: currentTab.index +1,
active:currentTab.active
});
}
await Utils.setOrRemoveAssignment(
currentTab.id,
currentTab.url,
assignedUserContextId,
currentTab.id,
currentTab.url,
assignedUserContextId,
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>
</td>
</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>
<hr>
<div class="sub-header">
@ -325,17 +334,41 @@
</h3>
<button class="btn-return arrow-left" id="close-container-assignment-panel"></button>
<hr>
<div class="scrollable edit-sites-assigned">
<div class="sub-header">Sites assigned to this container</div>
<table class="menu scrollable" id="edit-sites-assigned">
<tr class="menu-item hover-highlight" tabindex="0">
<td>
<div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div>
<span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllla.org</span>
<img class="trash-button" src="/img/container-delete.svg" />
</td>
</tr>
</table>
<div class="scrollable">
<div class="edit-sites-assigned">
<div class="sub-header">Sites assigned to this container</div>
<table class="menu scrollable" id="edit-sites-assigned">
<tr class="menu-item hover-highlight" tabindex="0">
<td>
<div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div>
<span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllla.org</span>
<img class="trash-button" src="/img/container-delete.svg" />
</td>
</tr>
</table>
<div 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>