This commit is contained in:
Noah Callaway 2021-08-09 16:20:14 +00:00 committed by GitHub
commit 0ef10bb123
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 407 additions and 68 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

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
<title>container-allowin-16</title>
<desc>Created with Sketch.</desc>
<g id="container-allowin-16" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M8,15 C4.13400675,15 1,11.8659932 1,8 C1,4.13400675 4.13400675,1 8,1 C11.8659932,1 15,4.13400675 15,8 C15,11.8659932 11.8659932,15 8,15 Z M10.2928932,5.29289322 L7,8.58578644 L5.70710678,7.29289322 C5.31658249,6.90236893 4.68341751,6.90236893 4.29289322,7.29289322 C3.90236893,7.68341751 3.90236893,8.31658249 4.29289322,8.70710678 L6.29289322,10.7071068 C6.68341751,11.0976311 7.31658249,11.0976311 7.70710678,10.7071068 L11.7071068,6.70710678 C12.0976311,6.31658249 12.0976311,5.68341751 11.7071068,5.29289322 C11.3165825,4.90236893 10.6834175,4.90236893 10.2928932,5.29289322 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -210,6 +210,7 @@ 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
// //
@ -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
@ -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;
@ -358,7 +362,21 @@ 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() {

View file

@ -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.runtime.getURL("manifest.json"); const manifestPath = browser.runtime.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");
@ -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

@ -129,7 +129,81 @@ const Utils = {
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-allowin-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>