diff --git a/src/_locales b/src/_locales index d3301069..bdaa0129 160000 --- a/src/_locales +++ b/src/_locales @@ -1 +1 @@ -Subproject commit d3301069f51262e8cf493a86aa2785cf3261141e +Subproject commit bdaa01291b7367a5e815470fd263ea36c862fe32 diff --git a/src/css/popup.css b/src/css/popup.css index 5980894c..7786f4ba 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -228,6 +228,7 @@ body { /* Hack for menu icons to use a light color without affecting container icons */ [data-theme="light"] img.delete-assignment, +[data-theme="dark"] img.reset-assignment, [data-theme="dark"] .trash-button, [data-theme="dark"] img.menu-icon, [data-theme="dark"] .menu-icon > img, @@ -236,6 +237,7 @@ body { filter: invert(1); } +[data-theme="dark"] img.clear-storage-icon, [data-theme="dark"] img.delete-assignment, [data-theme="dark"] #edit-sites-assigned .menu-icon, [data-theme="dark"] #container-info-table .menu-icon { @@ -285,9 +287,33 @@ table { display: none !important; } +.popup-notification-card { + opacity: 0; + pointer-events: none; + transition: opacity 2s; + border-radius: 4px; + font-size: 12px; + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + padding-block: 8px; + padding-inline: 8px; + margin-block: 8px; + margin-inline: 8px; + inline-size: calc(100vw - 25px); + background-color: var(--button-bg-active-color-secondary); + z-index: 3; +} + +.is-shown { + pointer-events: auto; + opacity: 1; + transition: opacity 0s; +} + /* effect borrowed from tabs in firefox, ensure that the element flexes to the full width */ .truncate-text { - inline-size: calc(100vw - 80px); + inline-size: calc(100vw - 100px); overflow: hidden; position: relative; white-space: nowrap; @@ -1505,8 +1531,9 @@ input[type=text] { min-block-size: 500px; } -.delete-container-panel { - min-block-size: 300px; +.delete-container-panel, +.clear-container-storage-panel { + min-block-size: 500px; } .panel.onboarding, @@ -1794,12 +1821,14 @@ manage things like container crud */ margin-inline-end: 0; } -.delete-container-confirm { +.delete-container-confirm, +.clear-container-storage-confirm { padding-inline-end: 20px; padding-inline-start: 20px; } -.delete-container-confirm-title { +.delete-container-confirm-title, +.clear-container-storage-confirm-title { color: var(--text-color-primary); font-size: var(--font-size-heading); } @@ -2173,6 +2202,11 @@ hr { text-align: center; } +.confirmation-destructive-ok-btn { + background-color: var(--button-destructive-bg-color); + color: var(--button-destructive-text-color); +} + .delete-btn { background-color: var(--button-destructive-bg-color); block-size: 32px; @@ -2303,7 +2337,8 @@ input { font-weight: bolder; } -.delete-warning { +.delete-warning, +.clear-container-storage-warning { padding-block-end: 8px; padding-block-start: 8px; padding-inline-end: 0; @@ -2314,7 +2349,8 @@ input { * rules grouped together at the beginning of the file */ /* stylelint-disable no-descending-specificity */ -.trash-button { +.trash-button, +.reset-button { display: inline-block; block-size: 20px; inline-size: 20px; @@ -2323,11 +2359,21 @@ input { text-align: center; } -tr > td > .trash-button { +.reset-button { + margin-inline-end: 12px; +} + +.tooltip-wrapper:hover .site-settings-tooltip { + display: block; +} + +tr > td > .trash-button, +tr > td > .reset-button { display: none; } -tr:hover > td > .trash-button { +tr:hover > td > .trash-button, +tr:hover > td > .reset-button { display: block; } diff --git a/src/js/background/assignManager.js b/src/js/background/assignManager.js index 7dbee723..617d5cab 100644 --- a/src/js/background/assignManager.js +++ b/src/js/background/assignManager.js @@ -577,6 +577,16 @@ window.assignManager = { return true; }, + async _resetCookiesForSite(hostname, cookieStoreId) { + const hostNameTruncated = hostname.replace(/^www\./, ""); // Remove "www." from the hostname + await browser.browsingData.removeCookies({ + cookieStoreId: cookieStoreId, + hostnames: [hostNameTruncated] // This does not remove cookies from associated domains. To remove all cookies, we have a container storage removal option. + }); + + return true; + }, + async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) { let actionName; // https://github.com/mozilla/testpilot-containers/issues/626 diff --git a/src/js/background/backgroundLogic.js b/src/js/background/backgroundLogic.js index 5d9f5765..54bdd0e5 100644 --- a/src/js/background/backgroundLogic.js +++ b/src/js/background/backgroundLogic.js @@ -75,6 +75,19 @@ const backgroundLogic = { return extensionInfo; }, + // Remove container data (cookies, localStorage and cache) + async deleteContainerDataOnly(userContextId) { + await browser.browsingData.removeCookies({ + cookieStoreId: this.cookieStoreId(userContextId) + }); + + await browser.browsingData.removeLocalStorage({ + cookieStoreId: this.cookieStoreId(userContextId) + }); + + return {done: true, userContextId}; + }, + getUserContextIdFromCookieStoreId(cookieStoreId) { if (!cookieStoreId) { return false; diff --git a/src/js/background/messageHandler.js b/src/js/background/messageHandler.js index 5d644b60..c7bce828 100644 --- a/src/js/background/messageHandler.js +++ b/src/js/background/messageHandler.js @@ -23,6 +23,9 @@ const messageHandler = { case "deleteContainer": response = backgroundLogic.deleteContainer(m.message.userContextId); break; + case "deleteContainerDataOnly": + response = backgroundLogic.deleteContainerDataOnly(m.message.userContextId); + break; case "createOrUpdateContainer": response = backgroundLogic.createOrUpdateContainer(m.message); break; @@ -45,6 +48,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 "resetCookiesForSite": + response = assignManager._resetCookiesForSite(m.pageUrl, m.cookieStoreId); + break; case "sortTabs": backgroundLogic.sortTabs(); break; diff --git a/src/js/popup.js b/src/js/popup.js index 72792301..61a3fdaf 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -32,6 +32,7 @@ const P_CONTAINER_EDIT = "containerEdit"; const P_CONTAINER_DELETE = "containerDelete"; const P_CONTAINERS_ACHIEVEMENT = "containersAchievement"; const P_CONTAINER_ASSIGNMENTS = "containerAssignments"; +const P_CLEAR_CONTAINER_STORAGE = "clearContainerStorage"; const P_MOZILLA_VPN_SERVER_LIST = "moz-vpn-server-list"; const P_ADVANCED_PROXY_SETTINGS = "advanced-proxy-settings-panel"; @@ -122,6 +123,19 @@ const Logic = { }, + notify(i18nOpts) { + const notificationCards = document.querySelectorAll(".popup-notification-card"); + const text = browser.i18n.getMessage(i18nOpts.messageId, i18nOpts.placeholders); + notificationCards.forEach(notificationCard => { + notificationCard.textContent = text; + notificationCard.classList.add("is-shown"); + + setTimeout(() => { + notificationCard.classList.remove("is-shown"); + }, 2000); + }); + }, + async showAchievementOrContainersListPanel() { // Do we need to show an achievement panel? let showAchievements = false; @@ -971,6 +985,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { Utils.alwaysOpenInContainer(identity); window.close(); }); + // Show or not the has-tabs section. for (let trHasTabs of document.getElementsByClassName("container-info-has-tabs")) { // eslint-disable-line prefer-const trHasTabs.style.display = !identity.hasHiddenTabs && !identity.hasOpenTabs ? "none" : ""; @@ -994,6 +1009,13 @@ Logic.registerPanel(P_CONTAINER_INFO, { Utils.addEnterHandler(manageContainer, async () => { Logic.showPanel(P_CONTAINER_EDIT, identity); }); + const clearContainerStorageButton = document.getElementById("clear-container-storage-info"); + Utils.addEnterHandler(clearContainerStorageButton, async () => { + const granted = await browser.permissions.request({ permissions: ["browsingData"] }); + if (granted) { + Logic.showPanel(P_CLEAR_CONTAINER_STORAGE, identity); + } + }); return this.buildOpenTabTable(tabs); }, @@ -1455,11 +1477,14 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, { /* 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`; + const resetSiteCookiesInfo = browser.i18n.getMessage("clearSiteCookiesTooltipInfo"); + const deleteSiteInfo = browser.i18n.getMessage("deleteSiteTooltipInfo"); trElement.innerHTML = Utils.escaped`
${site.hostname} - + + `; trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl)); const deleteButton = trElement.querySelector(".trash-button"); @@ -1471,6 +1496,20 @@ Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, { delete assignments[siteKey]; this.showAssignedContainers(assignments); }); + const resetButton = trElement.querySelector(".reset-button"); + Utils.addEnterHandler(resetButton, async () => { + const cookieStoreId = Logic.currentCookieStoreId(); + const granted = await browser.permissions.request({ permissions: ["browsingData"] }); + if (!granted) { + return; + } + const result = await Utils.resetCookiesForSite(site.hostname, cookieStoreId); + if (result === true) { + Logic.notify({messageId: "cookiesClearedSuccess", placeholders: [site.hostname]}); + } else { + Logic.notify({messageId: "cookiesCouldNotBeCleared", placeholders: [site.hostname]}); + } + }); trElement.classList.add("menu-item", "hover-highlight", "keyboard-nav"); tableElement.appendChild(trElement); }); @@ -2246,6 +2285,47 @@ Logic.registerPanel(P_MOZILLA_VPN_SERVER_LIST, { } }); +// P_CLEAR_CONTAINER_STORAGE: Page for confirming container storage removal. +// ---------------------------------------------------------------------------- + +Logic.registerPanel(P_CLEAR_CONTAINER_STORAGE, { + panelSelector: "#clear-container-storage-panel", + + // This method is called when the object is registered. + initialize() { + + Utils.addEnterHandler(document.querySelector("#clear-container-storage-cancel-link"), () => { + const identity = Logic.currentIdentity(); + Logic.showPanel(P_CONTAINER_INFO, identity, false, false); + }); + Utils.addEnterHandler(document.querySelector("#close-clear-container-storage-panel"), () => { + const identity = Logic.currentIdentity(); + Logic.showPanel(P_CONTAINER_INFO, identity, false, false); + }); + Utils.addEnterHandler(document.querySelector("#clear-container-storage-ok-link"), async () => { + const identity = Logic.currentIdentity(); + const userContextId = Utils.userContextId(identity.cookieStoreId); + const result = await browser.runtime.sendMessage({ + method: "deleteContainerDataOnly", + message: { userContextId } + }); + if (result.done === true) { + Logic.notify({messageId: "storageWasClearedConfirmation", placeholders: [identity.name]}); + } + Logic.showPanel(P_CONTAINER_INFO, identity, false, false); + }); + }, + + // This method is called when the panel is shown. + prepare() { + const identity = Logic.currentIdentity(); + + // Populating the panel: name, icon, and warning message + document.getElementById("container-clear-storage-title").textContent = identity.name; + return Promise.resolve(null); + }, +}); + // P_CONTAINER_DELETE: Delete a container. // ---------------------------------------------------------------------------- diff --git a/src/js/utils.js b/src/js/utils.js index f1932acd..5745a47f 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -138,6 +138,14 @@ const Utils = { }); }, + resetCookiesForSite(pageUrl, cookieStoreId) { + return browser.runtime.sendMessage({ + method: "resetCookiesForSite", + pageUrl, + cookieStoreId, + }); + }, + async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) { return await browser.runtime.sendMessage({ method: "reloadInContainer", diff --git a/src/manifest.json b/src/manifest.json index 21d3d0be..26f32018 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -26,6 +26,7 @@ ], "optional_permissions": [ "bookmarks", + "browsingData", "nativeMessaging", "proxy" ], diff --git a/src/popup.html b/src/popup.html index fac21fe2..ae460807 100644 --- a/src/popup.html +++ b/src/popup.html @@ -107,6 +107,7 @@