Skip to content

Commit ab400df

Browse files
committed
Wildcard subdomains - e.g. *.google.com
1 parent 873ba0a commit ab400df

File tree

6 files changed

+258
-7
lines changed

6 files changed

+258
-7
lines changed

src/css/popup.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,14 @@ manage things like container crud */
15961596
padding-inline-start: 16px;
15971597
}
15981598

1599+
#edit-sites-assigned .hostname .subdomain:hover {
1600+
text-decoration: underline;
1601+
}
1602+
1603+
#edit-sites-assigned .hostname .subdomain.wildcardSubdomain {
1604+
opacity: 0.2;
1605+
}
1606+
15991607
.assigned-sites-list > div {
16001608
display: flex;
16011609
padding-block-end: 6px;

src/js/background/assignManager.js

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ window.assignManager = {
2020
}
2121
},
2222

23+
getWildcardStoreKey(wildcardHostname) {
24+
return `wildcardMap@@_${wildcardHostname}`;
25+
},
26+
2327
setExempted(pageUrlorUrlKey, tabId) {
2428
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
2529
if (!(siteStoreKey in this.exemptedTabs)) {
@@ -46,6 +50,18 @@ window.assignManager = {
4650
return this.getByUrlKey(siteStoreKey);
4751
},
4852

53+
async getOrWildcardMatch(pageUrlorUrlKey) {
54+
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
55+
const siteSettings = await this.getByUrlKey(siteStoreKey);
56+
if (siteSettings) {
57+
return {
58+
siteStoreKey,
59+
siteSettings
60+
};
61+
}
62+
return this.getByWildcardMatch(siteStoreKey);
63+
},
64+
4965
async getSyncEnabled() {
5066
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
5167
return !!syncEnabled;
@@ -69,39 +85,90 @@ window.assignManager = {
6985
});
7086
},
7187

88+
async getByWildcardMatch(siteStoreKey) {
89+
// Keep stripping subdomains off site hostname until match a wildcard hostname
90+
let remainingHostname = siteStoreKey.replace(/^siteContainerMap@@_/, "");
91+
while (remainingHostname) {
92+
const wildcardStoreKey = this.getWildcardStoreKey(remainingHostname);
93+
siteStoreKey = await this.getByUrlKey(wildcardStoreKey);
94+
if (siteStoreKey) {
95+
const siteSettings = await this.getByUrlKey(siteStoreKey);
96+
if (siteSettings) {
97+
return {
98+
siteStoreKey,
99+
siteSettings
100+
};
101+
}
102+
}
103+
const indexOfDot = remainingHostname.indexOf(".");
104+
remainingHostname = indexOfDot < 0 ? null : remainingHostname.substring(indexOfDot + 1);
105+
}
106+
},
107+
72108
async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
73109
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
74110
if (exemptedTabIds) {
75111
exemptedTabIds.forEach((tabId) => {
76112
this.setExempted(pageUrlorUrlKey, tabId);
77113
});
78114
}
115+
await this.removeWildcardLookup(siteStoreKey);
79116
// eslint-disable-next-line require-atomic-updates
80117
data.identityMacAddonUUID =
81118
await identityState.lookupMACaddonUUID(data.userContextId);
82119
await this.area.set({
83120
[siteStoreKey]: data
84121
});
122+
if (data.wildcardHostname) {
123+
await this.setWildcardLookup(siteStoreKey, data.wildcardHostname);
124+
}
85125
const syncEnabled = await this.getSyncEnabled();
86126
if (backup && syncEnabled) {
87127
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
88128
}
89129
return;
90130
},
91131

