This commit is contained in:
Francis McKenzie 2022-07-09 00:35:20 +00:00 committed by GitHub
commit 2a9768228c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 304 additions and 8 deletions

View file

@ -1812,6 +1812,18 @@ manage things like container crud */
padding-inline-start: 16px;
}
#edit-sites-assigned .hostname .subdomain:hover {
text-decoration: underline;
}
#edit-sites-assigned .hostname .subdomain.wildcardSubdomain {
background-color: var(--identity-icon-color);
border-radius: 8px;
margin-right: 4px;
padding-left: 10px;
padding-right: 10px;
}
.assigned-sites-list > div {
display: flex;
padding-block-end: 6px;

View file

@ -20,6 +20,22 @@ window.assignManager = {
}
},
getWildcardStoreKey(wildcardHostname) {
return `wildcardMap@@_${wildcardHostname}`;
},
getWildcardStoreKeys(siteStoreKey) {
// E.g. "siteContainerMap@@_www.mozilla.org" =>
// ["wildcardMap@@_www.mozilla.org", "wildcardMap@@_mozilla.org", "wildcardMap@@_org"]
let previous;
return siteStoreKey.replace(/^siteContainerMap@@_/, "")
.split(".")
.reverse()
.map((subdomain) => previous = previous ? `${subdomain}.${previous}` : subdomain)
.map((hostname) => this.getWildcardStoreKey(hostname))
.reverse();
},
setExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) {
@ -46,6 +62,42 @@ window.assignManager = {
return this.getByUrlKey(siteStoreKey);
},
async getOrWildcardMatch(pageUrlorUrlKey) {
// 1st store request: siteStoreKey + wildcardStoreKeys
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
const wildcardStoreKeys = this.getWildcardStoreKeys(siteStoreKey);
const combinedStoreKeys = [siteStoreKey].concat(wildcardStoreKeys);
let storageResponse = await this.area.get(combinedStoreKeys);
if (!storageResponse) { return null; }
// Try exact match
const siteSettings = storageResponse[siteStoreKey];
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}
// 2nd store request (maybe): siteStoreKeys that were mapped from wildcardStoreKeys
const siteStoreKeys = wildcardStoreKeys.map((k) => storageResponse[k]).filter((k) => !!k);
if (siteStoreKeys.length > 0) {
storageResponse = await this.area.get(siteStoreKeys);
if (!storageResponse) { return null; }
// Try wildcard matches
for (const siteStoreKey of siteStoreKeys) {
const siteSettings = storageResponse[siteStoreKey];
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}
}
}
},
async getSyncEnabled() {
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
return !!syncEnabled;
@ -76,12 +128,19 @@ window.assignManager = {
this.setExempted(pageUrlorUrlKey, tabId);
});
}
if (data.wildcardHostname) {
await this.removeDuplicateWildcardHostname(data.wildcardHostname, siteStoreKey);
}
await this.removeWildcardLookup(siteStoreKey);
// eslint-disable-next-line require-atomic-updates
data.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(data.userContextId);
await this.area.set({
[siteStoreKey]: data
});
if (data.wildcardHostname) {
await this.setWildcardLookup(siteStoreKey, data.wildcardHostname);
}
const syncEnabled = await this.getSyncEnabled();
if (backup && syncEnabled) {
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
@ -89,19 +148,64 @@ window.assignManager = {
return;
},
async setWildcardLookup(siteStoreKey, wildcardHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
return this.area.set({
[wildcardStoreKey]: siteStoreKey
});
},
async remove(pageUrlorUrlKey, shouldSync = true) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
// When we remove an assignment we should clear all the exemptions
this.removeExempted(pageUrlorUrlKey);
// When we remove an assignment we should clear the wildcard lookup
await this.removeWildcardLookup(siteStoreKey);
await this.area.remove([siteStoreKey]);
const syncEnabled = await this.getSyncEnabled();
if (shouldSync && syncEnabled) await sync.storageArea.backup({siteStoreKey});
return;
},
async removeWildcardLookup(siteStoreKey) {
const siteSettings = await this.getByUrlKey(siteStoreKey);
const wildcardHostname = siteSettings && siteSettings.wildcardHostname;
if (wildcardHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
await this.area.remove([wildcardStoreKey]);
}
},
// Must not set the same wildcardHostname property on multiple sites.
// E.g. 'google.com' on both 'www.google.com' and 'mail.google.com'.
//
// Necessary because the stored wildcardLookup map is 1-to-1, i.e. either
// 'google.com' => 'www.google.com', or
// 'google.com' => 'mail.google.com', but not both!
async removeDuplicateWildcardHostname(wildcardHostname, expectedSiteStoreKey) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
const siteStoreKey = await this.getByUrlKey(wildcardStoreKey);
if (siteStoreKey && siteStoreKey !== expectedSiteStoreKey) {
const siteSettings = await this.getByUrlKey(siteStoreKey);
if (siteSettings && siteSettings.wildcardHostname === wildcardHostname) {
delete siteSettings.wildcardHostname;
await this.set(siteStoreKey, siteSettings); // Will cause wildcard mapping to be cleared
}
}
},
async deleteContainer(userContextId) {
const sitesByContainer = await this.getAssignedSites(userContextId);
this.area.remove(Object.keys(sitesByContainer));
// Delete wildcard lookups
const wildcardStoreKeys = Object.values(sitesByContainer)
.map((site) => {
if (site && site.wildcardHostname) {
return this.getWildcardStoreKey(site.wildcardHostname);
}
})
.filter((wildcardStoreKey) => { return !!wildcardStoreKey; });
this.area.remove(wildcardStoreKeys);
},
async getAssignedSites(userContextId = null) {
@ -166,10 +270,10 @@ window.assignManager = {
if (m.neverAsk === true) {
// 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;
this.storageArea.set(pageUrl, siteSettings);
this.storageArea.getOrWildcardMatch(pageUrl).then((siteMatchResult) => {
if (siteMatchResult) {
siteMatchResult.siteSettings.neverAsk = true;
this.storageArea.set(siteMatchResult.siteStoreKey, siteMatchResult.siteSettings);
}
}).catch((e) => {
throw e;
@ -217,10 +321,11 @@ window.assignManager = {
return {};
}
this.removeContextMenu();
const [tab, siteSettings] = await Promise.all([
const [tab, siteMatchResult] = await Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.get(options.url)
this.storageArea.getOrWildcardMatch(options.url)
]);
const siteSettings = siteMatchResult && siteMatchResult.siteSettings;
let container;
try {
container = await browser.contextualIdentities
@ -620,6 +725,14 @@ window.assignManager = {
}
},
async _setWildcardHostnameForAssignment(pageUrl, wildcardHostname) {
const siteSettings = await this.storageArea.get(pageUrl);
if (siteSettings) {
siteSettings.wildcardHostname = wildcardHostname;
await this.storageArea.set(pageUrl, siteSettings);
}
},
async _maybeRemoveSiteIsolation(userContextId) {
const assignments = await this.storageArea.getByContainer(userContextId);
const hasAssignments = assignments && Object.keys(assignments).length > 0;
@ -637,7 +750,8 @@ window.assignManager = {
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
return this.storageArea.get(tab.url);
const siteMatchResult = await this.storageArea.getOrWildcardMatch(tab.url);
return siteMatchResult && siteMatchResult.siteSettings;
}
return false;
},

View file

@ -45,6 +45,9 @@ const messageHandler = {
// m.url is the assignment to be removed/added
response = assignManager._setOrRemoveAssignment(m.tabId, m.url, m.userContextId, m.value);
break;
case "setWildcardHostnameForAssignment":
response = assignManager._setWildcardHostnameForAssignment(m.url, m.wildcardHostname);
break;
case "sortTabs":
backgroundLogic.sortTabs();
break;

View file

@ -1376,6 +1376,7 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
// Populating the panel: name and icon
document.getElementById("edit-assignments-title").textContent = identity.name;
document.getElementById("edit-sites-assigned").setAttribute("data-identity-color", identity.color);
const userContextId = Logic.currentUserContextId();
const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
@ -1410,10 +1411,11 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
trElement.innerHTML = Utils.escaped`
<td>
<div class="favicon"></div>
<span title="${site.hostname}" class="menu-text">${site.hostname}</span>
<span title="${site.hostname}" class="menu-text hostname"></span>
<img class="trash-button delete-assignment" src="/img/container-delete.svg" />
</td>`;
trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
trElement.querySelector(".hostname").appendChild(this.assignmentHostnameElement(site));
const deleteButton = trElement.querySelector(".trash-button");
Utils.addEnterHandler(deleteButton, async () => {
const userContextId = Logic.currentUserContextId();
@ -1423,11 +1425,92 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
delete assignments[siteKey];
this.showAssignedContainers(assignments);
});
// Wildcard click-to-toggle subdomains
trElement.querySelectorAll(".subdomain").forEach((subdomainLink) => {
subdomainLink.addEventListener("click", (e) => {
const wildcardHostname = e.target.getAttribute("data-wildcardHostname");
Utils.setWildcardHostnameForAssignment(assumedUrl, wildcardHostname);
if (wildcardHostname) {
// Remove wildcard from other site that has same wildcard
Object.values(assignments).forEach((site) => {
if (site.wildcardHostname === wildcardHostname) { delete site.wildcardHostname; }
});
site.wildcardHostname = wildcardHostname;
} else {
delete site.wildcardHostname;
}
this.showAssignedContainers(assignments);
});
});
trElement.classList.add("menu-item", "hover-highlight", "keyboard-nav");
tableElement.appendChild(trElement);
});
}
},
getSubdomains(site) {
const hostname = site.hostname;
const wildcardHostname = site.wildcardHostname;
if (wildcardHostname && wildcardHostname !== hostname) {
if (hostname.endsWith(wildcardHostname)) {
return {
wildcard: "★",
remaining: wildcardHostname
};
} else {
// In case something got corrupted, allow user to fix error
// by clicking '★' link to clear corrupted wildcard hostname
return {
wildcard: "★",
remaining: hostname
};
}
} else {
return {
wildcard: null,
remaining: hostname
};
}
},
assignmentHostnameElement(site) {
const result = document.createElement("span");
const subdomains = this.getSubdomains(site);
// Add wildcard subdomain(s)
if (subdomains.wildcard) {
result.appendChild(this.assignmentSubdomainLink(null, subdomains.wildcard));
result.appendChild(document.createTextNode("."));
}
// Add non-wildcard subdomains
let remainingHostname = subdomains.remaining;
let indexOfDot;
while ((indexOfDot = remainingHostname.indexOf(".")) >= 0) {
const subdomain = remainingHostname.substring(0, indexOfDot);
remainingHostname = remainingHostname.substring(indexOfDot + 1);
result.appendChild(this.assignmentSubdomainLink(remainingHostname, subdomain));
result.appendChild(document.createTextNode("."));
}
// Root domain
if (remainingHostname) { result.appendChild(document.createTextNode(remainingHostname)); }
return result;
},
assignmentSubdomainLink(wildcardHostnameOnClick, text) {
const result = document.createElement("a");
result.className = "subdomain";
if (wildcardHostnameOnClick) {
result.setAttribute("data-wildcardHostname", wildcardHostnameOnClick);
result.title = `*.${wildcardHostnameOnClick}`;
} else {
result.classList.add("wildcardSubdomain");
}
result.appendChild(document.createTextNode(text));
return result;
},
});
// P_CONTAINER_EDIT: Editor for a container.

