Skip to content

Commit

Permalink
Wildcard subdomains - e.g. *.google.com
Browse files Browse the repository at this point in the history
  • Loading branch information
mckenfra committed May 16, 2022
1 parent adeab46 commit 6ab8da3
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 7 deletions.
8 changes: 8 additions & 0 deletions src/css/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,14 @@ manage things like container crud */
padding-inline-start: 16px;
}

#edit-sites-assigned .hostname .subdomain:hover {
text-decoration: underline;
}

#edit-sites-assigned .hostname .subdomain.wildcardSubdomain {
opacity: 0.2;
}

.assigned-sites-list > div {
display: flex;
padding-block-end: 6px;
Expand Down
88 changes: 82 additions & 6 deletions src/js/background/assignManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ window.assignManager = {
}
},

getWildcardStoreKey(wildcardHostname) {
return `wildcardMap@@_${wildcardHostname}`;
},

setExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) {
Expand All @@ -46,6 +50,18 @@ window.assignManager = {
return this.getByUrlKey(siteStoreKey);
},

async getOrWildcardMatch(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
const siteSettings = await this.getByUrlKey(siteStoreKey);
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}
return this.getByWildcardMatch(siteStoreKey);
},

async getSyncEnabled() {
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
return !!syncEnabled;
Expand All @@ -69,39 +85,90 @@ window.assignManager = {
});
},

async getByWildcardMatch(siteStoreKey) {
// Keep stripping subdomains off site hostname until match a wildcard hostname
let remainingHostname = siteStoreKey.replace(/^siteContainerMap@@_/, "");
while (remainingHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(remainingHostname);
siteStoreKey = await this.getByUrlKey(wildcardStoreKey);
if (siteStoreKey) {
const siteSettings = await this.getByUrlKey(siteStoreKey);
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}
}
const indexOfDot = remainingHostname.indexOf(".");
remainingHostname = indexOfDot < 0 ? null : remainingHostname.substring(indexOfDot + 1);
}
},

async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (exemptedTabIds) {
exemptedTabIds.forEach((tabId) => {
this.setExempted(pageUrlorUrlKey, tabId);
});
}
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});
}
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]);
}
},

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) {
Expand Down Expand Up @@ -166,10 +233,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;
Expand Down Expand Up @@ -217,10 +284,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
Expand Down Expand Up @@ -620,6 +688,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;
Expand Down
3 changes: 3 additions & 0 deletions src/js/background/messageHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
82 changes: 81 additions & 1 deletion src/js/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -1411,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();
Expand All @@ -1424,11 +1425,90 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
delete assignments[siteKey];
this.showAssignedContainers(assignments);
});
// Wildcard click-to-toggle subdomains
trElement.querySelectorAll(".subdomain").forEach((subdomainLink) => {
subdomainLink.addEventListener("click", async (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: hostname.substring(0, hostname.length - wildcardHostname.length),
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));
}

// 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);
} else {
result.classList.add("wildcardSubdomain");
}
result.appendChild(document.createTextNode(text));
return result;
},
});

// P_CONTAINER_EDIT: Editor for a container.
Expand Down
8 changes: 8 additions & 0 deletions src/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 76 additions & 0 deletions test/features/wildcard.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit 6ab8da3

Please sign in to comment.