132+
async setWildcardLookup(siteStoreKey, wildcardHostname) {
133+
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
134+
return this.area.set({
135+
[wildcardStoreKey]: siteStoreKey
136+
});
137+
},
138+
92139
async remove(pageUrlorUrlKey, shouldSync = true) {
93140
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
94141
// When we remove an assignment we should clear all the exemptions
95142
this.removeExempted(pageUrlorUrlKey);
143+
// When we remove an assignment we should clear the wildcard lookup
144+
await this.removeWildcardLookup(siteStoreKey);
96145
await this.area.remove([siteStoreKey]);
97146
const syncEnabled = await this.getSyncEnabled();
98147
if (shouldSync && syncEnabled) await sync.storageArea.backup({siteStoreKey});
99148
return;
100149
},
101150

151+
async removeWildcardLookup(siteStoreKey) {
152+
const siteSettings = await this.getByUrlKey(siteStoreKey);
153+
const wildcardHostname = siteSettings && siteSettings.wildcardHostname;
154+
if (wildcardHostname) {
155+
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
156+
await this.area.remove([wildcardStoreKey]);
157+
}
158+
},
159+
102160
async deleteContainer(userContextId) {
103161
const sitesByContainer = await this.getAssignedSites(userContextId);
104162
this.area.remove(Object.keys(sitesByContainer));
163+
// Delete wildcard lookups
164+
const wildcardStoreKeys = Object.values(sitesByContainer)
165+
.map((site) => {
166+
if (site && site.wildcardHostname) {
167+
return this.getWildcardStoreKey(site.wildcardHostname);
168+
}
169+
})
170+
.filter((wildcardStoreKey) => { return !!wildcardStoreKey; });
171+
this.area.remove(wildcardStoreKeys);
105172
},
106173

