From 6ab8da3b3a1b1076b07458413c52f8d207b36f4a Mon Sep 17 00:00:00 2001 From: Francis McKenzie Date: Wed, 4 May 2022 14:48:08 +0200 Subject: [PATCH 1/5] 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); + }); + }); +}); From 8f5a7e6d396334cb9875b4080e3f44cee3086b3e Mon Sep 17 00:00:00 2001 From: Francis McKenzie Date: Wed, 4 May 2022 18:57:40 +0200 Subject: [PATCH 2/5] Wildcard subdomains - prevent duplicate wildcard mappings --- src/js/background/assignManager.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/js/background/assignManager.js b/src/js/background/assignManager.js index 9b4891ff..d8ff37c4 100644 --- a/src/js/background/assignManager.js +++ b/src/js/background/assignManager.js @@ -112,6 +112,9 @@ 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 = @@ -157,6 +160,24 @@ window.assignManager = { } }, + // 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)); From 83fd73b34214ce68b71f5988c1f3dab638fab74a Mon Sep 17 00:00:00 2001 From: Francis McKenzie Date: Mon, 16 May 2022 12:53:32 +0200 Subject: [PATCH 3/5] Wildcard subdomains - UI improvements --- src/css/popup.css | 6 +++++- src/js/popup.js | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/css/popup.css b/src/css/popup.css index 9943326f..c924518f 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -1817,7 +1817,11 @@ manage things like container crud */ } #edit-sites-assigned .hostname .subdomain.wildcardSubdomain { - opacity: 0.2; + background-color: var(--identity-icon-color); + border-radius: 8px; + margin-right: 4px; + padding-left: 10px; + padding-right: 10px; } .assigned-sites-list > div { diff --git a/src/js/popup.js b/src/js/popup.js index 67fea6c0..b996543c 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -1377,6 +1377,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); @@ -1427,7 +1428,7 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, { }); // Wildcard click-to-toggle subdomains trElement.querySelectorAll(".subdomain").forEach((subdomainLink) => { - subdomainLink.addEventListener("click", async (e) => { + subdomainLink.addEventListener("click", (e) => { const wildcardHostname = e.target.getAttribute("data-wildcardHostname"); Utils.setWildcardHostnameForAssignment(assumedUrl, wildcardHostname); if (wildcardHostname) { @@ -1454,14 +1455,14 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, { if (wildcardHostname && wildcardHostname !== hostname) { if (hostname.endsWith(wildcardHostname)) { return { - wildcard: hostname.substring(0, hostname.length - wildcardHostname.length), + wildcard: "★", remaining: wildcardHostname }; } else { // In case something got corrupted, allow user to fix error - // by clicking "____" link to clear corrupted wildcard hostname + // by clicking '★' link to clear corrupted wildcard hostname return { - wildcard: "___", + wildcard: "★", remaining: hostname }; } @@ -1480,6 +1481,7 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, { // Add wildcard subdomain(s) if (subdomains.wildcard) { result.appendChild(this.assignmentSubdomainLink(null, subdomains.wildcard)); + result.appendChild(document.createTextNode(".")); } // Add non-wildcard subdomains @@ -1503,6 +1505,7 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, { result.className = "subdomain"; if (wildcardHostnameOnClick) { result.setAttribute("data-wildcardHostname", wildcardHostnameOnClick); + result.title = `*.${wildcardHostnameOnClick}`; } else { result.classList.add("wildcardSubdomain"); } From cf662b5f016d147bcd7d4511b3e3d3050f0ace0e Mon Sep 17 00:00:00 2001 From: Francis McKenzie Date: Thu, 19 May 2022 11:15:51 +0200 Subject: [PATCH 4/5] Wildcard subdomains - fix right-click context menu displaying incorrect "Always open in" status --- src/js/background/assignManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/background/assignManager.js b/src/js/background/assignManager.js index d8ff37c4..308ef881 100644 --- a/src/js/background/assignManager.js +++ b/src/js/background/assignManager.js @@ -734,7 +734,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; }, From d4431c4eff634564b33a64b643f2cc5c07e658ee Mon Sep 17 00:00:00 2001 From: Francis McKenzie Date: Thu, 19 May 2022 17:46:29 +0200 Subject: [PATCH 5/5] Wildcard subdomains - optimisation: wildcard match logic now requires far fewer extra async storage requests (if any) --- src/js/background/assignManager.js | 60 +++++++++++++++++++----------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/js/background/assignManager.js b/src/js/background/assignManager.js index 308ef881..635c2f0c 100644 --- a/src/js/background/assignManager.js +++ b/src/js/background/assignManager.js @@ -24,6 +24,18 @@ window.assignManager = { 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)) { @@ -51,15 +63,39 @@ window.assignManager = { }, async getOrWildcardMatch(pageUrlorUrlKey) { + // 1st store request: siteStoreKey + wildcardStoreKeys const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey); - const siteSettings = await this.getByUrlKey(siteStoreKey); + 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 }; } - return this.getByWildcardMatch(siteStoreKey); + + // 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() { @@ -85,26 +121,6 @@ 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) {