diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml index 7018c65b2b8da5a5600550212e1ac8464cc0414c..35e3a981d0a3af554f20719d49440c0db3981055 100644 --- a/browser/base/content/appmenu-viewcache.inc.xhtml +++ b/browser/base/content/appmenu-viewcache.inc.xhtml @@ -48,6 +48,11 @@ key="key_privatebrowsing" command="Tools:PrivateBrowsing"/> <toolbarseparator/> + <toolbarbutton id="appMenu-new-identity" + class="subviewbutton" + data-l10n-id="appmenuitem-new-identity" + key="new-identity-key"/> + <toolbarseparator/> <toolbarbutton id="appMenu-bookmarks-button" class="subviewbutton subviewbutton-nav" data-l10n-id="library-bookmarks-menu" diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js index 2c7d8f544ca9f241af88468f7f105211878cf5ea..1799bc25ccf72d04cc8e397e87d272b179911554 100644 --- a/browser/base/content/browser-init.js +++ b/browser/base/content/browser-init.js @@ -222,6 +222,9 @@ var gBrowserInit = { // Init the SecurityLevelButton SecurityLevelButton.init(); + // Init the NewIdentityButton + NewIdentityButton.init(); + // Certain kinds of automigration rely on this notification to complete // their tasks BEFORE the browser window is shown. SessionStore uses it to // restore tabs into windows AFTER important parts like gMultiProcessBrowser @@ -1033,6 +1036,8 @@ var gBrowserInit = { SecurityLevelButton.uninit(); + NewIdentityButton.uninit(); + if (gToolbarKeyNavEnabled) { ToolbarKeyboardNavigator.uninit(); } diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc index 17b7ceb1c663edd283b892cd9d8319313b5b7cf2..0d3a07ed954ad78f21c09fe4bbadf484f21aab01 100644 --- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -28,6 +28,10 @@ <menuitem id="menu_newPrivateWindow" command="Tools:PrivateBrowsing" key="key_privatebrowsing" data-l10n-id="menu-file-new-private-window"/> + <menuseparator/> + <menuitem id="menu_newIdentity" + key="new-identity-key" data-l10n-id="menu-new-identity"/> + <menuseparator/> <menuitem id="menu_openLocation" hidden="true" command="Browser:OpenLocation" diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc index 89d56dad95da1ac1930dddce639cb7ea816605de..c56ab0783b36c4d3cd9f9bbbb3701b1580aef1f5 100644 --- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -391,4 +391,5 @@ modifiers="accel,alt" internal="true"/> #endif + <key id="new-identity-key" modifiers="accel shift" key="U" oncommand="NewIdentityButton.onCommand(event)"/> </keyset> diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index acf2dca7cd04328d6c62a9751635e491b2d2b9bc..2a80c05774c8110f21cfb2709f27f4dfbdc87661 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -251,6 +251,11 @@ XPCOMUtils.defineLazyScriptGetter( ["SecurityLevelButton"], "chrome://browser/content/securitylevel/securityLevel.js" ); +XPCOMUtils.defineLazyScriptGetter( + this, + ["NewIdentityButton"], + "chrome://browser/content/newidentity.js" +); XPCOMUtils.defineLazyScriptGetter( this, "gEditItemOverlay", diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml index 2c17a8a5f329f513f657af77af8f8c3ba5c91610..8cf06697bc6647fcb6ab544213b29e377de00433 100644 --- a/browser/base/content/navigator-toolbox.inc.xhtml +++ b/browser/base/content/navigator-toolbox.inc.xhtml @@ -589,6 +589,9 @@ ondragover="newWindowButtonObserver.onDragOver(event)" ondragenter="newWindowButtonObserver.onDragOver(event)"/> + <toolbarbutton id="new-identity-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + data-l10n-id="toolbar-new-identity"/> + <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional" observes="View:FullScreen" type="checkbox" diff --git a/browser/components/moz.build b/browser/components/moz.build index 4d45a1816730a6d95de904f255df9b929d725968..d380f6eeae95b1247edd5bc9b388fb91ec2427fc 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -43,6 +43,7 @@ DIRS += [ "genai", "messagepreview", "migration", + "newidentity", "newtab", "originattributes", "pagedata", diff --git a/browser/components/newidentity/content/newIdentityDialog.css b/browser/components/newidentity/content/newIdentityDialog.css new file mode 100644 index 0000000000000000000000000000000000000000..850e7c7da625c616e9e71a44aac65391298eb656 --- /dev/null +++ b/browser/components/newidentity/content/newIdentityDialog.css @@ -0,0 +1,7 @@ +/* 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/. */ + +#infoTitle { + font-weight: 600; +} diff --git a/browser/components/newidentity/content/newIdentityDialog.js b/browser/components/newidentity/content/newIdentityDialog.js new file mode 100644 index 0000000000000000000000000000000000000000..113cf236f7de00592491339c894731171cb30273 --- /dev/null +++ b/browser/components/newidentity/content/newIdentityDialog.js @@ -0,0 +1,17 @@ +/* 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/. */ + +document.addEventListener("dialogaccept", () => { + const retvals = window.arguments[0]; + retvals.confirmed = true; + retvals.neverAskAgain = document.getElementById("neverAskAgain").checked; +}); + +document.addEventListener("DOMContentLoaded", () => { + const dialog = document.getElementById("newIdentityDialog"); + + const accept = dialog.getButton("accept"); + document.l10n.setAttributes(accept, "new-identity-dialog-confirm"); + accept.classList.add("danger-button"); +}); diff --git a/browser/components/newidentity/content/newIdentityDialog.xhtml b/browser/components/newidentity/content/newIdentityDialog.xhtml new file mode 100644 index 0000000000000000000000000000000000000000..e6c0b5f83208f2f52b76e5b7e4638336c8552eb0 --- /dev/null +++ b/browser/components/newidentity/content/newIdentityDialog.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!-- based on resetProfile.xhtml --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://global/content/commonDialog.css"?> +<?xml-stylesheet href="chrome://global/skin/commonDialog.css"?> +<?xml-stylesheet href="chrome://browser/content/newIdentityDialog.css"?> + +<window + id="newIdentityDialogWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + aria-labelledby="infoTitle" + aria-describedby="infoBody" +> + <dialog id="newIdentityDialog" buttons="accept,cancel" defaultButton="accept"> + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link rel="localization" href="toolkit/global/base-browser.ftl" /> + </linkset> + + <div xmlns="http://www.w3.org/1999/xhtml"> + <div id="dialogGrid"> + <div class="dialogRow" id="infoRow"> + <div id="iconContainer"> + <xul:image id="infoIcon" /> + </div> + <div id="infoContainer"> + <xul:description + id="infoTitle" + data-l10n-id="new-identity-dialog-title" + /> + <xul:description + id="infoBody" + data-l10n-id="new-identity-dialog-description" + /> + <xul:checkbox + id="neverAskAgain" + data-l10n-id="new-identity-dialog-never-ask-checkbox" + /> + </div> + </div> + </div> + </div> + + <script src="chrome://browser/content/newIdentityDialog.js" /> + </dialog> +</window> diff --git a/browser/components/newidentity/content/newidentity.js b/browser/components/newidentity/content/newidentity.js new file mode 100644 index 0000000000000000000000000000000000000000..0b86ff9915aae3faded79993bb09b7edbce9420e --- /dev/null +++ b/browser/components/newidentity/content/newidentity.js @@ -0,0 +1,573 @@ +"use strict"; + +/* eslint-env mozilla/browser-window */ + +// Use a lazy getter because NewIdentityButton is declared more than once +// otherwise. +XPCOMUtils.defineLazyGetter(this, "NewIdentityButton", () => { + // Logger adapted from CustomizableUI.jsm + const logger = (() => { + const { ConsoleAPI } = ChromeUtils.import( + "resource://gre/modules/Console.jsm" + ); + const consoleOptions = { + maxLogLevel: "info", + maxLogLevelPref: "browser.new_identity.log_level", + prefix: "NewIdentity", + }; + return new ConsoleAPI(consoleOptions); + })(); + + const topics = Object.freeze({ + newIdentityRequested: "new-identity-requested", + }); + + class NewIdentityImpl { + async run() { + this.disableAllJS(); + await this.clearState(); + await this.openNewWindow(); + this.closeOldWindow(); + this.broadcast(); + } + + // Disable JS (as a defense-in-depth measure) + + disableAllJS() { + logger.info("Disabling JavaScript"); + const enumerator = Services.wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + const win = enumerator.getNext(); + this.disableWindowJS(win); + } + } + + disableWindowJS(win) { + const browsers = win.gBrowser?.browsers || []; + for (const browser of browsers) { + if (!browser) { + continue; + } + this.disableBrowserJS(browser); + try { + browser.webNavigation?.stop(browser.webNavigation.STOP_ALL); + } catch (e) { + logger.warn("Could not stop navigation", e, browser.currentURI); + } + } + } + + disableBrowserJS(browser) { + if (!browser) { + return; + } + // Does the following still apply? + // Solution from: https://bugzilla.mozilla.org/show_bug.cgi?id=409737 + // XXX: This kills the entire window. We need to redirect + // focus and inform the user via a lightbox. + const eventSuppressor = browser.contentWindow?.windowUtils; + if (browser.browsingContext) { + browser.browsingContext.allowJavascript = false; + } + try { + // My estimation is that this does not get the inner iframe windows, + // but that does not matter, because iframes should be destroyed + // on the next load. + // Should we log when browser.contentWindow is null? + if (browser.contentWindow) { + browser.contentWindow.name = null; + browser.contentWindow.window.name = null; + } + } catch (e) { + logger.warn("Failed to reset window.name", e); + } + eventSuppressor?.suppressEventHandling(true); + } + + // Clear state + + async clearState() { + logger.info("Clearing the state"); + this.closeTabs(); + this.clearSearchBar(); + this.clearPrivateSessionHistory(); + this.clearHTTPAuths(); + this.clearCryptoTokens(); + this.clearOCSPCache(); + this.clearSecuritySettings(); + this.clearImageCaches(); + this.clearStorage(); + this.clearPreferencesAndPermissions(); + await this.clearData(); + await this.reloadAddons(); + this.clearConnections(); + this.clearPrivateSession(); + } + + clearSiteSpecificZoom() { + Services.prefs.setBoolPref( + "browser.zoom.siteSpecific", + !Services.prefs.getBoolPref("browser.zoom.siteSpecific") + ); + Services.prefs.setBoolPref( + "browser.zoom.siteSpecific", + !Services.prefs.getBoolPref("browser.zoom.siteSpecific") + ); + } + + closeTabs() { + if ( + !Services.prefs.getBoolPref("browser.new_identity.close_newnym", true) + ) { + logger.info("Not closing tabs"); + return; + } + // TODO: muck around with browser.tabs.warnOnClose.. maybe.. + logger.info("Closing tabs..."); + const enumerator = Services.wm.getEnumerator("navigator:browser"); + const windowsToClose = []; + while (enumerator.hasMoreElements()) { + const win = enumerator.getNext(); + const browser = win.gBrowser; + if (!browser) { + logger.warn("No browser for possible window to close"); + continue; + } + const tabsToRemove = []; + for (const b of browser.browsers) { + const tab = browser.getTabForBrowser(b); + if (tab) { + tabsToRemove.push(tab); + } else { + logger.warn("Browser has a null tab", b); + } + } + if (win == window) { + browser.addWebTab("about:blank"); + } else { + // It is a bad idea to alter the window list while iterating + // over it, so add this window to an array and close it later. + windowsToClose.push(win); + } + // Close each tab except the new blank one that we created. + tabsToRemove.forEach(aTab => browser.removeTab(aTab)); + } + // Close all XUL windows except this one. + logger.info("Closing windows..."); + windowsToClose.forEach(aWin => aWin.close()); + logger.info("Closed all tabs"); + + // This clears the undo tab history. + const tabs = Services.prefs.getIntPref( + "browser.sessionstore.max_tabs_undo" + ); + Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", 0); + Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", tabs); + } + + clearSearchBar() { + logger.info("Clearing searchbox"); + // Bug #10800: Trying to clear search/find can cause exceptions + // in unknown cases. Just log for now. + try { + const searchBar = window.document.getElementById("searchbar"); + if (searchBar) { + searchBar.textbox.reset(); + } + } catch (e) { + logger.error("Exception on clearing search box", e); + } + try { + if (gFindBarInitialized) { + const findbox = gFindBar.getElement("findbar-textbox"); + findbox.reset(); + gFindBar.close(); + } + } catch (e) { + logger.error("Exception on clearing find bar", e); + } + } + + clearPrivateSessionHistory() { + logger.info("Emitting Private Browsing Session clear event"); + Services.obs.notifyObservers(null, "browser:purge-session-history"); + } + + clearHTTPAuths() { + if ( + !Services.prefs.getBoolPref( + "browser.new_identity.clear_http_auth", + true + ) + ) { + logger.info("Skipping HTTP Auths, because disabled"); + return; + } + logger.info("Clearing HTTP Auths"); + const auth = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + auth.clearAll(); + } + + clearCryptoTokens() { + logger.info("Clearing Crypto Tokens"); + // Clear all crypto auth tokens. This includes calls to PK11_LogoutAll(), + // nsNSSComponent::LogoutAuthenticatedPK11() and clearing the SSL session + // cache. + const sdr = Cc["@mozilla.org/security/sdr;1"].getService( + Ci.nsISecretDecoderRing + ); + sdr.logoutAndTeardown(); + } + + clearOCSPCache() { + // nsNSSComponent::Observe() watches security.OCSP.enabled, which calls + // setValidationOptions(), which in turn calls setNonPkixOcspEnabled() which, + // if security.OCSP.enabled is set to 0, calls CERT_DisableOCSPChecking(), + // which calls CERT_ClearOCSPCache(). + // See: https://mxr.mozilla.org/comm-esr24/source/mozilla/security/manager/ssl/src/nsNSSComponent.cpp + const ocsp = Services.prefs.getIntPref("security.OCSP.enabled"); + Services.prefs.setIntPref("security.OCSP.enabled", 0); + Services.prefs.setIntPref("security.OCSP.enabled", ocsp); + } + + clearSecuritySettings() { + // Clear site security settings + const sss = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + sss.clearAll(); + } + + clearImageCaches() { + logger.info("Clearing Image Cache"); + // In Firefox 18 and newer, there are two image caches: one that is used + // for regular browsing, and one that is used for private browsing. + this.clearImageCacheRB(); + this.clearImageCachePB(); + } + + clearImageCacheRB() { + try { + const imgTools = Cc["@mozilla.org/image/tools;1"].getService( + Ci.imgITools + ); + const imgCache = imgTools.getImgCacheForDocument(null); + // Evict all but chrome cache + imgCache.clearCache(false); + } catch (e) { + // FIXME: This can happen in some rare cases involving XULish image data + // in combination with our image cache isolation patch. Sure isn't + // a good thing, but it's not really a super-cookie vector either. + // We should fix it eventually. + logger.error("Exception on image cache clearing", e); + } + } + + clearImageCachePB() { + const imgTools = Cc["@mozilla.org/image/tools;1"].getService( + Ci.imgITools + ); + try { + // Try to clear the private browsing cache. To do so, we must locate a + // content document that is contained within a private browsing window. + let didClearPBCache = false; + const enumerator = Services.wm.getEnumerator("navigator:browser"); + while (!didClearPBCache && enumerator.hasMoreElements()) { + const win = enumerator.getNext(); + let browserDoc = win.document.documentElement; + if (!browserDoc.hasAttribute("privatebrowsingmode")) { + continue; + } + const tabbrowser = win.gBrowser; + if (!tabbrowser) { + continue; + } + for (const browser of tabbrowser.browsers) { + const doc = browser.contentDocument; + if (doc) { + const imgCache = imgTools.getImgCacheForDocument(doc); + // Evict all but chrome cache + imgCache.clearCache(false); + didClearPBCache = true; + break; + } + } + } + } catch (e) { + logger.error("Exception on private browsing image cache clearing", e); + } + } + + clearStorage() { + logger.info("Clearing Disk and Memory Caches"); + try { + Services.cache2.clear(); + } catch (e) { + logger.error("Exception on cache clearing", e); + } + + logger.info("Clearing Cookies and DOM Storage"); + Services.cookies.removeAll(); + } + + clearPreferencesAndPermissions() { + logger.info("Clearing Content Preferences"); + ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" + ); + const pbCtxt = PrivateBrowsingUtils.privacyContextFromWindow(window); + const cps = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + cps.removeAllDomains(pbCtxt); + this.clearSiteSpecificZoom(); + + logger.info("Clearing permissions"); + try { + Services.perms.removeAll(); + } catch (e) { + // Actually, this catch does not appear to be needed. Leaving it in for + // safety though. + logger.error("Cannot clear permissions", e); + } + + logger.info("Syncing prefs"); + // Force prefs to be synced to disk + Services.prefs.savePrefFile(null); + } + + async clearData() { + logger.info("Calling the clearDataService"); + const flags = + Services.clearData.CLEAR_ALL ^ Services.clearData.CLEAR_PASSWORDS; + return new Promise(resolve => { + Services.clearData.deleteData(flags, { + onDataDeleted(code) { + if (code !== Cr.NS_OK) { + logger.error(`Error while calling the clearDataService: ${code}`); + } + // We always resolve, because we do not want to interrupt the new + // identity procedure. + resolve(); + }, + }); + }); + } + + clearConnections() { + logger.info("Closing open connections"); + // Clear keep-alive + Services.obs.notifyObservers(this, "net:prune-all-connections"); + } + + clearPrivateSession() { + logger.info("Ending any remaining private browsing sessions."); + Services.obs.notifyObservers(null, "last-pb-context-exited"); + } + + async reloadAddons() { + logger.info("Reloading add-ons to clear their temporary state."); + // Reload all active extensions except search engines, which would throw. + const addons = await AddonManager.getAddonsByTypes(["extension"]); + const isSearchEngine = async addon => + (await (await fetch(addon.getResourceURI("manifest.json").spec)).json()) + ?.chrome_settings_overrides?.search_provider; + const reloadIfNeeded = async addon => + addon.isActive && !(await isSearchEngine(addon)) && addon.reload(); + await Promise.all(addons.map(addon => reloadIfNeeded(addon))); + } + + // Broadcast as a hook to clear other data + + broadcast() { + logger.info("Broadcasting the new identity"); + Services.obs.notifyObservers({}, topics.newIdentityRequested); + } + + // Window management + + openNewWindow() { + logger.info("Opening a new window"); + return new Promise(resolve => { + // Open a new window forcing the about:privatebrowsing page (tor-browser#41765) + // unless user explicitly overrides this policy (tor-browser #42236) + const trustedHomePref = "browser.startup.homepage.new_identity"; + const homeURL = HomePage.get(); + const defaultHomeURL = HomePage.getDefault(); + const isTrustedHome = + homeURL === defaultHomeURL || + homeURL === "chrome://browser/content/blanktab.html" || // about:blank + homeURL === Services.prefs.getStringPref(trustedHomePref, ""); + const isCustomHome = + Services.prefs.getIntPref("browser.startup.page") === 1; + const win = OpenBrowserWindow({ + private: isCustomHome && isTrustedHome ? "private" : "no-home", + }); + // This mechanism to know when the new window is ready is used by + // OpenBrowserWindow itself (see its definition in browser.js). + win.addEventListener( + "MozAfterPaint", + () => { + resolve(); + if (isTrustedHome || !isCustomHome) { + return; + } + const tbl = win.TabsProgressListener; + const { onLocationChange } = tbl; + tbl.onLocationChange = (...args) => { + tbl.onLocationChange = onLocationChange; + tbl.onLocationChange(...args); + let displayAddress; + try { + const url = new URL(homeURL); + displayAddress = url.hostname; + if (!displayAddress) { + // no host, use full address and truncate if too long + const MAX_LEN = 32; + displayAddress = url.href; + if (displayAddress.length > MAX_LEN) { + displayAddress = `${displayAddress.substring(0, MAX_LEN)}…`; + } + } + } catch (e) { + // malformed URL, bail out + return; + } + const callback = () => { + Services.prefs.setStringPref(trustedHomePref, homeURL); + win.BrowserHome(); + }; + const notificationBox = win.gBrowser.getNotificationBox(); + notificationBox.appendNotification( + "new-identity-safe-home", + { + label: { + "l10n-id": "new-identity-blocked-home-notification", + "l10n-args": { url: displayAddress }, + }, + priority: notificationBox.PRIORITY_INFO_MEDIUM, + }, + [ + { + "l10n-id": "new-identity-blocked-home-ignore-button", + callback, + }, + ] + ); + }; + }, + { once: true } + ); + }); + } + + closeOldWindow() { + logger.info("Closing the old window"); + + // Run garbage collection and cycle collection after window is gone. + // This ensures that blob URIs are forgotten. + window.addEventListener("unload", function (event) { + logger.debug("Initiating New Identity GC pass"); + // Clear out potential pending sInterSliceGCTimer: + window.windowUtils.runNextCollectorTimer(); + // Clear out potential pending sICCTimer: + window.windowUtils.runNextCollectorTimer(); + // Schedule a garbage collection in 4000-1000ms... + window.windowUtils.garbageCollect(); + // To ensure the GC runs immediately instead of 4-10s from now, we need + // to poke it at least 11 times. + // We need 5 pokes for GC, 1 poke for the interSliceGC, and 5 pokes for + // CC. + // See nsJSContext::RunNextCollectorTimer() in + // https://mxr.mozilla.org/mozilla-central/source/dom/base/nsJSEnvironment.cpp#1970. + // XXX: We might want to make our own method for immediate full GC... + for (let poke = 0; poke < 11; poke++) { + window.windowUtils.runNextCollectorTimer(); + } + // And now, since the GC probably actually ran *after* the CC last time, + // run the whole thing again. + window.windowUtils.garbageCollect(); + for (let poke = 0; poke < 11; poke++) { + window.windowUtils.runNextCollectorTimer(); + } + logger.debug("Completed New Identity GC pass"); + }); + + // Close the current window for added safety + window.close(); + } + } + + let newIdentityInProgress = false; + return { + topics, + + init() { + // We first search in the DOM for the identity button. If it does not + // exist it may be in the toolbox palette. In the latter case we still + // need to initialize the button in case it is added back later through + // customization. + const button = + document.getElementById("new-identity-button") || + window.gNavToolbox.palette.querySelector("#new-identity-button"); + button?.addEventListener("command", () => { + this.onCommand(); + }); + document + .getElementById("appMenu-viewCache") + .content.querySelector("#appMenu-new-identity") + ?.addEventListener("command", () => { + this.onCommand(); + }); + document + .getElementById("menu_newIdentity") + ?.addEventListener("command", () => { + this.onCommand(); + }); + }, + + uninit() {}, + + async onCommand() { + try { + // Ignore if there's a New Identity in progress to avoid race + // conditions leading to failures (see bug 11783 for an example). + if (newIdentityInProgress) { + return; + } + newIdentityInProgress = true; + + const prefConfirm = "browser.new_identity.confirm_newnym"; + const shouldConfirm = Services.prefs.getBoolPref(prefConfirm, true); + if (shouldConfirm) { + const params = { + confirmed: false, + neverAskAgain: false, + }; + await window.gDialogBox.open( + "chrome://browser/content/newIdentityDialog.xhtml", + params + ); + Services.prefs.setBoolPref(prefConfirm, !params.neverAskAgain); + if (!params.confirmed) { + return; + } + } + + const impl = new NewIdentityImpl(); + await impl.run(); + } catch (e) { + // If something went wrong make sure we have the New Identity button + // enabled (again). + logger.error("Unexpected error", e); + window.alert("New Identity unexpected error: " + e); + } finally { + newIdentityInProgress = false; + } + }, + }; +}); diff --git a/browser/components/newidentity/jar.mn b/browser/components/newidentity/jar.mn new file mode 100644 index 0000000000000000000000000000000000000000..6e4642e1f5f34edf07d2a6cfce88ff718206577a --- /dev/null +++ b/browser/components/newidentity/jar.mn @@ -0,0 +1,5 @@ +browser.jar: + content/browser/newidentity.js (content/newidentity.js) + content/browser/newIdentityDialog.xhtml (content/newIdentityDialog.xhtml) + content/browser/newIdentityDialog.css (content/newIdentityDialog.css) + content/browser/newIdentityDialog.js (content/newIdentityDialog.js) diff --git a/browser/components/newidentity/moz.build b/browser/components/newidentity/moz.build new file mode 100644 index 0000000000000000000000000000000000000000..2661ad7cb9f3df2b3953a6e1e999aee82d29be78 --- /dev/null +++ b/browser/components/newidentity/moz.build @@ -0,0 +1 @@ +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/modules/BrowserWindowTracker.sys.mjs b/browser/modules/BrowserWindowTracker.sys.mjs index b6e3ad2eea42fe5dd4f69bc15e8931229214e84c..3a59603226e28a12964c469590d49c58573d5b77 100644 --- a/browser/modules/BrowserWindowTracker.sys.mjs +++ b/browser/modules/BrowserWindowTracker.sys.mjs @@ -294,7 +294,10 @@ export const BrowserWindowTracker = { let loadURIString; if (isPrivate && lazy.PrivateBrowsingUtils.enabled) { windowFeatures += ",private"; - if (!args && !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { + if ( + (!args && !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) || + args.private === "no-home" + ) { // Force the new window to load about:privatebrowsing instead of the // default home page. loadURIString = "about:privatebrowsing"; diff --git a/browser/themes/shared/icons/new_identity.svg b/browser/themes/shared/icons/new_identity.svg new file mode 100644 index 0000000000000000000000000000000000000000..096ff169c02f318c7968e77986743ca1e5086eb8 --- /dev/null +++ b/browser/themes/shared/icons/new_identity.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <g fill="context-fill" fill-opacity="context-fill-opacity"> + <path d="m13.5383 14.5627c-.1712-.0053-.3194-.1334-.3505-.3028-.0419-.294-.1441-.5789-.3001-.8369-.2583-.1558-.5436-.2579-.838-.2998-.1694-.0313-.2974-.1793-.3026-.3501-.0053-.1708.1136-.3146.2813-.3402.2944-.0329.5762-.1254.8284-.272.1426-.2476.2313-.5243.2608-.8129.0237-.1679.1662-.2884.3372-.2851.1699.0042.3181.1295.3517.2973.0471.2931.1533.5763.312.8323.2565.1573.5396.263.8326.3109.1682.0345.2929.1836.2958.3536.0028.17-.1171.3116-.2843.3357-.2894.0285-.5669.1172-.8147.2604-.1462.2521-.2386.5335-.2717.8274-.025.167-.1675.2861-.3379.2822z"/> + <path d="m6.49858 2.99992c-.14675-.00459-.27377-.11436-.3004-.25961-.03593-.25196-.12354-.49621-.25729-.71731-.22137-.13358-.46594-.22109-.71822-.25699-.14526-.02682-.25492-.15363-.25945-.30004-.00454-.14641.09737-.26967.24112-.29164.25236-.02817.49393-.10747.71013-.233093.12217-.2123.19825-.449454.22353-.696834.0203-.143878.14242-.24714456.28897-.24434753.14565.00358504.27273.11100153.30149.25484453.0404.251183.13139.493923.2674.713349.21988.134841.46256.225461.71364.266481.14417.02957.25114.15744.25358.30313.00244.1457-.10035.26707-.24368.28774-.2481.02441-.48592.10041-.69835.22319-.1253.2161-.20449.45729-.23284.7092-.0214.14312-.14361.24521-.28963.24193z"/> + <path d="m1.82093 5.3609c-.15279-.00473-.28512-.11875-.31315-.26981-.02739-.18014-.08781-.35525-.1782-.51643-.16152-.09021-.336989-.15052-.517512-.17788-.151437-.02794-.265749-.16003-.270474-.31254-.004724-.15251.101518-.2809.251381-.30378.181146-.02145.355265-.07593.513815-.16075.08209-.15545.13363-.32622.15197-.50355.02095-.15059.14903-.25861.3025-.25512.15164.00368.28404.11525.31428.26484.03021.18029.09338.35503.18632.51538.16048.09192.33508.15452.51517.18469.1503.0308.26181.164.26435.31577.00254.15176-.10462.27819-.25404.29971-.17764.01914-.34855.07141-.50396.15412-.08502.1582-.13963.33194-.16114.5127-.022.14911-.14912.25571-.30131.25265z"/> + <path clip-rule="evenodd" d="m15.3213 1.06694c.2441-.244076.2441-.639804 0-.883882-.2441-.2440775-.6398-.2440774-.8839 0l-5.96506 5.965062h-.50519c-1.996-1.09517-4.49023.42233-6.49079 1.63948-.41545.25277-.80961.49258-1.173597.69335-.16756.10002-.289261.26641-.30145394.48048-.01219156.21407.06079654.41038.21802994.56743l1.243691 1.24224 2.37084-1.02603c.15392-.06661.30331.14022.18601.25753l-1.66213 1.6621 1.46329 1.4616 1.66126-1.6613c.1173-.1173.32413.0321.25752.186l-1.02482 2.3682 1.25462 1.2531c.15724.157.35379.23.56815.2178.19095-.0561.35851-.1561.45869-.3234.20012-.3592.43577-.7455.68321-1.1511 1.22241-2.0039 2.73233-4.47901 1.66484-6.47533v-.49654zm-7.46715 6.55077c1.12692 1.12692.64113 2.69369-.05278 3.70149h-.50137l-3.13-3.1492v-.5c1.00858-.68566 2.56556-1.17088 3.68415-.05229z" fill-rule="evenodd"/> + </g> +</svg> diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn index d9540424ae321f734ed9a530df451ab72e0bc867..6123044c04177ccd61cf18ccca447ba54541e729 100644 --- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -285,3 +285,5 @@ skin/classic/browser/weather/night-partly-cloudy-with-showers.svg (../shared/weather/night-partly-cloudy-with-showers.svg) skin/classic/browser/weather/night-partly-cloudy-with-thunderstorms.svg (../shared/weather/night-partly-cloudy-with-thunderstorms.svg) skin/classic/browser/weather/night-mostly-cloudy-with-flurries.svg (../shared/weather/night-mostly-cloudy-with-flurries.svg) + + skin/classic/browser/new_identity.svg (../shared/icons/new_identity.svg) diff --git a/browser/themes/shared/toolbarbutton-icons.css b/browser/themes/shared/toolbarbutton-icons.css index 3d83c0a771e4b48bbd4a29c2c475877e4d79c506..6d5ce70ef29e7f45fce479b4aedb034d7b33594f 100644 --- a/browser/themes/shared/toolbarbutton-icons.css +++ b/browser/themes/shared/toolbarbutton-icons.css @@ -236,6 +236,10 @@ list-style-image: url("chrome://browser/skin/new-tab.svg"); } +#new-identity-button { + list-style-image: url("chrome://browser/skin/new_identity.svg"); +} + #privatebrowsing-button { list-style-image: url("chrome://browser/skin/privateBrowsing.svg"); }