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");
}