Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wildcard subdomains v2 - e.g. *.google.com #2352

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/css/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,18 @@ manage things like container crud */
padding-inline-start: 16px;
}

#edit-sites-assigned .hostname .subdomain:hover {
text-decoration: underline;
}

#edit-sites-assigned .hostname .subdomain.wildcardSubdomain {
background-color: var(--identity-icon-color);
border-radius: 8px;
margin-right: 4px;
padding-left: 10px;
padding-right: 10px;
}

.assigned-sites-list > div {
display: flex;
padding-block-end: 6px;
Expand Down
128 changes: 121 additions & 7 deletions src/js/background/assignManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ window.assignManager = {
}
},

getWildcardStoreKey(wildcardHostname) {
return `wildcardMap@@_${wildcardHostname}`;
},

getWildcardStoreKeys(siteStoreKey) {
// E.g. "siteContainerMap@@_www.mozilla.org" =>
// ["wildcardMap@@_www.mozilla.org", "wildcardMap@@_mozilla.org", "wildcardMap@@_org"]
let previous;
return siteStoreKey.replace(/^siteContainerMap@@_/, "")
.split(".")
.reverse()
.map((subdomain) => previous = previous ? `${subdomain}.${previous}` : subdomain)
.map((hostname) => this.getWildcardStoreKey(hostname))
.reverse();
},

setExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) {
Expand All @@ -46,6 +62,42 @@ window.assignManager = {
return this.getByUrlKey(siteStoreKey);
},

async getOrWildcardMatch(pageUrlorUrlKey) {
// 1st store request: siteStoreKey + wildcardStoreKeys
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
const wildcardStoreKeys = this.getWildcardStoreKeys(siteStoreKey);
const combinedStoreKeys = [siteStoreKey].concat(wildcardStoreKeys);
let storageResponse = await this.area.get(combinedStoreKeys);
if (!storageResponse) { return null; }

// Try exact match
const siteSettings = storageResponse[siteStoreKey];
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}

// 2nd store request (maybe): siteStoreKeys that were mapped from wildcardStoreKeys
const siteStoreKeys = wildcardStoreKeys.map((k) => storageResponse[k]).filter((k) => !!k);
if (siteStoreKeys.length > 0) {
storageResponse = await this.area.get(siteStoreKeys);
if (!storageResponse) { return null; }

// Try wildcard matches
for (const siteStoreKey of siteStoreKeys) {
const siteSettings = storageResponse[siteStoreKey];
if (siteSettings) {
return {
siteStoreKey,
siteSettings
};
}
}
}
},

async getSyncEnabled() {
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
return !!syncEnabled;
Expand Down Expand Up @@ -76,32 +128,84 @@ window.assignManager = {
this.setExempted(pageUrlorUrlKey, tabId);
});
}
if (data.wildcardHostname) {
await this.removeDuplicateWildcardHostname(data.wildcardHostname, siteStoreKey);
}
await this.removeWildcardLookup(siteStoreKey);
// eslint-disable-next-line require-atomic-updates
data.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(data.userContextId);
await this.area.set({
[siteStoreKey]: data
});
if (data.wildcardHostname) {
await this.setWildcardLookup(siteStoreKey, data.wildcardHostname);
}
const syncEnabled = await this.getSyncEnabled();
if (backup && syncEnabled) {
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
}
return;
},

async setWildcardLookup(siteStoreKey, wildcardHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
return this.area.set({
[wildcardStoreKey]: siteStoreKey
});
},

async remove(pageUrlorUrlKey, shouldSync = true) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
// When we remove an assignment we should clear all the exemptions
this.removeExempted(pageUrlorUrlKey);
// When we remove an assignment we should clear the wildcard lookup
await this.removeWildcardLookup(siteStoreKey);
await this.area.remove([siteStoreKey]);
const syncEnabled = await this.getSyncEnabled();
if (shouldSync && syncEnabled) await sync.storageArea.backup({siteStoreKey});
return;
},

