Skip to content

Commit e6eff75

Browse files
committed
Wildcard subdomains - e.g. *.google.com
1 parent dc9e8f6 commit e6eff75

13 files changed

+500
-111
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"stylelint-config-standard": "^16.0.0",
2727
"stylelint-order": "^0.3.0",
2828
"web-ext": "^2.9.3",
29-
"webextensions-jsdom": "^1.1.0"
29+
"webextensions-jsdom": "^1.1.1"
3030
},
3131
"homepage": "https://github.com/mozilla/multi-account-containers#readme",
3232
"license": "MPL-2.0",

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: 155 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,115 +6,150 @@ 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+
area: 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+
async matchUrl(pageUrl) {
13+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
14+
15+
// Try exact match
16+
let siteSettings = await this.get(siteId);
17+
18+
if (!siteSettings) {
19+
// Try wildcard match
20+
const wildcard = await wildcardManager.match(siteId);
21+
if (wildcard) {
22+
siteSettings = await this.get(wildcard);
23+
}
1924
}
25+
26+
return siteSettings;
2027
},
21-
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);
28+
29+
create(siteId, userContextId, options = {}) {
30+
const siteSettings = { userContextId, neverAsk:!!options.neverAsk };
31+
this._setTransientProperties(siteId, siteSettings, options.wildcard);
32+
return siteSettings;
2833
},
2934

30-
removeExempted(pageUrl) {
31-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
32-
this.exemptedTabs[siteStoreKey] = [];
35+
async get(siteId) {
36+
const siteSettings = await this.area.get(siteId);
37+
await this._loadTransientProperties(siteId, siteSettings);
38+
return siteSettings;
3339
},
3440

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);
41-
},
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);
53-
});
54-
});
55-
},
56-
57-
set(pageUrl, data, exemptedTabIds) {
58-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
59-
if (exemptedTabIds) {
60-
exemptedTabIds.forEach((tabId) => {
61-
this.setExempted(pageUrl, tabId);
62-
});
41+
async set(siteSettings) {
42+
const siteId = siteSettings.siteId;
43+
const exemptedTabs = siteSettings.exemptedTabs;
44+
const wildcard = siteSettings.wildcard;
45+
46+
// Store exempted tabs
47+
this.exemptedTabs[siteId] = exemptedTabs;
48+
49+
// Store/remove wildcard mapping
50+
if (wildcard && wildcard !== siteId) {
51+
await wildcardManager.set(siteId, wildcard);
52+
} else {
53+
await wildcardManager.remove(siteId);
6354
}
64-
return this.area.set({
65-
[siteStoreKey]: data
66-
});
55+
56+
// Remove transient properties before storing
57+
const cleanSiteSettings = Object.assign({}, siteSettings);
58+
this._unsetTransientProperties(cleanSiteSettings);
59+
60+
// Store assignment
61+
return this.area.set(siteId, cleanSiteSettings);
6762
},
6863

69-
remove(pageUrl) {
70-
const siteStoreKey = this.getSiteStoreKey(pageUrl);
64+
async remove(siteId) {
7165
// When we remove an assignment we should clear all the exemptions
72-
this.removeExempted(pageUrl);
73-
return this.area.remove([siteStoreKey]);
66+
delete this.exemptedTabs[siteId];
67+
// ...and also clear the wildcard mapping
68+
await wildcardManager.remove(siteId);
69+
70+
return this.area.remove(siteId);
7471
},
7572

7673
async deleteContainer(userContextId) {
77-
const sitesByContainer = await this.getByContainer(userContextId);
78-
this.area.remove(Object.keys(sitesByContainer));
74+
const siteSettingsById = await this.getByContainer(userContextId);
75+
const siteIds = Object.keys(siteSettingsById);
76+
77+
siteIds.forEach((siteId) => {
78+
// When we remove an assignment we should clear all the exemptions
79+
delete this.exemptedTabs[siteId];
80+
});
81+
82+
// ...and also clear the wildcard mappings
83+
await wildcardManager.removeAll(siteIds);
84+
85+
return this.area.removeAll(siteIds);
7986
},
8087

8188
async getByContainer(userContextId) {
82-
const sites = {};
83-
const siteConfigs = await this.area.get();
84-
Object.keys(siteConfigs).forEach((key) => {
89+
const siteSettingsById = await this.area.getSome((siteId, siteSettings) => {
8590
// 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-
}
91+
return String(siteSettings.userContextId) === String(userContextId);
9392
});
94-
return sites;
93+
await this._loadTransientPropertiesForAll(siteSettingsById);
94+
return siteSettingsById;
95+
},
96+
97+
async _loadTransientProperties(siteId, siteSettings) {
98+
if (siteId && siteSettings) {
99+
const wildcard = await wildcardManager.get(siteId);
100+
const exemptedTabs = this.exemptedTabs[siteId];
101+
this._setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs);
102+
}
103+
},
104+
105+
async _loadTransientPropertiesForAll(siteSettingsById) {
106+
const siteIds = Object.keys(siteSettingsById);
107+
if (siteIds.length > 0) {
108+
const siteIdsToWildcards = await wildcardManager.getAll(siteIds);
109+
siteIds.forEach((siteId) => {
110+
const siteSettings = siteSettingsById[siteId];
111+
const wildcard = siteIdsToWildcards[siteId];
112+
const exemptedTabs = this.exemptedTabs[siteId];
113+
this._setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs);
114+
});
115+
}
116+
},
117+
118+
_setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs = []) {
119+
siteSettings.siteId = siteId;
120+
siteSettings.hostname = siteId;
121+
siteSettings.wildcard = wildcard;
122+
siteSettings.exemptedTabs = exemptedTabs;
123+
},
124+
125+
_unsetTransientProperties(siteSettings) {
126+
delete siteSettings.siteId;
127+
delete siteSettings.hostname;
128+
delete siteSettings.wildcard;
129+
delete siteSettings.exemptedTabs;
95130
}
96131
},
97132

