Skip to content

Commit 7922a39

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

11 files changed

+485
-80
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: 154 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,147 @@ 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,
9+
store: new utils.NamedStore("siteContainerMap"),
1010
exemptedTabs: {},
1111

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}`;
12+
setExempted(siteId, tabId) {
13+
if (!(siteId in this.exemptedTabs)) {
14+
this.exemptedTabs[siteId] = [];
1915
}
16+
this.exemptedTabs[siteId].push(tabId);
2017
},
2118

22-
setExempted(pageUrl, tabId) {
23-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
24-
if (!(siteStoreKey in this.exemptedTabs)) {
25-
this.exemptedTabs[siteStoreKey] = [];
26-
}
27-
this.exemptedTabs[siteStoreKey].push(tabId);
19+
removeExempted(siteId) {
20+
this.exemptedTabs[siteId] = [];
2821
},
2922

30-
removeExempted(pageUrl) {
31-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
32-
this.exemptedTabs[siteStoreKey] = [];
33-
},
34-
35-
isExempted(pageUrl, tabId) {
36-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
37-
if (!(siteStoreKey in this.exemptedTabs)) {
23+
isExemptedUrl(pageUrl, tabId) {
24+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
25+
if (!(siteId in this.exemptedTabs)) {
3826
return false;
3927
}
40-
return this.exemptedTabs[siteStoreKey].includes(tabId);
28+
return this.exemptedTabs[siteId].includes(tabId);
4129
},
42-
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);
30+
31+
// Add convenience properties after loading siteSettings from store.
32+
async _addConvenienceProperties(siteId, siteSettings) {
33+
if (siteId && siteSettings) {
34+
// The travis build incorrectly fails on github when running the below, due to
35+
// the require-atomic-updates rule. So we have to manually disable the rule.
36+
const wildcard = await wildcardManager.get(siteId);
37+
siteSettings.siteId = siteId; // eslint-disable-line require-atomic-updates
38+
siteSettings.hostname = siteId; // eslint-disable-line require-atomic-updates
39+
siteSettings.wildcard = wildcard; // eslint-disable-line require-atomic-updates
40+
}
41+
},
42+
43+
// Add convenience properties after loading multiple siteSettings from store.
44+
async _addConveniencePropertiesToAll(siteSettingsById) {
45+
const siteIds = Object.keys(siteSettingsById);
46+
siteIds.forEach((siteId) => {
47+
const siteSettings = siteSettingsById[siteId];
48+
siteSettings.siteId = siteId;
49+
siteSettings.hostname = siteId;
50+
});
51+
52+
if (siteIds.length > 0) {
53+
const siteIdsToWildcards = await wildcardManager.getAll(siteIds);
54+
Object.entries(siteIdsToWildcards).forEach(([siteId, wildcard]) => {
55+
const siteSettings = siteSettingsById[siteId];
56+
siteSettings.wildcard = wildcard;
5357
});
58+
}
59+
},
60+
61+
// Remove convenience properties before saving siteSettings to store.
62+
_excludingConvenienceProperties(siteId, siteSettings) {
63+
return utils.filterObj(siteSettings, (propertyName) => {
64+
if (propertyName === "siteId") { return false; }
65+
if (propertyName === "hostname") { return false; }
66+
if (propertyName === "wildcard") { return false; }
67+
return true;
5468
});
5569
},
5670

57-
set(pageUrl, data, exemptedTabIds) {
58-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
71+
async matchUrl(pageUrl) {
72+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
73+
74+
// Try exact match
75+
let siteSettings = await this.get(siteId);
76+
77+
if (!siteSettings) {
78+
// Try wildcard match
79+
const wildcard = await wildcardManager.match(siteId);
80+
if (wildcard) {
81+
siteSettings = await this.get(wildcard);
82+
}
83+
}
84+
85+
return siteSettings;
86+
},
87+
88+
async get(siteId) {
89+
const siteSettings = await this.store.get(siteId);
90+
// Convenience properties are not stored - add them
91+
await this._addConvenienceProperties(siteId, siteSettings);
92+
return siteSettings;
93+
},
94+
95+
async set(siteId, siteSettings, exemptedTabIds) {
96+
// Store exempted tabs
5997
if (exemptedTabIds) {
6098
exemptedTabIds.forEach((tabId) => {
61-
this.setExempted(pageUrl, tabId);
99+
this.setExempted(siteId, tabId);
62100
});
63101
}
64-
return this.area.set({
65-
[siteStoreKey]: data
66-
});
102+
// Store/remove wildcard mapping
103+
if (siteSettings.wildcard && siteSettings.wildcard !== siteId) {
104+
await wildcardManager.set(siteId, siteSettings.wildcard);
105+
} else {
106+
await wildcardManager.remove(siteId);
107+
}
108+
109+
// Convenience properties are not stored - remove them
110+
siteSettings = this._excludingConvenienceProperties(siteId, siteSettings);
111+
112+
// Store assignment
113+
return this.store.set(siteId, siteSettings);
67114
},
68115

69-
remove(pageUrl) {
70-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
116+
async remove(siteId) {
71117
// When we remove an assignment we should clear all the exemptions
72-
this.removeExempted(pageUrl);
73-
return this.area.remove([siteStoreKey]);
118+
this.removeExempted(siteId);
119+
// ...and also clear the wildcard mapping
120+
await wildcardManager.remove(siteId);
121+
122+
return this.store.remove(siteId);
74123
},
75124

76125
async deleteContainer(userContextId) {
77126
const sitesByContainer = await this.getByContainer(userContextId);
78-
this.area.remove(Object.keys(sitesByContainer));
127+
const siteIds = Object.keys(sitesByContainer);
128+
129+
siteIds.forEach((siteId) => {
130+
// When we remove an assignment we should clear all the exemptions
131+
this.removeExempted(siteId);
132+
});
133+
134+
// ...and also clear the wildcard mappings
135+
await wildcardManager.removeAll(siteIds);
136+
137+
return this.store.removeAll(siteIds);
79138
},
80139

81140
async getByContainer(userContextId) {
82-
const sites = {};
83-
const siteConfigs = await this.area.get();
84-
Object.keys(siteConfigs).forEach((key) => {
141+
const allSites = await this.store.getAll();
142+
const sites = utils.filterObj(allSites, (siteId, site) => {
85143
// 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-
}
144+
return String(site.userContextId) === String(userContextId);
93145
});
146+
147+
// Convenience properties are not stored - add them
148+
this._addConveniencePropertiesToAll(sites);
149+
94150
return sites;
95151
}
96152
},
@@ -99,10 +155,10 @@ const assignManager = {
99155
const pageUrl = m.pageUrl;
100156
if (m.neverAsk === true) {
101157
// 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) => {
158+
this.storageArea.matchUrl(pageUrl).then((siteSettings) => {
103159
if (siteSettings) {
104160
siteSettings.neverAsk = true;
105-
this.storageArea.set(pageUrl, siteSettings);
161+
return this.storageArea.set(siteSettings.siteId, siteSettings);
106162
}
107163
}).catch((e) => {
108164
throw e;
@@ -113,7 +169,8 @@ const assignManager = {
113169
// We return here so the confirm page can load the tab when exempted
114170
async _exemptTab(m) {
115171
const pageUrl = m.pageUrl;
116-
this.storageArea.setExempted(pageUrl, m.tabId);
172+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
173+
this.storageArea.setExempted(siteId, m.tabId);
117174
return true;
118175
},
119176

@@ -125,7 +182,7 @@ const assignManager = {
125182
this.removeContextMenu();
126183
const [tab, siteSettings] = await Promise.all([
127184
browser.tabs.get(options.tabId),
128-
this.storageArea.get(options.url)
185+
this.storageArea.matchUrl(options.url)
129186
]);
130187
let container;
131188
try {
@@ -143,7 +200,7 @@ const assignManager = {
143200
const userContextId = this.getUserContextIdFromCookieStore(tab);
144201
if (!siteSettings
145202
|| userContextId === siteSettings.userContextId
146-
|| this.storageArea.isExempted(options.url, tab.id)) {
203+
|| this.storageArea.isExemptedUrl(options.url, tab.id)) {
147204
return {};
148205
}
149206
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
@@ -367,51 +424,74 @@ const assignManager = {
367424
return true;
368425
},
369426

370-
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
427+
_determineAssignmentMatchesUrl(siteSettings, url) {
428+
const siteId = backgroundLogic.getSiteIdFromUrl(url);
429+
if (siteSettings.siteId === siteId) { return true; }
430+
if (siteSettings.wildcard && siteId.endsWith(siteSettings.wildcard)) { return true; }
431+
return false;
432+
},
433+
434+
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove, options = {}) {
371435
let actionName;
372436

373437
// https://github.com/mozilla/testpilot-containers/issues/626
374438
// Context menu has stored context IDs as strings, so we need to coerce
375439
// the value to a string for accurate checking
376440
userContextId = String(userContextId);
377441

442+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
378443
if (!remove) {
444+
const siteSettings = {
445+
siteId,
446+
hostname: siteId,
447+
wildcard: options.wildcard,
448+
userContextId,
449+
neverAsk: !!options.neverAsk
450+
};
451+
379452
const tabs = await browser.tabs.query({});
380-
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
381453
const exemptedTabIds = tabs.filter((tab) => {
382-
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
383454
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
384-
if (tabStoreKey === assignmentStoreKey &&
455+
if (this._determineAssignmentMatchesUrl(siteSettings, tab.url) &&
385456
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
386457
return true;
387458
}
388459
return false;
389460
}).map((tab) => {
390461
return tab.id;
391462
});
392-
393-
await this.storageArea.set(pageUrl, {
394-
userContextId,
395-
neverAsk: false
396-
}, exemptedTabIds);
463+
464+
await this.storageArea.set(siteId, siteSettings, exemptedTabIds);
397465
actionName = "added";
398466
} else {
399-
await this.storageArea.remove(pageUrl);
467+
await this.storageArea.remove(siteId);
400468
actionName = "removed";
401469
}
402-
browser.tabs.sendMessage(tabId, {
403-
text: `Successfully ${actionName} site to always open in this container`
404-
});
470+
if (!options.silent) {
471+
browser.tabs.sendMessage(tabId, {
472+
text: `Successfully ${actionName} site to always open in this container`
473+
});
474+
}
405475
const tab = await browser.tabs.get(tabId);
406476
this.calculateContextMenu(tab);
407477
},
478+
479+
async _setOrRemoveWildcard(tabId, pageUrl, userContextId, wildcard) {
480+
// Get existing settings, so we can preserve neverAsk property
481+
const oldSiteSettings = await this.storageArea.get(backgroundLogic.getSiteIdFromUrl(pageUrl)) || {};
482+
483+
// Remove assignment
484+
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, true, {silent:true});
485+
// Add assignment
486+
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, false, {silent:true, wildcard:wildcard, neverAsk:oldSiteSettings.neverAsk});
487+
},
408488

409489
async _getAssignment(tab) {
410490
const cookieStore = this.getUserContextIdFromCookieStore(tab);
411491
// Ensure we have a cookieStore to assign to
412492
if (cookieStore
413493
&& this.isTabPermittedAssign(tab)) {
414-
return await this.storageArea.get(tab.url);
494+
return await this.storageArea.matchUrl(tab.url);
415495
}
416496
return false;
417497
},

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>

src/js/background/messageHandler.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ const messageHandler = {
3737
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
3838
});
3939
break;
40+
case "setOrRemoveWildcard":
41+
// Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
42+
response = browser.tabs.get(m.tabId).then((tab) => {
43+
return assignManager._setOrRemoveWildcard(tab.id, m.url, m.userContextId, m.wildcard);
44+
});
45+
break;
4046
case "sortTabs":
4147
backgroundLogic.sortTabs();
4248
break;

0 commit comments

Comments
 (0)