async removeWildcardLookup(siteStoreKey) {
const siteSettings = await this.getByUrlKey(siteStoreKey);
const wildcardHostname = siteSettings && siteSettings.wildcardHostname;
if (wildcardHostname) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
await this.area.remove([wildcardStoreKey]);
}
},

// Must not set the same wildcardHostname property on multiple sites.
// E.g. 'google.com' on both 'www.google.com' and 'mail.google.com'.
//
// Necessary because the stored wildcardLookup map is 1-to-1, i.e. either
// 'google.com' => 'www.google.com', or
// 'google.com' => 'mail.google.com', but not both!
async removeDuplicateWildcardHostname(wildcardHostname, expectedSiteStoreKey) {
const wildcardStoreKey = this.getWildcardStoreKey(wildcardHostname);
const siteStoreKey = await this.getByUrlKey(wildcardStoreKey);
if (siteStoreKey && siteStoreKey !== expectedSiteStoreKey) {
const siteSettings = await this.getByUrlKey(siteStoreKey);
if (siteSettings && siteSettings.wildcardHostname === wildcardHostname) {
delete siteSettings.wildcardHostname;
await this.set(siteStoreKey, siteSettings); // Will cause wildcard mapping to be cleared
}
}
},

async deleteContainer(userContextId) {
const sitesByContainer = await this.getAssignedSites(userContextId);
this.area.remove(Object.keys(sitesByContainer));
// Delete wildcard lookups
const wildcardStoreKeys = Object.values(sitesByContainer)
.map((site) => {
if (site && site.wildcardHostname) {
return this.getWildcardStoreKey(site.wildcardHostname);
}
})
.filter((wildcardStoreKey) => { return !!wildcardStoreKey; });
this.area.remove(wildcardStoreKeys);
},