98-
_neverAsk(m) {
133+
async _neverAsk(m) {
99134
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-
});
135+
const neverAsk = m.neverAsk;
136+
if (neverAsk === true) {
137+
const siteSettings = await this.storageArea.matchUrl(pageUrl);
138+
if (siteSettings && !siteSettings.neverAsk) {
139+
siteSettings.neverAsk = true;
140+
await this.storageArea.set(siteSettings);
141+
}
110142
}
111143
},
112144

113-
// We return here so the confirm page can load the tab when exempted
114145
async _exemptTab(m) {
115146
const pageUrl = m.pageUrl;
116-
this.storageArea.setExempted(pageUrl, m.tabId);
117-
return true;
147+
const tabId = m.tabId;
148+
const siteSettings = await this.storageArea.matchUrl(pageUrl);
149+
if (siteSettings && siteSettings.exemptedTabs.indexOf(tabId) === -1) {
150+
siteSettings.exemptedTabs.push(tabId);
151+
await this.storageArea.set(siteSettings);
152+
}
118153
},
119154

120155
// Before a request is handled by the browser we decide if we should route through a different container
@@ -125,7 +160,7 @@ const assignManager = {
125160
this.removeContextMenu();
126161
const [tab, siteSettings] = await Promise.all([
127162
browser.tabs.get(options.tabId),
128-
this.storageArea.get(options.url)
163+
this.storageArea.matchUrl(options.url)
129164
]);
130165
let container;
131166
try {
@@ -142,8 +177,8 @@ const assignManager = {
142177
}
143178
const userContextId = this.getUserContextIdFromCookieStore(tab);
144179
if (!siteSettings
145-
|| userContextId === siteSettings.userContextId
146-
|| this.storageArea.isExempted(options.url, tab.id)) {
180+
|| siteSettings.userContextId === userContextId
181+
|| siteSettings.exemptedTabs.includes(tab.id)) {
147182
return {};
148183
}
149184
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
@@ -367,51 +402,68 @@ const assignManager = {
367402
return true;
368403
},
369404

370-
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
405+
_determineAssignmentMatchesUrl(siteSettings, url) {
406+
const siteId = backgroundLogic.getSiteIdFromUrl(url);
407+
if (siteSettings.siteId === siteId) { return true; }
408+
if (siteSettings.wildcard && siteId.endsWith(siteSettings.wildcard)) { return true; }
409+
return false;
410+
},
411+
412+
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove, options = {}) {
371413
let actionName;
372414

373415
// https://github.com/mozilla/testpilot-containers/issues/626
374416
// Context menu has stored context IDs as strings, so we need to coerce
375417
// the value to a string for accurate checking
376418
userContextId = String(userContextId);
377419

420+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
378421
if (!remove) {
422+
const siteSettings = this.storageArea.create(siteId, userContextId, options);
423+
424+
// Auto exempt all tabs that exist for this hostname that are not in the same container
379425
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;
426+
siteSettings.exemptedTabs = tabs.filter((tab) => {
427+
if (!this._determineAssignmentMatchesUrl(siteSettings, tab.url)) { return false; }
428+
if (this.getUserContextIdFromCookieStore(tab) === userContextId) { return false; }
429+
return true;
389430
}).map((tab) => {
390431
return tab.id;
391432
});
392-
393-
await this.storageArea.set(pageUrl, {
394-
userContextId,
395-
neverAsk: false
396-
}, exemptedTabIds);
433+
434+
await this.storageArea.set(siteSettings);
397435
actionName = "added";
398436
} else {
399-
await this.storageArea.remove(pageUrl);
437+
await this.storageArea.remove(siteId);
400438
actionName = "removed";
401439
}
402-
browser.tabs.sendMessage(tabId, {
403-
text: `Successfully ${actionName} site to always open in this container`
404-
});
440+
if (!options.silent) {
441+
browser.tabs.sendMessage(tabId, {
442+
text: `Successfully ${actionName} site to always open in this container`
443+
});
444+
}
405445
const tab = await browser.tabs.get(tabId);
406446
this.calculateContextMenu(tab);
407447
},
448+
449+
async _setOrRemoveWildcard(tabId, pageUrl, userContextId, wildcard) {
450+
// Get existing settings, so we can preserve neverAsk property
451+
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
452+
const siteSettings = await this.storageArea.get(siteId);
453+
const neverAsk = siteSettings && siteSettings.neverAsk;
454+
455+
// Remove assignment
456+
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, true, {silent:true});
457+
// Add assignment
458+
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, false, {silent:true, wildcard:wildcard, neverAsk:neverAsk});
459+
},
408460

409461
async _getAssignment(tab) {
410462
const cookieStore = this.getUserContextIdFromCookieStore(tab);
411463
// Ensure we have a cookieStore to assign to
412464
if (cookieStore
413465
&& this.isTabPermittedAssign(tab)) {
414-
return await this.storageArea.get(tab.url);
466+
return await this.storageArea.matchUrl(tab.url);
415467
}
416468
return false;
417469
},

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)