diff --git a/src/css/popup.css b/src/css/popup.css index 5980894cf..1ad1e80b3 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -1923,6 +1923,19 @@ manage things like container crud */ margin-inline: 4px; } +.edit-container-panel input[type="text"]#edit-container-panel-site-input { + inline-size: 80%; +} + +#edit-container-site-link { + background: #ebebeb; + block-size: 36px; +} + +#edit-container-site-link:hover { + background: #e3e3e3; +} + input[type="text"]:focus { /* Both a border and box-shadow of 1px are needed because using a 2px border * would redraw the input 1px farther to the left. diff --git a/src/js/popup.js b/src/js/popup.js index ec5300604..7106f1086 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -1818,6 +1818,12 @@ Logic.registerPanel(P_CONTAINER_EDIT, { Utils.addEnterHandler(document.querySelector("#create-container-ok-link"), () => { this._submitForm(); }); + + // Add new site to current container + const siteLink = document.querySelector("#edit-container-site-link"); + Utils.addEnterHandler(siteLink, () => { + this._addSite(); + }); }, async _submitForm() { @@ -1860,6 +1866,113 @@ Logic.registerPanel(P_CONTAINER_EDIT, { return editedIdentity; }, + async _addSite() { + // Get URL and container ID from form + const formValues = new FormData(this._editForm); + const url = formValues.get("site-name"); + const userContextId = formValues.get("container-id"); + const currentTab = await Logic.currentTab(); + const tabId = currentTab.id; + const baseURL = this.normalizeUrl(url); + + if (baseURL !== null) { + // Assign URL to container + await Utils.setOrRemoveAssignment(tabId, baseURL, userContextId, false); + + // Clear form + document.querySelector("#edit-container-panel-site-input").value = ""; + + // Show new assignments + const assignments = await Logic.getAssignmentObjectByContainer(userContextId); + this.showAssignedContainers(assignments); + } + }, + + normalizeUrl(url){ + /* + * Important: the rules that automatically open a site in a container only + * look at the URL up to but excluding the / after the domainname. + * + * Furthermore, containers are only useful for http & https URLs because + * those are the only protocols that transmit cookies to maintain + * sessions. + */ + + // Preface with "https://" if no protocol present + const startsWithProtocol = /^\w+:\/\/|^mailto:/; // all protocols are followed by :// (except mailto) + if (!url.match(startsWithProtocol)) { + url = "https://" + url; + } + + /* + * Dual-purpose match: (1) check that url start with http or https proto, + * and (2) exclude everything from the / after the domain (any path, any + * query string, and any anchor) + */ + const basePart = /^(https?:\/\/)([^/?#]*)/; + const r = url.match(basePart); + if (!r) return null; + const urlProto = r[1]; // includes :// if any + const urlConnection = r[2]; // [user[:passwd]@]domain[:port] + + // Extract domain from [user[:passwd]@]domain[:port] + const domainPart = /^(?:.*@)?([^:]+)/; + const d = urlConnection.match(domainPart); + if (!d) return null; + const urlDomain = d[1]; + + // Check that the domain is valid (RFC-1034) + const validDomain = /^(?:\w(?:[\w-]*\w)?)(?:\.\w(?:[\w-]*\w)?)+$/; + if (!urlDomain.match(validDomain)) return null; + + return urlProto+urlDomain; + }, + + showAssignedContainers(assignments) { + const assignmentPanel = document.getElementById("edit-sites-assigned"); + const assignmentKeys = Object.keys(assignments); + assignmentPanel.hidden = !(assignmentKeys.length > 0); + if (assignments) { + const tableElement = assignmentPanel.querySelector(".assigned-sites-list"); + /* Remove previous assignment list, + after removing one we rerender the list */ + while (tableElement.firstChild) { + tableElement.firstChild.remove(); + } + + assignmentKeys.forEach((siteKey) => { + const site = assignments[siteKey]; + const trElement = document.createElement("div"); + /* As we don't have the full or correct path the best we can assume is the path is HTTPS and then replace with a broken icon later if it doesn't load. + This is pending a better solution for favicons from web extensions */ + const assumedUrl = `https://${site.hostname}/favicon.ico`; + trElement.innerHTML = Utils.escaped` +
+
+ ${site.hostname} +
+ `; + trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl)); + const deleteButton = trElement.querySelector(".delete-assignment"); + const that = this; + Utils.addEnterHandler(deleteButton, async () => { + const userContextId = Logic.currentUserContextId(); + // Lets show the message to the current tab + // TODO remove then when firefox supports arrow fn async + const currentTab = await Logic.currentTab(); + Utils.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true); + delete assignments[siteKey]; + that.showAssignedContainers(assignments); + }); + trElement.classList.add("container-info-tab-row", "clickable"); + tableElement.appendChild(trElement); + }); + } + }, + initializeRadioButtons() { const colorRadioTemplate = (containerColor) => { return Utils.escaped` @@ -1910,7 +2023,10 @@ Logic.registerPanel(P_CONTAINER_EDIT, { Utils.addEnterHandler(document.querySelector("#manage-assigned-sites-list"), () => { Logic.showPanel(P_CONTAINER_ASSIGNMENTS, this.getEditInProgressIdentity(), false, false); }); - + // Only show ability to add site if it's an existing container + document.querySelector("#edit-container-panel-add-site").hidden = !userContextId; + // Make site input empty + document.querySelector("#edit-container-panel-site-input").value = ""; document.querySelector("#edit-container-panel-name-input").value = identity.name || ""; document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID; const containerName = document.querySelector("#edit-container-panel-name-input"); diff --git a/src/js/utils.js b/src/js/utils.js index f1932acd5..c65a84c4d 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -59,29 +59,29 @@ const Utils = { }, /** - * Escapes any occurances of &, ", <, > or / with XML entities. - * - * @param {string} str - * The string to escape. - * @return {string} The escaped string. - */ + * Escapes any occurances of &, ", <, > or / with XML entities. + * + * @param {string} str + * The string to escape. + * @return {string} The escaped string. + */ escapeXML(str) { const replacements = { "&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/" }; return String(str).replace(/[&"'<>/]/g, m => replacements[m]); }, /** - * A tagged template function which escapes any XML metacharacters in - * interpolated values. - * - * @param {Array} strings - * An array of literal strings extracted from the templates. - * @param {Array} values - * An array of interpolated values extracted from the template. - * @returns {string} - * The result of the escaped values interpolated with the literal - * strings. - */ + * A tagged template function which escapes any XML metacharacters in + * interpolated values. + * + * @param {Array} strings + * An array of literal strings extracted from the templates. + * @param {Array} values + * An array of interpolated values extracted from the template. + * @returns {string} + * The result of the escaped values interpolated with the literal + * strings. + */ escaped(strings, ...values) { const result = []; diff --git a/src/manifest.json b/src/manifest.json index 5dd8cf26f..b05f6cc3f 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -131,7 +131,9 @@ "default_title": "__MSG_alwaysOpenSiteInContainer__", "default_popup": "pageActionPopup.html", "pinned": false, - "show_matches": ["*://*/*"] + "show_matches": [ + "*://*/*" + ] }, "background": { "page": "js/background/index.html" diff --git a/src/popup.html b/src/popup.html index fac21fe2a..78d322860 100644 --- a/src/popup.html +++ b/src/popup.html @@ -309,12 +309,17 @@

-
+
+
diff --git a/test/issues/1670.test.js b/test/issues/1670.test.js new file mode 100644 index 000000000..90753376e --- /dev/null +++ b/test/issues/1670.test.js @@ -0,0 +1,137 @@ +const {initializeWithTab} = require("../common"); + +console.log("TRACE START"); +describe("#1670", function () { + console.log("TRACE 0"); + beforeEach(async function () { + console.log("TRACE 1.0"); + this.webExt = await initializeWithTab(); + console.log("TRACE 1.1"); + }); + console.log("TRACE 2"); + + afterEach(function () { + console.log("TRACE 3.0"); + this.webExt.destroy(); + console.log("TRACE 3.1"); + }); + console.log("TRACE 4"); + + describe("creating a new container", function () { + console.log("TRACE 5.0"); + beforeEach(async function () { + console.log("TRACE 5.1.0"); + await this.webExt.popup.helper.clickElementById("manage-containers-link"); + console.log("TRACE 5.1.1"); + await this.webExt.popup.helper.clickElementById("new-container"); + console.log("TRACE 5.1.2"); + await this.webExt.popup.helper.clickElementById("create-container-ok-link"); + console.log("TRACE 5.1.3"); + }); + console.log("TRACE 5.2"); + + it("should create it in the browser as well", function () { + console.log("TRACE 5.3.0"); + this.webExt.background.browser.contextualIdentities.create.should.have.been.calledOnce; + }); + console.log("TRACE 5.4"); + + describe("manually assign a valid URL to a container", function () { + console.log("TRACE 5.5.0"); + const exampleUrl = "https://github.com/mozilla/multi-account-containers"; + beforeEach(async function () { + console.log("TRACE 5.5.1.0"); + await this.webExt.popup.helper.clickElementById("manage-containers-link"); + console.log("TRACE 5.5.1.1"); + await this.webExt.popup.helper.clickElementByQuerySelectorAll(".edit-container-icon", "last"); + console.log("TRACE 5.5.1.2"); + this.webExt.popup.window.document.getElementById("edit-container-panel-site-input").value = exampleUrl; + console.log("TRACE 5.5.1.3"); + await this.webExt.popup.helper.clickElementById("edit-container-site-link"); + console.log("TRACE 5.5.1.4"); + }); + console.log("TRACE 5.5.2"); + + it("should assign the URL to a container", function () { + console.log("TRACE 5.5.3.0"); + this.webExt.background.browser.contextualIdentities.create.should.have.been.calledOnce; + }); + console.log("TRACE 5.5.4"); + }); + console.log("TRACE 5.6"); + + describe("manually assign valid URL without protocol to a container", function () { + console.log("TRACE 5.7.0"); + const exampleUrl = "github.com/mozilla/multi-account-containers"; + beforeEach(async function () { + console.log("TRACE 5.7.1.0"); + await this.webExt.popup.helper.clickElementById("manage-containers-link"); + console.log("TRACE 5.7.1.1"); + await this.webExt.popup.helper.clickElementByQuerySelectorAll(".edit-container-icon", "last"); + console.log("TRACE 5.7.1.2"); + this.webExt.popup.window.document.getElementById("edit-container-panel-site-input").value = exampleUrl; + console.log("TRACE 5.7.1.3"); + await this.webExt.popup.helper.clickElementById("edit-container-site-link"); + console.log("TRACE 5.7.1.4"); + }); + console.log("TRACE 5.7.2"); + + it("should assign the URL without protocol to a container", function () { + console.log("TRACE 5.7.3.0"); + this.webExt.background.browser.contextualIdentities.create.should.have.been.calledOnce; + }); + console.log("TRACE 5.7.4"); + }); + console.log("TRACE 5.8"); + + describe("manually assign an invalid URL to a container", function () { + console.log("TRACE 5.9.0"); + const exampleUrl = "github"; + beforeEach(async function () { + console.log("TRACE 5.9.1.0"); + await this.webExt.popup.helper.clickElementById("manage-containers-link"); + console.log("TRACE 5.9.1.1"); + await this.webExt.popup.helper.clickElementByQuerySelectorAll(".edit-container-icon", "last"); + console.log("TRACE 5.9.1.2"); + this.webExt.popup.window.document.getElementById("edit-container-panel-site-input").value = exampleUrl; + console.log("TRACE 5.9.1.3"); + await this.webExt.popup.helper.clickElementById("edit-container-site-link"); + console.log("TRACE 5.9.1.4"); + }); + console.log("TRACE 5.9.2"); + + it("should not assign the URL to a container", function () { + console.log("TRACE 5.9.3.0"); + this.webExt.background.browser.contextualIdentities.update.should.not.have.been.called; + }); + console.log("TRACE 5.9.4"); + }); + console.log("TRACE 5.10"); + + describe("manually assign empty URL to a container", function () { + console.log("TRACE 5.11.0"); + const exampleUrl = ""; + beforeEach(async function () { + console.log("TRACE 5.11.1.0"); + await this.webExt.popup.helper.clickElementById("manage-containers-link"); + console.log("TRACE 5.11.1.1"); + await this.webExt.popup.helper.clickElementByQuerySelectorAll(".edit-container-icon", "last"); + console.log("TRACE 5.11.1.2"); + this.webExt.popup.window.document.getElementById("edit-container-panel-site-input").value = exampleUrl; + console.log("TRACE 5.11.1.3"); + await this.webExt.popup.helper.clickElementById("edit-container-site-link"); + console.log("TRACE 5.11.1.4"); + }); + console.log("TRACE 5.11.2"); + + it("should not assign the URL to a container", function () { + console.log("TRACE 5.11.3.0"); + this.webExt.background.browser.contextualIdentities.update.should.not.have.been.called; + }); + console.log("TRACE 5.11.4"); + }); + console.log("TRACE 5.12"); + }); + console.log("TRACE 6"); +}); +console.log("TRACE END");