async getAssignedSites(userContextId = null) {
Expand Down Expand Up @@ -166,10 +270,10 @@ window.assignManager = {
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);
this.storageArea.getOrWildcardMatch(pageUrl).then((siteMatchResult) => {
if (siteMatchResult) {
siteMatchResult.siteSettings.neverAsk = true;
this.storageArea.set(siteMatchResult.siteStoreKey, siteMatchResult.siteSettings);
}
}).catch((e) => {
throw e;
Expand Down Expand Up @@ -217,10 +321,11 @@ window.assignManager = {
return {};
}
this.removeContextMenu();
const [tab, siteSettings] = await Promise.all([
const [tab, siteMatchResult] = await Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.get(options.url)
this.storageArea.getOrWildcardMatch(options.url)
]);
const siteSettings = siteMatchResult && siteMatchResult.siteSettings;
let container;
try {
container = await browser.contextualIdentities
Expand Down Expand Up @@ -620,6 +725,14 @@ window.assignManager = {
}
},

async _setWildcardHostnameForAssignment(pageUrl, wildcardHostname) {
const siteSettings = await this.storageArea.get(pageUrl);
if (siteSettings) {
siteSettings.wildcardHostname = wildcardHostname;
await this.storageArea.set(pageUrl, siteSettings);
}
},

async _maybeRemoveSiteIsolation(userContextId) {
const assignments = await this.storageArea.getByContainer(userContextId);
const hasAssignments = assignments && Object.keys(assignments).length > 0;
Expand All @@ -637,7 +750,8 @@ window.assignManager = {
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
return this.storageArea.get(tab.url);
const siteMatchResult = await this.storageArea.getOrWildcardMatch(tab.url);
return siteMatchResult && siteMatchResult.siteSettings;
}
return false;
},
Expand Down
3 changes: 3 additions & 0 deletions src/js/background/messageHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const messageHandler = {
// m.url is the assignment to be removed/added
response = assignManager._setOrRemoveAssignment(m.tabId, m.url, m.userContextId, m.value);
break;
case "setWildcardHostnameForAssignment":
response = assignManager._setWildcardHostnameForAssignment(m.url, m.wildcardHostname);
break;
case "sortTabs":
backgroundLogic.sortTabs();
break;
Expand Down
85 changes: 84 additions & 1 deletion src/js/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,7 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {

// Populating the panel: name and icon
document.getElementById("edit-assignments-title").textContent = identity.name;
document.getElementById("edit-sites-assigned").setAttribute("data-identity-color", identity.color);

const userContextId = Logic.currentUserContextId();
const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
Expand Down Expand Up @@ -1411,10 +1412,11 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
trElement.innerHTML = Utils.escaped`
<td>
<div class="favicon"></div>
<span title="${site.hostname}" class="menu-text">${site.hostname}</span>
<span title="${site.hostname}" class="menu-text hostname"></span>
<img class="trash-button delete-assignment" src="/img/container-delete.svg" />
</td>`;
trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
trElement.querySelector(".hostname").appendChild(this.assignmentHostnameElement(site));
const deleteButton = trElement.querySelector(".trash-button");
Utils.addEnterHandler(deleteButton, async () => {
const userContextId = Logic.currentUserContextId();
Expand All @@ -1424,11 +1426,92 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
delete assignments[siteKey];
this.showAssignedContainers(assignments);
});
// Wildcard click-to-toggle subdomains
trElement.querySelectorAll(".subdomain").forEach((subdomainLink) => {
subdomainLink.addEventListener("click", (e) => {
const wildcardHostname = e.target.getAttribute("data-wildcardHostname");
Utils.setWildcardHostnameForAssignment(assumedUrl, wildcardHostname);
if (wildcardHostname) {
// Remove wildcard from other site that has same wildcard
Object.values(assignments).forEach((site) => {
if (site.wildcardHostname === wildcardHostname) { delete site.wildcardHostname; }
});
site.wildcardHostname = wildcardHostname;
} else {
delete site.wildcardHostname;
}
this.showAssignedContainers(assignments);
});
});
trElement.classList.add("menu-item", "hover-highlight", "keyboard-nav");
tableElement.appendChild(trElement);
});
}
},

getSubdomains(site) {
const hostname = site.hostname;
const wildcardHostname = site.wildcardHostname;
if (wildcardHostname && wildcardHostname !== hostname) {
if (hostname.endsWith(wildcardHostname)) {
return {
wildcard: "★",
remaining: wildcardHostname
};
} else {
// In case something got corrupted, allow user to fix error
// by clicking '★' link to clear corrupted wildcard hostname
return {
wildcard: "★",
remaining: hostname
};
}
} else {
return {
wildcard: null,
remaining: hostname
};
}
},

assignmentHostnameElement(site) {
const result = document.createElement("span");
const subdomains = this.getSubdomains(site);

// Add wildcard subdomain(s)
if (subdomains.wildcard) {
result.appendChild(this.assignmentSubdomainLink(null, subdomains.wildcard));
result.appendChild(document.createTextNode("."));
}

// Add non-wildcard subdomains
let remainingHostname = subdomains.remaining;
let indexOfDot;
while ((indexOfDot = remainingHostname.indexOf(".")) >= 0) {
const subdomain = remainingHostname.substring(0, indexOfDot);
remainingHostname = remainingHostname.substring(indexOfDot + 1);
result.appendChild(this.assignmentSubdomainLink(remainingHostname, subdomain));
result.appendChild(document.createTextNode("."));
}

// Root domain
if (remainingHostname) { result.appendChild(document.createTextNode(remainingHostname)); }

return result;
},

assignmentSubdomainLink(wildcardHostnameOnClick, text) {
const result = document.createElement("a");
result.className = "subdomain";
if (wildcardHostnameOnClick) {
result.setAttribute("data-wildcardHostname", wildcardHostnameOnClick);
result.title = `*.${wildcardHostnameOnClick}`;
} else {
result.classList.add("wildcardSubdomain");
}
result.appendChild(document.createTextNode(text));
return result;
},
});

// P_CONTAINER_EDIT: Editor for a container.
Expand Down
8 changes: 8 additions & 0 deletions src/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ const Utils = {
});
},

setWildcardHostnameForAssignment(url, wildcardHostname) {
return browser.runtime.sendMessage({
method: "setWildcardHostnameForAssignment",
url,
wildcardHostname
});
},

async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
return await browser.runtime.sendMessage({
method: "reloadInContainer",
Expand Down