View file

@ -138,6 +138,14 @@ const Utils = {
});
},
setWildcardHostnameForAssignment(url, wildcardHostname) {
return browser.runtime.sendMessage({
method: "setWildcardHostnameForAssignment",
url,
wildcardHostname
});
},
async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
return await browser.runtime.sendMessage({
method: "reloadInContainer",

View file

@ -0,0 +1,76 @@
const {initializeWithTab} = require("../common");
describe("Wildcard Subdomains Feature", function () {
const url1 = "http://www.example.com";
const url2 = "http://zzz.example.com";
const wildcardHostname = "example.com";
beforeEach(async function () {
this.webExt = await initializeWithTab({
cookieStoreId: "firefox-container-4",
url: url1
});
await this.webExt.popup.helper.clickElementById("always-open-in");
await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item");
});
afterEach(function () {
this.webExt.destroy();
});
describe("open new Tab with different subdomain in the default container", function () {
beforeEach(async function () {
// new Tab opening url2 in default container
await this.webExt.background.browser.tabs._create({
cookieStoreId: "firefox-default",
url: url2
}, {
options: {
webRequestError: true // because request is canceled due to reopening
}
});
});
it("should not open the confirm page", async function () {
this.webExt.background.browser.tabs.create.should.not.have.been.called;
});
it("should not remove the new Tab that got opened in the default container", function () {
this.webExt.background.browser.tabs.remove.should.not.have.been.called;
});
});
describe("set wildcard hostname and then open new Tab with different subdomain in the default container", function () {
let newTab;
beforeEach(async function () {
// Set wildcard
await this.webExt.background.window.assignManager._setWildcardHostnameForAssignment(url1, wildcardHostname);
// new Tab opening url2 in default container
newTab = await this.webExt.background.browser.tabs._create({
cookieStoreId: "firefox-default",
url: url2
}, {
options: {
webRequestError: true // because request is canceled due to reopening
}
});
});
it("should open the confirm page", async function () {
this.webExt.background.browser.tabs.create.should.have.been.calledWithMatch({
url: "moz-extension://fake/confirm-page.html?" +
`url=${encodeURIComponent(url2)}` +
`&cookieStoreId=${this.webExt.tab.cookieStoreId}`,
cookieStoreId: undefined,
openerTabId: null,
index: 2,
active: true
});
});
it("should remove the new Tab that got opened in the default container", function () {
this.webExt.background.browser.tabs.remove.should.have.been.calledWith(newTab.id);
});
});
});