Skip to content

Commit 9dfdc83

Browse files
committed
Wildcard subdomains - e.g. *.google.com
1 parent 426e81b commit 9dfdc83

11 files changed

+498
-109
lines changed

src/css/popup.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,11 @@ span ~ .panel-header-text {
832832
flex: 1;
833833
}
834834

835+
/* Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473 */
836+
.assigned-sites-list .hostname .subdomain:hover {
837+
text-decoration: underline;
838+
}
839+
835840
.radio-choice > .radio-container {
836841
align-items: center;
837842
block-size: 29px;

src/js/.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module.exports = {
33
"../../.eslintrc.js"
44
],
55
"globals": {
6+
"utils": false,
7+
"wildcardManager": false,
68
"assignManager": true,
79
"badge": true,
810
"backgroundLogic": true,

src/js/background/assignManager.js

Lines changed: 160 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,115 +6,155 @@ const assignManager = {
66
MENU_MOVE_ID: "move-to-new-window-container",
77
OPEN_IN_CONTAINER: "open-bookmark-in-container-tab",
88
storageArea: {
9-
area: browser.storage.local,
10-
exemptedTabs: {},
11-
12-
getSiteStoreKey(pageUrl) {
13-
const url = new window.URL(pageUrl);
14-
const storagePrefix = "siteContainerMap@@_";
15-
if (url.port === "80" || url.port === "443") {
16-
return `${storagePrefix}${url.hostname}`;
17-
} else {
18-
return `${storagePrefix}${url.hostname}${url.port}`;
19-
}
9+
store: new utils.NamedStore("siteContainerMap"),
10+
exemptedTabsStore: {
11+
data: {},
12+
get(siteId) { return this.data[siteId]; },
13+
set(siteId, tabIds) { this.data[siteId] = tabIds; },
14+
remove(siteId) { delete this.data[siteId]; }
2015
},
2116

22-
setExempted(pageUrl, tabId) {
23-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
24-
if (!(siteStoreKey in this.exemptedTabs)) {
25-
this.exemptedTabs[siteStoreKey] = [];
17+
async matchUrl(pageUrl) {
18+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
19+
20+
// Try exact match
21+
let siteSettings = await this.get(siteId);
22+
23+
if (!siteSettings) {
24+
// Try wildcard match
25+
const wildcard = await wildcardManager.match(siteId);
26+
if (wildcard) {
27+
siteSettings = await this.get(wildcard);
28+
}
2629
}
27-
this.exemptedTabs[siteStoreKey].push(tabId);
28-
},
29-
30-
removeExempted(pageUrl) {
31-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
32-
this.exemptedTabs[siteStoreKey] = [];
30+
31+
return siteSettings;
3332
},
34-
35-
isExempted(pageUrl, tabId) {
36-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
37-
if (!(siteStoreKey in this.exemptedTabs)) {
38-
return false;
39-
}
40-
return this.exemptedTabs[siteStoreKey].includes(tabId);
33+
34+
create(siteId, userContextId, options = {}) {
35+
const siteSettings = { userContextId, neverAsk:!!options.neverAsk };
36+
this._setTransientProperties(siteId, siteSettings, options.wildcard);
37+
return siteSettings;
4138
},
4239

43-
get(pageUrl) {
44-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
45-
return new Promise((resolve, reject) => {
46-
this.area.get([siteStoreKey]).then((storageResponse) => {
47-
if (storageResponse && siteStoreKey in storageResponse) {
48-
resolve(storageResponse[siteStoreKey]);
49-
}
50-
resolve(null);
51-
}).catch((e) => {
52-
reject(e);
53-
});
54-
});
40+
async get(siteId) {
41+
const siteSettings = await this.store.get(siteId);
42+
await this._loadTransientProperties(siteId, siteSettings);
43+
return siteSettings;
5544
},
5645

57-
set(pageUrl, data, exemptedTabIds) {
58-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
59-
if (exemptedTabIds) {
60-
exemptedTabIds.forEach((tabId) => {
61-
this.setExempted(pageUrl, tabId);
62-
});
46+
async set(siteSettings) {
47+
const siteId = siteSettings.siteId;
48+
const exemptedTabs = siteSettings.exemptedTabs;
49+
const wildcard = siteSettings.wildcard;
50+
51+
// Store/remove exempted tabs
52+
this.exemptedTabsStore.set(siteId, exemptedTabs);
53+
54+
// Store/remove wildcard mapping
55+
if (wildcard && wildcard !== siteId) {
56+
await wildcardManager.set(siteId, wildcard);
57+
} else {
58+
await wildcardManager.remove(siteId);
6359
}
64-
return this.area.set({
65-
[siteStoreKey]: data
66-
});
60+
61+
// Remove transient properties
62+
const cleanSiteSettings = Object.assign({}, siteSettings);
63+
this._unsetTransientProperties(cleanSiteSettings);
64+
65+
// Store assignment
66+
return this.store.set(siteId, cleanSiteSettings);
6767
},
6868

69-
remove(pageUrl) {
70-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
69+
async remove(siteId) {
7170
// When we remove an assignment we should clear all the exemptions
72-
this.removeExempted(pageUrl);
73-
return this.area.remove([siteStoreKey]);
71+
this.exemptedTabsStore.remove(siteId);
72+
// ...and also clear the wildcard mapping
73+
await wildcardManager.remove(siteId);
74+
75+
return this.store.remove(siteId);
7476
},
7577

7678
async deleteContainer(userContextId) {
77-
const sitesByContainer = await this.getByContainer(userContextId);
78-
this.area.remove(Object.keys(sitesByContainer));
79+
const siteSettingsById = await this.getByContainer(userContextId);
80+
const siteIds = Object.keys(siteSettingsById);
81+
82+
siteIds.forEach((siteId) => {
83+
// When we remove an assignment we should clear all the exemptions
84+
this.exemptedTabsStore.remove(siteId);
85+
});
86+
87+
// ...and also clear the wildcard mappings
88+
await wildcardManager.removeAll(siteIds);
89+
90+
return this.store.removeAll(siteIds);
7991
},
8092

8193
async getByContainer(userContextId) {
82-
const sites = {};
83-
const siteConfigs = await this.area.get();
84-
Object.keys(siteConfigs).forEach((key) => {
94+
const siteSettingsById = await this.store.getSome((siteId, siteSettings) => {
8595
// For some reason this is stored as string... lets check them both as that
86-
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
87-
const site = siteConfigs[key];
88-
// In hindsight we should have stored this
89-
// TODO file a follow up to clean the storage onLoad
90-
site.hostname = key.replace(/^siteContainerMap@@_/, "");
91-
sites[key] = site;
92-
}
96+
return String(siteSettings.userContextId) === String(userContextId);
9397
});
94-
return sites;
98+
await this._loadTransientPropertiesForAll(siteSettingsById);
99+
return siteSettingsById;
100+
},
101+
102+
async _loadTransientProperties(siteId, siteSettings) {
103+
if (siteId && siteSettings) {
104+
const wildcard = await wildcardManager.get(siteId);
105+
const exemptedTabs = this.exemptedTabsStore.get(siteId);
106+
this._setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs);
107+
}
108+
},
109+
110+
async _loadTransientPropertiesForAll(siteSettingsById) {
111+
const siteIds = Object.keys(siteSettingsById);
112+
if (siteIds.length > 0) {
113+
const siteIdsToWildcards = await wildcardManager.getAll(siteIds);
114+
siteIds.forEach((siteId) => {
115+
const siteSettings = siteSettingsById[siteId];
116+
const wildcard = siteIdsToWildcards[siteId];
117+
const exemptedTabs = this.exemptedTabsStore.get(siteId);
118+
this._setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs);
119+
});
120+
}
121+
},
122+
123+
_setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs = []) {
124+
siteSettings.siteId = siteId;
125+
siteSettings.hostname = siteId;
126+
siteSettings.wildcard = wildcard;
127+
siteSettings.exemptedTabs = exemptedTabs;
128+
},
129+
130+
_unsetTransientProperties(siteSettings) {
131+
delete siteSettings.siteId;
132+
delete siteSettings.hostname;
133+
delete siteSettings.wildcard;
134+
delete siteSettings.exemptedTabs;
95135
}
96136
},
97137

98-
_neverAsk(m) {
138+
async _neverAsk(m) {
99139
const pageUrl = m.pageUrl;
100-
if (m.neverAsk === true) {
101-
// If we have existing data and for some reason it hasn't been deleted etc lets update it
102-
this.storageArea.get(pageUrl).then((siteSettings) => {
103-
if (siteSettings) {
104-
siteSettings.neverAsk = true;
105-
this.storageArea.set(pageUrl, siteSettings);
106-
}
107-
}).catch((e) => {
108-
throw e;
109-
});
140+
const neverAsk = m.neverAsk;
141+
if (neverAsk === true) {
142+
const siteSettings = await this.storageArea.matchUrl(pageUrl);
143+
if (siteSettings && !siteSettings.neverAsk) {
144+
siteSettings.neverAsk = true;
145+
await this.storageArea.set(siteSettings);
146+
}
110147
}
111148
},
112149

113-
// We return here so the confirm page can load the tab when exempted
114150
async _exemptTab(m) {
115151
const pageUrl = m.pageUrl;
116-
this.storageArea.setExempted(pageUrl, m.tabId);
117-
return true;
152+
const tabId = m.tabId;
153+
const siteSettings = await this.storageArea.matchUrl(pageUrl);
154+
if (siteSettings && siteSettings.exemptedTabs.indexOf(tabId) === -1) {
155+
siteSettings.exemptedTabs.push(tabId);
156+
await this.storageArea.set(siteSettings);
157+
}
118158
},
119159

120160
// Before a request is handled by the browser we decide if we should route through a different container
@@ -125,7 +165,7 @@ const assignManager = {
125165
this.removeContextMenu();
126166
const [tab, siteSettings] = await Promise.all([
127167
browser.tabs.get(options.tabId),
128-
this.storageArea.get(options.url)
168+
this.storageArea.matchUrl(options.url)
129169
]);
130170
let container;
131171
try {
@@ -142,8 +182,8 @@ const assignManager = {
142182
}
143183
const userContextId = this.getUserContextIdFromCookieStore(tab);
144184
if (!siteSettings
145-
|| userContextId === siteSettings.userContextId
146-
|| this.storageArea.isExempted(options.url, tab.id)) {
185+
|| siteSettings.userContextId === userContextId
186+
|| siteSettings.exemptedTabs.includes(tab.id)) {
147187
return {};
148188
}
149189
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
@@ -367,51 +407,68 @@ const assignManager = {
367407
return true;
368408
},
369409

370-
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
410+
_determineAssignmentMatchesUrl(siteSettings, url) {
411+
const siteId = backgroundLogic.getSiteIdFromUrl(url);
412+
if (siteSettings.siteId === siteId) { return true; }
413+
if (siteSettings.wildcard && siteId.endsWith(siteSettings.wildcard)) { return true; }
414+
return false;
415+
},
416+
417+
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove, options = {}) {
371418
let actionName;
372419

373420
// https://github.com/mozilla/testpilot-containers/issues/626
374421
// Context menu has stored context IDs as strings, so we need to coerce
375422
// the value to a string for accurate checking
376423
userContextId = String(userContextId);
377424

425+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
378426
if (!remove) {
427+
const siteSettings = this.storageArea.create(siteId, userContextId, options);
428+
429+
// Auto exempt all tabs that exist for this hostname that are not in the same container
379430
const tabs = await browser.tabs.query({});
380-
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
381-
const exemptedTabIds = tabs.filter((tab) => {
382-
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
383-
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
384-
if (tabStoreKey === assignmentStoreKey &&
385-
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
386-
return true;
387-
}
388-
return false;
431+
siteSettings.exemptedTabs = tabs.filter((tab) => {
432+
if (!this._determineAssignmentMatchesUrl(siteSettings, tab.url)) { return false; }
433+
if (this.getUserContextIdFromCookieStore(tab) === userContextId) { return false; }
434+
return true;
389435
}).map((tab) => {
390436
return tab.id;
391437
});
392-
393-
await this.storageArea.set(pageUrl, {
394-
userContextId,
395-
neverAsk: false
396-
}, exemptedTabIds);
438+
439+
await this.storageArea.set(siteSettings);
397440
actionName = "added";
398441
} else {
399-
await this.storageArea.remove(pageUrl);
442+
await this.storageArea.remove(siteId);
400443
actionName = "removed";
401444
}
402-
browser.tabs.sendMessage(tabId, {
403-
text: `Successfully ${actionName} site to always open in this container`
404-
});
445+
if (!options.silent) {
446+
browser.tabs.sendMessage(tabId, {
447+
text: `Successfully ${actionName} site to always open in this container`
448+
});
449+
}
405450
const tab = await browser.tabs.get(tabId);
406451
this.calculateContextMenu(tab);
407452
},
453+
454+
async _setOrRemoveWildcard(tabId, pageUrl, userContextId, wildcard) {
455+
// Get existing settings, so we can preserve neverAsk property
456+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
457+
const siteSettings = await this.storageArea.get(siteId);
458+
const neverAsk = siteSettings && siteSettings.neverAsk;
459+
460+
// Remove assignment
461+
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, true, {silent:true});
462+
// Add assignment
463+
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, false, {silent:true, wildcard:wildcard, neverAsk:neverAsk});
464+
},
408465

409466
async _getAssignment(tab) {
410467
const cookieStore = this.getUserContextIdFromCookieStore(tab);
411468
// Ensure we have a cookieStore to assign to
412469
if (cookieStore
413470
&& this.isTabPermittedAssign(tab)) {
414-
return await this.storageArea.get(tab.url);
471+
return await this.storageArea.matchUrl(tab.url);
415472
}
416473
return false;
417474
},

src/js/background/backgroundLogic.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,5 +329,17 @@ const backgroundLogic = {
329329

330330
cookieStoreId(userContextId) {
331331
return `firefox-container-${userContextId}`;
332+
},
333+
334+
// A URL host string that is used to identify a site assignment, e.g.:
335+
// www.example.com
336+
// www.example.com:8080
337+
getSiteIdFromUrl(pageUrl) {
338+
const url = new window.URL(pageUrl);
339+
if (url.port === "" || url.port === "80" || url.port === "443") {
340+
return `${url.hostname}`;
341+
} else {
342+
return `${url.hostname}:${url.port}`;
343+
}
332344
}
333345
};

src/js/background/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"js/background/messageHandler.js",
1414
]
1515
-->
16+
<script type="text/javascript" src="utils.js"></script>
1617
<script type="text/javascript" src="backgroundLogic.js"></script>
18+
<script type="text/javascript" src="wildcardManager.js"></script>
1719
<script type="text/javascript" src="assignManager.js"></script>
1820
<script type="text/javascript" src="badge.js"></script>
1921
<script type="text/javascript" src="identityState.js"></script>

0 commit comments

Comments
 (0)