From 6ab8da3b3a1b1076b07458413c52f8d207b36f4a Mon Sep 17 00:00:00 2001 From: Francis McKenzie Date: Wed, 4 May 2022 14:48:08 +0200 Subject: [PATCH] Wildcard subdomains - e.g. *.google.com --- src/css/popup.css | 8 +++ src/js/background/assignManager.js | 88 +++++++++++++++++++++++++++-- src/js/background/messageHandler.js | 3 + src/js/popup.js | 82 ++++++++++++++++++++++++++- src/js/utils.js | 8 +++ test/features/wildcard.test.js | 76 +++++++++++++++++++++++++ 6 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 test/features/wildcard.test.js diff --git a/src/css/popup.css b/src/css/popup.css index d07c25ec..9943326f 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -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; diff --git a/src/js/background/assignManager.js b/src/js/background/assignManager.js index 907e3c38..9b4891ff 100644 --- a/src/js/background/assignManager.js +++ b/src/js/background/assignManager.js @@ -20,6 +20,10 @@ window.assignManager = { } }, + getWildcardStoreKey(wildcardHostname) { + return `wildcardMap@@_${wildcardHostname}`; + }, + setExempted(pageUrlorUrlKey, tabId) { const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey); if (!(siteStoreKey in this.exemptedTabs)) { @@ -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; @@ -69,6 +85,26 @@ 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) { @@ -76,12 +112,16 @@ window.assignManager = { 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}); @@ -89,19 +129,46 @@ 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]); + } + }, + 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 +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; @@ -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 @@ -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; diff --git a/src/js/background/messageHandler.js b/src/js/background/messageHandler.js index 5d644b60..b748916c 100644 --- a/src/js/background/messageHandler.js +++ b/src/js/background/messageHandler.js @@ -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; diff --git a/src/js/popup.js b/src/js/popup.js index c10242f8..67fea6c0 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -1411,10 +1411,11 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, { trElement.innerHTML = Utils.escaped`
- ${site.hostname} + `; 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(); @@ -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. diff --git a/src/js/utils.js b/src/js/utils.js index f1932acd..4cde3790 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -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", diff --git a/test/features/wildcard.test.js b/test/features/wildcard.test.js new file mode 100644 index 00000000..50f65c67 --- /dev/null +++ b/test/features/wildcard.test.js @@ -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); + }); + }); +});