diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 748be0ba6185..3b8f6f7f0daf 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -2879,3 +2879,9 @@ pref("cookiebanners.ui.desktop.cfrVariant", 0); #ifdef NIGHTLY_BUILD pref("dom.security.credentialmanagement.identity.enabled", true); #endif + +// Reset Private Browsing Session feature +pref("browser.privatebrowsing.resetPBM.enabled", false); +// Whether the reset private browsing panel should ask for confirmation before +// performing the clear action. +pref("browser.privatebrowsing.resetPBM.showConfirmationDialog", true); diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml index 1278fa35d2a3..1dedc098503d 100644 --- a/browser/base/content/appmenu-viewcache.inc.xhtml +++ b/browser/base/content/appmenu-viewcache.inc.xhtml @@ -687,4 +687,27 @@ onclick="ToolbarPanelHub.toggleWhatsNewPref(event)" data-l10n-id="whatsnew-panel-footer-checkbox"/> + + + + + + + + + + + + + + + + diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index a8d194121ffd..4a89fd24072d 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -63,6 +63,7 @@ ChromeUtils.defineESModuleGetters(this, { PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs", + ResetPBMPanel: "resource:///modules/ResetPBMPanel.sys.mjs", SafeBrowsing: "resource://gre/modules/SafeBrowsing.sys.mjs", Sanitizer: "resource:///modules/Sanitizer.sys.mjs", SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs", @@ -9739,6 +9740,7 @@ var ConfirmationHint = { * An object with the following optional properties: * - event (DOM event): The event that triggered the feedback * - descriptionId (string): message ID of the description text + * - position (string): position of the panel relative to the anchor. * */ show(anchor, messageId, options = {}) { @@ -9784,7 +9786,7 @@ var ConfirmationHint = { ); this._panel.openPopup(anchor, { - position: "bottomcenter topleft", + position: options.position ?? "bottomcenter topleft", triggerEvent: options.event, }); }, diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs index 62168440cd24..ad297510d310 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -69,6 +69,7 @@ ChromeUtils.defineESModuleGetters(lazy, { RemoteSecuritySettings: "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + ResetPBMPanel: "resource:///modules/ResetPBMPanel.sys.mjs", SafeBrowsing: "resource://gre/modules/SafeBrowsing.sys.mjs", Sanitizer: "resource:///modules/Sanitizer.sys.mjs", SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs", @@ -1394,6 +1395,8 @@ BrowserGlue.prototype = { lazy.SaveToPocket.init(); + lazy.ResetPBMPanel.init(); + AboutHomeStartupCache.init(); Services.obs.notifyObservers(null, "browser-ui-startup-complete"); diff --git a/browser/components/customizableui/CustomizableUI.sys.mjs b/browser/components/customizableui/CustomizableUI.sys.mjs index d4e18649bc4f..d748b93a9245 100644 --- a/browser/components/customizableui/CustomizableUI.sys.mjs +++ b/browser/components/customizableui/CustomizableUI.sys.mjs @@ -57,7 +57,7 @@ const kSubviewEvents = ["ViewShowing", "ViewHiding"]; * The current version. We can use this to auto-add new default widgets as necessary. * (would be const but isn't because of testing purposes) */ -var kVersion = 19; +var kVersion = 20; /** * Buttons removed from built-ins by version they were removed. kVersion must be @@ -185,6 +185,13 @@ XPCOMUtils.defineLazyPreferenceGetter( } ); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "resetPBMToolbarButtonEnabled", + "browser.privatebrowsing.resetPBM.enabled", + false +); + ChromeUtils.defineLazyGetter(lazy, "log", () => { let { ConsoleAPI } = ChromeUtils.importESModule( "resource://gre/modules/Console.sys.mjs" @@ -249,6 +256,7 @@ var CustomizableUIInternal = { "downloads-button", AppConstants.MOZ_DEV_EDITION ? "developer-button" : null, "fxa-toolbar-menu-button", + lazy.resetPBMToolbarButtonEnabled ? "reset-pbm-toolbar-button" : null, ].filter(name => name); this.registerArea( @@ -658,6 +666,18 @@ var CustomizableUIInternal = { ...addonsPlacements, ]; } + + // Add the PBM reset button as the right most button item + if (currentVersion < 20) { + let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; + // Place the button as the first item to the left of the hamburger menu + if ( + navbarPlacements && + !navbarPlacements.includes("reset-pbm-toolbar-button") + ) { + navbarPlacements.push("reset-pbm-toolbar-button"); + } + } }, _updateForNewProtonVersion() { diff --git a/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs b/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs new file mode 100644 index 000000000000..31714c6374f1 --- /dev/null +++ b/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs @@ -0,0 +1,216 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-env mozilla/browser-window */ + +/** + * ResetPBMPanel contains the logic for the restart private browsing action. + * The feature is exposed via a toolbar button in private browsing windows. It + * allows users to restart their private browsing session, clearing all site + * data and closing all PBM tabs / windows. + * The toolbar button for triggering the panel is only shown in private browsing + * windows or if permanent private browsing mode is enabled. + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const ENABLED_PREF = "browser.privatebrowsing.resetPBM.enabled"; +const SHOW_CONFIRM_DIALOG_PREF = + "browser.privatebrowsing.resetPBM.showConfirmationDialog"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +export const ResetPBMPanel = { + // Button and view config for CustomizableUI. + _widgetConfig: null, + + /** + * Initialize the widget code depending on pref state. + */ + init() { + // Populate _widgetConfig during init to defer (lazy) CustomizableUI import. + this._widgetConfig ??= { + id: "reset-pbm-toolbar-button", + l10nId: "reset-pbm-toolbar-button", + type: "view", + viewId: "reset-pbm-panel", + defaultArea: lazy.CustomizableUI.AREA_NAVBAR, + onViewShowing(aEvent) { + ResetPBMPanel.onViewShowing(aEvent); + }, + }; + + if (this._enabled) { + lazy.CustomizableUI.createWidget(this._widgetConfig); + } else { + lazy.CustomizableUI.destroyWidget(this._widgetConfig.id); + } + }, + + /** + * Called when the reset pbm panelview is showing as the result of clicking + * the toolbar button. + */ + async onViewShowing(event) { + let triggeringWindow = event.target.ownerGlobal; + + // We may skip the confirmation panel if disabled via pref. + if (!this._shouldConfirmClear) { + // Prevent the panel from showing up. + event.preventDefault(); + + // If the action is triggered from the overflow menu make sure that the + // panel gets hidden. + lazy.CustomizableUI.hidePanelForNode(event.target); + + // Trigger the restart action. + await this._restartPBM(triggeringWindow); + return; + } + + // Before the panel is shown, update checkbox state based on pref. + this._rememberCheck(triggeringWindow).checked = this._shouldConfirmClear; + }, + + /** + * Handles the confirmation panel cancel button. + * @param {MozButton} button - Cancel button that triggered the action. + */ + onCancel(button) { + if (!this._enabled) { + throw new Error("Not initialized."); + } + lazy.CustomizableUI.hidePanelForNode(button); + }, + + /** + * Handles the confirmation panel confirm button which triggers the clear + * action. + * @param {MozButton} button - Confirm button that triggered the action. + */ + async onConfirm(button) { + if (!this._enabled) { + throw new Error("Not initialized."); + } + let triggeringWindow = button.ownerGlobal; + + // Write the checkbox state to pref. Only do this when the user + // confirms. + // Setting this pref to true means there is no way to see the panel + // again other than flipping the pref back via about:config or resetting + // the profile. This is by design. + Services.prefs.setBoolPref( + SHOW_CONFIRM_DIALOG_PREF, + this._rememberCheck(triggeringWindow).checked + ); + + lazy.CustomizableUI.hidePanelForNode(button); + + // Clear the private browsing session. + await this._restartPBM(triggeringWindow); + }, + + /** + * Restart the private browsing session. This is achieved by closing all other + * PBM windows, closing all tabs in the current window but + * about:privatebrowsing and triggering PBM data clearing. + * + * @param {ChromeWindow} triggeringWindow - The (private browsing) chrome window which + * triggered the restart action. + */ + async _restartPBM(triggeringWindow) { + if ( + !triggeringWindow || + !lazy.PrivateBrowsingUtils.isWindowPrivate(triggeringWindow) + ) { + throw new Error("Invalid triggering window."); + } + + // 1. Close all PBM windows but the current one. + for (let w of Services.ww.getWindowEnumerator()) { + if ( + w != triggeringWindow && + lazy.PrivateBrowsingUtils.isWindowPrivate(w) + ) { + // This suppresses confirmation dialogs like the beforeunload + // handler and the tab close warning. + // Skip over windows that don't have the closeWindow method. + w.closeWindow?.(true, null, "restart-pbm"); + } + } + + // 2. For the current PBM window create a new tab which will be used for + // the initial newtab page. + let newTab = triggeringWindow.gBrowser.addTab( + triggeringWindow.BROWSER_NEW_TAB_URL, + { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + if (!newTab) { + throw new Error("Could not open new tab."); + } + + // 3. Close all other tabs. + triggeringWindow.gBrowser.removeAllTabsBut(newTab, { + skipPermitUnload: true, + animate: false, + }); + + // 4. Clear private browsing data. + // TODO: this doesn't wait for data to be cleared. This is probably + // fine since PBM data is stored in memory and can be cleared quick + // enough. The mechanism is brittle though, some callers still + // perform clearing async. Bug 1846494 will address this. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + // Once clearing is complete show a toast message. + + let toolbarButton = this._toolbarButton(triggeringWindow); + + // Find the anchor used for the confirmation hint panel. If the toolbar + // button is in the overflow menu we can't use it as an anchor. Instead we + // anchor off the overflow button as indicated by cui-anchorid. + let anchor; + let anchorID = toolbarButton.getAttribute("cui-anchorid"); + if (anchorID) { + anchor = triggeringWindow.document.getElementById(anchorID); + } + triggeringWindow.ConfirmationHint.show( + anchor ?? toolbarButton, + "reset-pbm-panel-complete", + { position: "bottomright topright" } + ); + }, + + _toolbarButton(win) { + return lazy.CustomizableUI.getWidget(this._widgetConfig.id).forWindow(win) + .node; + }, + + _rememberCheck(win) { + return win.document.getElementById("reset-pbm-panel-checkbox"); + }, +}; + +XPCOMUtils.defineLazyPreferenceGetter( + ResetPBMPanel, + "_enabled", + ENABLED_PREF, + false, + // On pref change update the init state. + ResetPBMPanel.init.bind(ResetPBMPanel) +); +XPCOMUtils.defineLazyPreferenceGetter( + ResetPBMPanel, + "_shouldConfirmClear", + SHOW_CONFIRM_DIALOG_PREF, + true +); diff --git a/browser/components/privatebrowsing/jar.mn b/browser/components/privatebrowsing/jar.mn index 6fef71a2acea..f7f78e615f9e 100644 --- a/browser/components/privatebrowsing/jar.mn +++ b/browser/components/privatebrowsing/jar.mn @@ -4,6 +4,6 @@ browser.jar: content/browser/aboutPrivateBrowsing.css (content/aboutPrivateBrowsing.css) - content/browser/aboutPrivateBrowsing.html (content/aboutPrivateBrowsing.html) + content/browser/aboutPrivateBrowsing.html (content/aboutPrivateBrowsing.html) content/browser/aboutPrivateBrowsing.js (content/aboutPrivateBrowsing.js) content/browser/assets/ (content/assets/*) diff --git a/browser/components/privatebrowsing/moz.build b/browser/components/privatebrowsing/moz.build index d5e806f2bbcb..822b875e2add 100644 --- a/browser/components/privatebrowsing/moz.build +++ b/browser/components/privatebrowsing/moz.build @@ -10,5 +10,9 @@ BROWSER_CHROME_MANIFESTS += [ JAR_MANIFESTS += ["jar.mn"] +EXTRA_JS_MODULES += [ + "ResetPBMPanel.sys.mjs", +] + with Files("**"): BUG_COMPONENT = ("Firefox", "Private Browsing") diff --git a/browser/locales/en-US/browser/browser.ftl b/browser/locales/en-US/browser/browser.ftl index c60feb84dedc..95372a4b4260 100644 --- a/browser/locales/en-US/browser/browser.ftl +++ b/browser/locales/en-US/browser/browser.ftl @@ -974,6 +974,24 @@ unified-extensions-button-quarantined = Extensions Some extensions are not allowed +## Private browsing reset button + +reset-pbm-toolbar-button = + .label = End Private Session + .tooltiptext = End Private Session +reset-pbm-panel-heading = End your private session? +reset-pbm-panel-description = Close all private tabs and delete history, cookies, and all other site data. +reset-pbm-panel-always-ask-checkbox = + .label = Always ask me + .accesskey = A +reset-pbm-panel-cancel-button = + .label = Cancel + .accesskey = C +reset-pbm-panel-confirm-button = + .label = Delete session data + .accesskey = D +reset-pbm-panel-complete = Private session data deleted + ## Autorefresh blocker refresh-blocked-refresh-label = { -brand-short-name } prevented this page from automatically reloading. diff --git a/browser/themes/shared/customizableui/panelUI-shared.css b/browser/themes/shared/customizableui/panelUI-shared.css index 0bd11cb9489f..204957b09a06 100644 --- a/browser/themes/shared/customizableui/panelUI-shared.css +++ b/browser/themes/shared/customizableui/panelUI-shared.css @@ -1972,3 +1972,34 @@ panelview:not([mainview]) #PanelUI-whatsNew-title { .webextension-popup-stack { position: relative; } + +/* Reset/Restart Private Browsing Panel */ + +#reset-pbm-panel { + max-width: var(--menu-panel-width-wide); +} + +#reset-pbm-panel-container { + padding: 16px 16px 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +#reset-pbm-panel-header > description { + margin-bottom: 0; +} + +#reset-pbm-panel-header > description, +#reset-pbm-panel-description, +#reset-pbm-panel-footer { + margin-inline: 0; +} + +#reset-pbm-panel-header > description { + font-weight: var(--font-weight-bold); +} + +#reset-pbm-panel-checkbox { + margin-inline: 0 8px; +} diff --git a/browser/themes/shared/icons/flame.svg b/browser/themes/shared/icons/flame.svg new file mode 100644 index 000000000000..8d6fb7782d48 --- /dev/null +++ b/browser/themes/shared/icons/flame.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn index 8c9d80896c81..c9da489d9e51 100644 --- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -165,6 +165,7 @@ skin/classic/browser/edit-paste.svg (../shared/icons/edit-paste.svg) skin/classic/browser/fingerprint.svg (../shared/icons/fingerprint.svg) skin/classic/browser/firefox-view.svg (../shared/icons/firefox-view.svg) + skin/classic/browser/flame.svg (../shared/icons/flame.svg) skin/classic/browser/forget.svg (../shared/icons/forget.svg) skin/classic/browser/forward.svg (../shared/icons/forward.svg) skin/classic/browser/fullscreen.svg (../shared/icons/fullscreen.svg) diff --git a/browser/themes/shared/toolbarbutton-icons.css b/browser/themes/shared/toolbarbutton-icons.css index 170b790300e9..c629688547db 100644 --- a/browser/themes/shared/toolbarbutton-icons.css +++ b/browser/themes/shared/toolbarbutton-icons.css @@ -368,6 +368,14 @@ list-style-image: url("chrome://mozapps/skin/extensions/extension.svg"); } +#reset-pbm-toolbar-button { + list-style-image: url("chrome://browser/skin/flame.svg"); + + :root:not([privatebrowsingmode]) & { + display: none; + } +} + #email-link-button { list-style-image: url("chrome://browser/skin/mail.svg"); }