Wildcard subdomains - e.g. *.google.com
This commit is contained in:
parent
dc9e8f6399
commit
e6eff75677
13 changed files with 505 additions and 116 deletions
|
@ -26,7 +26,7 @@
|
|||
"stylelint-config-standard": "^16.0.0",
|
||||
"stylelint-order": "^0.3.0",
|
||||
"web-ext": "^2.9.3",
|
||||
"webextensions-jsdom": "^1.1.0"
|
||||
"webextensions-jsdom": "^1.1.1"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/multi-account-containers#readme",
|
||||
"license": "MPL-2.0",
|
||||
|
|
|
@ -832,6 +832,11 @@ span ~ .panel-header-text {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
/* Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473 */
|
||||
.assigned-sites-list .hostname .subdomain:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container {
|
||||
align-items: center;
|
||||
block-size: 29px;
|
||||
|
|
|
@ -3,6 +3,8 @@ module.exports = {
|
|||
"../../.eslintrc.js"
|
||||
],
|
||||
"globals": {
|
||||
"utils": false,
|
||||
"wildcardManager": false,
|
||||
"assignManager": true,
|
||||
"badge": true,
|
||||
"backgroundLogic": true,
|
||||
|
|
|
@ -6,115 +6,150 @@ const assignManager = {
|
|||
MENU_MOVE_ID: "move-to-new-window-container",
|
||||
OPEN_IN_CONTAINER: "open-bookmark-in-container-tab",
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
area: new utils.NamedStore("siteContainerMap"),
|
||||
exemptedTabs: {},
|
||||
|
||||
getSiteStoreKey(pageUrl) {
|
||||
const url = new window.URL(pageUrl);
|
||||
const storagePrefix = "siteContainerMap@@_";
|
||||
if (url.port === "80" || url.port === "443") {
|
||||
return `${storagePrefix}${url.hostname}`;
|
||||
async matchUrl(pageUrl) {
|
||||
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
|
||||
|
||||
// Try exact match
|
||||
let siteSettings = await this.get(siteId);
|
||||
|
||||
if (!siteSettings) {
|
||||
// Try wildcard match
|
||||
const wildcard = await wildcardManager.match(siteId);
|
||||
if (wildcard) {
|
||||
siteSettings = await this.get(wildcard);
|
||||
}
|
||||
}
|
||||
|
||||
return siteSettings;
|
||||
},
|
||||
|
||||
create(siteId, userContextId, options = {}) {
|
||||
const siteSettings = { userContextId, neverAsk:!!options.neverAsk };
|
||||
this._setTransientProperties(siteId, siteSettings, options.wildcard);
|
||||
return siteSettings;
|
||||
},
|
||||
|
||||
async get(siteId) {
|
||||
const siteSettings = await this.area.get(siteId);
|
||||
await this._loadTransientProperties(siteId, siteSettings);
|
||||
return siteSettings;
|
||||
},
|
||||
|
||||
async set(siteSettings) {
|
||||
const siteId = siteSettings.siteId;
|
||||
const exemptedTabs = siteSettings.exemptedTabs;
|
||||
const wildcard = siteSettings.wildcard;
|
||||
|
||||
// Store exempted tabs
|
||||
this.exemptedTabs[siteId] = exemptedTabs;
|
||||
|
||||
// Store/remove wildcard mapping
|
||||
if (wildcard && wildcard !== siteId) {
|
||||
await wildcardManager.set(siteId, wildcard);
|
||||
} else {
|
||||
return `${storagePrefix}${url.hostname}${url.port}`;
|
||||
await wildcardManager.remove(siteId);
|
||||
}
|
||||
|
||||
// Remove transient properties before storing
|
||||
const cleanSiteSettings = Object.assign({}, siteSettings);
|
||||
this._unsetTransientProperties(cleanSiteSettings);
|
||||
|
||||
// Store assignment
|
||||
return this.area.set(siteId, cleanSiteSettings);
|
||||
},
|
||||
|
||||
setExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
}
|
||||
this.exemptedTabs[siteStoreKey].push(tabId);
|
||||
},
|
||||
|
||||
removeExempted(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
},
|
||||
|
||||
isExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
return false;
|
||||
}
|
||||
return this.exemptedTabs[siteStoreKey].includes(tabId);
|
||||
},
|
||||
|
||||
get(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.area.get([siteStoreKey]).then((storageResponse) => {
|
||||
if (storageResponse && siteStoreKey in storageResponse) {
|
||||
resolve(storageResponse[siteStoreKey]);
|
||||
}
|
||||
resolve(null);
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
set(pageUrl, data, exemptedTabIds) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
if (exemptedTabIds) {
|
||||
exemptedTabIds.forEach((tabId) => {
|
||||
this.setExempted(pageUrl, tabId);
|
||||
});
|
||||
}
|
||||
return this.area.set({
|
||||
[siteStoreKey]: data
|
||||
});
|
||||
},
|
||||
|
||||
remove(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
async remove(siteId) {
|
||||
// When we remove an assignment we should clear all the exemptions
|
||||
this.removeExempted(pageUrl);
|
||||
return this.area.remove([siteStoreKey]);
|
||||
delete this.exemptedTabs[siteId];
|
||||
// ...and also clear the wildcard mapping
|
||||
await wildcardManager.remove(siteId);
|
||||
|
||||
return this.area.remove(siteId);
|
||||
},
|
||||
|
||||
async deleteContainer(userContextId) {
|
||||
const sitesByContainer = await this.getByContainer(userContextId);
|
||||
this.area.remove(Object.keys(sitesByContainer));
|
||||
const siteSettingsById = await this.getByContainer(userContextId);
|
||||
const siteIds = Object.keys(siteSettingsById);
|
||||
|
||||
siteIds.forEach((siteId) => {
|
||||
// When we remove an assignment we should clear all the exemptions
|
||||
delete this.exemptedTabs[siteId];
|
||||
});
|
||||
|
||||
// ...and also clear the wildcard mappings
|
||||
await wildcardManager.removeAll(siteIds);
|
||||
|
||||
return this.area.removeAll(siteIds);
|
||||
},
|
||||
|
||||
async getByContainer(userContextId) {
|
||||
const sites = {};
|
||||
const siteConfigs = await this.area.get();
|
||||
Object.keys(siteConfigs).forEach((key) => {
|
||||
const siteSettingsById = await this.area.getSome((siteId, siteSettings) => {
|
||||
// For some reason this is stored as string... lets check them both as that
|
||||
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
|
||||
const site = siteConfigs[key];
|
||||
// In hindsight we should have stored this
|
||||
// TODO file a follow up to clean the storage onLoad
|
||||
site.hostname = key.replace(/^siteContainerMap@@_/, "");
|
||||
sites[key] = site;
|
||||
}
|
||||
return String(siteSettings.userContextId) === String(userContextId);
|
||||
});
|
||||
return sites;
|
||||
await this._loadTransientPropertiesForAll(siteSettingsById);
|
||||
return siteSettingsById;
|
||||
},
|
||||
|
||||
async _loadTransientProperties(siteId, siteSettings) {
|
||||
if (siteId && siteSettings) {
|
||||
const wildcard = await wildcardManager.get(siteId);
|
||||
const exemptedTabs = this.exemptedTabs[siteId];
|
||||
this._setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs);
|
||||
}
|
||||
},
|
||||
|
||||
async _loadTransientPropertiesForAll(siteSettingsById) {
|
||||
const siteIds = Object.keys(siteSettingsById);
|
||||
if (siteIds.length > 0) {
|
||||
const siteIdsToWildcards = await wildcardManager.getAll(siteIds);
|
||||
siteIds.forEach((siteId) => {
|
||||
const siteSettings = siteSettingsById[siteId];
|
||||
const wildcard = siteIdsToWildcards[siteId];
|
||||
const exemptedTabs = this.exemptedTabs[siteId];
|
||||
this._setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_setTransientProperties(siteId, siteSettings, wildcard, exemptedTabs = []) {
|
||||
siteSettings.siteId = siteId;
|
||||
siteSettings.hostname = siteId;
|
||||
siteSettings.wildcard = wildcard;
|
||||
siteSettings.exemptedTabs = exemptedTabs;
|
||||
},
|
||||
|
||||
_unsetTransientProperties(siteSettings) {
|
||||
delete siteSettings.siteId;
|
||||
delete siteSettings.hostname;
|
||||
delete siteSettings.wildcard;
|
||||
delete siteSettings.exemptedTabs;
|
||||
}
|
||||
},
|
||||
|
||||
_neverAsk(m) {
|
||||
async _neverAsk(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
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);
|
||||
}
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
const neverAsk = m.neverAsk;
|
||||
if (neverAsk === true) {
|
||||
const siteSettings = await this.storageArea.matchUrl(pageUrl);
|
||||
if (siteSettings && !siteSettings.neverAsk) {
|
||||
siteSettings.neverAsk = true;
|
||||
await this.storageArea.set(siteSettings);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// We return here so the confirm page can load the tab when exempted
|
||||
async _exemptTab(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
this.storageArea.setExempted(pageUrl, m.tabId);
|
||||
return true;
|
||||
const tabId = m.tabId;
|
||||
const siteSettings = await this.storageArea.matchUrl(pageUrl);
|
||||
if (siteSettings && siteSettings.exemptedTabs.indexOf(tabId) === -1) {
|
||||
siteSettings.exemptedTabs.push(tabId);
|
||||
await this.storageArea.set(siteSettings);
|
||||
}
|
||||
},
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
|
@ -125,7 +160,7 @@ const assignManager = {
|
|||
this.removeContextMenu();
|
||||
const [tab, siteSettings] = await Promise.all([
|
||||
browser.tabs.get(options.tabId),
|
||||
this.storageArea.get(options.url)
|
||||
this.storageArea.matchUrl(options.url)
|
||||
]);
|
||||
let container;
|
||||
try {
|
||||
|
@ -142,8 +177,8 @@ const assignManager = {
|
|||
}
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (!siteSettings
|
||||
|| userContextId === siteSettings.userContextId
|
||||
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||
|| siteSettings.userContextId === userContextId
|
||||
|| siteSettings.exemptedTabs.includes(tab.id)) {
|
||||
return {};
|
||||
}
|
||||
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|
||||
|
@ -367,7 +402,14 @@ const assignManager = {
|
|||
return true;
|
||||
},
|
||||
|
||||
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
|
||||
_determineAssignmentMatchesUrl(siteSettings, url) {
|
||||
const siteId = backgroundLogic.getSiteIdFromUrl(url);
|
||||
if (siteSettings.siteId === siteId) { return true; }
|
||||
if (siteSettings.wildcard && siteId.endsWith(siteSettings.wildcard)) { return true; }
|
||||
return false;
|
||||
},
|
||||
|
||||
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove, options = {}) {
|
||||
let actionName;
|
||||
|
||||
// https://github.com/mozilla/testpilot-containers/issues/626
|
||||
|
@ -375,43 +417,53 @@ const assignManager = {
|
|||
// the value to a string for accurate checking
|
||||
userContextId = String(userContextId);
|
||||
|
||||
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
|
||||
if (!remove) {
|
||||
const siteSettings = this.storageArea.create(siteId, userContextId, options);
|
||||
|
||||
// Auto exempt all tabs that exist for this hostname that are not in the same container
|
||||
const tabs = await browser.tabs.query({});
|
||||
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
|
||||
const exemptedTabIds = tabs.filter((tab) => {
|
||||
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
|
||||
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
|
||||
if (tabStoreKey === assignmentStoreKey &&
|
||||
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
siteSettings.exemptedTabs = tabs.filter((tab) => {
|
||||
if (!this._determineAssignmentMatchesUrl(siteSettings, tab.url)) { return false; }
|
||||
if (this.getUserContextIdFromCookieStore(tab) === userContextId) { return false; }
|
||||
return true;
|
||||
}).map((tab) => {
|
||||
return tab.id;
|
||||
});
|
||||
|
||||
await this.storageArea.set(pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
}, exemptedTabIds);
|
||||
|
||||
await this.storageArea.set(siteSettings);
|
||||
actionName = "added";
|
||||
} else {
|
||||
await this.storageArea.remove(pageUrl);
|
||||
await this.storageArea.remove(siteId);
|
||||
actionName = "removed";
|
||||
}
|
||||
browser.tabs.sendMessage(tabId, {
|
||||
text: `Successfully ${actionName} site to always open in this container`
|
||||
});
|
||||
if (!options.silent) {
|
||||
browser.tabs.sendMessage(tabId, {
|
||||
text: `Successfully ${actionName} site to always open in this container`
|
||||
});
|
||||
}
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
this.calculateContextMenu(tab);
|
||||
},
|
||||
|
||||
async _setOrRemoveWildcard(tabId, pageUrl, userContextId, wildcard) {
|
||||
// Get existing settings, so we can preserve neverAsk property
|
||||
const siteId = backgroundLogic.getSiteIdFromUrl(pageUrl);
|
||||
const siteSettings = await this.storageArea.get(siteId);
|
||||
const neverAsk = siteSettings && siteSettings.neverAsk;
|
||||
|
||||
// Remove assignment
|
||||
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, true, {silent:true});
|
||||
// Add assignment
|
||||
await this._setOrRemoveAssignment(tabId, pageUrl, userContextId, false, {silent:true, wildcard:wildcard, neverAsk:neverAsk});
|
||||
},
|
||||
|
||||
async _getAssignment(tab) {
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
return await this.storageArea.get(tab.url);
|
||||
return await this.storageArea.matchUrl(tab.url);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
|
|
@ -329,5 +329,17 @@ const backgroundLogic = {
|
|||
|
||||
cookieStoreId(userContextId) {
|
||||
return `firefox-container-${userContextId}`;
|
||||
},
|
||||
|
||||
// A URL host string that is used to identify a site assignment, e.g.:
|
||||
// www.example.com
|
||||
// www.example.com:8080
|
||||
getSiteIdFromUrl(pageUrl) {
|
||||
const url = new window.URL(pageUrl);
|
||||
if (url.port === "" || url.port === "80" || url.port === "443") {
|
||||
return `${url.hostname}`;
|
||||
} else {
|
||||
return `${url.hostname}:${url.port}`;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -13,7 +13,9 @@
|
|||
"js/background/messageHandler.js",
|
||||
]
|
||||
-->
|
||||
<script type="text/javascript" src="utils.js"></script>
|
||||
<script type="text/javascript" src="backgroundLogic.js"></script>
|
||||
<script type="text/javascript" src="wildcardManager.js"></script>
|
||||
<script type="text/javascript" src="assignManager.js"></script>
|
||||
<script type="text/javascript" src="badge.js"></script>
|
||||
<script type="text/javascript" src="identityState.js"></script>
|
||||
|
|
|
@ -37,6 +37,12 @@ const messageHandler = {
|
|||
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
||||
});
|
||||
break;
|
||||
case "setOrRemoveWildcard":
|
||||
// Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._setOrRemoveWildcard(tab.id, m.url, m.userContextId, m.wildcard);
|
||||
});
|
||||
break;
|
||||
case "sortTabs":
|
||||
backgroundLogic.sortTabs();
|
||||
break;
|
||||
|
@ -91,7 +97,7 @@ const messageHandler = {
|
|||
if (typeof message.url === "undefined") {
|
||||
throw new Error("Missing message.url");
|
||||
}
|
||||
response = assignManager.storageArea.get(message.url);
|
||||
response = assignManager.storageArea.matchUrl(message.url);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown message.method");
|
||||
|
|
106
src/js/background/utils.js
Normal file
106
src/js/background/utils.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
const utils = { // eslint-disable-line no-unused-vars
|
||||
|
||||
// Copy object and remove keys with predicate
|
||||
filterObj(obj, predicate) {
|
||||
if (obj && typeof obj !== "object") { throw new Error(`Invalid arg: ${obj}`); }
|
||||
if (!obj) { return {}; }
|
||||
return Object.assign({}, ...Object.entries(obj).map(([k,v]) => {
|
||||
if (predicate(k, v)) {
|
||||
return { [k]: v };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
// Store data in a named storage area.
|
||||
//
|
||||
// (Note that all data for all stores is stored in the same single storage area,
|
||||
// but this class provides accessor methods to get/set only the data that applies
|
||||
// to one specific named store, as identified in the constructor.)
|
||||
NamedStore: class {
|
||||
constructor(name) {
|
||||
this.prefix = `${name}@@_`;
|
||||
}
|
||||
|
||||
_storeKeyForKey(key) {
|
||||
if (Array.isArray(key)) {
|
||||
return key.map(oneKey => oneKey.startsWith(this.prefix) ? oneKey : `${this.prefix}${oneKey}`);
|
||||
} else if (key) {
|
||||
return key.startsWith(this.prefix) ? key : `${this.prefix}${key}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_keyForStoreKey(storeKey) {
|
||||
if (Array.isArray(storeKey)) {
|
||||
return storeKey.map(oneStoreKey => oneStoreKey.startsWith(this.prefix) ? oneStoreKey.substring(this.prefix.length) : null);
|
||||
} else if (storeKey) {
|
||||
return storeKey.startsWith(this.prefix) ? storeKey.substring(this.prefix.length) : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get(key) {
|
||||
if (typeof key !== "string") { return Promise.reject(new Error(`Invalid arg: ${key}`)); }
|
||||
const storeKey = this._storeKeyForKey(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
browser.storage.local.get([storeKey]).then((storageResponse) => {
|
||||
if (storeKey in storageResponse) {
|
||||
resolve(storageResponse[storeKey]);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAll(keys) {
|
||||
if (keys && !Array.isArray(keys)) { return Promise.reject(new Error(`Invalid arg: ${keys}`)); }
|
||||
const storeKeys = this._storeKeyForKey(keys);
|
||||
return new Promise((resolve, reject) => {
|
||||
browser.storage.local.get(storeKeys).then((storageResponse) => {
|
||||
if (storageResponse) {
|
||||
resolve(Object.assign({}, ...Object.entries(storageResponse).map(([oneStoreKey, data]) => {
|
||||
const key = this._keyForStoreKey(oneStoreKey);
|
||||
return key ? { [key]: data } : null;
|
||||
})));
|
||||
} else {
|
||||
resolve({});
|
||||
}
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getSome(predicate) {
|
||||
const all = await this.getAll();
|
||||
return utils.filterObj(all, predicate);
|
||||
}
|
||||
|
||||
set(key, data) {
|
||||
if (typeof key !== "string") { return Promise.reject(new Error(`Invalid arg: ${key}`)); }
|
||||
const storeKey = this._storeKeyForKey(key);
|
||||
return browser.storage.local.set({
|
||||
[storeKey]: data
|
||||
});
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
if (typeof key !== "string") { return Promise.reject(new Error(`Invalid arg: ${key}`)); }
|
||||
const storeKey = this._storeKeyForKey(key);
|
||||
return browser.storage.local.remove(storeKey);
|
||||
}
|
||||
|
||||
removeAll(keys) {
|
||||
if (keys && !Array.isArray(keys)) { return Promise.reject(new Error(`Invalid arg: ${keys}`)); }
|
||||
const storeKeys = this._storeKeyForKey(keys);
|
||||
return browser.storage.local.remove(storeKeys);
|
||||
}
|
||||
}
|
||||
};
|
69
src/js/background/wildcardManager.js
Normal file
69
src/js/background/wildcardManager.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
Manages mappings of Site Host <-> Wildcard Host.
|
||||
|
||||
E.g. drive.google.com <-> google.com
|
||||
|
||||
Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
|
||||
*/
|
||||
const wildcardManager = { // eslint-disable-line no-unused-vars
|
||||
bySite: new utils.NamedStore("siteToWildcardMap"),
|
||||
byWildcard: new utils.NamedStore("wildcardToSiteMap"),
|
||||
|
||||
// Site -> Wildcard
|
||||
get(site) {
|
||||
return this.bySite.get(site);
|
||||
},
|
||||
|
||||
async getAll(sites) {
|
||||
return this.bySite.getAll(sites);
|
||||
},
|
||||
|
||||
async set(site, wildcard) {
|
||||
// Remove existing site -> wildcard
|
||||
const oldSite = await this.byWildcard.get(wildcard);
|
||||
if (oldSite === site) { return; } // Wildcard already set
|
||||
if (oldSite) { await this.bySite.remove(oldSite); }
|
||||
|
||||
// Set new mappings site <-> wildcard
|
||||
await this.bySite.set(site, wildcard);
|
||||
await this.byWildcard.set(wildcard, site);
|
||||
},
|
||||
|
||||
async remove(site) {
|
||||
const wildcard = await this.bySite.get(site);
|
||||
if (!wildcard) { return; }
|
||||
|
||||
await this.bySite.remove(site);
|
||||
await this.byWildcard.remove(wildcard);
|
||||
},
|
||||
|
||||
async removeAll(sites) {
|
||||
const data = await this.bySite.getAll(sites);
|
||||
const existingSites = Object.keys(data);
|
||||
const existingWildcards = Object.values(data);
|
||||
|
||||
await this.bySite.removeAll(existingSites);
|
||||
await this.byWildcard.removeAll(existingWildcards);
|
||||
},
|
||||
|
||||
// Site -> Site that owns Wildcard
|
||||
async match(site) {
|
||||
// Keep stripping subdomains off site domain until match a wildcard domain
|
||||
do {
|
||||
// Use the ever-shortening site hostname as if it is a wildcard
|
||||
const siteHavingWildcard = await this.byWildcard.get(site);
|
||||
if (siteHavingWildcard) { return siteHavingWildcard; }
|
||||
} while ((site = this._removeSubdomain(site)));
|
||||
return null;
|
||||
},
|
||||
|
||||
_removeSubdomain(site) {
|
||||
const indexOfDot = site.indexOf(".");
|
||||
if (indexOfDot < 0) {
|
||||
return null;
|
||||
} else {
|
||||
return site.substring(indexOfDot + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -368,6 +368,17 @@ const Logic = {
|
|||
});
|
||||
},
|
||||
|
||||
// Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
|
||||
setOrRemoveWildcard(tabId, url, userContextId, wildcard) {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "setOrRemoveWildcard",
|
||||
tabId,
|
||||
url,
|
||||
userContextId,
|
||||
wildcard
|
||||
});
|
||||
},
|
||||
|
||||
generateIdentityName() {
|
||||
const defaultName = "Container #";
|
||||
const ids = [];
|
||||
|
@ -393,7 +404,7 @@ const Logic = {
|
|||
getCurrentPanelElement() {
|
||||
const panelItem = this._panels[this._currentPanel];
|
||||
return document.querySelector(this.getPanelSelector(panelItem));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// P_ONBOARDING_1: First page for Onboarding.
|
||||
|
@ -1030,16 +1041,38 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||
const assumedUrl = `https://${site.hostname}/favicon.ico`;
|
||||
trElement.innerHTML = escaped`
|
||||
<div class="favicon"></div>
|
||||
<div title="${site.hostname}" class="truncate-text hostname">
|
||||
${site.hostname}
|
||||
</div>
|
||||
<div title="${site.hostname}" class="truncate-text hostname"></div>
|
||||
<img
|
||||
class="pop-button-image delete-assignment"
|
||||
src="/img/container-delete.svg"
|
||||
/>`;
|
||||
trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
|
||||
const deleteButton = trElement.querySelector(".delete-assignment");
|
||||
trElement.querySelector(".hostname").appendChild(this.assignmentElement(site));
|
||||
|
||||
const that = this;
|
||||
|
||||
// Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
|
||||
trElement.querySelectorAll(".subdomain").forEach(function(subdomainLink) {
|
||||
subdomainLink.addEventListener("click", async (e) => {
|
||||
const userContextId = Logic.currentUserContextId();
|
||||
// Wildcard hostname is stored in id attribute
|
||||
const wildcard = e.target.id;
|
||||
if (wildcard) {
|
||||
// Remove wildcard from other site that has same wildcard
|
||||
Object.values(assignments).forEach((site) => {
|
||||
if (site.wildcard === wildcard) { delete site.wildcard; }
|
||||
});
|
||||
site.wildcard = wildcard;
|
||||
} else {
|
||||
delete site.wildcard;
|
||||
}
|
||||
const currentTab = await Logic.currentTab();
|
||||
Logic.setOrRemoveWildcard(currentTab.id, assumedUrl, userContextId, wildcard);
|
||||
that.showAssignedContainers(assignments);
|
||||
});
|
||||
});
|
||||
|
||||
const deleteButton = trElement.querySelector(".delete-assignment");
|
||||
Logic.addEnterHandler(deleteButton, async () => {
|
||||
const userContextId = Logic.currentUserContextId();
|
||||
// Lets show the message to the current tab
|
||||
|
@ -1049,11 +1082,46 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||
delete assignments[siteKey];
|
||||
that.showAssignedContainers(assignments);
|
||||
});
|
||||
|
||||
trElement.classList.add("container-info-tab-row", "clickable");
|
||||
tableElement.appendChild(trElement);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
|
||||
assignmentElement(site) {
|
||||
const result = document.createElement("span");
|
||||
|
||||
// Remove wildcard subdomain
|
||||
if (site.wildcard && site.wildcard !== site.hostname) {
|
||||
result.appendChild(this.assignmentSubdomainLink(null, "___"));
|
||||
result.appendChild(document.createTextNode("."));
|
||||
}
|
||||
|
||||
// Add wildcard subdomain
|
||||
let hostname = site.wildcard ? site.wildcard : site.hostname;
|
||||
let indexOfDot;
|
||||
while ((indexOfDot = hostname.indexOf(".")) >= 0) {
|
||||
const subdomain = hostname.substring(0, indexOfDot);
|
||||
hostname = hostname.substring(indexOfDot + 1);
|
||||
result.appendChild(this.assignmentSubdomainLink(hostname, subdomain));
|
||||
result.appendChild(document.createTextNode("."));
|
||||
}
|
||||
|
||||
// Root domain
|
||||
result.appendChild(document.createTextNode(hostname));
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
assignmentSubdomainLink(wildcard, text) {
|
||||
const result = document.createElement("a");
|
||||
if (wildcard) { result.id = wildcard; }
|
||||
result.className = "subdomain";
|
||||
result.appendChild(document.createTextNode(text));
|
||||
return result;
|
||||
},
|
||||
|
||||
initializeRadioButtons() {
|
||||
const colorRadioTemplate = (containerColor) => {
|
||||
|
|
|
@ -25,7 +25,7 @@ describe("External Webextensions", () => {
|
|||
|
||||
const [promise] = background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
|
||||
const answer = await promise;
|
||||
expect(answer).to.deep.equal({
|
||||
expect(answer).to.deep.include({
|
||||
userContextId: "1",
|
||||
neverAsk: false
|
||||
});
|
||||
|
|
58
test/features/wildcard.test.js
Normal file
58
test/features/wildcard.test.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
|
||||
describe("Wildcard Subdomains Feature", () => {
|
||||
const url1 = "http://www.example.com";
|
||||
const url2 = "http://mail.example.com";
|
||||
|
||||
let activeTab;
|
||||
beforeEach(async () => {
|
||||
activeTab = await helper.browser.initializeWithTab({
|
||||
cookieStoreId: "firefox-container-1",
|
||||
url: url1
|
||||
});
|
||||
});
|
||||
|
||||
describe("click the 'Always open in' checkbox in the popup", () => {
|
||||
beforeEach(async () => {
|
||||
// popup click to set assignment for activeTab.url
|
||||
await helper.popup.clickElementById("container-page-assigned");
|
||||
});
|
||||
|
||||
describe("click the assigned URL's subdomain to convert it to a wildcard", () => {
|
||||
beforeEach(async () => {
|
||||
await helper.popup.setWildcard(activeTab, "example.com");
|
||||
});
|
||||
|
||||
describe("open new Tab with a different subdomain in the default container", () => {
|
||||
let newTab;
|
||||
beforeEach(async () => {
|
||||
// new Tab opening activeTab.url in default container
|
||||
newTab = await helper.browser.openNewTab({
|
||||
cookieStoreId: "firefox-default",
|
||||
url: url2
|
||||
}, {
|
||||
options: {
|
||||
webRequestError: true // because request is canceled due to reopening
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should open the confirm page", async () => {
|
||||
// should have created a new tab with the confirm page
|
||||
background.browser.tabs.create.should.have.been.calledWithMatch({
|
||||
url: "moz-extension://fake/confirm-page.html?" +
|
||||
`url=${encodeURIComponent(url2)}` +
|
||||
`&cookieStoreId=${activeTab.cookieStoreId}`,
|
||||
cookieStoreId: undefined,
|
||||
openerTabId: null,
|
||||
index: 2,
|
||||
active: true
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove the new Tab that got opened in the default container", () => {
|
||||
background.browser.tabs.remove.should.have.been.calledWith(newTab.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,6 +11,8 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
popup: {
|
||||
// Required to access variables, because nyc messes up 'eval'
|
||||
script: "function evalScript(v) { return eval(v); }",
|
||||
jsdom: {
|
||||
beforeParse(window) {
|
||||
window.browser.storage.local.set({
|
||||
|
@ -39,6 +41,13 @@ module.exports = {
|
|||
|
||||
async clickLastMatchingElementByQuerySelector(querySelector) {
|
||||
await popup.helper.clickElementByQuerySelectorAll(querySelector, "last");
|
||||
},
|
||||
|
||||
// Wildcard subdomains: https://github.com/mozilla/multi-account-containers/issues/473
|
||||
async setWildcard(tab, wildcard) {
|
||||
const Logic = popup.window.evalScript("Logic");
|
||||
const userContextId = Logic.userContextId(tab.cookieStoreId);
|
||||
await Logic.setOrRemoveWildcard(tab.id, tab.url, userContextId, wildcard);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue