diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 1165565c1ae..becafc6283c 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1519,7 +1519,10 @@ pref("browser.bookmarks.editDialog.maxRecentFolders", 7);
pref("browser.taskbar.lists.legacyBackend", false);
- pref("browser.taskbar.lists.enabled", true);
+// Log levels
+ pref("browser.new_identity.log_level", "Info");
+
+ pref("browser.taskbar.lists.enabled", false);
pref("browser.taskbar.lists.frequent.enabled", true);
pref("browser.taskbar.lists.recent.enabled", false);
pref("browser.taskbar.lists.maxListItemCount", 7);
diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml
index 51b21bb4736..5cdf76f6560 100644
--- a/browser/base/content/appmenu-viewcache.inc.xhtml
+++ b/browser/base/content/appmenu-viewcache.inc.xhtml
@@ -47,6 +47,10 @@
data-l10n-id="appmenuitem-new-private-window"
key="key_privatebrowsing"
command="Tools:PrivateBrowsing"/>
+
diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js
index 2ac19168354..e15df4ab7d6 100644
--- a/browser/base/content/browser-init.js
+++ b/browser/base/content/browser-init.js
@@ -233,6 +233,9 @@ var gBrowserInit = {
// doesn't flicker as the window is being shown.
DownloadsButton.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
@@ -1128,6 +1131,8 @@ var gBrowserInit = {
DownloadsButton.uninit();
+ NewIdentityButton.uninit();
+
if (gToolbarKeyNavEnabled) {
ToolbarKeyboardNavigator.uninit();
}
diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc
index 0075a610d86..2483668bef5 100644
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -28,6 +28,10 @@
+
+
+
#endif
+
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 033c19c4ddd..968291deb7d 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -247,6 +247,11 @@ XPCOMUtils.defineLazyScriptGetter(
["DownloadsButton", "DownloadsIndicatorView"],
"chrome://browser/content/downloads/indicator.js"
);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["NewIdentityButton"],
+ "chrome://browser/content/newidentity.js"
+);
XPCOMUtils.defineLazyScriptGetter(
this,
"gEditItemOverlay",
diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals
index 910e0b5ca9c..47a3c298ad1 100644
--- a/browser/base/content/browser.js.globals
+++ b/browser/base/content/browser.js.globals
@@ -274,5 +274,6 @@
"gFindBar",
"gFindBarInitialized",
"gFindBarPromise",
- "BrowserSearch"
+ "BrowserSearch",
+ "NewIdentityButton"
]
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index 390a1300a12..3b0c02a5b64 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -587,6 +587,11 @@
ondragover="newWindowButtonObserver.onDragOver(event)"
ondragenter="newWindowButtonObserver.onDragOver(event)"/>
+
+
+
+
{
+ 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");
+ });
+
+
\ No newline at end of file
diff --git a/browser/components/newidentity/content/newIdentityDialog.xhtml b/browser/components/newidentity/content/newIdentityDialog.xhtml
new file mode 100644
index 00000000000..f90271ada4a
--- /dev/null
+++ b/browser/components/newidentity/content/newIdentityDialog.xhtml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
diff --git a/browser/components/newidentity/content/newidentity.js b/browser/components/newidentity/content/newidentity.js
new file mode 100644
index 00000000000..90520bbe501
--- /dev/null
+++ b/browser/components/newidentity/content/newidentity.js
@@ -0,0 +1,572 @@
+"use strict";
+
+/* eslint-env mozilla/browser-window */
+
+// Use a lazy getter because NewIdentityButton is declared more than once
+// otherwise.
+ChromeUtils.defineLazyGetter(this, "NewIdentityButton", () => {
+ // Logger adapted from CustomizableUI.jsm
+ const logger = (() => {
+ const consoleOptions = {
+ maxLogLevelPref: "browser.new_identity.log_level",
+ prefix: "NewIdentity",
+ };
+ return console.createInstance(consoleOptions);
+ })();
+
+ const topics = Object.freeze({
+ newIdentityRequested: "new-identity-requested",
+ });
+
+ /**
+ * This class contains the actual implementation of the various step involved
+ * when running new identity.
+ */
+ 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.defineESModuleGetters(this, {
+ PrivateBrowsingUtils:
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ });
+ 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: false,
+ });
+ // 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 () {
+ 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 00000000000..6e4642e1f5f
--- /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 00000000000..8a88d6184cb
--- /dev/null
+++ b/browser/components/newidentity/moz.build
@@ -0,0 +1 @@
+JAR_MANIFESTS += ["jar.mn"]
\ No newline at end of file
diff --git a/browser/themes/shared/icons/new_identity.svg b/browser/themes/shared/icons/new_identity.svg
new file mode 100644
index 00000000000..5abb6da228d
--- /dev/null
+++ b/browser/themes/shared/icons/new_identity.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index 5703087aa5a..df0c4169280 100644
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -284,3 +284,4 @@
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)
\ No newline at end of file
diff --git a/browser/themes/shared/toolbarbutton-icons.css b/browser/themes/shared/toolbarbutton-icons.css
index 3d83c0a771e..6d5ce70ef29 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");
}