107174
async getAssignedSites(userContextId = null) {
@@ -166,10 +233,10 @@ window.assignManager = {
166233
if (m.neverAsk === true) {
167234
// If we have existing data and for some reason it hasn't been
168235
// deleted etc lets update it
169-
this.storageArea.get(pageUrl).then((siteSettings) => {
170-
if (siteSettings) {
171-
siteSettings.neverAsk = true;
172-
this.storageArea.set(pageUrl, siteSettings);
236+
this.storageArea.getOrWildcardMatch(pageUrl).then((siteMatchResult) => {
237+
if (siteMatchResult) {
238+
siteMatchResult.siteSettings.neverAsk = true;
239+
this.storageArea.set(siteMatchResult.siteStoreKey, siteMatchResult.siteSettings);
173240
}
174241
}).catch((e) => {
175242
throw e;
@@ -217,10 +284,11 @@ window.assignManager = {
217284
return {};
218285
}
219286
this.removeContextMenu();
220-
const [tab, siteSettings] = await Promise.all([
287+
const [tab, siteMatchResult] = await Promise.all([
221288
browser.tabs.get(options.tabId),
222-
this.storageArea.get(options.url)
289+
this.storageArea.getOrWildcardMatch(options.url)
223290
]);
291+
const siteSettings = siteMatchResult && siteMatchResult.siteSettings;
224292
let container;
225293
try {
226294
container = await browser.contextualIdentities
@@ -620,6 +688,14 @@ window.assignManager = {
620688
}
621689
},
622690

691+
async _setWildcardHostnameForAssignment(pageUrl, wildcardHostname) {
692+
const siteSettings = await this.storageArea.get(pageUrl);
693+
if (siteSettings) {
694+
siteSettings.wildcardHostname = wildcardHostname;
695+
await this.storageArea.set(pageUrl, siteSettings);
696+
}
697+
},
698+
623699
async _maybeRemoveSiteIsolation(userContextId) {
624700
const assignments = await this.storageArea.getByContainer(userContextId);
625701
const hasAssignments = assignments && Object.keys(assignments).length > 0;

src/js/background/messageHandler.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ const messageHandler = {
4545
// m.url is the assignment to be removed/added
4646
response = assignManager._setOrRemoveAssignment(m.tabId, m.url, m.userContextId, m.value);
4747
break;
48+
case "setWildcardHostnameForAssignment":
49+
response = assignManager._setWildcardHostnameForAssignment(m.url, m.wildcardHostname);
50+
break;
4851
case "sortTabs":
4952
backgroundLogic.sortTabs();
5053
break;

src/js/popup.js

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1407,10 +1407,11 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
14071407
trElement.innerHTML = Utils.escaped`
14081408
<td>
14091409
<div class="favicon"></div>
1410-
<span title="${site.hostname}" class="menu-text">${site.hostname}</span>
1410+
<span title="${site.hostname}" class="menu-text hostname"></span>
14111411
<img class="trash-button delete-assignment" src="/img/container-delete.svg" />
14121412
</td>`;
14131413
trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
1414+
trElement.querySelector(".hostname").appendChild(this.assignmentHostnameElement(site));
14141415
const deleteButton = trElement.querySelector(".trash-button");
14151416
Utils.addEnterHandler(deleteButton, async () => {
14161417
const userContextId = Logic.currentUserContextId();
@@ -1420,11 +1421,90 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
14201421
delete assignments[siteKey];
14211422
this.showAssignedContainers(assignments);
14221423
});
1424+
// Wildcard click-to-toggle subdomains
1425+
trElement.querySelectorAll(".subdomain").forEach((subdomainLink) => {
1426+
subdomainLink.addEventListener("click", async (e) => {
1427+
const wildcardHostname = e.target.getAttribute("data-wildcardHostname");
1428+
Utils.setWildcardHostnameForAssignment(assumedUrl, wildcardHostname);
1429+
if (wildcardHostname) {
1430+
// Remove wildcard from other site that has same wildcard
1431+
Object.values(assignments).forEach((site) => {
1432+
if (site.wildcardHostname === wildcardHostname) { delete site.wildcardHostname; }
1433+
});
1434+
site.wildcardHostname = wildcardHostname;
1435+
} else {
1436+
delete site.wildcardHostname;
1437+
}
1438+
this.showAssignedContainers(assignments);
1439+
});
1440+
});
14231441
trElement.classList.add("menu-item", "hover-highlight", "keyboard-nav");
14241442
tableElement.appendChild(trElement);
14251443
});
14261444
}
14271445
},
1446+
1447+
getSubdomains(site) {
1448+
const hostname = site.hostname;
1449+
const wildcardHostname = site.wildcardHostname;
1450+
if (wildcardHostname && wildcardHostname !== hostname) {
1451+
if (hostname.endsWith(wildcardHostname)) {
1452+
return {
1453+
wildcard: hostname.substring(0, hostname.length - wildcardHostname.length),
1454+
remaining: wildcardHostname
1455+
};
1456+
} else {
1457+
// In case something got corrupted, allow user to fix error
1458+
// by clicking "____" link to clear corrupted wildcard hostname
1459+
return {
1460+
wildcard: "___",
1461+
remaining: hostname
1462+
};
1463+
}
1464+
} else {
1465+
return {
1466+
wildcard: null,
1467+
remaining: hostname
1468+
};
1469+
}
1470+
},
1471+
1472+
assignmentHostnameElement(site) {
1473+
const result = document.createElement("span");
1474+
const subdomains = this.getSubdomains(site);
1475+
1476+
// Add wildcard subdomain(s)
1477+
if (subdomains.wildcard) {
1478+
result.appendChild(this.assignmentSubdomainLink(null, subdomains.wildcard));
1479+
}
1480+
1481+
// Add non-wildcard subdomains
1482+
let remainingHostname = subdomains.remaining;
1483+
let indexOfDot;
1484+
while ((indexOfDot = remainingHostname.indexOf(".")) >= 0) {
1485+
const subdomain = remainingHostname.substring(0, indexOfDot);
1486+
remainingHostname = remainingHostname.substring(indexOfDot + 1);
1487+
result.appendChild(this.assignmentSubdomainLink(remainingHostname, subdomain));
1488+
result.appendChild(document.createTextNode("."));
1489+
}
1490+
1491+
// Root domain
1492+
if (remainingHostname) { result.appendChild(document.createTextNode(remainingHostname)); }
1493+
1494+
return result;
1495+
},
1496+
1497+
assignmentSubdomainLink(wildcardHostnameOnClick, text) {
1498+
const result = document.createElement("a");
1499+
result.className = "subdomain";
1500+
if (wildcardHostnameOnClick) {
1501+
result.setAttribute("data-wildcardHostname", wildcardHostnameOnClick);
1502+
} else {
1503+
result.classList.add("wildcardSubdomain");
1504+
}
1505+
result.appendChild(document.createTextNode(text));
1506+
return result;
1507+
},
14281508
});
14291509

14301510
// P_CONTAINER_EDIT: Editor for a container.

src/js/utils.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@ const Utils = {
138138
});
139139
},
140140

141+
setWildcardHostnameForAssignment(url, wildcardHostname) {
142+
return browser.runtime.sendMessage({
143+
method: "setWildcardHostnameForAssignment",
144+
url,
145+
wildcardHostname
146+
});
147+
},
148+
141149
async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
142150
return await browser.runtime.sendMessage({
143151
method: "reloadInContainer",

test/features/wildcard.test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
const {initializeWithTab} = require("../common");
2+
3+
describe("Wildcard Subdomains Feature", function () {
4+
const url1 = "http://www.example.com";
5+
const url2 = "http://zzz.example.com";
6+
const wildcardHostname = "example.com";
7+
8+
beforeEach(async function () {
9+
this.webExt = await initializeWithTab({
10+
cookieStoreId: "firefox-container-4",
11+
url: url1
12+
});
13+
await this.webExt.popup.helper.clickElementById("always-open-in");
14+
await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item");
15+
});
16+
17+
afterEach(function () {
18+
this.webExt.destroy();
19+
});
20+
21+
describe("open new Tab with different subdomain in the default container", function () {
22+
beforeEach(async function () {
23+
// new Tab opening url2 in default container
24+
await this.webExt.background.browser.tabs._create({
25+
cookieStoreId: "firefox-default",
26+
url: url2
27+
}, {
28+
options: {
29+
webRequestError: true // because request is canceled due to reopening
30+
}
31+
});
32+
});
33+
34+
it("should not open the confirm page", async function () {
35+
this.webExt.background.browser.tabs.create.should.not.have.been.called;
36+
});
37+
38+
it("should not remove the new Tab that got opened in the default container", function () {
39+
this.webExt.background.browser.tabs.remove.should.not.have.been.called;
40+
});
41+
});
42+
43+
describe("set wildcard hostname and then open new Tab with different subdomain in the default container", function () {
44+
let newTab;
45+
beforeEach(async function () {
46+
// Set wildcard
47+
await this.webExt.background.window.assignManager._setWildcardHostnameForAssignment(url1, wildcardHostname);
48+
49+
// new Tab opening url2 in default container
50+
newTab = await this.webExt.background.browser.tabs._create({
51+
cookieStoreId: "firefox-default",
52+
url: url2
53+
}, {
54+
options: {
55+
webRequestError: true // because request is canceled due to reopening
56+
}
57+
});
58+
});
59+
60+
it("should open the confirm page", async function () {
61+
this.webExt.background.browser.tabs.create.should.have.been.calledWithMatch({
62+
url: "moz-extension://fake/confirm-page.html?" +
63+
`url=${encodeURIComponent(url2)}` +
64+
`&cookieStoreId=${this.webExt.tab.cookieStoreId}`,
65+
cookieStoreId: undefined,
66+
openerTabId: null,
67+
index: 2,
68+
active: true
69+
});
70+
});
71+
72+
it("should remove the new Tab that got opened in the default container", function () {
73+
this.webExt.background.browser.tabs.remove.should.have.been.calledWith(newTab.id);
74+
});
75+
});
76+
});

0 commit comments

Comments
 (0)