[tbb-commits] [tor-browser] 01/21: Bug 10760: Integrate TorButton to TorBrowser core

gitolite role git at cupani.torproject.org
Tue Dec 6 08:31:57 UTC 2022


This is an automated email from the git hooks/post-receive script.

pierov pushed a commit to branch tor-browser-102.5.0esr-12.5-1
in repository tor-browser.

commit e1eb5ce81dc8f0bd786597415a0069c3f603c74c
Author: Alex Catarineu <acat at torproject.org>
AuthorDate: Wed Feb 19 23:05:08 2020 +0100

    Bug 10760: Integrate TorButton to TorBrowser core
    
    Because of the non-restartless nature of Torbutton, it required
    a two-stage installation process. On mobile, it was a problem,
    because it was not loading when the user opened the browser for
    the first time.
    
    Moving it to tor-browser and making it a system extension allows it
    to load when the user opens the browser for first time.
    
    Additionally, this patch also fixes Bug 27611.
    
    Bug 26321: New Circuit and New Identity menu items
    
    Bug 14392: Make about:tor behave like other initial pages.
    
    Bug 25013: Add torbutton as a tor-browser submodule
    
    Bug 31575: Replace Firefox Home (newtab) with about:tor
    
    Avoid loading AboutNewTab in BrowserGlue.jsm in order
    to avoid several network requests that we do not need. Besides,
    about:newtab will now point to about:blank or about:tor (depending
    on browser.newtabpage.enabled) and about:home will point to
    about:tor.
---
 browser/base/content/aboutDialog.xhtml             |   40 +-
 browser/base/content/appmenu-viewcache.inc.xhtml   |   11 +-
 browser/base/content/browser-doctype.inc           |    8 +
 browser/base/content/browser-menubar.inc           |   24 +-
 browser/base/content/browser-sets.inc              |    1 +
 browser/base/content/browser.js                    |    1 +
 browser/base/content/browser.xhtml                 |   13 +
 browser/base/content/hiddenWindowMac.xhtml         |    4 +
 browser/base/content/navigator-toolbox.inc.xhtml   |    5 +
 browser/base/content/pageinfo/pageInfo.xhtml       |    6 +
 browser/components/BrowserGlue.jsm                 |   33 +-
 .../controlcenter/content/identityPanel.inc.xhtml  |   22 +
 browser/components/newtab/AboutNewTabService.jsm   |   15 +-
 browser/components/places/content/places.xhtml     |    3 +
 browser/components/preferences/home.inc.xhtml      |    4 +-
 browser/components/preferences/preferences.xhtml   |    5 +-
 .../shell/content/setDesktopBackground.xhtml       |    6 +
 browser/installer/package-manifest.in              |    2 +
 browser/modules/HomePage.jsm                       |    2 +-
 browser/themes/shared/icons/new_circuit.svg        |    6 +
 browser/themes/shared/jar.inc.mn                   |    2 +
 browser/themes/shared/toolbarbutton-icons.css      |    4 +
 docshell/base/nsAboutRedirector.cpp                |    6 +-
 docshell/build/components.conf                     |    1 +
 mobile/android/installer/package-manifest.in       |    4 +
 toolkit/moz.build                                  |    1 +
 toolkit/torbutton/.gitignore                       |    1 +
 toolkit/torbutton/CREDITS                          |    5 +
 toolkit/torbutton/LICENSE                          |   29 +
 .../chrome/content/aboutTor/aboutTor-content.js    |  139 +++
 .../chrome/content/aboutTor/aboutTor.xhtml         |  112 +++
 .../chrome/content/aboutTor/resources/aboutTor.js  |   11 +
 .../torbutton/chrome/content/preferences-mobile.js |   59 ++
 toolkit/torbutton/chrome/content/preferences.xhtml |   84 ++
 .../chrome/content/tor-circuit-display.js          |  604 +++++++++++
 toolkit/torbutton/chrome/content/torbutton.js      | 1044 ++++++++++++++++++++
 toolkit/torbutton/chrome/skin/about-wordmark.png   |  Bin 0 -> 4609 bytes
 toolkit/torbutton/chrome/skin/aboutDialog.css      |   34 +
 toolkit/torbutton/chrome/skin/aboutTor.css         |  313 ++++++
 toolkit/torbutton/chrome/skin/banner-warning.svg   |    1 +
 toolkit/torbutton/chrome/skin/dax-logo.svg         |    1 +
 toolkit/torbutton/chrome/skin/icon-newsletter.png  |  Bin 0 -> 649 bytes
 .../torbutton/chrome/skin/preferences-mobile.css   |   47 +
 toolkit/torbutton/chrome/skin/preferences.css      |    7 +
 .../torbutton/chrome/skin/tor-circuit-display.css  |  193 ++++
 .../chrome/skin/tor-circuit-line-first.svg         |    6 +
 .../chrome/skin/tor-circuit-line-last.svg          |    6 +
 toolkit/torbutton/chrome/skin/tor-circuit-line.svg |    7 +
 toolkit/torbutton/chrome/skin/tor.png              |  Bin 0 -> 2073 bytes
 .../chrome/skin/torbrowser_mobile_logo.png         |  Bin 0 -> 9345 bytes
 toolkit/torbutton/chrome/skin/torbutton.css        |   14 +
 toolkit/torbutton/chrome/skin/torbutton.svg        |    3 +
 toolkit/torbutton/components/domain-isolator.js    |  228 +++++
 toolkit/torbutton/components/dragDropFilter.js     |  134 +++
 .../torbutton/components/external-app-blocker.js   |  160 +++
 toolkit/torbutton/components/startup-observer.js   |  196 ++++
 toolkit/torbutton/components/torCheckService.js    |  140 +++
 toolkit/torbutton/components/torbutton-logger.js   |  185 ++++
 toolkit/torbutton/jar.mn                           |   46 +
 toolkit/torbutton/modules/tor-control-port.js      |  982 ++++++++++++++++++
 toolkit/torbutton/modules/utils.js                 |  318 ++++++
 toolkit/torbutton/moz.build                        |    6 +
 .../lib/environments/browser-window.js             |    6 +-
 63 files changed, 5284 insertions(+), 66 deletions(-)

diff --git a/browser/base/content/aboutDialog.xhtml b/browser/base/content/aboutDialog.xhtml
index 90a568a17dd6..60b1e15b637c 100644
--- a/browser/base/content/aboutDialog.xhtml
+++ b/browser/base/content/aboutDialog.xhtml
@@ -7,6 +7,12 @@
 <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/aboutDialog.css" type="text/css"?>
 <?xml-stylesheet href="chrome://branding/content/aboutDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://torbutton/skin/aboutDialog.css" type="text/css"?>
+
+<!-- We need to include the localization DTDs until we migrate to Fluent -->
+<!DOCTYPE window [
+#include browser-doctype.inc
+]>
 
 <window xmlns:html="http://www.w3.org/1999/xhtml"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
@@ -22,7 +28,7 @@
         data-l10n-id="aboutDialog-title"
 #endif
         role="dialog"
-        aria-describedby="version distribution distributionId communityDesc contributeDesc trademark"
+        aria-describedby="version distribution distributionId projectDesc helpDesc trademark trademarkTor"
         >
 #ifdef XP_MACOSX
 #include macWindow.inc.xhtml
@@ -140,24 +146,36 @@
               <label is="text-link" useoriginprincipal="true" href="about:credits" data-l10n-name="community-exp-creditsLink"/>
             </description>
           </vbox>
-          <description class="text-blurb" id="communityDesc" data-l10n-id="community-2">
-            <label is="text-link" href="https://www.mozilla.org/?utm_source=firefox-browser&utm_medium=firefox-desktop&utm_campaign=about-dialog" data-l10n-name="community-mozillaLink"/>
-            <label is="text-link" useoriginprincipal="true" href="about:credits" data-l10n-name="community-creditsLink"/>
+          <!-- Keep communityDesc and contributeDesc to avoid JS errors trying to hide them -->
+          <description class="text-blurb" id="communityDesc" data-l10n-id="community-2" hidden="true"></description>
+          <description class="text-blurb" id="contributeDesc" data-l10n-id="helpus" hidden="true"></description>
+          <description class="text-blurb" id="projectDesc">
+            &project.start;
+            <label is="text-link" href="https://www.torproject.org/">
+              &project.tpoLink;
+            </label>&project.end;
           </description>
-          <description class="text-blurb" id="contributeDesc" data-l10n-id="helpus">
-            <label is="text-link" href="https://donate.mozilla.org/?utm_source=firefox&utm_medium=referral&utm_campaign=firefox_about&utm_content=firefox_about" data-l10n-name="helpus-donateLink"/>
-            <label is="text-link" href="https://www.mozilla.org/contribute/?utm_source=firefox-browser&utm_medium=firefox-desktop&utm_campaign=about-dialog" data-l10n-name="helpus-getInvolvedLink"/>
+          <description class="text-blurb" id="helpDesc">
+            &help.start;
+            <label is="text-link" href="https://donate.torproject.org/">
+              &help.donateLink;
+            </label>
+            &help.or;
+            <label is="text-link" href="https://community.torproject.org/">
+              &help.getInvolvedLink;
+            </label>&help.end;
           </description>
         </vbox>
       </vbox>
     </hbox>
     <vbox id="bottomBox">
-      <hbox pack="center">
-        <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:license" data-l10n-id="bottomLinks-license"/>
-        <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:rights" data-l10n-id="bottomLinks-rights"/>
-        <label is="text-link" class="bottom-link" href="https://www.mozilla.org/privacy/?utm_source=firefox-browser&utm_medium=firefox-desktop&utm_campaign=about-dialog" data-l10n-id="bottomLinks-privacy"/>
+      <hbox id="newBottom" pack="center" position="1">
+        <label is="text-link" class="bottom-link" href="https://support.torproject.org/">&bottomLinks.questions;</label>
+        <label is="text-link" class="bottom-link" href="https://community.torproject.org/relay/">&bottomLinks.grow;</label>
+        <label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:license">&bottomLinks.license;</label>
       </hbox>
       <description id="trademark" data-l10n-id="trademarkInfo"></description>
+      <description id="trademarkTor">&tor.TrademarkStatement;</description>
     </vbox>
   </vbox>
 
diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml
index c138be3ee86c..cb685a3d0f52 100644
--- a/browser/base/content/appmenu-viewcache.inc.xhtml
+++ b/browser/base/content/appmenu-viewcache.inc.xhtml
@@ -55,11 +55,17 @@
                      class="subviewbutton"
                      data-l10n-id="appmenuitem-new-private-window"
                      key="key_privatebrowsing"
-                     command="Tools:PrivateBrowsing"/>
+                     command="Tools:PrivateBrowsing"
+                     hidden="true"/>
       <toolbarseparator/>
       <toolbarbutton id="appMenu-new-identity"
                      class="subviewbutton"
                      key="new-identity-key"/>
+      <toolbarbutton id="appMenuNewCircuit"
+                     class="subviewbutton"
+                     key="torbutton-new-circuit-key"
+                     label="&torbutton.context_menu.new_circuit_sentence_case;"
+                     oncommand="torbutton_new_circuit();"/>
       <toolbarseparator/>
       <toolbarbutton id="appMenu-bookmarks-button"
                      class="subviewbutton subviewbutton-nav"
@@ -179,7 +185,8 @@
       <toolbarbutton id="appMenu-restoreSession"
                      data-l10n-id="appmenu-restore-session"
                      class="subviewbutton"
-                     command="Browser:RestoreLastSession"/>
+                     command="Browser:RestoreLastSession"
+                     hidden="true"/>
       <toolbarseparator/>
       <toolbarbutton id="appMenuClearRecentHistory"
                      data-l10n-id="appmenu-clear-history"
diff --git a/browser/base/content/browser-doctype.inc b/browser/base/content/browser-doctype.inc
new file mode 100644
index 000000000000..fdc302e79de3
--- /dev/null
+++ b/browser/base/content/browser-doctype.inc
@@ -0,0 +1,8 @@
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % torbuttonDTD SYSTEM "chrome://torbutton/locale/torbutton.dtd">
+%torbuttonDTD;
+<!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
+%aboutTorDTD;
+<!ENTITY % aboutDialogDTD SYSTEM "chrome://torbutton/locale/aboutDialog.dtd">
+%aboutDialogDTD;
diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc
index 7c3dd4fe6d25..cde4d205b763 100644
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -31,6 +31,11 @@
                 <menuseparator/>
                 <menuitem id="menu_newIdentity"
                           key="new-identity-key"/>
+                <menuitem id="menu_newCircuit"
+                          accesskey="&torbutton.context_menu.new_circuit_key;"
+                          key="torbutton-new-circuit-key"
+                          label="&torbutton.context_menu.new_circuit;"
+                          oncommand="torbutton_new_circuit();"/>
                 <menuseparator/>
                 <menuitem id="menu_openLocation"
                           hidden="true"
@@ -456,8 +461,20 @@
               <menupopup id="menu_HelpPopup" onpopupshowing="buildHelpMenu();">
 <!-- Note: Items under here are cloned to the AppMenu Help submenu. The cloned items
      have their strings defined by appmenu-data-l10n-id. -->
+                <!-- dummy elements to avoid 'getElementById' errors -->
+                <box id="feedbackPage"/>
+                <box id="helpSafeMode"/>
+                <box id="menu_HelpPopup_reportPhishingtoolmenu"/>
+                <box id="menu_HelpPopup_reportPhishingErrortoolmenu"/>
+                <!-- Add Tor Browser manual link -->
+                <menuitem id="torBrowserUserManual"
+                          oncommand="gBrowser.selectedTab = gBrowser.addTab('about:manual', {triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()});"
+                          label="&aboutTor.torbrowser_user_manual.label;"
+                          accesskey="&aboutTor.torbrowser_user_manual.accesskey;"/>
+                <!-- Bug 18905: Hide unused help menu items -->
                 <menuitem id="menu_openHelp"
                           oncommand="openHelpLink('firefox-help')"
+                          hidden="true"
                           data-l10n-id="menu-get-help"
                           appmenu-data-l10n-id="appmenu-get-help"
 #ifdef XP_MACOSX
@@ -467,14 +484,17 @@
 #endif
                 <menuitem id="feedbackPage"
                           oncommand="openFeedbackPage()"
-                          data-l10n-id="menu-help-share-ideas"
-                          appmenu-data-l10n-id="appmenu-help-share-ideas"/>
+                          hidden="true"
+                          data-l10n-id="menu-help-feedback-page"
+                          appmenu-data-l10n-id="appmenu-help-feedback-page"/>
                 <menuitem id="helpSafeMode"
                           oncommand="safeModeRestart();"
+                          hidden="true"
                           data-l10n-id="menu-help-enter-troubleshoot-mode2"
                           appmenu-data-l10n-id="appmenu-help-enter-troubleshoot-mode2"/>
                 <menuitem id="troubleShooting"
                           oncommand="openTroubleshootingPage()"
+                          hidden="true"
                           data-l10n-id="menu-help-more-troubleshooting-info"
                           appmenu-data-l10n-id="appmenu-help-more-troubleshooting-info"/>
                 <menuitem id="help_reportSiteIssue"
diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc
index 17edb35baf6a..dc5d93978950 100644
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -389,4 +389,5 @@
          internal="true"/>
 #endif
     <key id="new-identity-key" modifiers="accel shift" key="U" oncommand="NewIdentityButton.onCommand(event)"/>
+    <key id="torbutton-new-circuit-key" modifiers="accel shift" key="L" oncommand="torbutton_new_circuit()"/>
   </keyset>
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index ee392d55b572..597bff687349 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -645,6 +645,7 @@ var gPageIcons = {
 };
 
 var gInitialPages = [
+  "about:tor",
   "about:blank",
   "about:home",
   ...(AppConstants.NIGHTLY_BUILD ? ["about:firefoxview"] : []),
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index df33143a7365..5959615f9dc1 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -35,6 +35,12 @@
 <?xml-stylesheet href="chrome://browser/skin/searchbar.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/places/editBookmark.css" type="text/css"?>
+<?xml-stylesheet href="chrome://torbutton/skin/tor-circuit-display.css" type="text/css"?>
+<?xml-stylesheet href="chrome://torbutton/skin/torbutton.css" type="text/css"?>
+
+<!DOCTYPE window [
+#include browser-doctype.inc
+]>
 
 <html id="main-window"
         xmlns:html="http://www.w3.org/1999/xhtml"
@@ -116,11 +122,18 @@
   Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/languageNotification.js", this);
+  Services.scriptloader.loadSubScript("chrome://torbutton/content/tor-circuit-display.js", this);
+  Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this);
 
   window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
   window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
   window.onclose = WindowIsClosing;
 
+  //onLoad Handler
+  try {
+    window.addEventListener("load", torbutton_init, false);
+  } catch (e) {}
+
   window.addEventListener("MozBeforeInitialXULLayout",
     gBrowserInit.onBeforeInitialXULLayout.bind(gBrowserInit), { once: true });
 
diff --git a/browser/base/content/hiddenWindowMac.xhtml b/browser/base/content/hiddenWindowMac.xhtml
index c27d394d3707..b272a076bb30 100644
--- a/browser/base/content/hiddenWindowMac.xhtml
+++ b/browser/base/content/hiddenWindowMac.xhtml
@@ -8,6 +8,10 @@
 
 <?xml-stylesheet href="chrome://browser/skin/webRTC-menubar-indicator.css" type="text/css"?>
 
+<!DOCTYPE window [
+#include browser-doctype.inc
+]>
+
 <window id="main-window"
         xmlns:html="http://www.w3.org/1999/xhtml"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index cadf68c91679..6123e1336aed 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -539,6 +539,11 @@
 
     <toolbarbutton id="new-identity-button" class="toolbarbutton-1 chromeclass-toolbar-additional"/>
 
+    <toolbarbutton id="new-circuit-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+                   label="&torbutton.context_menu.new_circuit;"
+                   oncommand="torbutton_new_circuit();"
+                   tooltiptext="&torbutton.context_menu.new_circuit;"/>
+
     <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                    observes="View:FullScreen"
                    type="checkbox"
diff --git a/browser/base/content/pageinfo/pageInfo.xhtml b/browser/base/content/pageinfo/pageInfo.xhtml
index 4fe7f4909180..b2b09f753902 100644
--- a/browser/base/content/pageinfo/pageInfo.xhtml
+++ b/browser/base/content/pageinfo/pageInfo.xhtml
@@ -6,6 +6,12 @@
 <?xml-stylesheet href="chrome://browser/content/pageinfo/pageInfo.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/pageInfo.css" type="text/css"?>
 
+<!DOCTYPE window [
+#ifdef XP_MACOSX
+#include ../browser-doctype.inc
+#endif
+]>
+
 <window id="main-window"
   xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:html="http://www.w3.org/1999/xhtml"
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 99b289bd934a..6d0947a464ba 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -18,7 +18,6 @@ const { AppConstants } = ChromeUtils.import(
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  AboutNewTab: "resource:///modules/AboutNewTab.jsm",
   ActorManagerParent: "resource://gre/modules/ActorManagerParent.jsm",
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm",
@@ -225,28 +224,6 @@ let JSWINDOWACTORS = {
     remoteTypes: ["privilegedabout"],
   },
 
-  AboutNewTab: {
-    parent: {
-      moduleURI: "resource:///actors/AboutNewTabParent.jsm",
-    },
-    child: {
-      moduleURI: "resource:///actors/AboutNewTabChild.jsm",
-      events: {
-        DOMContentLoaded: {},
-        pageshow: {},
-        visibilitychange: {},
-      },
-    },
-    // The wildcard on about:newtab is for the ?endpoint query parameter
-    // that is used for snippets debugging. The wildcard for about:home
-    // is similar, and also allows for falling back to loading the
-    // about:home document dynamically if an attempt is made to load
-    // about:home?jscache from the AboutHomeStartupCache as a top-level
-    // load.
-    matches: ["about:home*", "about:welcome", "about:newtab*"],
-    remoteTypes: ["privilegedabout"],
-  },
-
   AboutPlugins: {
     parent: {
       moduleURI: "resource:///actors/AboutPluginsParent.jsm",
@@ -1599,8 +1576,6 @@ BrowserGlue.prototype = {
 
   // the first browser window has finished initializing
   _onFirstWindowLoaded: function BG__onFirstWindowLoaded(aWindow) {
-    AboutNewTab.init();
-
     TabCrashHandler.init();
 
     ProcessHangMonitor.init();
@@ -5792,12 +5767,8 @@ var AboutHomeStartupCache = {
       return { pageInputStream: null, scriptInputStream: null };
     }
 
-    let state = AboutNewTab.activityStream.store.getState();
-    return new Promise(resolve => {
-      this._cacheDeferred = resolve;
-      this.log.trace("Parent is requesting cache streams.");
-      this._procManager.sendAsyncMessage(this.CACHE_REQUEST_MESSAGE, { state });
-    });
+    this.log.error("Activity Stream is disabled in Tor Browser.");
+    return { pageInputStream: null, scriptInputStream: null };
   },
 
   /**
diff --git a/browser/components/controlcenter/content/identityPanel.inc.xhtml b/browser/components/controlcenter/content/identityPanel.inc.xhtml
index ad6f9db340ef..498f374ffde8 100644
--- a/browser/components/controlcenter/content/identityPanel.inc.xhtml
+++ b/browser/components/controlcenter/content/identityPanel.inc.xhtml
@@ -92,6 +92,28 @@
         </vbox>
       </hbox>
 
+      <!-- Circuit display section -->
+
+      <vbox id="circuit-display-container" class="identity-popup-section">
+        <toolbarseparator/>
+        <vbox id="circuit-display-header" flex="1" role="group"
+              aria-labelledby="circuit-display-headline">
+          <hbox flex="1">
+            <label id="circuit-display-headline"
+                   role="heading" aria-level="2">&torbutton.circuit_display.title;</label>
+          </hbox>
+        </vbox>
+        <vbox id="circuit-display-content">
+          <html:ul id="circuit-display-nodes" dir="auto"/>
+          <hbox id="circuit-guard-note-container"/>
+          <hbox id="circuit-reload-button-container">
+            <html:button id="circuit-reload-button"
+                    onclick="torbutton_new_circuit()"
+                    default="true">&torbutton.circuit_display.new_circuit;</html:button>
+          </hbox>
+        </vbox>
+      </vbox>
+
       <!-- Clear Site Data Button -->
       <vbox hidden="true"
             id="identity-popup-clear-sitedata-footer">
diff --git a/browser/components/newtab/AboutNewTabService.jsm b/browser/components/newtab/AboutNewTabService.jsm
index f3bc40019f8f..471a3139baa7 100644
--- a/browser/components/newtab/AboutNewTabService.jsm
+++ b/browser/components/newtab/AboutNewTabService.jsm
@@ -420,20 +420,7 @@ class BaseAboutNewTabService {
    * the newtab page has no effect on the result of this function.
    */
   get defaultURL() {
-    // Generate the desired activity stream resource depending on state, e.g.,
-    // "resource://activity-stream/prerendered/activity-stream.html"
-    // "resource://activity-stream/prerendered/activity-stream-debug.html"
-    // "resource://activity-stream/prerendered/activity-stream-noscripts.html"
-    return [
-      "resource://activity-stream/prerendered/",
-      "activity-stream",
-      // Debug version loads dev scripts but noscripts separately loads scripts
-      this.activityStreamDebug && !this.privilegedAboutProcessEnabled
-        ? "-debug"
-        : "",
-      this.privilegedAboutProcessEnabled ? "-noscripts" : "",
-      ".html",
-    ].join("");
+    return "about:tor";
   }
 
   get welcomeURL() {
diff --git a/browser/components/places/content/places.xhtml b/browser/components/places/content/places.xhtml
index e19a58029828..f55d813664f6 100644
--- a/browser/components/places/content/places.xhtml
+++ b/browser/components/places/content/places.xhtml
@@ -18,6 +18,9 @@
 <!DOCTYPE window [
 <!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
 %editMenuOverlayDTD;
+#ifdef XP_MACOSX
+#include ../../../base/content/browser-doctype.inc
+#endif
 ]>
 
 <window id="places"
diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml
index 5bb936782ed9..e812d969837e 100644
--- a/browser/components/preferences/home.inc.xhtml
+++ b/browser/components/preferences/home.inc.xhtml
@@ -33,7 +33,7 @@
                 class="check-home-page-controlled"
                 data-preference-related="browser.startup.homepage">
         <menupopup>
-          <menuitem value="0" data-l10n-id="home-mode-choice-default" />
+          <menuitem value="0" label="&aboutTor.title;" />
           <menuitem value="2" data-l10n-id="home-mode-choice-custom" />
           <menuitem value="1" data-l10n-id="home-mode-choice-blank" />
         </menupopup>
@@ -84,7 +84,7 @@
         Preferences so we need to handle setting the pref manually.-->
       <menulist id="newTabMode" flex="1" data-preference-related="browser.newtabpage.enabled">
         <menupopup>
-          <menuitem value="0" data-l10n-id="home-mode-choice-default" />
+          <menuitem value="0" label="&aboutTor.title;" />
           <menuitem value="1" data-l10n-id="home-mode-choice-blank" />
         </menupopup>
       </menulist>
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index 8706870466fa..f1a8115843a3 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -15,7 +15,10 @@
 <?xml-stylesheet href="chrome://browser/skin/preferences/privacy.css"?>
 <?xml-stylesheet href="chrome://browser/content/securitylevel/securityLevelPreferences.css"?>
 
-<!DOCTYPE html>
+<!DOCTYPE html [
+<!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
+  %aboutTorDTD;
+]>
 
 <html xmlns="http://www.w3.org/1999/xhtml"
         xmlns:html="http://www.w3.org/1999/xhtml"
diff --git a/browser/components/shell/content/setDesktopBackground.xhtml b/browser/components/shell/content/setDesktopBackground.xhtml
index a7d4ced792b6..ef102450ff2c 100644
--- a/browser/components/shell/content/setDesktopBackground.xhtml
+++ b/browser/components/shell/content/setDesktopBackground.xhtml
@@ -7,6 +7,12 @@
 <?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/setDesktopBackground.css" type="text/css"?>
 
+<!DOCTYPE window [
+#ifdef XP_MACOSX
+#include ../../../base/content/browser-doctype.inc
+#endif
+]>
+
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml"
         windowtype="Shell:SetDesktopBackground"
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index cb7891d5b43e..04acc38fa5cf 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -235,6 +235,8 @@
 @RESPATH@/chrome/pdfjs.manifest
 @RESPATH@/chrome/pdfjs/*
 @RESPATH@/components/tor-launcher.manifest
+ at RESPATH@/chrome/torbutton.manifest
+ at RESPATH@/chrome/torbutton/*
 @RESPATH@/chrome/toolkit at JAREXT@
 @RESPATH@/chrome/toolkit.manifest
 #ifdef MOZ_GTK
diff --git a/browser/modules/HomePage.jsm b/browser/modules/HomePage.jsm
index f73b0f3e6c8c..26618374df3a 100644
--- a/browser/modules/HomePage.jsm
+++ b/browser/modules/HomePage.jsm
@@ -21,7 +21,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
 });
 
 const kPrefName = "browser.startup.homepage";
-const kDefaultHomePage = "about:home";
+const kDefaultHomePage = "about:tor";
 const kExtensionControllerPref =
   "browser.startup.homepage_override.extensionControlled";
 const kHomePageIgnoreListId = "homepage-urls";
diff --git a/browser/themes/shared/icons/new_circuit.svg b/browser/themes/shared/icons/new_circuit.svg
new file mode 100644
index 000000000000..ddc819946818
--- /dev/null
+++ b/browser/themes/shared/icons/new_circuit.svg
@@ -0,0 +1,6 @@
+<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 stroke="none" stroke-width="1" fill="context-fill" fill-rule="evenodd" opacity="context-fill-opacity">
+        <path d="m10.707 6h3.993l.3-.3v-3.993c.0002-.09902-.0291-.19586-.084-.27825s-.1331-.14661-.2245-.18453c-.0915-.03792-.1922-.04782-.2893-.02845-.0971.01936-.1863.06713-.2562.13723l-1.459 1.459c-1.2817-1.16743-2.95335-1.813714-4.687-1.812-3.859 0-7 3.141-7 7s3.141 7 7 7c1.74123.007 3.422-.6379 4.7116-1.8079 1.2896-1.1701 2.0945-2.7804 2.2564-4.5141.0156-.1649-.0348-.32927-.1401-.4571s-.2571-.2087-.4219-.2249c-.1644-.01324-.3275.03801-.4548.1429s-.2088.2552-.2272.4191c-.1334 1.42392 [...]
+        <path d="m8 12.5c-2.48528 0-4.5-2.0147-4.5-4.5 0-2.48528 2.01472-4.5 4.5-4.5z"/>
+    </g>
+</svg>
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index d04f95e59ea0..95fe3fdbe299 100644
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -266,3 +266,5 @@
   skin/classic/browser/syncedtabs/sidebar.css                  (../shared/syncedtabs/sidebar.css)
 
   skin/classic/browser/new_identity.svg                        (../shared/icons/new_identity.svg)
+
+  skin/classic/browser/new_circuit.svg                         (../shared/icons/new_circuit.svg)
diff --git a/browser/themes/shared/toolbarbutton-icons.css b/browser/themes/shared/toolbarbutton-icons.css
index 8e285fdfd7c2..b2cb4d277e1e 100644
--- a/browser/themes/shared/toolbarbutton-icons.css
+++ b/browser/themes/shared/toolbarbutton-icons.css
@@ -267,6 +267,10 @@ toolbar {
   list-style-image: url("chrome://browser/skin/new_identity.svg");
 }
 
+#new-circuit-button {
+  list-style-image: url("chrome://browser/skin/new_circuit.svg");
+}
+
 #privatebrowsing-button {
   list-style-image: url("chrome://browser/skin/privateBrowsing.svg");
 }
diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp
index e28bec9bd2c2..232104214844 100644
--- a/docshell/base/nsAboutRedirector.cpp
+++ b/docshell/base/nsAboutRedirector.cpp
@@ -174,7 +174,11 @@ static const RedirEntry kRedirMap[] = {
      nsIAboutModule::HIDE_FROM_ABOUTABOUT |
          nsIAboutModule::URI_CAN_LOAD_IN_CHILD |
          nsIAboutModule::URI_MUST_LOAD_IN_CHILD},
-    {"crashgpu", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT}};
+    {"crashgpu", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT},
+    {"tor", "chrome://torbutton/content/aboutTor/aboutTor.xhtml",
+     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
+         nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+         nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}};
 static const int kRedirTotal = mozilla::ArrayLength(kRedirMap);
 
 NS_IMETHODIMP
diff --git a/docshell/build/components.conf b/docshell/build/components.conf
index 5f11df641e37..6bc8617c8f0a 100644
--- a/docshell/build/components.conf
+++ b/docshell/build/components.conf
@@ -29,6 +29,7 @@ about_pages = [
     'srcdoc',
     'support',
     'telemetry',
+    'tor',
     'url-classifier',
     'webrtc',
 ]
diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in
index d9e8407ade7a..ead4a138d927 100644
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -137,6 +137,10 @@
 ; Base Browser
 @BINPATH@/components/SecurityLevel.manifest
 
+; Torbutton
+ at BINPATH@/chrome/torbutton at JAREXT@
+ at BINPATH@/chrome/torbutton.manifest
+
 ; [Default Preferences]
 ; All the pref files must be part of base to prevent migration bugs
 #ifndef MOZ_ANDROID_FAT_AAR_ARCHITECTURES
diff --git a/toolkit/moz.build b/toolkit/moz.build
index d464f7eb8092..cf9f7202ece0 100644
--- a/toolkit/moz.build
+++ b/toolkit/moz.build
@@ -22,6 +22,7 @@ DIRS += [
     "mozapps/preferences",
     "profile",
     "themes",
+    "torbutton",
 ]
 
 if CONFIG["OS_ARCH"] == "WINNT" and CONFIG["MOZ_DEFAULT_BROWSER_AGENT"]:
diff --git a/toolkit/torbutton/.gitignore b/toolkit/torbutton/.gitignore
new file mode 100644
index 000000000000..753ac2e10e5a
--- /dev/null
+++ b/toolkit/torbutton/.gitignore
@@ -0,0 +1 @@
+translation
diff --git a/toolkit/torbutton/CREDITS b/toolkit/torbutton/CREDITS
new file mode 100644
index 000000000000..c2a0be95196a
--- /dev/null
+++ b/toolkit/torbutton/CREDITS
@@ -0,0 +1,5 @@
+TorButton was adapted by Scott Squires from ProxyButton
+(proxybutton.mozdev.com), which was written by Oleg Ivanov.
+
+Security components by Mike Perry with History blocking and cookie jar code
+from Collin Jackson.
diff --git a/toolkit/torbutton/LICENSE b/toolkit/torbutton/LICENSE
new file mode 100644
index 000000000000..783d6a4705a1
--- /dev/null
+++ b/toolkit/torbutton/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2020, The Tor Project, Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    (1) Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+
+    (2) Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in
+    the documentation and/or other materials provided with the
+    distribution.
+
+    (3)The name of the author may not be used to
+    endorse or promote products derived from this software without
+    specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/toolkit/torbutton/chrome/content/aboutTor/aboutTor-content.js b/toolkit/torbutton/chrome/content/aboutTor/aboutTor-content.js
new file mode 100644
index 000000000000..6c53dff832e5
--- /dev/null
+++ b/toolkit/torbutton/chrome/content/aboutTor/aboutTor-content.js
@@ -0,0 +1,139 @@
+/*************************************************************************
+ * Copyright (c) 2019, The Tor Project, Inc.
+ * See LICENSE for licensing information.
+ *
+ * vim: set sw=2 sts=2 ts=8 et syntax=javascript:
+ *
+ * about:tor content script
+ *************************************************************************/
+
+/*
+ * The following about:tor IPC messages are exchanged by this code and
+ * the code in torbutton.js:
+ *   AboutTor:Loaded          page loaded            content -> chrome
+ *   AboutTor:ChromeData      privileged data        chrome -> content
+ */
+
+/* globals content, addMessageListener, sendAsyncMessage,
+   removeMessageListener */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { bindPrefAndInit, getLocale } = ChromeUtils.import(
+  "resource://torbutton/modules/utils.js"
+);
+
+var AboutTorListener = {
+  kAboutTorLoadedMessage: "AboutTor:Loaded",
+  kAboutTorChromeDataMessage: "AboutTor:ChromeData",
+
+  get isAboutTor() {
+    return content.document.documentURI.toLowerCase() == "about:tor";
+  },
+
+  init(aChromeGlobal) {
+    aChromeGlobal.addEventListener("AboutTorLoad", this, false, true);
+  },
+
+  handleEvent(aEvent) {
+    if (!this.isAboutTor) {
+      return;
+    }
+
+    switch (aEvent.type) {
+      case "AboutTorLoad":
+        this.onPageLoad();
+        break;
+      case "pagehide":
+        this.onPageHide();
+        break;
+    }
+  },
+
+  receiveMessage(aMessage) {
+    if (!this.isAboutTor) {
+      return;
+    }
+
+    switch (aMessage.name) {
+      case this.kAboutTorChromeDataMessage:
+        this.onChromeDataUpdate(aMessage.data);
+        break;
+    }
+  },
+
+  onPageLoad() {
+    // Arrange to update localized text and links.
+    bindPrefAndInit("intl.locale.requested", () => {
+      this.onLocaleChange();
+    });
+
+    // Add message and event listeners.
+    addMessageListener(this.kAboutTorChromeDataMessage, this);
+    addEventListener("pagehide", this, false);
+    addEventListener("resize", this, false);
+
+    sendAsyncMessage(this.kAboutTorLoadedMessage);
+  },
+
+  onPageHide() {
+    removeEventListener("resize", this, false);
+    removeEventListener("pagehide", this, false);
+    removeMessageListener(this.kAboutTorChromeDataMessage, this);
+  },
+
+  onChromeDataUpdate(aData) {
+    let body = content.document.body;
+
+    // Update status: tor on/off, Tor Browser manual shown.
+    if (aData.torOn) {
+      body.setAttribute("toron", "yes");
+    } else {
+      body.removeAttribute("toron");
+    }
+
+    if (aData.updateChannel) {
+      body.setAttribute("updatechannel", aData.updateChannel);
+    } else {
+      body.removeAttribute("updatechannel");
+    }
+
+    if (aData.hasBeenUpdated) {
+      body.setAttribute("hasbeenupdated", "yes");
+      content.document
+        .getElementById("update-infolink")
+        .setAttribute("href", aData.updateMoreInfoURL);
+    }
+
+    if (aData.mobile) {
+      body.setAttribute("mobile", "yes");
+    }
+
+    // Setting body.initialized="yes" displays the body.
+    body.setAttribute("initialized", "yes");
+  },
+
+  onLocaleChange() {
+    // Set localized "Get Involved" link.
+    content.document.getElementById("getInvolvedLink").href =
+      `https://community.torproject.org/${getLocale()}`;
+
+    // Display the Tor Browser product name and version.
+    try {
+      const kBrandBundle = "chrome://branding/locale/brand.properties";
+      let brandBundle = Services.strings.createBundle(kBrandBundle);
+      let productName = brandBundle.GetStringFromName("brandFullName");
+      let tbbVersion = Services.prefs.getCharPref("torbrowser.version");
+      let elem = content.document.getElementById("torbrowser-version");
+
+      while (elem.firstChild) {
+        elem.firstChild.remove();
+      }
+      elem.appendChild(
+        content.document.createTextNode(productName + " " + tbbVersion)
+      );
+    } catch (e) {}
+  },
+};
+
+AboutTorListener.init(this);
diff --git a/toolkit/torbutton/chrome/content/aboutTor/aboutTor.xhtml b/toolkit/torbutton/chrome/content/aboutTor/aboutTor.xhtml
new file mode 100644
index 000000000000..e48c22630150
--- /dev/null
+++ b/toolkit/torbutton/chrome/content/aboutTor/aboutTor.xhtml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   - Copyright (c) 2020, The Tor Project, Inc.
+   - See LICENSE for licensing information.
+   - vim: set sw=2 sts=2 ts=8 et syntax=xml:
+  -->
+
+<!DOCTYPE html [
+  <!ENTITY % htmlDTD
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "DTD/xhtml1-strict.dtd">
+  %htmlDTD;
+  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+  %globalDTD;
+  <!ENTITY % aboutTorDTD SYSTEM "chrome://torbutton/locale/aboutTor.dtd">
+  %aboutTorDTD;
+  <!ENTITY % tbUpdateDTD SYSTEM "chrome://browser/locale/aboutTBUpdate.dtd">
+  %tbUpdateDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta http-equiv="Content-Security-Policy" content="default-src resource:; object-src 'none'" />
+  <meta name="viewport" content="width=device-width, initial-scale=1"/>
+  <title>&aboutTor.title;</title>
+  <link rel="stylesheet" href="chrome://browser/skin/onionPattern.css" type="text/css" media="all" />
+  <link rel="stylesheet" href="resource://torbutton-assets/aboutTor.css" type="text/css" media="all" />
+  <script type="text/javascript" src="resource://torbutton-abouttor/aboutTor.js"></script>
+</head>
+<body dir="&locale.dir;">
+  <div class="torcontent-container">
+    <div id="torbrowser-info">
+      <div id="torbrowser-version"/>
+      <div id="torbrowser-changelog-link"><a href="about:tbupdate">&aboutTor.viewChangelog.label;</a></div>
+    </div>
+    <img class="torcontent-logo" src="resource://torbutton-assets/torbrowser_mobile_logo.png"/>
+    <div id="torstatus" class="top">
+      <div class="hideIfTorOff hideIfHasBeenUpdated torstatus-container">
+#if MOZ_UPDATE_CHANNEL == alpha
+        <div class="heading1">&aboutTor.alpha.ready.label;</div>
+        <br/>
+        <div class="heading2">&aboutTor.alpha.ready2.label;</div>
+#elif MOZ_UPDATE_CHANNEL == nightly
+        <div class="heading1">&aboutTor.nightly.ready.label;</div>
+        <br/>
+        <div class="heading2">&aboutTor.nightly.ready2.label;</div>
+#else
+        <div class="heading1">&aboutTor.ready.label;</div>
+        <br/>
+        <div class="heading2">&aboutTor.ready2.label;</div>
+#endif
+      </div>
+      <div class="showIfHasBeenUpdated torstatus-container">
+        <div class="heading1">&aboutTBUpdate.updated;</div>
+        <br/>
+        <div class="heading2">&aboutTBUpdate.linkPrefix;<a id="update-infolink">&aboutTBUpdate.linkLabel;</a>&aboutTBUpdate.linkSuffix;</div>
+      </div>
+      <div class="hideIfTorOn torstatus-container">
+        <div class="heading1">&aboutTor.failure.label;</div>
+        <br/>
+        <div class="heading2">&aboutTor.failure2.label;</div>
+      </div>
+    </div>
+
+    <div class="searchbox hideIfTorOff"> <!-- begin form based search -->
+      <form action="&aboutTor.searchDDGPost.link;" method="get">
+        <div class="searchwrapper">
+          <label class="searchlabel" for="search-text"></label>
+          <input name="q" id="search-text" placeholder="&aboutTor.search.label;"
+                 autocomplete="off" type="text"/>
+          <input id="search-button" value=""
+                 title="&aboutTor.search.label;"
+                 alt="&aboutTor.search.label;" type="submit"/>
+        </div>
+      </form>
+    </div>
+
+#if MOZ_UPDATE_CHANNEL != release
+    <div id="bannerRoot">
+        <img id="bannerImg" src="resource://torbutton-assets/banner-warning.svg" />
+        <div id="bannerTextCol">
+            <!--<div id="bannerHeader"></div>
+            <br />-->
+#if MOZ_UPDATE_CHANNEL == alpha
+            <div id="bannerDescription">&aboutTor.alpha.bannerDescription;</div>
+            <br />
+            <a id="bannerLink" target="_blank" href="https://forum.torproject.net/">&aboutTor.alpha.bannerLink;</a>
+#else
+            <div id="bannerDescription">&aboutTor.nightly.bannerDescription;</div>
+            <br />
+            <a id="bannerLink" target="_blank" href="https://forum.torproject.net/">&aboutTor.nightly.bannerLink;</a>
+#endif
+        </div>
+    </div>
+#endif
+
+    <div id="bottom">
+      <p id="donate" class="moreInfoLink">&aboutTor.donationBanner.freeToUse;
+        <a href="https://donate.torproject.org/" target="_blank">&aboutTor.donationBanner.buttonA; »</a>
+      </p>
+      <p id="manual" class="showForManual moreInfoLink">&aboutTor.torbrowser_user_manual_questions.label;
+        <a id="manualLink" href="about:manual" target="_blank">&aboutTor.torbrowser_user_manual_link.label;</a></p>
+      <p id="newsletter" class="moreInfoLink"><img class="imageStyle" src="chrome://browser/skin/mail.svg"/>&aboutTor.newsletter.tagline;<br/>
+        <a href="https://newsletter.torproject.org">&aboutTor.newsletter.link_text; »</a>
+      </p>
+      <p id="mission">&aboutTor.tor_mission.label;
+        <a id="getInvolvedLink">&aboutTor.getInvolved.label;</a></p>
+    </div>
+  </div>
+#include ../../../../../browser/themes/shared/onionPattern.inc.xhtml
+</body>
+</html>
diff --git a/toolkit/torbutton/chrome/content/aboutTor/resources/aboutTor.js b/toolkit/torbutton/chrome/content/aboutTor/resources/aboutTor.js
new file mode 100644
index 000000000000..6687390b6aca
--- /dev/null
+++ b/toolkit/torbutton/chrome/content/aboutTor/resources/aboutTor.js
@@ -0,0 +1,11 @@
+/*************************************************************************
+ * Copyright (c) 2020, The Tor Project, Inc.
+ * See LICENSE for licensing information.
+ *
+ * vim: set sw=2 sts=2 ts=8 et syntax=javascript:
+ *************************************************************************/
+
+window.addEventListener("pageshow", function() {
+  let evt = new CustomEvent("AboutTorLoad", { bubbles: true });
+  document.dispatchEvent(evt);
+});
diff --git a/toolkit/torbutton/chrome/content/preferences-mobile.js b/toolkit/torbutton/chrome/content/preferences-mobile.js
new file mode 100644
index 000000000000..b696ef17fd32
--- /dev/null
+++ b/toolkit/torbutton/chrome/content/preferences-mobile.js
@@ -0,0 +1,59 @@
+// # Security Settings User Interface for Mobile
+
+// Utilities
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {
+  getBoolPref,
+  getIntPref,
+  setBoolPref,
+  setIntPref,
+  getCharPref,
+} = Services.prefs;
+
+// Description elements have the follow names.
+const descNames = ["", "desc_standard", "desc_safer", "desc_safest"];
+// "Learn-more"-elements have the follow names.
+const linkNames = ["", "link_standard", "link_safer", "link_safest"];
+// A single `state` object that reflects the user settings in this UI.
+
+let state = { slider: 0, custom: false };
+
+// Utility functions to convert between the legacy 4-value pref index
+// and the 3-valued security slider.
+let sliderPositionToPrefSetting = pos => [0, 4, 2, 1][pos];
+let prefSettingToSliderPosition = pref => [0, 3, 2, 2, 1][pref];
+
+// Set the desired slider value and update UI.
+function torbutton_set_slider(sliderValue) {
+  state.slider = sliderValue;
+  let slider = document.getElementById("torbutton_sec_slider");
+  slider.value = sliderValue.toString();
+  let descs = descNames.map(name => document.getElementById(name));
+  descs.forEach((desc, i) => {
+    if (state.slider !== i) {
+      desc.style.display = "none";
+    } else {
+      desc.style.display = "block";
+    }
+  });
+  torbutton_save_security_settings();
+}
+
+// Read prefs 'browser.security_level.security_slider' and
+// 'browser.security_level.security_custom', and initialize the UI.
+function torbutton_init_security_ui() {
+  torbutton_set_slider(
+    prefSettingToSliderPosition(
+      getIntPref("browser.security_level.security_slider")
+    )
+  );
+}
+
+// Write the two prefs from the current settings.
+function torbutton_save_security_settings() {
+  setIntPref(
+    "browser.security_level.security_slider",
+    sliderPositionToPrefSetting(state.slider)
+  );
+  setBoolPref("browser.security_level.security_custom", state.custom);
+}
diff --git a/toolkit/torbutton/chrome/content/preferences.xhtml b/toolkit/torbutton/chrome/content/preferences.xhtml
new file mode 100644
index 000000000000..eafbf01e7d6c
--- /dev/null
+++ b/toolkit/torbutton/chrome/content/preferences.xhtml
@@ -0,0 +1,84 @@
+<!DOCTYPE overlay SYSTEM "chrome://torbutton/locale/torbutton.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <meta charset="UTF-8"/>
+  <meta name="viewport" content="width=device-width, initial-scale=1"/>
+  <head>
+    <title>&torbutton.prefs.security_settings;</title>
+    <link type="text/css" rel="stylesheet" charset="UTF-8" href="chrome://torbutton/skin/preferences.css"/>
+    <link type="text/css" rel="stylesheet" charset="UTF-8" href="chrome://torbutton/skin/preferences-mobile.css"/>
+    <script type="text/javascript" src="preferences-mobile.js"></script>
+    <style>
+    </style>
+  </head>
+  <body onload="torbutton_init_security_ui()">
+    <div class="wrapper outer-wrapper">
+      <div class="title">
+        &torbutton.prefs.sec_caption;
+      </div>
+      <div class="wrapper inner-wrapper">
+	      <input id="torbutton_sec_slider" type="range" min="1" max="3" list="datalist" onchange="torbutton_set_slider(parseInt(this.value, 10))"/>
+	      <datalist id="datalist">
+          <option onclick="torbutton_set_slider(1)">
+            &torbutton.prefs.sec_standard_label;
+          </option>
+          <option onclick="torbutton_set_slider(2)">
+            &torbutton.prefs.sec_safer_label;
+          </option>
+          <option onclick="torbutton_set_slider(3)">
+            &torbutton.prefs.sec_safest_label;
+          </option>
+	      </datalist>
+        <div class="description-wrapper">
+          <div id="desc_safest" class="description">
+            <p class="slider-text-size slider-text-weight">
+              &torbutton.prefs.sec_safest_description;
+            </p>
+            <p class="slider-text-size slider-text-weight">
+              &torbutton.prefs.sec_safest_list_label;
+            </p>
+            <p class="slider-text-size">
+              &torbutton.prefs.sec_js_disabled;
+            </p>
+            <p class="slider-text-size">
+              &torbutton.prefs.sec_limit_graphics_and_typography;
+            </p>
+            <p class="slider-text-size">
+              &torbutton.prefs.sec_click_to_play_media;
+            </p>
+            <a id="link_safest" class="text-link">
+              &torbutton.prefs.sec_learn_more_label;
+            </a>
+          </div>
+          <div id="desc_safer" class="description">
+            <p class="slider-text-size slider-text-weight">
+              &torbutton.prefs.sec_safer_description;
+            </p>
+            <p class="slider-text-size slider-text-weight">
+              &torbutton.prefs.sec_safer_list_label;
+            </p>
+            <p class="slider-text-size">
+              &torbutton.prefs.sec_js_on_https_sites_only;
+            </p>
+            <p class="slider-text-size">
+              &torbutton.prefs.sec_limit_typography;
+            </p>
+            <p class="slider-text-size">
+              &torbutton.prefs.sec_click_to_play_media;
+            </p>
+            <a id="link_safer" class="text-link">
+              &torbutton.prefs.sec_learn_more_label;
+            </a>
+          </div>
+          <div id="desc_standard" class="description">
+            <p class="slider-text-size slider-text-weight">
+              &torbutton.prefs.sec_standard_description;
+            </p>
+            <a id="link_standard" class="text-link">
+              &torbutton.prefs.sec_learn_more_label;
+            </a>
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
diff --git a/toolkit/torbutton/chrome/content/tor-circuit-display.js b/toolkit/torbutton/chrome/content/tor-circuit-display.js
new file mode 100644
index 000000000000..76fb74546ac6
--- /dev/null
+++ b/toolkit/torbutton/chrome/content/tor-circuit-display.js
@@ -0,0 +1,604 @@
+// A script that automatically displays the Tor Circuit used for the
+// current domain for the currently selected tab.
+//
+// This file is written in call stack order (later functions
+// call earlier functions). The file can be processed
+// with docco.js to produce pretty documentation.
+//
+// This script is to be embedded in torbutton.xhtml. It defines a single global
+// function, createTorCircuitDisplay(), which activates the automatic Tor
+// circuit display for the current tab and any future tabs.
+//
+// See https://trac.torproject.org/8641
+
+/* jshint esnext: true */
+/* global document, gBrowser, Components */
+
+// ### Main function
+// __createTorCircuitDisplay(enablePrefName)__.
+// The single function that prepares tor circuit display. Connects to a tor
+// control port using information provided to the control port module via
+// a previous call to configureControlPortModule(), and binds to a named
+// bool pref whose value determines whether the circuit display is enabled
+// or disabled.
+let createTorCircuitDisplay = (function() {
+  "use strict";
+
+  // Mozilla utilities
+  const { Services } = ChromeUtils.import(
+    "resource://gre/modules/Services.jsm"
+  );
+
+  // Import the controller code.
+  const { wait_for_controller } = ChromeUtils.import(
+    "resource://torbutton/modules/tor-control-port.js"
+  );
+
+  // Utility functions
+  let {
+    bindPrefAndInit,
+    observe,
+    getLocale,
+    getDomainForBrowser,
+    torbutton_get_property_string,
+  } = ChromeUtils.import("resource://torbutton/modules/utils.js");
+
+  // Make the TorButton logger available.
+  let logger = Cc["@torproject.org/torbutton-logger;1"].getService(
+    Ci.nsISupports
+  ).wrappedJSObject;
+
+  // ## Circuit/stream credentials and node monitoring
+
+  // A mutable map that stores the current nodes for each
+  // SOCKS username/password pair.
+  let credentialsToNodeDataMap = new Map(),
+    // A mutable map that reports `true` for IDs of "mature" circuits
+    // (those that have conveyed a stream).
+    knownCircuitIDs = new Map(),
+    // A mutable map that records the SOCKS credentials for the
+    // latest channels for each browser + domain.
+    browserToCredentialsMap = new Map();
+
+  // __trimQuotes(s)__.
+  // Removes quotation marks around a quoted string.
+  let trimQuotes = s => (s ? s.match(/^"(.*)"$/)[1] : undefined);
+
+  // __getBridge(id)__.
+  // Gets the bridge parameters for a given node ID. If the node
+  // is not currently used as a bridge, returns null.
+  let getBridge = async function(controller, id) {
+    let bridges = await controller.getConf("bridge");
+    if (bridges) {
+      for (let bridge of bridges) {
+        if (bridge.ID && bridge.ID.toUpperCase() === id.toUpperCase()) {
+          return bridge;
+        }
+      }
+    }
+    return null;
+  };
+
+  // nodeDataForID(controller, id)__.
+  // Returns the type, IP addresses and country code of a node with given ID.
+  // Example: `nodeDataForID(controller, "20BC91DC525C3DC9974B29FBEAB51230DE024C44")`
+  // => `{ type: "default", ipAddrs: ["12.23.34.45", "2001:db8::"], countryCode: "fr" }`
+  let nodeDataForID = async function(controller, id) {
+    let result = { ipAddrs: [] };
+    const bridge = await getBridge(controller, id); // type, ip, countryCode;
+    const addrRe = /^\[?([^\]]+)\]?:\d+$/;
+    if (bridge) {
+      result.type = "bridge";
+      result.bridgeType = bridge.type;
+      // Attempt to get an IP address from bridge address string.
+      try {
+        const ip = bridge.address.match(addrRe)[1];
+        if (!ip.startsWith("0.")) {
+          result.ipAddrs = [ip];
+        }
+      } catch (e) {}
+    } else {
+      // either dealing with a relay, or a bridge whose fingerprint is not saved in torrc
+      try {
+        const statusMap = await controller.getInfo("ns/id/" + id);
+        result.type = "default";
+        if (!statusMap.IP.startsWith("0.")) {
+          result.ipAddrs.push(statusMap.IP);
+        }
+        try {
+          result.ipAddrs.push(statusMap.IPv6.match(addrRe)[1]);
+        } catch (e) {}
+      } catch (e) {
+        // getInfo will throw if the given id is not a relay
+        // this probably means we are dealing with a user-provided bridge with no fingerprint
+        result.type = "bridge";
+        // we don't know the ip/ipv6 or type, so leave blank
+        result.ipAddrs = [];
+        result.bridgeType = "";
+      }
+    }
+    if (result.ipAddrs.length) {
+      // Get the country code for the node's IP address.
+      try {
+        const countryCode = await controller.getInfo(
+          "ip-to-country/" + result.ipAddrs[0]
+        );
+        result.countryCode = countryCode === "??" ? null : countryCode;
+      } catch (e) {}
+    }
+    return result;
+  };
+
+  // __nodeDataForCircuit(controller, circuitEvent)__.
+  // Gets the information for a circuit.
+  let nodeDataForCircuit = async function(controller, circuitEvent) {
+    let rawIDs = circuitEvent.circuit.map(circ => circ[0]),
+      // Remove the leading '$' if present.
+      ids = rawIDs.map(id => (id[0] === "$" ? id.substring(1) : id));
+    // Get the node data for all IDs in circuit.
+    return Promise.all(ids.map(id => nodeDataForID(controller, id)));
+  };
+
+  // __getCircuitStatusByID(aController, circuitID)__
+  // Returns the circuit status for the circuit with the given ID.
+  let getCircuitStatusByID = async function(aController, circuitID) {
+    let circuitStatuses = await aController.getInfo("circuit-status");
+    if (circuitStatuses) {
+      for (let circuitStatus of circuitStatuses) {
+        if (circuitStatus.id === circuitID) {
+          return circuitStatus;
+        }
+      }
+    }
+    return null;
+  };
+
+  // __collectIsolationData(aController, updateUI)__.
+  // Watches for STREAM SENTCONNECT events. When a SENTCONNECT event occurs, then
+  // we assume isolation settings (SOCKS username+password) are now fixed for the
+  // corresponding circuit. Whenever the first stream on a new circuit is seen,
+  // looks up u+p and records the node data in the credentialsToNodeDataMap.
+  // We need to update the circuit display immediately after any new node data
+  // is received. So the `updateUI` callback will be called at that point.
+  // See https://trac.torproject.org/projects/tor/ticket/15493
+  let collectIsolationData = function(aController, updateUI) {
+    return aController.watchEvent(
+      "STREAM",
+      streamEvent => streamEvent.StreamStatus === "SENTCONNECT",
+      async streamEvent => {
+        if (!knownCircuitIDs.get(streamEvent.CircuitID)) {
+          logger.eclog(3, "streamEvent.CircuitID: " + streamEvent.CircuitID);
+          knownCircuitIDs.set(streamEvent.CircuitID, true);
+          let circuitStatus = await getCircuitStatusByID(
+              aController,
+              streamEvent.CircuitID
+            ),
+            credentials = circuitStatus
+              ? trimQuotes(circuitStatus.SOCKS_USERNAME) +
+                "|" +
+                trimQuotes(circuitStatus.SOCKS_PASSWORD)
+              : null;
+          if (credentials) {
+            let nodeData = await nodeDataForCircuit(aController, circuitStatus);
+            credentialsToNodeDataMap.set(credentials, nodeData);
+            updateUI();
+          }
+        }
+      }
+    );
+  };
+
+  // __browserForChannel(channel)__.
+  // Returns the browser that loaded a given channel.
+  let browserForChannel = function(channel) {
+    if (!channel) {
+      return null;
+    }
+    let chan = channel.QueryInterface(Ci.nsIChannel);
+    let callbacks = chan.notificationCallbacks;
+    if (!callbacks) {
+      return null;
+    }
+    let loadContext;
+    try {
+      loadContext = callbacks.getInterface(Ci.nsILoadContext);
+    } catch (e) {
+      // Ignore
+      return null;
+    }
+    if (!loadContext) {
+      return null;
+    }
+    return loadContext.topFrameElement;
+  };
+
+  // __collectBrowserCredentials()__.
+  // Starts observing http channels. Each channel's proxyInfo
+  // username and password is recorded for the channel's browser.
+  let collectBrowserCredentials = function() {
+    return observe("http-on-modify-request", chan => {
+      try {
+        let proxyInfo = chan.QueryInterface(Ci.nsIProxiedChannel).proxyInfo;
+        let browser = browserForChannel(chan);
+        if (browser && proxyInfo) {
+          if (!browserToCredentialsMap.has(browser)) {
+            browserToCredentialsMap.set(browser, new Map());
+          }
+          let domainMap = browserToCredentialsMap.get(browser);
+          domainMap.set(proxyInfo.username, [
+            proxyInfo.username,
+            proxyInfo.password,
+          ]);
+        }
+      } catch (e) {
+        logger.eclog(
+          3,
+          `Error collecting browser credentials: ${e.message}, ${chan.URI.spec}`
+        );
+      }
+    });
+  };
+
+  // ## User interface
+
+  // __uiString__.
+  // Read the localized strings for this UI.
+  let uiString = function(shortName) {
+    return torbutton_get_property_string(
+      "torbutton.circuit_display." + shortName
+    );
+  };
+
+  // __localizedCountryNameFromCode(countryCode)__.
+  // Convert a country code to a localized country name.
+  // Example: `'de'` -> `'Deutschland'` in German locale.
+  let localizedCountryNameFromCode = function(countryCode) {
+    if (!countryCode) {
+      return uiString("unknown_country");
+    }
+    try {
+      return Services.intl.getRegionDisplayNames(undefined, [countryCode])[0];
+    } catch (e) {
+      return countryCode.toUpperCase();
+    }
+  };
+
+  // __showCircuitDisplay(show)__.
+  // If show === true, makes the circuit display visible.
+  let showCircuitDisplay = function(show) {
+    document.getElementById("circuit-display-container").hidden = !show;
+  };
+
+  // __xmlTree(ns, data)__.
+  // Takes an xml namespace, ns, and a
+  // data structure representing xml elements like
+  // [tag, { attr-key: attr-value }, ...xml-children]
+  // and returns nested xml element objects.
+  let xmlTree = function xmlTree(ns, data) {
+    let [type, attrs, ...children] = data;
+    let element = type.startsWith("html:")
+      ? document.createXULElement(type)
+      : document.createElementNS(ns, type);
+    for (let [key, val] of Object.entries(attrs)) {
+      element.setAttribute(key, val);
+    }
+    for (let child of children) {
+      if (child !== null && child !== undefined) {
+        element.append(typeof child === "string" ? child : xmlTree(ns, child));
+      }
+    }
+    return element;
+  };
+
+  // __htmlTree(data)__.
+  // Takes a data structure representing html elements like
+  // [tag, { attr-key: attr-value }, ...html-children]
+  // and returns nested html element objects.
+  let htmlTree = data => xmlTree("http://www.w3.org/1999/xhtml", data);
+
+  // __appendHtml(parent, data)__.
+  // Takes a data structure representing html elements like
+  // [tag, { attr-key: attr-value }, ...html-children]
+  // and appends nested html element objects to the parent element.
+  let appendHtml = (parent, data) => parent.appendChild(htmlTree(data));
+
+  // __circuitCircuitData()__.
+  // Obtains the circuit used by the given browser.
+  let currentCircuitData = function(browser) {
+    if (browser) {
+      let firstPartyDomain = getDomainForBrowser(browser);
+      let domain = firstPartyDomain || "--unknown--";
+      let domainMap = browserToCredentialsMap.get(browser);
+      let credentials = domainMap && domainMap.get(domain);
+      if (credentials) {
+        let [SOCKS_username, SOCKS_password] = credentials;
+        let nodeData = credentialsToNodeDataMap.get(
+          `${SOCKS_username}|${SOCKS_password}`
+        );
+        let domain = SOCKS_username;
+        if (browser.documentURI.host.endsWith(".tor.onion")) {
+          const service = Cc[
+            "@torproject.org/onion-alias-service;1"
+          ].getService(Ci.IOnionAliasService);
+          domain = service.getOnionAlias(browser.documentURI.host);
+        }
+        return { domain, nodeData };
+      }
+    }
+    return { domain: null, nodeData: null };
+  };
+
+  // __updateCircuitDisplay()__.
+  // Updates the Tor circuit display, showing the current domain
+  // and the relay nodes for that domain.
+  let updateCircuitDisplay = function() {
+    let { domain, nodeData } = currentCircuitData(gBrowser.selectedBrowser);
+    if (domain && nodeData) {
+      // Update the displayed information for the relay nodes.
+      let nodeHtmlList = document.getElementById("circuit-display-nodes");
+      let li = (...data) => appendHtml(nodeHtmlList, ["li", {}, ...data]);
+      nodeHtmlList.innerHTML = "";
+      li(uiString("this_browser"));
+      for (let i = 0; i < nodeData.length; ++i) {
+        let relayText;
+        if (nodeData[i].type === "bridge") {
+          relayText = uiString("tor_bridge");
+          let bridgeType = nodeData[i].bridgeType;
+          if (bridgeType === "meek_lite") {
+            relayText += ": meek";
+          } else if (bridgeType !== "vanilla" && bridgeType !== "") {
+            relayText += ": " + bridgeType;
+          }
+        } else if (nodeData[i].type == "default") {
+          relayText = localizedCountryNameFromCode(nodeData[i].countryCode);
+        }
+        const ipAddrs = nodeData[i].ipAddrs.join(", ");
+        li(
+          relayText,
+          " ",
+          ["span", { class: "circuit-ip-address" }, ipAddrs],
+          " ",
+          i === 0 && nodeData[0].type !== "bridge"
+            ? ["span", { class: "circuit-guard-info" }, uiString("guard")]
+            : null
+        );
+      }
+
+      let domainParts = [];
+      if (domain.endsWith(".onion")) {
+        for (let i = 0; i < 3; ++i) {
+          li(uiString("relay"));
+        }
+        if (domain.length > 22) {
+          domainParts.push(domain.slice(0, 7), "…", domain.slice(-12));
+        } else {
+          domainParts.push(domain);
+        }
+      } else {
+        domainParts.push(domain);
+      }
+
+      // We use a XUL html:span element so that the tooltiptext is displayed.
+      li([
+        "html:span",
+        {
+          class: "circuit-onion",
+          onclick: `
+          this.classList.add("circuit-onion-copied");
+          Cc[
+            "@mozilla.org/widget/clipboardhelper;1"
+          ].getService(Ci.nsIClipboardHelper).copyString(this.getAttribute("data-onion"))
+        `,
+          "data-onion": domain,
+          "data-text-clicktocopy": torbutton_get_property_string(
+            "torbutton.circuit_display.click_to_copy"
+          ),
+          "data-text-copied": torbutton_get_property_string(
+            "torbutton.circuit_display.copied"
+          ),
+          tooltiptext: domain,
+        },
+        ...domainParts,
+      ]);
+
+      // Hide the note about guards if we are using a bridge.
+      document.getElementById("circuit-guard-note-container").hidden =
+        nodeData[0].type === "bridge";
+    } else {
+      // Only show the Tor circuit if we have credentials and node data.
+      logger.eclog(4, "no SOCKS credentials found for current document.");
+    }
+    showCircuitDisplay(domain && nodeData);
+  };
+
+  // __syncDisplayWithSelectedTab(syncOn)__.
+  // Whenever the user starts to open the popup menu, make sure the display
+  // is the correct one for this tab. It's also possible that a new site
+  // can be loaded while the popup menu is open.
+  // Update the display if this happens.
+  let syncDisplayWithSelectedTab = (function() {
+    let listener = {
+      onLocationChange(aBrowser) {
+        if (aBrowser === gBrowser.selectedBrowser) {
+          updateCircuitDisplay();
+        }
+      },
+    };
+    return function(syncOn) {
+      let popupMenu = document.getElementById("identity-popup");
+      if (syncOn) {
+        // Update the circuit display just before the popup menu is shown.
+        popupMenu.addEventListener("popupshowing", updateCircuitDisplay);
+        // If the currently selected tab has been sent to a new location,
+        // update the circuit to reflect that.
+        gBrowser.addTabsProgressListener(listener);
+      } else {
+        // Stop syncing.
+        gBrowser.removeTabsProgressListener(listener);
+        popupMenu.removeEventListener("popupshowing", updateCircuitDisplay);
+        // Hide the display.
+        showCircuitDisplay(false);
+      }
+    };
+  })();
+
+  // __setupGuardNote()__.
+  // Call once to show the Guard note as intended.
+  let setupGuardNote = function() {
+    let guardNote = document.getElementById("circuit-guard-note-container");
+    let guardNoteString = uiString("guard_note");
+    let learnMoreString = uiString("learn_more");
+    let [noteBefore, name, noteAfter] = guardNoteString.split(/[\[\]]/);
+    let localeCode = getLocale();
+    appendHtml(guardNote, [
+      "div",
+      {},
+      noteBefore,
+      ["span", { class: "circuit-guard-name" }, name],
+      noteAfter,
+      " ",
+      [
+        "span",
+        {
+          onclick: `gBrowser.selectedTab = gBrowser.addWebTab('https://support.torproject.org/${localeCode}/tbb/tbb-2/');`,
+          class: "circuit-link",
+        },
+        learnMoreString,
+      ],
+    ]);
+  };
+
+  // __ensureCorrectPopupDimensions()__.
+  // Make sure the identity popup always displays with the correct height.
+  let ensureCorrectPopupDimensions = function() {
+    // FIXME: This is hacking with the internals of the panel view, which also
+    // sets the width and height for transitions (see PanelMultiView.jsm), so
+    // this fix is fragile.
+    // Ideally the panel view would start using a non-XUL CSS layout, which
+    // would work regardless of the content.
+    let popupMenu = document.getElementById("identity-popup");
+    let setDimensions = event => {
+      if (event.target !== popupMenu) {
+        return;
+      }
+      setTimeout(() => {
+        let view = document.querySelector(
+          "#identity-popup-multiView .panel-viewcontainer"
+        );
+        let stack = document.querySelector(
+          "#identity-popup-multiView .panel-viewstack"
+        );
+        let view2 = document.getElementById("identity-popup-mainView");
+        if (view && stack && view2) {
+          let newWidth = Math.max(
+            ...[...view2.children].map(el => el.clientWidth)
+          );
+          let newHeight = stack.clientHeight;
+          stack.setAttribute("width", newWidth);
+          view2.style.minWidth = view2.style.maxWidth = newWidth + "px";
+          view.setAttribute("width", newWidth);
+          view.setAttribute("height", newHeight);
+        }
+      });
+    };
+    let removeDimensions = event => {
+      if (event.target !== popupMenu) {
+        return;
+      }
+      let view = document.querySelector(
+        "#identity-popup-multiView .panel-viewcontainer"
+      );
+      let stack = document.querySelector(
+        "#identity-popup-multiView .panel-viewstack"
+      );
+      let view2 = document.getElementById("identity-popup-mainView");
+      if (view && stack && view2) {
+        view.removeAttribute("width");
+        view.removeAttribute("height");
+        stack.removeAttribute("width");
+        view2.style.minWidth = view2.style.maxWidth = "";
+      }
+    };
+    popupMenu.addEventListener("popupshowing", setDimensions);
+    popupMenu.addEventListener("popuphiding", removeDimensions);
+    return () => {
+      popupMenu.removeEventListener("popupshowing", setDimensions);
+      popupMenu.removeEventListener("popuphiding", removeDimensions);
+    };
+  };
+
+  // ## Main function
+
+  // __setupDisplay(enablePrefName)__.
+  // Once called, the Tor circuit display will be started whenever
+  // the "enablePref" is set to true, and stopped when it is set to false.
+  // A reference to this function (called createTorCircuitDisplay) is exported as a global.
+  let setupDisplay = function(enablePrefName) {
+    // From 79 on the identity popup is initialized lazily
+    if (gIdentityHandler._initializePopup) {
+      gIdentityHandler._initializePopup();
+    }
+    setupGuardNote();
+    let myController = null,
+      stopCollectingIsolationData = null,
+      stopCollectingBrowserCredentials = null,
+      stopEnsuringCorrectPopupDimensions = null,
+      stop = function() {
+        syncDisplayWithSelectedTab(false);
+        if (myController) {
+          if (stopCollectingIsolationData) {
+            stopCollectingIsolationData();
+          }
+          if (stopCollectingBrowserCredentials) {
+            stopCollectingBrowserCredentials();
+          }
+          if (stopEnsuringCorrectPopupDimensions) {
+            stopEnsuringCorrectPopupDimensions();
+          }
+          myController = null;
+        }
+      },
+      start = async function() {
+        if (!myController) {
+          try {
+            myController = await wait_for_controller();
+            syncDisplayWithSelectedTab(true);
+            stopCollectingIsolationData = collectIsolationData(
+              myController,
+              updateCircuitDisplay
+            );
+            stopCollectingBrowserCredentials = collectBrowserCredentials();
+            stopEnsuringCorrectPopupDimensions = ensureCorrectPopupDimensions();
+          } catch (err) {
+            logger.eclog(5, err);
+            logger.eclog(
+              5,
+              "Disabling tor display circuit because of an error."
+            );
+            myController.close();
+            stop();
+          }
+        }
+      };
+    try {
+      let unbindPref = bindPrefAndInit(enablePrefName, on => {
+        if (on) {
+          start();
+        } else {
+          stop();
+        }
+      });
+      // When this chrome window is unloaded, we need to unbind the pref.
+      window.addEventListener("unload", function() {
+        unbindPref();
+        stop();
+      });
+    } catch (e) {
+      logger.eclog(5, "Error: " + e.message + "\n" + e.stack);
+    }
+  };
+
+  return setupDisplay;
+
+  // Finish createTorCircuitDisplay()
+})();
diff --git a/toolkit/torbutton/chrome/content/torbutton.js b/toolkit/torbutton/chrome/content/torbutton.js
new file mode 100644
index 000000000000..0959efc4fd31
--- /dev/null
+++ b/toolkit/torbutton/chrome/content/torbutton.js
@@ -0,0 +1,1044 @@
+// window globals
+var torbutton_init;
+var torbutton_new_circuit;
+
+(() => {
+  // Bug 1506 P1-P5: This is the main Torbutton overlay file. Much needs to be
+  // preserved here, but in an ideal world, most of this code should perhaps be
+  // moved into an XPCOM service, and much can also be tossed. See also
+  // individual 1506 comments for details.
+
+  // TODO: check for leaks: http://www.mozilla.org/scriptable/avoiding-leaks.html
+  // TODO: Double-check there are no strange exploits to defeat:
+  //       http://kb.mozillazine.org/Links_to_local_pages_don%27t_work
+
+  /* global gBrowser, CustomizableUI,
+   createTorCircuitDisplay, gFindBarInitialized,
+   gFindBar, OpenBrowserWindow, PrivateBrowsingUtils,
+   Services, AppConstants
+ */
+
+  let {
+    unescapeTorString,
+    bindPrefAndInit,
+    getDomainForBrowser,
+    torbutton_log,
+    torbutton_get_property_string,
+  } = ChromeUtils.import("resource://torbutton/modules/utils.js");
+  let { configureControlPortModule, wait_for_controller } = ChromeUtils.import(
+    "resource://torbutton/modules/tor-control-port.js"
+  );
+
+  const { TorProtocolService } = ChromeUtils.import(
+    "resource://gre/modules/TorProtocolService.jsm"
+  );
+
+  const k_tb_tor_check_failed_topic = "Torbutton:TorCheckFailed";
+
+  var m_tb_prefs = Services.prefs;
+
+  // status
+  var m_tb_wasinited = false;
+  var m_tb_is_main_window = false;
+
+  var m_tb_control_ipc_file = null; // Set if using IPC (UNIX domain socket).
+  var m_tb_control_port = null; // Set if using TCP.
+  var m_tb_control_host = null; // Set if using TCP.
+  var m_tb_control_pass = null;
+
+  // Bug 1506 P2: This object keeps Firefox prefs in sync with Torbutton prefs.
+  // It probably could stand some simplification (See #3100). It also belongs
+  // in a component, not the XUL overlay.
+  var torbutton_unique_pref_observer = {
+    register() {
+      this.forced_ua = false;
+      m_tb_prefs.addObserver("extensions.torbutton", this);
+      m_tb_prefs.addObserver("browser.privatebrowsing.autostart", this);
+      m_tb_prefs.addObserver("javascript", this);
+      m_tb_prefs.addObserver("privacy.resistFingerprinting", this);
+      m_tb_prefs.addObserver("privacy.resistFingerprinting.letterboxing", this);
+    },
+
+    unregister() {
+      m_tb_prefs.removeObserver("extensions.torbutton", this);
+      m_tb_prefs.removeObserver("browser.privatebrowsing.autostart", this);
+      m_tb_prefs.removeObserver("javascript", this);
+      m_tb_prefs.removeObserver("privacy.resistFingerprinting", this);
+      m_tb_prefs.removeObserver(
+        "privacy.resistFingerprinting.letterboxing",
+        this
+      );
+    },
+
+    // topic:   what event occurred
+    // subject: what nsIPrefBranch we're observing
+    // data:    which pref has been changed (relative to subject)
+    observe(subject, topic, data) {
+      if (topic !== "nsPref:changed") {
+        return;
+      }
+      switch (data) {
+        case "browser.privatebrowsing.autostart":
+          torbutton_update_disk_prefs();
+          break;
+        case "extensions.torbutton.use_nontor_proxy":
+          torbutton_use_nontor_proxy();
+          break;
+        case "privacy.resistFingerprinting":
+        case "privacy.resistFingerprinting.letterboxing":
+          torbutton_update_fingerprinting_prefs();
+          break;
+      }
+    },
+  };
+
+  var torbutton_tor_check_observer = {
+    register() {
+      this._obsSvc = Services.obs;
+      this._obsSvc.addObserver(this, k_tb_tor_check_failed_topic);
+    },
+
+    unregister() {
+      if (this._obsSvc) {
+        this._obsSvc.removeObserver(this, k_tb_tor_check_failed_topic);
+      }
+    },
+
+    observe(subject, topic, data) {
+      if (topic === k_tb_tor_check_failed_topic) {
+        // Update all open about:tor pages.
+        torbutton_abouttor_message_handler.updateAllOpenPages();
+
+        // If the user does not have an about:tor tab open in the front most
+        // window, open one.
+        var wm = Services.wm;
+        var win = wm.getMostRecentWindow("navigator:browser");
+        if (win == window) {
+          let foundTab = false;
+          let tabBrowser = top.gBrowser;
+          for (let i = 0; !foundTab && i < tabBrowser.browsers.length; ++i) {
+            let b = tabBrowser.getBrowserAtIndex(i);
+            foundTab = b.currentURI.spec.toLowerCase() == "about:tor";
+          }
+
+          if (!foundTab) {
+            gBrowser.selectedTab = gBrowser.addTrustedTab("about:tor");
+          }
+        }
+      }
+    },
+  };
+
+  var torbutton_new_identity_observers = {
+    register() {
+      Services.obs.addObserver(this, "new-identity-requested");
+    },
+
+    observe(aSubject, aTopic, aData) {
+      if (aTopic !== "new-identity-requested") {
+        return;
+      }
+
+      // Clear the domain isolation state.
+      torbutton_log(3, "Clearing domain isolator");
+      const domainIsolator = Cc["@torproject.org/domain-isolator;1"].getService(
+        Ci.nsISupports
+      ).wrappedJSObject;
+      domainIsolator.clearIsolation();
+
+      torbutton_log(3, "New Identity: Sending NEWNYM");
+      // We only support TBB for newnym.
+      if (
+        !m_tb_control_pass ||
+        (!m_tb_control_ipc_file && !m_tb_control_port)
+      ) {
+        const warning = torbutton_get_property_string(
+          "torbutton.popup.no_newnym"
+        );
+        torbutton_log(
+          5,
+          "Torbutton cannot safely newnym. It does not have access to the Tor Control Port."
+        );
+        window.alert(warning);
+      } else {
+        const warning = torbutton_get_property_string(
+          "torbutton.popup.no_newnym"
+        );
+        torbutton_send_ctrl_cmd("SIGNAL NEWNYM")
+          .then(res => {
+            if (!res) {
+              torbutton_log(
+                5,
+                "Torbutton was unable to request a new circuit from Tor"
+              );
+              window.alert(warning);
+            }
+          })
+          .catch(e => {
+            torbutton_log(
+              5,
+              "Torbutton was unable to request a new circuit from Tor " + e
+            );
+            window.alert(warning);
+          });
+      }
+    },
+  };
+
+  function torbutton_is_mobile() {
+    return Services.appinfo.OS === "Android";
+  }
+
+  // Bug 1506 P2-P4: This code sets some version variables that are irrelevant.
+  // It does read out some important environment variables, though. It is
+  // called once per browser window.. This might belong in a component.
+  torbutton_init = function() {
+    torbutton_log(3, "called init()");
+
+    if (m_tb_wasinited) {
+      return;
+    }
+    m_tb_wasinited = true;
+
+    // Bug 1506 P4: These vars are very important for New Identity
+    var environ = Cc["@mozilla.org/process/environment;1"].getService(
+      Ci.nsIEnvironment
+    );
+
+    if (environ.exists("TOR_CONTROL_PASSWD")) {
+      m_tb_control_pass = environ.get("TOR_CONTROL_PASSWD");
+    } else if (environ.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
+      var cookie_path = environ.get("TOR_CONTROL_COOKIE_AUTH_FILE");
+      try {
+        if ("" != cookie_path) {
+          m_tb_control_pass = torbutton_read_authentication_cookie(cookie_path);
+        }
+      } catch (e) {
+        torbutton_log(4, "unable to read authentication cookie");
+      }
+    } else {
+      try {
+        // Try to get password from Tor Launcher.
+        m_tb_control_pass = TorProtocolService.torGetPassword(false);
+      } catch (e) {}
+    }
+
+    // Try to get the control port IPC file (an nsIFile) from Tor Launcher,
+    // since Tor Launcher knows how to handle its own preferences and how to
+    // resolve relative paths.
+    try {
+      m_tb_control_ipc_file = TorProtocolService.torGetControlIPCFile();
+    } catch (e) {}
+
+    if (!m_tb_control_ipc_file) {
+      if (environ.exists("TOR_CONTROL_PORT")) {
+        m_tb_control_port = environ.get("TOR_CONTROL_PORT");
+      } else {
+        try {
+          const kTLControlPortPref = "extensions.torlauncher.control_port";
+          m_tb_control_port = m_tb_prefs.getIntPref(kTLControlPortPref);
+        } catch (e) {
+          // Since we want to disable some features when Tor Launcher is
+          // not installed (e.g., New Identity), we do not set a default
+          // port value here.
+        }
+      }
+
+      if (environ.exists("TOR_CONTROL_HOST")) {
+        m_tb_control_host = environ.get("TOR_CONTROL_HOST");
+      } else {
+        try {
+          const kTLControlHostPref = "extensions.torlauncher.control_host";
+          m_tb_control_host = m_tb_prefs.getCharPref(kTLControlHostPref);
+        } catch (e) {
+          m_tb_control_host = "127.0.0.1";
+        }
+      }
+    }
+
+    configureControlPortModule(
+      m_tb_control_ipc_file,
+      m_tb_control_host,
+      m_tb_control_port,
+      m_tb_control_pass
+    );
+
+    // Add about:tor IPC message listener.
+    window.messageManager.addMessageListener(
+      "AboutTor:Loaded",
+      torbutton_abouttor_message_handler
+    );
+
+    setupPreferencesForMobile();
+
+    torbutton_log(1, "registering Tor check observer");
+    torbutton_tor_check_observer.register();
+
+    // Create the circuit display even though the control port might not be
+    // ready yet, as the circuit display will wait for the controller to be
+    // available anyway.
+    try {
+      createTorCircuitDisplay("extensions.torbutton.display_circuit");
+      circuitDisplayCreated = true;
+    } catch (e) {
+      torbutton_log(4, "Error creating the tor circuit display " + e);
+    }
+
+    // Arrange for our about:tor content script to be loaded in each frame.
+    window.messageManager.loadFrameScript(
+      "chrome://torbutton/content/aboutTor/aboutTor-content.js",
+      true
+    );
+
+    torbutton_new_identity_observers.register();
+
+    torbutton_log(3, "init completed");
+  };
+
+  var torbutton_abouttor_message_handler = {
+    // Receive IPC messages from the about:tor content script.
+    async receiveMessage(aMessage) {
+      switch (aMessage.name) {
+        case "AboutTor:Loaded":
+          aMessage.target.messageManager.sendAsyncMessage(
+            "AboutTor:ChromeData",
+            await this.getChromeData(true)
+          );
+          break;
+      }
+    },
+
+    // Send privileged data to all of the about:tor content scripts.
+    async updateAllOpenPages() {
+      window.messageManager.broadcastAsyncMessage(
+        "AboutTor:ChromeData",
+        await this.getChromeData(false)
+      );
+    },
+
+    // The chrome data contains all of the data needed by the about:tor
+    // content process that is only available here (in the chrome process).
+    // It is sent to the content process when an about:tor window is opened
+    // and in response to events such as the browser noticing that Tor is
+    // not working.
+    async getChromeData(aIsRespondingToPageLoad) {
+      let dataObj = {
+        mobile: torbutton_is_mobile(),
+        updateChannel: AppConstants.MOZ_UPDATE_CHANNEL,
+        torOn: await torbutton_tor_check_ok(),
+      };
+
+      if (aIsRespondingToPageLoad) {
+        const kShouldNotifyPref = "torbrowser.post_update.shouldNotify";
+        if (m_tb_prefs.getBoolPref(kShouldNotifyPref, false)) {
+          m_tb_prefs.clearUserPref(kShouldNotifyPref);
+          dataObj.hasBeenUpdated = true;
+          dataObj.updateMoreInfoURL = this.getUpdateMoreInfoURL();
+        }
+      }
+
+      return dataObj;
+    },
+
+    getUpdateMoreInfoURL() {
+      try {
+        return Services.prefs.getCharPref("torbrowser.post_update.url");
+      } catch (e) {}
+
+      // Use the default URL as a fallback.
+      return Services.urlFormatter.formatURLPref(
+        "startup.homepage_override_url"
+      );
+    },
+  };
+
+  // Bug 1506 P4: Control port interaction. Needed for New Identity.
+  function torbutton_read_authentication_cookie(path) {
+    var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    file.initWithPath(path);
+    var fileStream = Cc[
+      "@mozilla.org/network/file-input-stream;1"
+    ].createInstance(Ci.nsIFileInputStream);
+    fileStream.init(file, 1, 0, false);
+    var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+      Ci.nsIBinaryInputStream
+    );
+    binaryStream.setInputStream(fileStream);
+    var array = binaryStream.readByteArray(fileStream.available());
+    binaryStream.close();
+    fileStream.close();
+    return torbutton_array_to_hexdigits(array);
+  }
+
+  // Bug 1506 P4: Control port interaction. Needed for New Identity.
+  function torbutton_array_to_hexdigits(array) {
+    return array
+      .map(function(c) {
+        return String("0" + c.toString(16)).slice(-2);
+      })
+      .join("");
+  }
+
+  // Bug 1506 P4: Control port interaction. Needed for New Identity.
+  //
+  // Asynchronously executes a command on the control port.
+  // returns the response as a string, or null on error
+  async function torbutton_send_ctrl_cmd(command) {
+    const getErrorMessage = e => (e && (e.torMessage || e.message)) || "";
+    let response = null;
+    try {
+      const avoidCache = true;
+      let torController = await wait_for_controller(avoidCache);
+
+      let bytes = await torController.sendCommand(command);
+      if (!bytes.startsWith("250")) {
+        throw new Error(
+          `Unexpected command response on control port '${bytes}'`
+        );
+      }
+      response = bytes.slice(4);
+
+      torController.close();
+    } catch (err) {
+      let msg = getErrorMessage(err);
+      torbutton_log(4, `Error: ${msg}`);
+    }
+    return response;
+  }
+
+  // Bug 1506 P4: Needed for New IP Address
+  torbutton_new_circuit = function() {
+    let firstPartyDomain = getDomainForBrowser(gBrowser.selectedBrowser);
+
+    let domainIsolator = Cc["@torproject.org/domain-isolator;1"].getService(
+      Ci.nsISupports
+    ).wrappedJSObject;
+
+    domainIsolator.newCircuitForDomain(firstPartyDomain);
+
+    gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+  };
+
+  /* Called when we switch the use_nontor_proxy pref in either direction.
+   *
+   * Enables/disables domain isolation and then does new identity
+   */
+  function torbutton_use_nontor_proxy() {
+    let domainIsolator = Cc["@torproject.org/domain-isolator;1"].getService(
+      Ci.nsISupports
+    ).wrappedJSObject;
+
+    if (m_tb_prefs.getBoolPref("extensions.torbutton.use_nontor_proxy")) {
+      // Disable domain isolation
+      domainIsolator.disableIsolation();
+    } else {
+      domainIsolator.enableIsolation();
+    }
+  }
+
+  async function torbutton_do_tor_check() {
+    let checkSvc = Cc["@torproject.org/torbutton-torCheckService;1"].getService(
+      Ci.nsISupports
+    ).wrappedJSObject;
+    if (
+      m_tb_prefs.getBoolPref("extensions.torbutton.use_nontor_proxy") ||
+      !m_tb_prefs.getBoolPref("extensions.torbutton.test_enabled")
+    ) {
+      return;
+    } // Only do the check once.
+
+    // If we have a tor control port and transparent torification is off,
+    // perform a check via the control port.
+    const kEnvSkipControlPortTest = "TOR_SKIP_CONTROLPORTTEST";
+    const kEnvUseTransparentProxy = "TOR_TRANSPROXY";
+    var env = Cc["@mozilla.org/process/environment;1"].getService(
+      Ci.nsIEnvironment
+    );
+    if (
+      (m_tb_control_ipc_file || m_tb_control_port) &&
+      !env.exists(kEnvUseTransparentProxy) &&
+      !env.exists(kEnvSkipControlPortTest) &&
+      m_tb_prefs.getBoolPref("extensions.torbutton.local_tor_check")
+    ) {
+      if (await torbutton_local_tor_check()) {
+        checkSvc.statusOfTorCheck = checkSvc.kCheckSuccessful;
+      } else {
+        // The check failed.  Update toolbar icon and tooltip.
+        checkSvc.statusOfTorCheck = checkSvc.kCheckFailed;
+      }
+    } else {
+      // A local check is not possible, so perform a remote check.
+      torbutton_initiate_remote_tor_check();
+    }
+  }
+
+  async function torbutton_local_tor_check() {
+    let didLogError = false;
+
+    let proxyType = m_tb_prefs.getIntPref("network.proxy.type");
+    if (0 == proxyType) {
+      return false;
+    }
+
+    // Ask tor for its SOCKS listener address and port and compare to the
+    // browser preferences.
+    const kCmdArg = "net/listeners/socks";
+    let resp = await torbutton_send_ctrl_cmd("GETINFO " + kCmdArg);
+    if (!resp) {
+      return false;
+    }
+
+    function logUnexpectedResponse() {
+      if (!didLogError) {
+        didLogError = true;
+        torbutton_log(
+          5,
+          "Local Tor check: unexpected GETINFO response: " + resp
+        );
+      }
+    }
+
+    function removeBrackets(aStr) {
+      // Remove enclosing square brackets if present.
+      if (aStr.startsWith("[") && aStr.endsWith("]")) {
+        return aStr.substr(1, aStr.length - 2);
+      }
+
+      return aStr;
+    }
+
+    // Sample response: net/listeners/socks="127.0.0.1:9149" "127.0.0.1:9150"
+    // First, check for and remove the command argument prefix.
+    if (0 != resp.indexOf(kCmdArg + "=")) {
+      logUnexpectedResponse();
+      return false;
+    }
+    resp = resp.substr(kCmdArg.length + 1);
+
+    // Retrieve configured proxy settings and check each listener against them.
+    // When the SOCKS prefs are set to use IPC (e.g., a Unix domain socket), a
+    // file URL should be present in network.proxy.socks.
+    // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1211567
+    let socksAddr = m_tb_prefs.getCharPref("network.proxy.socks");
+    let socksPort = m_tb_prefs.getIntPref("network.proxy.socks_port");
+    let socksIPCPath;
+    if (socksAddr && socksAddr.startsWith("file:")) {
+      // Convert the file URL to a file path.
+      try {
+        let ioService = Services.io;
+        let fph = ioService
+          .getProtocolHandler("file")
+          .QueryInterface(Ci.nsIFileProtocolHandler);
+        socksIPCPath = fph.getFileFromURLSpec(socksAddr).path;
+      } catch (e) {
+        torbutton_log(5, "Local Tor check: IPC file error: " + e);
+        return false;
+      }
+    } else {
+      socksAddr = removeBrackets(socksAddr);
+    }
+
+    // Split into quoted strings. This code is adapted from utils.splitAtSpaces()
+    // within tor-control-port.js; someday this code should use the entire
+    // tor-control-port.js framework.
+    let addrArray = [];
+    resp.replace(/((\S*?"(.*?)")+\S*|\S+)/g, function(a, captured) {
+      addrArray.push(captured);
+    });
+
+    let foundSocksListener = false;
+    for (let i = 0; !foundSocksListener && i < addrArray.length; ++i) {
+      let addr;
+      try {
+        addr = unescapeTorString(addrArray[i]);
+      } catch (e) {}
+      if (!addr) {
+        continue;
+      }
+
+      // Remove double quotes if present.
+      let len = addr.length;
+      if (len > 2 && '"' == addr.charAt(0) && '"' == addr.charAt(len - 1)) {
+        addr = addr.substring(1, len - 1);
+      }
+
+      if (addr.startsWith("unix:")) {
+        if (!socksIPCPath) {
+          continue;
+        }
+
+        // Check against the configured UNIX domain socket proxy.
+        let path = addr.substring(5);
+        torbutton_log(2, "Tor socks listener (Unix domain socket): " + path);
+        foundSocksListener = socksIPCPath === path;
+      } else if (!socksIPCPath) {
+        // Check against the configured TCP proxy. We expect addr:port where addr
+        // may be an IPv6 address; that is, it may contain colon characters.
+        // Also, we remove enclosing square brackets before comparing addresses
+        // because tor requires them but Firefox does not.
+        let idx = addr.lastIndexOf(":");
+        if (idx < 0) {
+          logUnexpectedResponse();
+        } else {
+          let torSocksAddr = removeBrackets(addr.substring(0, idx));
+          let torSocksPort = parseInt(addr.substring(idx + 1), 10);
+          if (torSocksAddr.length < 1 || isNaN(torSocksPort)) {
+            logUnexpectedResponse();
+          } else {
+            torbutton_log(
+              2,
+              "Tor socks listener: " + torSocksAddr + ":" + torSocksPort
+            );
+            foundSocksListener =
+              socksAddr === torSocksAddr && socksPort === torSocksPort;
+          }
+        }
+      }
+    }
+
+    return foundSocksListener;
+  } // torbutton_local_tor_check
+
+  function torbutton_initiate_remote_tor_check() {
+    let obsSvc = Services.obs;
+    try {
+      let checkSvc = Cc[
+        "@torproject.org/torbutton-torCheckService;1"
+      ].getService(Ci.nsISupports).wrappedJSObject;
+      let req = checkSvc.createCheckRequest(true); // async
+      req.onreadystatechange = function(aEvent) {
+        if (req.readyState === 4) {
+          let ret = checkSvc.parseCheckResponse(req);
+
+          // If we received an error response from check.torproject.org,
+          // set the status of the tor check to failure (we don't want
+          // to indicate failure if we didn't receive a response).
+          if (
+            ret == 2 ||
+            ret == 3 ||
+            ret == 5 ||
+            ret == 6 ||
+            ret == 7 ||
+            ret == 8
+          ) {
+            checkSvc.statusOfTorCheck = checkSvc.kCheckFailed;
+            obsSvc.notifyObservers(null, k_tb_tor_check_failed_topic);
+          } else if (ret == 4) {
+            checkSvc.statusOfTorCheck = checkSvc.kCheckSuccessful;
+          } // Otherwise, redo the check later
+
+          torbutton_log(3, "Tor remote check done. Result: " + ret);
+        }
+      };
+
+      torbutton_log(3, "Sending async Tor remote check");
+      req.send(null);
+    } catch (e) {
+      if (e.result == 0x80004005) {
+        // NS_ERROR_FAILURE
+        torbutton_log(5, "Tor check failed! Is tor running?");
+      } else {
+        torbutton_log(5, "Tor check failed! Tor internal error: " + e);
+      }
+
+      obsSvc.notifyObservers(null, k_tb_tor_check_failed_topic);
+    }
+  } // torbutton_initiate_remote_tor_check()
+
+  async function torbutton_tor_check_ok() {
+    await torbutton_do_tor_check();
+    let checkSvc = Cc["@torproject.org/torbutton-torCheckService;1"].getService(
+      Ci.nsISupports
+    ).wrappedJSObject;
+    return checkSvc.kCheckFailed != checkSvc.statusOfTorCheck;
+  }
+
+  function torbutton_update_disk_prefs() {
+    var mode = m_tb_prefs.getBoolPref("browser.privatebrowsing.autostart");
+
+    m_tb_prefs.setBoolPref("browser.cache.disk.enable", !mode);
+    m_tb_prefs.setBoolPref("places.history.enabled", !mode);
+
+    m_tb_prefs.setBoolPref("security.nocertdb", mode);
+
+    // No way to clear this beast during New Identity. Leave it off.
+    //m_tb_prefs.setBoolPref("dom.indexedDB.enabled", !mode);
+
+    m_tb_prefs.setBoolPref("permissions.memory_only", mode);
+
+    // Third party abuse. Leave it off for now.
+    //m_tb_prefs.setBoolPref("browser.cache.offline.enable", !mode);
+
+    // Force prefs to be synced to disk
+    Services.prefs.savePrefFile(null);
+  }
+
+  function torbutton_update_fingerprinting_prefs() {
+    var mode = m_tb_prefs.getBoolPref("privacy.resistFingerprinting");
+    var letterboxing = m_tb_prefs.getBoolPref(
+      "privacy.resistFingerprinting.letterboxing",
+      false
+    );
+    m_tb_prefs.setBoolPref(
+      "extensions.torbutton.resize_new_windows",
+      mode && !letterboxing
+    );
+
+    // Force prefs to be synced to disk
+    Services.prefs.savePrefFile(null);
+  }
+
+  // Bug 1506 P1: This function just cleans up prefs that got set badly in previous releases
+  function torbutton_fixup_old_prefs() {
+    if (m_tb_prefs.getIntPref("extensions.torbutton.pref_fixup_version") < 1) {
+      // TBB 5.0a3 had bad Firefox code that silently flipped this pref on us
+      if (m_tb_prefs.prefHasUserValue("browser.newtabpage.enhanced")) {
+        m_tb_prefs.clearUserPref("browser.newtabpage.enhanced");
+        // TBB 5.0a3 users had all the necessary data cached in
+        // directoryLinks.json. This meant that resetting the pref above
+        // alone was not sufficient as the tiles features uses the cache
+        // even if the pref indicates that feature should be disabled.
+        // We flip the preference below as this forces a refetching which
+        // effectively results in an empty JSON file due to our spoofed
+        // URLs.
+        let matchOS = m_tb_prefs.getBoolPref("intl.locale.matchOS");
+        m_tb_prefs.setBoolPref("intl.locale.matchOS", !matchOS);
+        m_tb_prefs.setBoolPref("intl.locale.matchOS", matchOS);
+      }
+
+      // For some reason, the Share This Page button also survived the
+      // TBB 5.0a4 update's attempt to remove it.
+      if (m_tb_prefs.prefHasUserValue("browser.uiCustomization.state")) {
+        m_tb_prefs.clearUserPref("browser.uiCustomization.state");
+      }
+
+      m_tb_prefs.setIntPref("extensions.torbutton.pref_fixup_version", 1);
+    }
+  }
+
+  // ---------------------- Event handlers -----------------
+
+  // Bug 1506 P1-P3: Most of these observers aren't very important.
+  // See their comments for details
+  function torbutton_do_main_window_startup() {
+    torbutton_log(3, "Torbutton main window startup");
+    m_tb_is_main_window = true;
+    torbutton_unique_pref_observer.register();
+  }
+
+  // Bug 1506 P4: Most of this function is now useless, save
+  // for the very important SOCKS environment vars at the end.
+  // Those could probably be rolled into a function with the
+  // control port vars, though. See 1506 comments inside.
+  function torbutton_do_startup() {
+    if (m_tb_prefs.getBoolPref("extensions.torbutton.startup")) {
+      // Bug 1506: Should probably be moved to an XPCOM component
+      torbutton_do_main_window_startup();
+
+      // For charsets
+      torbutton_update_fingerprinting_prefs();
+
+      // Bug 30565: sync browser.privatebrowsing.autostart with security.nocertdb
+      torbutton_update_disk_prefs();
+
+      // For general pref fixups to handle pref damage in older versions
+      torbutton_fixup_old_prefs();
+
+      m_tb_prefs.setBoolPref("extensions.torbutton.startup", false);
+    }
+  }
+
+  // Bug 1506 P3: Used to decide if we should resize the window.
+  //
+  // Returns true if the window wind is neither maximized, full screen,
+  // ratpoisioned/evilwmed, nor minimized.
+  function torbutton_is_windowed(wind) {
+    torbutton_log(
+      3,
+      "Window: (" +
+        wind.outerWidth +
+        "," +
+        wind.outerHeight +
+        ") ?= (" +
+        wind.screen.availWidth +
+        "," +
+        wind.screen.availHeight +
+        ")"
+    );
+    if (
+      wind.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED ||
+      wind.windowState == Ci.nsIDOMChromeWindow.STATE_MAXIMIZED
+    ) {
+      torbutton_log(2, "Window is minimized/maximized");
+      return false;
+    }
+    if ("fullScreen" in wind && wind.fullScreen) {
+      torbutton_log(2, "Window is fullScreen");
+      return false;
+    }
+    if (
+      wind.outerHeight == wind.screen.availHeight &&
+      wind.outerWidth == wind.screen.availWidth
+    ) {
+      torbutton_log(3, "Window is ratpoisoned/evilwm'ed");
+      return false;
+    }
+
+    torbutton_log(2, "Window is normal");
+    return true;
+  }
+
+  function showSecurityPreferencesPanel(chromeWindow) {
+    const tabBrowser = chromeWindow.BrowserApp;
+    let settingsTab = null;
+
+    const SECURITY_PREFERENCES_URI =
+      "chrome://torbutton/content/preferences.xhtml";
+
+    tabBrowser.tabs.some(function(tab) {
+      // If the security prefs tab is opened, send the user to it
+      if (tab.browser.currentURI.spec === SECURITY_PREFERENCES_URI) {
+        settingsTab = tab;
+        return true;
+      }
+      return false;
+    });
+
+    if (settingsTab === null) {
+      // Open up the settings panel in a new tab.
+      tabBrowser.addTab(SECURITY_PREFERENCES_URI, {
+        selected: true,
+        parentId: tabBrowser.selectedTab.id,
+        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+      });
+    } else {
+      // Activate an existing settings panel tab.
+      tabBrowser.selectTab(settingsTab);
+    }
+  }
+
+  function setupPreferencesForMobile() {
+    if (!torbutton_is_mobile()) {
+      return;
+    }
+
+    torbutton_log(4, "Setting up settings preferences for Android.");
+
+    const chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
+
+    // Add the extension's chrome menu item to the main browser menu.
+    chromeWindow.NativeWindow.menu.add({
+      name: torbutton_get_property_string(
+        "torbutton.security_settings.menu.title"
+      ),
+      callback: showSecurityPreferencesPanel.bind(this, chromeWindow),
+    });
+  }
+
+  // Bug 1506 P3: This is needed pretty much only for the window resizing.
+  // See comments for individual functions for details
+  function torbutton_new_window(event) {
+    torbutton_log(3, "New window");
+    var browser = window.gBrowser;
+
+    if (!browser) {
+      torbutton_log(5, "No browser for new window.");
+      return;
+    }
+
+    if (!m_tb_wasinited) {
+      torbutton_init();
+    }
+
+    torbutton_do_startup();
+
+    let progress = Cc["@mozilla.org/docloaderservice;1"].getService(
+      Ci.nsIWebProgress
+    );
+
+    if (torbutton_is_windowed(window)) {
+      progress.addProgressListener(
+        torbutton_resizelistener,
+        Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+      );
+    }
+  }
+
+  // Bug 1506 P2: This is only needed because we have observers
+  // in XUL that should be in an XPCOM component
+  function torbutton_close_window(event) {
+    torbutton_tor_check_observer.unregister();
+
+    window.removeEventListener("sizemodechange", m_tb_resize_handler);
+
+    // TODO: This is a real ghetto hack.. When the original window
+    // closes, we need to find another window to handle observing
+    // unique events... The right way to do this is to move the
+    // majority of torbutton functionality into a XPCOM component..
+    // But that is a major overhaul..
+    if (m_tb_is_main_window) {
+      torbutton_log(3, "Original window closed. Searching for another");
+      var wm = Services.wm;
+      var enumerator = wm.getEnumerator("navigator:browser");
+      while (enumerator.hasMoreElements()) {
+        var win = enumerator.getNext();
+        // For some reason, when New Identity is called from a pref
+        // observer (ex: torbutton_use_nontor_proxy) on an ASAN build,
+        // we sometimes don't have this symbol set in the new window yet.
+        // However, the new window will run this init later in that case,
+        // as it does in the OSX case.
+        if (win != window && "torbutton_do_main_window_startup" in win) {
+          torbutton_log(3, "Found another window");
+          win.torbutton_do_main_window_startup();
+          m_tb_is_main_window = false;
+          break;
+        }
+      }
+
+      torbutton_unique_pref_observer.unregister();
+
+      if (m_tb_is_main_window) {
+        // main window not reset above
+        // This happens on Mac OS because they allow firefox
+        // to still persist without a navigator window
+        torbutton_log(3, "Last window closed. None remain.");
+        m_tb_prefs.setBoolPref("extensions.torbutton.startup", true);
+        m_tb_is_main_window = false;
+      }
+    }
+  }
+
+  window.addEventListener("load", torbutton_new_window);
+  window.addEventListener("unload", torbutton_close_window);
+
+  var m_tb_resize_handler = null;
+  var m_tb_resize_date = null;
+
+  // Bug 1506 P1/P3: Setting a fixed window size is important, but
+  // probably not for android.
+  var torbutton_resizelistener = {
+    QueryInterface: ChromeUtils.generateQI([
+      "nsIWebProgressListener",
+      "nsISupportsWeakReference",
+    ]),
+
+    onLocationChange(aProgress, aRequest, aURI) {},
+    onStateChange(aProgress, aRequest, aFlag, aStatus) {
+      if (aFlag & Ci.nsIWebProgressListener.STATE_STOP) {
+        m_tb_resize_handler = async function() {
+          // Wait for end of execution queue to ensure we have correct windowState.
+          await new Promise(resolve => setTimeout(resolve, 0));
+          if (
+            window.windowState === window.STATE_MAXIMIZED ||
+            window.windowState === window.STATE_FULLSCREEN
+          ) {
+            if (
+              m_tb_prefs.getBoolPref(
+                "extensions.torbutton.resize_new_windows"
+              ) &&
+              m_tb_prefs.getIntPref(
+                "extensions.torbutton.maximize_warnings_remaining"
+              ) > 0
+            ) {
+              // Do not add another notification if one is already showing.
+              const kNotificationName = "torbutton-maximize-notification";
+              let box = gBrowser.getNotificationBox();
+              if (box.getNotificationWithValue(kNotificationName)) {
+                return;
+              }
+
+              // Rate-limit showing our notification if needed.
+              if (m_tb_resize_date === null) {
+                m_tb_resize_date = Date.now();
+              } else {
+                // We wait at least another second before we show a new
+                // notification. Should be enough to rule out OSes that call our
+                // handler rapidly due to internal workings.
+                if (Date.now() - m_tb_resize_date < 1000) {
+                  return;
+                }
+                // Resizing but we need to reset |m_tb_resize_date| now.
+                m_tb_resize_date = Date.now();
+              }
+
+              // No need to get "OK" translated again.
+              let sbSvc = Services.strings;
+              let bundle = sbSvc.createBundle(
+                "chrome://global/locale/commonDialogs.properties"
+              );
+              let button_label = bundle.GetStringFromName("OK");
+
+              let buttons = [
+                {
+                  label: button_label,
+                  accessKey: "O",
+                  popup: null,
+                  callback() {
+                    m_tb_prefs.setIntPref(
+                      "extensions.torbutton.maximize_warnings_remaining",
+                      m_tb_prefs.getIntPref(
+                        "extensions.torbutton.maximize_warnings_remaining"
+                      ) - 1
+                    );
+                  },
+                },
+              ];
+
+              let priority = box.PRIORITY_WARNING_LOW;
+              let message = torbutton_get_property_string(
+                "torbutton.maximize_warning"
+              );
+
+              box.appendNotification(
+                message,
+                kNotificationName,
+                null,
+                priority,
+                buttons
+              );
+            }
+          }
+        }; // m_tb_resize_handler
+
+        // We need to handle OSes that auto-maximize windows depending on user
+        // settings and/or screen resolution in the start-up phase and users that
+        // try to shoot themselves in the foot by maximizing the window manually.
+        // We add a listener which is triggerred as soon as the window gets
+        // maximized (windowState = 1). We are resizing during start-up but not
+        // later as the user should see only a warning there as a stopgap before
+        // #14229 lands.
+        // Alas, the Firefox window code is handling the event not itself:
+        // "// Note the current implementation of SetSizeMode just stores
+        //  // the new state; it doesn't actually resize. So here we store
+        //  // the state and pass the event on to the OS."
+        // (See: https://mxr.mozilla.org/mozilla-esr31/source/xpfe/appshell/src/
+        // nsWebShellWindow.cpp#348)
+        // This means we have to cope with race conditions and resizing in the
+        // sizemodechange listener is likely to fail. Thus, we add a specific
+        // resize listener that is doing the work for us. It seems (at least on
+        // Ubuntu) to be the case that maximizing (and then again normalizing) of
+        // the window triggers more than one resize event the first being not the
+        // one we need. Thus we can't remove the listener after the first resize
+        // event got fired. Thus, we have the rather klunky setTimeout() call.
+        window.addEventListener("sizemodechange", m_tb_resize_handler);
+
+        let progress = Cc["@mozilla.org/docloaderservice;1"].getService(
+          Ci.nsIWebProgress
+        );
+        progress.removeProgressListener(this);
+      }
+    }, // onStateChange
+
+    onProgressChange(
+      aProgress,
+      aRequest,
+      curSelfProgress,
+      maxSelfProgress,
+      curTotalProgress,
+      maxTotalProgress
+    ) {},
+    onStatusChange(aProgress, aRequest, stat, message) {},
+    onSecurityChange() {},
+  };
+})();
+//vim:set ts=4
diff --git a/toolkit/torbutton/chrome/skin/about-wordmark.png b/toolkit/torbutton/chrome/skin/about-wordmark.png
new file mode 100644
index 000000000000..1becd875f9ac
Binary files /dev/null and b/toolkit/torbutton/chrome/skin/about-wordmark.png differ
diff --git a/toolkit/torbutton/chrome/skin/aboutDialog.css b/toolkit/torbutton/chrome/skin/aboutDialog.css
new file mode 100644
index 000000000000..981d68e22698
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/aboutDialog.css
@@ -0,0 +1,34 @@
+#leftBox {
+  background-image: url('chrome://branding/content/icon256.png');
+  background-position: right top;
+  background-size: 180px;
+}
+
+#rightBox {
+  background-image: url('chrome://torbutton/skin/about-wordmark.png');
+  background-position: left top;
+}
+
+#contributeDesc {
+  display: none;
+}
+
+#communityDesc {
+  display: none;
+}
+
+#trademark {
+  display: none;
+}
+
+#trademarkTor {
+  font-size: xx-small;
+  text-align: center;
+  color: #999999;
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+#bottomBox > hbox:not(#newBottom) {
+  display: none;
+}
diff --git a/toolkit/torbutton/chrome/skin/aboutTor.css b/toolkit/torbutton/chrome/skin/aboutTor.css
new file mode 100644
index 000000000000..007c46d390b4
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/aboutTor.css
@@ -0,0 +1,313 @@
+/*
+ * Copyright (c) 2019, The Tor Project, Inc.
+ * See LICENSE for licensing information.
+ *
+ * vim: set sw=2 sts=2 ts=8 et syntax=css:
+ */
+
+:root {
+  --abouttor-text-color: white;
+  --abouttor-bg-toron-color: #420C5D;
+  --abouttor-bg-toroff-color: #A4000F;
+  --onion-opacity: 0.2;
+  --onion-color: #fff;
+  --onion-radius: 75px;
+}
+
+* {
+  padding: 0px;
+  margin: 0px;
+}
+
+html {
+  height: 100%;
+}
+
+body {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  width: 100%;
+  height: 100%;
+  margin: 0px auto;
+  padding: 0px 0px;
+  font-family: Helvetica, Arial, sans-serif;
+  color: var(--abouttor-text-color);
+  background-color: var(--abouttor-bg-toroff-color);
+  background-attachment: fixed;
+  background-size: 100% 100%;
+}
+
+body[toron] {
+  background-color: var(--abouttor-bg-toron-color);
+}
+
+/* Hide the entire document by default to avoid showing the incorrect
+ * Tor on / off status (that info must be retrieved from the chrome
+ * process, which involves IPC when multiprocess mode is enabled). An
+ * initialized attribute will be added as soon as the status is known.
+ */
+body:not([initialized]) {
+  display: none;
+}
+
+.torcontent-container {
+  margin: 40px 20px 28px 20px;
+  display: flex;
+  flex-direction: column;
+}
+
+ at media only screen and (min-width: 900px) {
+  .torcontent-container {
+    margin: 40px auto 28px auto;
+    max-width: 1000px;
+  }
+}
+
+#torbrowser-info {
+  position: absolute;
+  top: 16px;
+  offset-inline-end: 16px;
+  inset-inline-end: 16px;
+  height: 36px;
+  width: 200px;
+}
+
+#torbrowser-info div {
+  font-size: 14px;
+  white-space: pre-wrap;
+  text-align: end;
+  margin-bottom: 6px;
+}
+
+/* Hide "View Changelog" link if update channel is not release or alpha. */
+body:not([updatechannel="release"]):not([updatechannel="alpha"]) #torbrowser-changelog-link {
+  display: none;
+}
+
+a {
+  color: var(--abouttor-text-color);
+}
+
+#torstatus {
+  margin-top: 135px;
+  display: flex;
+  flex-direction: column;
+  align-content: center;
+  justify-content: flex-end;
+  min-height: 92px;
+}
+
+.top {
+  white-space: nowrap;
+}
+
+.torstatus-container {
+  text-align: center;
+}
+
+body[hasbeenupdated] .hideIfHasBeenUpdated,
+body[toron] .hideIfTorOn,
+body:not([toron]) .hideIfTorOff {
+  display: none;
+}
+
+body:not([hasbeenupdated]) .showIfHasBeenUpdated {
+  display: none;
+}
+
+.torstatus-container * {
+  text-align: center;
+}
+
+.top div.hideIfTorOff .heading1 {
+  margin-top: 20px;
+}
+
+.top .heading1 {
+  font-size: 50px;
+  font-weight: 300;
+  margin-bottom: 5px;
+}
+
+.top .heading2 {
+  font-size: 15px;
+  margin-bottom: 20px;
+}
+
+#bottom {
+  margin-top: 10vh;
+  margin-inline: auto;
+  max-width: 730px;
+}
+
+#bottom p {
+  font-size: 10px;
+  text-align: start;
+  padding-bottom: 20px;
+}
+
+#bottom p:last-child {
+  padding-bottom: 0px;
+}
+
+#bottom p.moreInfoLink {
+  font-size: 15px;
+}
+
+#bottom img.imageStyle {
+  padding-inline-end: 0.5em;
+  height: 1.5em;
+  vertical-align: bottom;
+  -moz-context-properties: fill;
+  fill: white;
+}
+
+/* Hide the linebreaks on large enough screens (desktops, laptops, and
+ * tablets).
+ */
+ at media only screen and (min-width: 768px) {
+  #bottom br {
+    display: none;
+  }
+}
+
+.searchbox form {
+  max-width: 575px;
+  width: auto;
+  margin-block: 0;
+  margin-inline: auto;
+  text-align: left;
+}
+
+.searchwrapper {
+  display: flex;
+  height: 46px;
+  border: 1px solid black;
+  border-radius: 8px;
+  background-color: white;
+}
+
+.searchlabel {
+  height: auto;
+  width: 50px;
+  display: inline-block;
+  background: url('dax-logo.svg') no-repeat center center;
+  background-size: 30px 30px;
+}
+
+#search-button {
+  height: auto;
+  width: 36px;
+  border: 0;
+  background: url('chrome://browser/skin/forward.svg') no-repeat center center;
+  background-size: 16px 16px;
+  cursor: pointer;
+}
+
+#search-button:dir(rtl) {
+  transform: scaleX(-1);
+}
+
+#search-button:hover {
+  background-color: rgba(12, 12, 13, 0.1);  /* same as Firefox's about:home */
+}
+
+#bannerRoot {
+  width: 690px;
+  padding: 20px 25px 25px;
+  background: rgba(0, 0, 0, 0.3);
+  border-radius: 6px;
+  margin: 35px auto 0px;
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  position: static;
+}
+
+#bannerTextCol {
+  padding-top: 8px;
+  padding-inline-start: 15px;
+  font-size: 15px;
+  line-height: normal;
+  color: #FFFFFF;
+  width: 633px;
+}
+
+#bannerHeader {
+  font-weight: bold;
+}
+
+#bannerImg {
+    width: 32px;
+    position: static;
+}
+
+a#bannerLink {
+  text-decoration: underline;
+}
+
+a#bannerLink:hover {
+  text-decoration: none;
+}
+
+/* #search-text is the search input field */
+#search-text {
+  flex: 1;
+  border: none;
+  padding: 4px 2px 5px 2px;
+  margin: 0;
+  font-size: 15px;
+}
+
+#search-text:focus-visible {
+  outline: none;
+}
+
+#search-button:focus-visible {
+  outline: 1px dotted black;
+  outline-offset: -6px;
+}
+
+
+/*
+ * Mobile specific css
+ */
+
+.torcontent-logo {
+  display: none;
+}
+
+body[mobile] #torbrowser-changelog-link,
+body[mobile] .searchbox,
+body[mobile] .top .heading2,
+body[mobile] #manual,
+body[mobile] #mission {
+  display: none;
+}
+
+body[mobile] .top {
+  white-space: normal;
+  margin-top: 0px !important;
+}
+
+body[mobile] .torcontent-logo {
+  display: block;
+  margin-top: 20px;
+  width: 50%;
+  height: auto;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+body[mobile] .top .heading1 {
+  font-size: 62px;
+  line-height: 1.1
+}
+
+body[mobile] .onion-pattern-container {
+  flex: none;
+  z-index: -1; /* ensure that circles do not block access to clickable links */
+  position: absolute;
+  bottom: 0px;
+}
diff --git a/toolkit/torbutton/chrome/skin/banner-warning.svg b/toolkit/torbutton/chrome/skin/banner-warning.svg
new file mode 100644
index 000000000000..2f4bf1b887d8
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/banner-warning.svg
@@ -0,0 +1 @@
+<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" fill="#ffa436" r="16"/><g fill="#0c0c0d"><path d="m14.2303 23.393c-3.5375-.6033-6.2303-3.6837-6.2303-7.393 0-4.1421 3.3579-7.5 7.5-7.5s7.5 3.3579 7.5 7.5c0 .669-.0876 1.3176-.252 1.9349l-.9277-1.6176c.0052-.1051.0078-.2109.0078-.3173 0-3.4949-2.8332-6.32812-6.3281-6.32812s-6.32812 2.83322-6.32812 6.32812c0 2.9851 2.06682 5.4874 4.84752 6.154-.06.4351.0216.8644.2109 1.239 [...]
\ No newline at end of file
diff --git a/toolkit/torbutton/chrome/skin/dax-logo.svg b/toolkit/torbutton/chrome/skin/dax-logo.svg
new file mode 100644
index 000000000000..94ad7c356329
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/dax-logo.svg
@@ -0,0 +1 @@
+<svg width="120" height="120" viewbox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="50.833" cy="50.833" r="50.25"/><linearGradient x1="3.084%" y1="49.368%" x2="100.592%" y2="49.368%" id="c"><stop stop-color="#6176B9" offset=".56%"/><stop stop-color="#394A9F" offset="69.1%"/></linearGradient><linearGradient x1="-.006%" y1="49.006%" x2="98.932%" y2="49.006%" id="d"><stop stop-color="#6176B9" offset=".56%"/><stop stop-co [...]
\ No newline at end of file
diff --git a/toolkit/torbutton/chrome/skin/icon-newsletter.png b/toolkit/torbutton/chrome/skin/icon-newsletter.png
new file mode 100644
index 000000000000..7532ba9c273f
Binary files /dev/null and b/toolkit/torbutton/chrome/skin/icon-newsletter.png differ
diff --git a/toolkit/torbutton/chrome/skin/preferences-mobile.css b/toolkit/torbutton/chrome/skin/preferences-mobile.css
new file mode 100644
index 000000000000..b6d54e8dff63
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/preferences-mobile.css
@@ -0,0 +1,47 @@
+.wrapper {
+  display: flex;
+}
+
+.outer-wrapper {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.inner-wrapper {
+  height: 350px;
+  width: 100%;
+  max-width: 600px;
+  flex-direction: column;
+  align-items: center;
+}
+
+#torbutton_sec_slider {
+  align-self: stretch;
+  width: auto;
+  height: auto;
+}
+
+datalist {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-self: stretch;
+}
+
+p {
+  text-align: justify;
+}
+
+div > p {
+  margin-top: 0px;
+}
+
+.title {
+  font-weight: bold;
+  line-height: 30px;
+}
+
+.wrapper .inner-wrapper * {
+  margin-top: 10px;
+}
diff --git a/toolkit/torbutton/chrome/skin/preferences.css b/toolkit/torbutton/chrome/skin/preferences.css
new file mode 100644
index 000000000000..013b36771aea
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/preferences.css
@@ -0,0 +1,7 @@
+.slider-text-weight {
+  font-weight: bold;
+}
+
+.slider-text-size {
+  font-size: 95%;
+}
diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-display.css b/toolkit/torbutton/chrome/skin/tor-circuit-display.css
new file mode 100644
index 000000000000..4f03b8824464
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/tor-circuit-display.css
@@ -0,0 +1,193 @@
+/*
+This CSS file is for styling the tor circuit display, which consists of a title,
+a domain, and a bulleted list.
+
+Each bullet in the circuit node list is supposed to represent a Tor circuit node,
+and lines drawn between them to represent Tor network inter-relay connections.
+*/
+
+#circuit-display-container {
+  margin-inline: 8px;
+  margin-top: 0px;
+  margin-bottom: 4px;
+  padding: 0px 3px;
+}
+
+#circuit-display-container > toolbarseparator {
+  margin-block: 0px;
+  margin-inline: -3px;
+  width: calc(100% + 6px);
+}
+
+#circuit-display-headline {
+  margin-inline-start: 8px;
+}
+
+#circuit-display-header {
+  background-image: url(chrome://torbutton/skin/torbutton.svg);
+  background-repeat: no-repeat;
+  background-position: 4px 50%;
+  background-size: 16px auto;
+  min-height:  16px;
+  -moz-context-properties: fill, fill-opacity;
+  fill: currentColor;
+  margin-block: 8px;
+}
+
+#circuit-display-header:-moz-locale-dir(rtl) {
+  background-position-x: right;
+}
+
+#circuit-display-header > hbox {
+  margin-inline-start: 16px;
+}
+
+#circuit-display-content {
+  width: 100%;
+}
+
+#circuit-display-content > * {
+  margin-inline: 0px;
+  margin-inline-start: 16px;
+  margin-top: 0px;
+  margin-bottom: 8px;
+}
+
+#circuit-display-content > hbox:not([hidden]) {
+  display: flex;
+}
+
+#circuit-reload-button-container {
+  padding-top: 8px;
+  justify-content: flex-end;
+}
+
+#circuit-reload-content {
+  cursor: default;
+  width: 100%;
+}
+
+/* Format the domain text. */
+#circuit-display-domain {
+  opacity: 0.8;
+}
+
+#circuit-div {
+  position: relative;
+  margin-inline-start: 6px;
+}
+
+/* Format the circuit node list. */
+ul#circuit-display-nodes {
+  line-height:32px;
+  padding-inline-start: 8px;
+}
+
+/* Hide default bullets, and draw our own bullets */
+ul#circuit-display-nodes li {
+  background-image: url(chrome://torbutton/skin/tor-circuit-line.svg);
+  background-position: left center;
+  background-repeat: no-repeat;
+  list-style: none;
+  /* tor circuit svg are 16px wide */
+  padding-inline-start: calc(16px + 0.5em);
+  white-space: nowrap;
+  max-height: 32px;
+  -moz-context-properties: stroke, stroke-opacity;
+  stroke: currentColor;
+  stroke-opacity: 100%;
+}
+
+ul#circuit-display-nodes li:dir(rtl) {
+  background-position: right center;
+}
+
+ul#circuit-display-nodes li:first-child {
+  background-image: url(chrome://torbutton/skin/tor-circuit-line-first.svg);
+}
+
+ul#circuit-display-nodes li:last-child {
+  background-image: url(chrome://torbutton/skin/tor-circuit-line-last.svg);
+}
+
+.circuit-ip-address {
+  font-size: 80%;
+  opacity: 0.7;
+  padding-inline-start: 3px;
+}
+
+.circuit-guard-info {
+  font-size: 80%;
+  color: var(--button-primary-bgcolor);
+  font-weight: bold;
+  padding-inline-start: 3px;
+}
+
+#circuit-reload-button {
+  background-color: var(--button-primary-bgcolor);
+  border-radius: 4px;
+  border-width: 0px;
+  color: var(--button-primary-color);
+  font-weight: 600;
+  padding: 8px 16px;
+  margin: 0;
+}
+#circuit-reload-button:hover {
+  background-color: var(--button-primary-hover-bgcolor);
+}
+
+#circuit-reload-button:active {
+  background-color: var(--button-primary-active-bgcolor);
+}
+
+#circuit-guard-note-container div {
+  margin-inline-start: 8px;
+}
+
+.circuit-guard-name {
+  font-weight: bold;
+}
+
+.circuit-link {
+  cursor: pointer;
+  color: var(--urlbar-popup-url-color);
+}
+
+.circuit-link:hover {
+  text-decoration: underline;
+}
+
+.circuit-onion {
+  cursor: pointer;
+}
+
+.circuit-onion:hover::after {
+  opacity: 1;
+}
+
+.circuit-onion::after {
+  background-color: var(--button-bgcolor);;
+  border-radius: 4px;
+  color: var(--button-color);
+  content: attr(data-text-clicktocopy);
+  font-size: 80%;
+  opacity: 0;
+  padding: 5px 10px;
+  margin: 10px;
+  text-align: center;
+  transition: opacity 0.3s cubic-bezier(0.07, 0.95, 0, 1);
+}
+
+.circuit-onion:hover::after {
+  background-color: var(--button-hover-bgcolor);
+}
+
+.circuit-onion:active::after {
+  background-color: var(--button-active-bgcolor);
+}
+
+
+.circuit-onion-copied::after {
+  content: attr(data-text-copied);
+  opacity: 1;
+}
diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-line-first.svg b/toolkit/torbutton/chrome/skin/tor-circuit-line-first.svg
new file mode 100644
index 000000000000..1981ee875aeb
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/tor-circuit-line-first.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32">
+  <g stroke="context-stroke" opacity="context-stroke-opacity" stroke-width="2" fill="transparent">
+    <circle cx="50%" cy="50%" r="6"/>
+    <line x1="50%" y1="22" x2="50%" y2="100%"/>
+  </g>
+</svg>
diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-line-last.svg b/toolkit/torbutton/chrome/skin/tor-circuit-line-last.svg
new file mode 100644
index 000000000000..19c4adaae2d2
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/tor-circuit-line-last.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32">
+  <g stroke="context-stroke" opacity="context-stroke-opacity" stroke-width="2" fill="transparent">
+    <line x1="50%" y1="0%" x2="50%" y2="10"/>
+    <circle cx="50%" cy="50%" r="6"/>
+  </g>
+</svg>
diff --git a/toolkit/torbutton/chrome/skin/tor-circuit-line.svg b/toolkit/torbutton/chrome/skin/tor-circuit-line.svg
new file mode 100644
index 000000000000..00717f6c2054
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/tor-circuit-line.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32">
+  <g stroke="context-stroke" opacity="context-stroke-opacity" stroke-width="2" fill="transparent">
+    <line x1="50%" y1="0%" x2="50%" y2="10"/>
+    <circle cx="50%" cy="50%" r="6"/>
+    <line x1="50%" y1="22" x2="50%" y2="100%"/>
+  </g>
+</svg>
diff --git a/toolkit/torbutton/chrome/skin/tor.png b/toolkit/torbutton/chrome/skin/tor.png
new file mode 100644
index 000000000000..75b86bfb940a
Binary files /dev/null and b/toolkit/torbutton/chrome/skin/tor.png differ
diff --git a/toolkit/torbutton/chrome/skin/torbrowser_mobile_logo.png b/toolkit/torbutton/chrome/skin/torbrowser_mobile_logo.png
new file mode 100644
index 000000000000..9d8317f02b2c
Binary files /dev/null and b/toolkit/torbutton/chrome/skin/torbrowser_mobile_logo.png differ
diff --git a/toolkit/torbutton/chrome/skin/torbutton.css b/toolkit/torbutton/chrome/skin/torbutton.css
new file mode 100644
index 000000000000..fff1714bcee5
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/torbutton.css
@@ -0,0 +1,14 @@
+svg.circuit text {
+    font-family: Arial;
+}
+
+svg#tor-circuit text.node-text {
+    dominant-baseline: central;
+    font-size: 14px;
+}
+
+svg#tor-circuit circle.node-circle {
+    stroke: #195021;
+    stroke-width: 2px;
+    fill: white;
+}
\ No newline at end of file
diff --git a/toolkit/torbutton/chrome/skin/torbutton.svg b/toolkit/torbutton/chrome/skin/torbutton.svg
new file mode 100644
index 000000000000..30cd52ba5c51
--- /dev/null
+++ b/toolkit/torbutton/chrome/skin/torbutton.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M12.0246161,21.8174863 L12.0246161,20.3628098 C16.6324777,20.3495038 20.3634751,16.6108555 20.3634751,11.9996673 C20.3634751,7.38881189 16.6324777,3.65016355 12.0246161,3.63685757 L12.0246161,2.18218107 C17.4358264,2.1958197 21.8178189,6.58546322 21.8178189,11.9996673 C21.8178189,17.4142042 17.4358264,21.8041803 12.0246161,21.8174863 L12.0246161,21.8174863 Z M12.0246161,16.7259522 C14.623607,16.7123136 16.7272828,14.60231 [...]
+</svg>
\ No newline at end of file
diff --git a/toolkit/torbutton/components/domain-isolator.js b/toolkit/torbutton/components/domain-isolator.js
new file mode 100644
index 000000000000..1c77b5776f0f
--- /dev/null
+++ b/toolkit/torbutton/components/domain-isolator.js
@@ -0,0 +1,228 @@
+// # domain-isolator.js
+// A component for TorBrowser that puts requests from different
+// first party domains on separate tor circuits.
+
+// This file is written in call stack order (later functions
+// call earlier functions). The code file can be processed
+// with docco.js to provide clear documentation.
+
+// ### Abbreviations
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
+});
+
+// Make the logger available.
+let logger = Cc["@torproject.org/torbutton-logger;1"].getService(Ci.nsISupports)
+  .wrappedJSObject;
+
+// Import crypto object (FF 37+).
+Cu.importGlobalProperties(["crypto"]);
+
+// ## mozilla namespace.
+// Useful functionality for interacting with Mozilla services.
+let mozilla = {};
+
+// __mozilla.protocolProxyService__.
+// Mozilla's protocol proxy service, useful for managing proxy connections made
+// by the browser.
+mozilla.protocolProxyService = Cc[
+  "@mozilla.org/network/protocol-proxy-service;1"
+].getService(Ci.nsIProtocolProxyService);
+
+// __mozilla.registerProxyChannelFilter(filterFunction, positionIndex)__.
+// Registers a proxy channel filter with the Mozilla Protocol Proxy Service,
+// which will help to decide the proxy to be used for a given channel.
+// The filterFunction should expect two arguments, (aChannel, aProxy),
+// where aProxy is the proxy or list of proxies that would be used by default
+// for the given channel, and should return a new Proxy or list of Proxies.
+mozilla.registerProxyChannelFilter = function(filterFunction, positionIndex) {
+  let proxyFilter = {
+    applyFilter(aChannel, aProxy, aCallback) {
+      aCallback.onProxyFilterResult(filterFunction(aChannel, aProxy));
+    },
+  };
+  mozilla.protocolProxyService.registerChannelFilter(
+    proxyFilter,
+    positionIndex
+  );
+};
+
+// ## tor functionality.
+let tor = {};
+
+// __tor.noncesForDomains__.
+// A mutable map that records what nonce we are using for each domain.
+tor.noncesForDomains = {};
+
+// __tor.isolationEabled__.
+// A bool that controls if we use SOCKS auth for isolation or not.
+tor.isolationEnabled = true;
+
+// __tor.unknownDirtySince__.
+// Specifies when the current catch-all circuit was first used
+tor.unknownDirtySince = Date.now();
+
+// __tor.socksProxyCredentials(originalProxy, domain)__.
+// Takes a proxyInfo object (originalProxy) and returns a new proxyInfo
+// object with the same properties, except the username is set to the
+// the domain, and the password is a nonce.
+tor.socksProxyCredentials = function(originalProxy, domain) {
+  // Check if we already have a nonce. If not, create
+  // one for this domain.
+  if (!tor.noncesForDomains.hasOwnProperty(domain)) {
+    tor.noncesForDomains[domain] = tor.nonce();
+  }
+  let proxy = originalProxy.QueryInterface(Ci.nsIProxyInfo);
+  return mozilla.protocolProxyService.newProxyInfoWithAuth(
+    "socks",
+    proxy.host,
+    proxy.port,
+    domain, // username
+    tor.noncesForDomains[domain], // password
+    "", // aProxyAuthorizationHeader
+    "", // aConnectionIsolationKey
+    proxy.flags,
+    proxy.failoverTimeout,
+    proxy.failoverProxy
+  );
+};
+
+tor.nonce = function() {
+  // Generate a new 128 bit random tag.  Strictly speaking both using a
+  // cryptographic entropy source and using 128 bits of entropy for the
+  // tag are likely overkill, as correct behavior only depends on how
+  // unlikely it is for there to be a collision.
+  let tag = new Uint8Array(16);
+  crypto.getRandomValues(tag);
+
+  // Convert the tag to a hex string.
+  let tagStr = "";
+  for (let i = 0; i < tag.length; i++) {
+    tagStr += (tag[i] >>> 4).toString(16);
+    tagStr += (tag[i] & 0x0f).toString(16);
+  }
+
+  return tagStr;
+};
+
+tor.newCircuitForDomain = function(domain) {
+  // Re-generate the nonce for the domain.
+  if (domain === "") {
+    domain = "--unknown--";
+  }
+  tor.noncesForDomains[domain] = tor.nonce();
+  logger.eclog(
+    3,
+    "New domain isolation for " + domain + ": " + tor.noncesForDomains[domain]
+  );
+};
+
+// __tor.clearIsolation()_.
+// Clear the isolation state cache, forcing new circuits to be used for all
+// subsequent requests.
+tor.clearIsolation = function() {
+  // Per-domain nonces are stored in a map, so simply re-initialize the map.
+  tor.noncesForDomains = {};
+
+  // Force a rotation on the next catch-all circuit use by setting the creation
+  // time to the epoch.
+  tor.unknownDirtySince = 0;
+};
+
+// __tor.isolateCircuitsByDomain()__.
+// For every HTTPChannel, replaces the default SOCKS proxy with one that authenticates
+// to the SOCKS server (the tor client process) with a username (the first party domain)
+// and a nonce password. Tor provides a separate circuit for each username+password
+// combination.
+tor.isolateCircuitsByDomain = function() {
+  mozilla.registerProxyChannelFilter(function(aChannel, aProxy) {
+    if (!tor.isolationEnabled) {
+      return aProxy;
+    }
+    try {
+      let channel = aChannel.QueryInterface(Ci.nsIChannel),
+        firstPartyDomain = channel.loadInfo.originAttributes.firstPartyDomain;
+      if (firstPartyDomain === "") {
+        firstPartyDomain = "--unknown--";
+        if (Date.now() - tor.unknownDirtySince > 1000 * 10 * 60) {
+          logger.eclog(
+            3,
+            "tor catchall circuit has been dirty for over 10 minutes. Rotating."
+          );
+          tor.newCircuitForDomain("--unknown--");
+          tor.unknownDirtySince = Date.now();
+        }
+      }
+      let replacementProxy = tor.socksProxyCredentials(
+        aProxy,
+        firstPartyDomain
+      );
+      logger.eclog(
+        3,
+        `tor SOCKS: ${channel.URI.spec} via
+                       ${replacementProxy.username}:${replacementProxy.password}`
+      );
+      return replacementProxy;
+    } catch (e) {
+      logger.eclog(4, `tor domain isolator error: ${e.message}`);
+      return null;
+    }
+  }, 0);
+};
+
+// ## XPCOM component construction.
+// Module specific constants
+const kMODULE_NAME = "TorBrowser Domain Isolator";
+const kMODULE_CONTRACTID = "@torproject.org/domain-isolator;1";
+const kMODULE_CID = Components.ID("e33fd6d4-270f-475f-a96f-ff3140279f68");
+
+// DomainIsolator object.
+function DomainIsolator() {
+  this.wrappedJSObject = this;
+}
+
+// Firefox component requirements
+DomainIsolator.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
+  classDescription: kMODULE_NAME,
+  classID: kMODULE_CID,
+  contractID: kMODULE_CONTRACTID,
+  observe(subject, topic, data) {
+    if (topic === "profile-after-change") {
+      logger.eclog(3, "domain isolator: set up isolating circuits by domain");
+
+      if (Services.prefs.getBoolPref("extensions.torbutton.use_nontor_proxy")) {
+        tor.isolationEnabled = false;
+      }
+      tor.isolateCircuitsByDomain();
+    }
+  },
+  newCircuitForDomain(domain) {
+    tor.newCircuitForDomain(domain);
+  },
+
+  enableIsolation() {
+    tor.isolationEnabled = true;
+  },
+
+  disableIsolation() {
+    tor.isolationEnabled = false;
+  },
+
+  clearIsolation() {
+    tor.clearIsolation();
+  },
+
+  wrappedJSObject: null,
+};
+
+// Assign factory to global object.
+const NSGetFactory = XPCOMUtils.generateNSGetFactory
+  ? XPCOMUtils.generateNSGetFactory([DomainIsolator])
+  : ComponentUtils.generateNSGetFactory([DomainIsolator]);
diff --git a/toolkit/torbutton/components/dragDropFilter.js b/toolkit/torbutton/components/dragDropFilter.js
new file mode 100644
index 000000000000..f763595be5e6
--- /dev/null
+++ b/toolkit/torbutton/components/dragDropFilter.js
@@ -0,0 +1,134 @@
+/*************************************************************************
+ * Drag and Drop Handler.
+ *
+ * Implements an observer that filters drag events to prevent OS
+ * access to URLs (a potential proxy bypass vector).
+ *************************************************************************/
+
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
+});
+
+// Module specific constants
+const kMODULE_NAME = "Torbutton Drag and Drop Handler";
+const kCONTRACT_ID = "@torproject.org/torbutton-dragDropFilter;1";
+const kMODULE_CID = Components.ID("f605ec27-d867-44b5-ad97-2a29276642c3");
+
+const kInterfaces = [Ci.nsIObserver, Ci.nsIClassInfo];
+
+const URLISH_TYPES = Object.freeze([
+  "text/x-moz-url",
+  "text/x-moz-url-data",
+  "text/uri-list",
+  "application/x-moz-file-promise-url",
+]);
+
+/*
+  Returns true if the text resembles a URL or even just a hostname
+  in a way that may prompt the O.S. or other applications to send out a
+  validation DNS query, if not a full request (e.g. " torproject.org",
+  even with the leading whitespace).
+*/
+function isURLish(text) {
+  // Ignore leading whitespace.
+  text = text.trim();
+
+  // Without any protocol or dot in the first chunk, this is unlikely
+  // to be considered URLish (exception: localhost, but we don't care).
+  if (!/^[a-z][a-z0-9+-]*:\/\//i.test(text)) {
+    // no protocol
+    if (!/^[^.\s\/]+\.[^.\s\/]/.test(text)) {
+      // no dot
+      return false;
+    }
+    // Prepare for hostname validation via relative URL building.
+    text = `//${text}`;
+  }
+  // Validate URL or hostname.
+  try {
+    new URL(text, "https://localhost");
+    return true;
+  } catch (e) {
+    // invalid URL, bail out
+  }
+  return false;
+}
+
+// Returns true if any chunk of text is URLish
+const hasURLish = text => text.split(/[^\p{L}_.-:\/%~@$-]+/u).some(isURLish);
+
+function DragDropFilter() {
+  this.logger = Cc["@torproject.org/torbutton-logger;1"].getService(
+    Ci.nsISupports
+  ).wrappedJSObject;
+  this.logger.log(3, "Component Load 0: New DragDropFilter.");
+
+  try {
+    Services.obs.addObserver(this, "on-datatransfer-available");
+  } catch (e) {
+    this.logger.log(5, "Failed to register drag observer");
+  }
+}
+
+DragDropFilter.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
+
+  // make this an nsIClassInfo object
+  flags: Ci.nsIClassInfo.DOM_OBJECT,
+  classDescription: kMODULE_NAME,
+  contractID: kCONTRACT_ID,
+  classID: kMODULE_CID,
+
+  // method of nsIClassInfo
+  getInterfaces(count) {
+    count.value = kInterfaces.length;
+    return kInterfaces;
+  },
+
+  // method of nsIClassInfo
+  getHelperForLanguage(count) {
+    return null;
+  },
+
+  // method of nsIObserver
+  observe(subject, topic, data) {
+    if (topic === "on-datatransfer-available") {
+      this.logger.log(3, "The DataTransfer is available");
+      this.filterDataTransferURLs(subject);
+    }
+  },
+
+  filterDataTransferURLs(aDataTransfer) {
+    for (let i = 0, count = aDataTransfer.mozItemCount; i < count; ++i) {
+      this.logger.log(3, `Inspecting the data transfer: ${i}.`);
+      const types = aDataTransfer.mozTypesAt(i);
+      for (const type of types) {
+        this.logger.log(3, `Type is: ${type}.`);
+        if (
+          URLISH_TYPES.includes(type) ||
+          ((type === "text/plain" || type === "text/html") &&
+            hasURLish(aDataTransfer.getData(type)))
+        ) {
+          this.logger.log(
+            3,
+            `Removing transfer data ${aDataTransfer.getData(type)}`
+          );
+          for (const type of types) {
+            aDataTransfer.clearData(type);
+          }
+          break;
+        }
+      }
+    }
+  },
+};
+
+// Assign factory to global object.
+const NSGetFactory = XPCOMUtils.generateNSGetFactory
+  ? XPCOMUtils.generateNSGetFactory([DragDropFilter])
+  : ComponentUtils.generateNSGetFactory([DragDropFilter]);
diff --git a/toolkit/torbutton/components/external-app-blocker.js b/toolkit/torbutton/components/external-app-blocker.js
new file mode 100644
index 000000000000..6a53fc01eaf9
--- /dev/null
+++ b/toolkit/torbutton/components/external-app-blocker.js
@@ -0,0 +1,160 @@
+// Bug 1506 Android P1/TBB P5: This code provides users with notification
+// in the event of external app launch. We want it to exist in the desktop
+// port, but it is probably useless for Android.
+
+/*************************************************************************
+ * External App Handler.
+ * Handles displaying confirmation dialogs for external apps and protocols
+ * due to Firefox Bug https://bugzilla.mozilla.org/show_bug.cgi?id=440892
+ *
+ * An instance of this module is created each time the browser starts to
+ * download a file and when an external application may be invoked to
+ * handle an URL (e.g., when the user clicks on a mailto: URL).
+ *************************************************************************/
+
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { PromptUtils } = ChromeUtils.import(
+  "resource://gre/modules/SharedPromptUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
+});
+
+let { torbutton_get_property_string } = ChromeUtils.import(
+  "resource://torbutton/modules/utils.js"
+);
+
+// Module specific constants
+const kMODULE_NAME = "Torbutton External App Handler";
+const kCONTRACT_ID = "@torproject.org/torbutton-extAppBlocker;1";
+const kMODULE_CID = Components.ID("3da0269f-fc29-4e9e-a678-c3b1cafcf13f");
+
+const kInterfaces = [Ci.nsIObserver, Ci.nsIClassInfo];
+
+function ExternalAppBlocker() {
+  this.logger = Cc["@torproject.org/torbutton-logger;1"].getService(
+    Ci.nsISupports
+  ).wrappedJSObject;
+  this.logger.log(3, "Component Load 0: New ExternalAppBlocker.");
+}
+
+ExternalAppBlocker.prototype = {
+  _helperAppLauncher: undefined,
+
+  QueryInterface: ChromeUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsIHelperAppWarningDialog,
+  ]),
+
+  // make this an nsIClassInfo object
+  flags: Ci.nsIClassInfo.DOM_OBJECT,
+  classDescription: kMODULE_NAME,
+  contractID: kCONTRACT_ID,
+  classID: kMODULE_CID,
+
+  // method of nsIClassInfo
+  getInterfaces(count) {
+    count.value = kInterfaces.length;
+    return kInterfaces;
+  },
+
+  // method of nsIClassInfo
+  getHelperForLanguage(count) {
+    return null;
+  },
+
+  // method of nsIHelperAppWarningDialog
+  maybeShow(aLauncher, aWindowContext) {
+    // Hold a reference to the object that called this component. This is
+    // important not just because we need to later invoke the
+    // continueRequest() or cancelRequest() callback on aLauncher, but also
+    // so that the launcher object (which is a reference counted object) is
+    // not released too soon.
+    this._helperAppLauncher = aLauncher;
+
+    if (!Services.prefs.getBoolPref("extensions.torbutton.launch_warning")) {
+      this._helperAppLauncher.continueRequest();
+      return;
+    }
+
+    this._showPrompt(aWindowContext);
+  },
+
+  /*
+   * The _showPrompt() implementation uses some XUL and JS that is part of the
+   * browser's confirmEx() implementation. Specifically, _showPrompt() depends
+   * on chrome://global/content/commonDialog.xhtml as well as some of the code
+   * in resource://gre/modules/SharedPromptUtils.jsm.
+   */
+  _showPrompt(aWindowContext) {
+    let parentWin;
+    try {
+      parentWin = aWindowContext.getInterface(Ci.nsIDOMWindow);
+    } catch (e) {
+      parentWin = Services.wm.getMostRecentWindow("navigator:browser");
+    }
+
+    let title = torbutton_get_property_string("torbutton.popup.external.title");
+    let app = torbutton_get_property_string("torbutton.popup.external.app");
+    let note = torbutton_get_property_string("torbutton.popup.external.note");
+    let suggest = torbutton_get_property_string(
+      "torbutton.popup.external.suggest"
+    );
+    let launch = torbutton_get_property_string("torbutton.popup.launch");
+    let cancel = torbutton_get_property_string("torbutton.popup.cancel");
+    let dontask = torbutton_get_property_string("torbutton.popup.dontask");
+
+    let args = {
+      promptType: "confirmEx",
+      title,
+      text: app + note + suggest + " ",
+      checkLabel: dontask,
+      checked: false,
+      ok: false,
+      button0Label: launch,
+      button1Label: cancel,
+      defaultButtonNum: 1, // Cancel
+      buttonNumClicked: 1, // Cancel
+      enableDelay: true,
+    };
+
+    let propBag = PromptUtils.objectToPropBag(args);
+    let uri = "chrome://global/content/commonDialog.xhtml";
+    let promptWin = Services.ww.openWindow(
+      parentWin,
+      uri,
+      "_blank",
+      "centerscreen,chrome,titlebar",
+      propBag
+    );
+    promptWin.addEventListener("load", aEvent => {
+      promptWin.addEventListener("unload", aEvent => {
+        PromptUtils.propBagToObject(propBag, args);
+
+        if (0 == args.buttonNumClicked) {
+          // Save the checkbox value and tell the browser's external helper app
+          // module about the user's choice.
+          if (args.checked) {
+            Services.prefs.setBoolPref(
+              "extensions.torbutton.launch_warning",
+              false
+            );
+          }
+
+          this._helperAppLauncher.continueRequest();
+        } else {
+          this._helperAppLauncher.cancelRequest(Cr.NS_BINDING_ABORTED);
+        }
+      });
+    });
+  },
+};
+
+// Assign factory to global object.
+const NSGetFactory = XPCOMUtils.generateNSGetFactory
+  ? XPCOMUtils.generateNSGetFactory([ExternalAppBlocker])
+  : ComponentUtils.generateNSGetFactory([ExternalAppBlocker]);
diff --git a/toolkit/torbutton/components/startup-observer.js b/toolkit/torbutton/components/startup-observer.js
new file mode 100644
index 000000000000..8f9b8cc24bc3
--- /dev/null
+++ b/toolkit/torbutton/components/startup-observer.js
@@ -0,0 +1,196 @@
+// Bug 1506 P1-3: This code is mostly hackish remnants of session store
+// support. There are a couple of observer events that *might* be worth
+// listening to. Search for 1506 in the code.
+
+/*************************************************************************
+ * Startup observer (JavaScript XPCOM component)
+ *
+ * Cases tested (each during Tor and Non-Tor, FF4 and FF3.6)
+ *    1. Crash
+ *    2. Upgrade
+ *    3. Fresh install
+ *
+ *************************************************************************/
+
+const { AppConstants } = ChromeUtils.import(
+  "resource://gre/modules/AppConstants.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource://gre/modules/TorProtocolService.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
+});
+
+// Module specific constants
+const kMODULE_NAME = "Startup";
+const kMODULE_CONTRACTID = "@torproject.org/startup-observer;1";
+const kMODULE_CID = Components.ID("06322def-6fde-4c06-aef6-47ae8e799629");
+
+function cleanupCookies() {
+  const migratedPref = "extensions.torbutton.cookiejar_migrated";
+  if (!Services.prefs.getBoolPref(migratedPref, false)) {
+    // Cleanup stored cookie-jar-selector json files
+    const profileFolder = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+    for (const file of profileFolder.directoryEntries) {
+      if (file.leafName.match(/^(cookies|protected)-.*[.]json$/)) {
+        try {
+          file.remove(false);
+        } catch (e) {}
+      }
+    }
+    Services.prefs.setBoolPref(migratedPref, true);
+  }
+}
+
+function StartupObserver() {
+  this.logger = Cc["@torproject.org/torbutton-logger;1"].getService(
+    Ci.nsISupports
+  ).wrappedJSObject;
+  this._prefs = Services.prefs;
+  this.logger.log(3, "Startup Observer created");
+
+  var env = Cc["@mozilla.org/process/environment;1"].getService(
+    Ci.nsIEnvironment
+  );
+  var prefName = "browser.startup.homepage";
+  if (env.exists("TOR_DEFAULT_HOMEPAGE")) {
+    // if the user has set this value in a previous installation, don't override it
+    if (!this._prefs.prefHasUserValue(prefName)) {
+      this._prefs.setCharPref(prefName, env.get("TOR_DEFAULT_HOMEPAGE"));
+    }
+  }
+
+  this.is_tbb = true;
+
+  try {
+    // XXX: We're in a race with HTTPS-Everywhere to update our proxy settings
+    // before the initial SSL-Observatory test... If we lose the race, Firefox
+    // caches the old proxy settings for check.tp.o somehwere, and it never loads :(
+    this.setProxySettings();
+  } catch (e) {
+    this.logger.log(
+      4,
+      "Early proxy change failed. Will try again at profile load. Error: " + e
+    );
+  }
+
+  cleanupCookies();
+}
+
+StartupObserver.prototype = {
+  // Bug 6803: We need to get the env vars early due to
+  // some weird proxy caching code that showed up in FF15.
+  // Otherwise, homepage domain loads fail forever.
+  setProxySettings() {
+    if (!this.is_tbb) {
+      return;
+    }
+
+    // Bug 1506: Still want to get these env vars
+    let environ = Cc["@mozilla.org/process/environment;1"].getService(
+      Ci.nsIEnvironment
+    );
+    if (environ.exists("TOR_TRANSPROXY")) {
+      this.logger.log(3, "Resetting Tor settings to transproxy");
+      this._prefs.setBoolPref("network.proxy.socks_remote_dns", false);
+      this._prefs.setIntPref("network.proxy.type", 0);
+      this._prefs.setIntPref("network.proxy.socks_port", 0);
+      this._prefs.setCharPref("network.proxy.socks", "");
+    } else {
+      // Try to retrieve SOCKS proxy settings from Tor Launcher.
+      let socksPortInfo;
+      try {
+        socksPortInfo = TorProtocolService.torGetSOCKSPortInfo();
+      } catch (e) {
+        this.logger.log(3, "tor launcher failed " + e);
+      }
+
+      // If Tor Launcher is not available, check environment variables.
+      if (!socksPortInfo) {
+        socksPortInfo = { ipcFile: undefined, host: undefined, port: 0 };
+
+        let isWindows = Services.appinfo.OS === "WINNT";
+        if (!isWindows && environ.exists("TOR_SOCKS_IPC_PATH")) {
+          socksPortInfo.ipcFile = new FileUtils.File(
+            environ.get("TOR_SOCKS_IPC_PATH")
+          );
+        } else {
+          if (environ.exists("TOR_SOCKS_HOST")) {
+            socksPortInfo.host = environ.get("TOR_SOCKS_HOST");
+          }
+          if (environ.exists("TOR_SOCKS_PORT")) {
+            socksPortInfo.port = parseInt(environ.get("TOR_SOCKS_PORT"));
+          }
+        }
+      }
+
+      // Adjust network.proxy prefs.
+      if (socksPortInfo.ipcFile) {
+        let fph = Services.io
+          .getProtocolHandler("file")
+          .QueryInterface(Ci.nsIFileProtocolHandler);
+        let fileURI = fph.newFileURI(socksPortInfo.ipcFile);
+        this.logger.log(3, "Reset socks to " + fileURI.spec);
+        this._prefs.setCharPref("network.proxy.socks", fileURI.spec);
+        this._prefs.setIntPref("network.proxy.socks_port", 0);
+      } else {
+        if (socksPortInfo.host) {
+          this._prefs.setCharPref("network.proxy.socks", socksPortInfo.host);
+          this.logger.log(3, "Reset socks host to " + socksPortInfo.host);
+        }
+        if (socksPortInfo.port) {
+          this._prefs.setIntPref(
+            "network.proxy.socks_port",
+            socksPortInfo.port
+          );
+          this.logger.log(3, "Reset socks port to " + socksPortInfo.port);
+        }
+      }
+
+      if (socksPortInfo.ipcFile || socksPortInfo.host || socksPortInfo.port) {
+        this._prefs.setBoolPref("network.proxy.socks_remote_dns", true);
+        this._prefs.setIntPref("network.proxy.type", 1);
+      }
+    }
+
+    // Force prefs to be synced to disk
+    Services.prefs.savePrefFile(null);
+
+    this.logger.log(3, "Synced network settings to environment.");
+  },
+
+  observe(subject, topic, data) {
+    if (topic == "profile-after-change") {
+      // Bug 1506 P1: We listen to these prefs as signals for startup,
+      // but only for hackish reasons.
+      this._prefs.setBoolPref("extensions.torbutton.startup", true);
+
+      this.setProxySettings();
+    }
+
+    // In all cases, force prefs to be synced to disk
+    Services.prefs.savePrefFile(null);
+  },
+
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]),
+
+  // method of nsIClassInfo
+  classDescription: "Torbutton Startup Observer",
+  classID: kMODULE_CID,
+  contractID: kMODULE_CONTRACTID,
+
+  // Hack to get us registered early to observe recovery
+  _xpcom_categories: [{ category: "profile-after-change" }],
+};
+
+// Assign factory to global object.
+const NSGetFactory = XPCOMUtils.generateNSGetFactory
+  ? XPCOMUtils.generateNSGetFactory([StartupObserver])
+  : ComponentUtils.generateNSGetFactory([StartupObserver]);
diff --git a/toolkit/torbutton/components/torCheckService.js b/toolkit/torbutton/components/torCheckService.js
new file mode 100644
index 000000000000..41d716ff935c
--- /dev/null
+++ b/toolkit/torbutton/components/torCheckService.js
@@ -0,0 +1,140 @@
+/*************************************************************************
+ * Copyright (c) 2013, The Tor Project, Inc.
+ * See LICENSE for licensing information.
+ *
+ * vim: set sw=2 sts=2 ts=8 et syntax=javascript:
+ *
+ * Tor check service
+ *************************************************************************/
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
+});
+
+// Module specific constants
+const kMODULE_NAME = "Torbutton Tor Check Service";
+const kMODULE_CONTRACTID = "@torproject.org/torbutton-torCheckService;1";
+const kMODULE_CID = Components.ID("5d57312b-5d8c-4169-b4af-e80d6a28a72e");
+
+function TBTorCheckService() {
+  this._logger = Cc["@torproject.org/torbutton-logger;1"].getService(
+    Ci.nsISupports
+  ).wrappedJSObject;
+  this._logger.log(3, "Torbutton Tor Check Service initialized");
+
+  this._statusOfTorCheck = this.kCheckNotInitiated;
+  this.wrappedJSObject = this;
+}
+
+TBTorCheckService.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]),
+
+  kCheckNotInitiated: 0, // Possible values for statusOfTorCheck.
+  kCheckSuccessful: 1,
+  kCheckFailed: 2,
+
+  wrappedJSObject: null,
+  _logger: null,
+  _statusOfTorCheck: 0, // this.kCheckNotInitiated,
+
+  // make this an nsIClassInfo object
+  flags: Ci.nsIClassInfo.DOM_OBJECT,
+
+  // method of nsIClassInfo
+  classDescription: kMODULE_NAME,
+  classID: kMODULE_CID,
+  contractID: kMODULE_CONTRACTID,
+
+  // method of nsIClassInfo
+  getInterfaces(count) {
+    var interfaceList = [Ci.nsIClassInfo];
+    count.value = interfaceList.length;
+    return interfaceList;
+  },
+
+  // method of nsIClassInfo
+  getHelperForLanguage(count) {
+    return null;
+  },
+
+  // Public methods.
+  get statusOfTorCheck() {
+    return this._statusOfTorCheck;
+  },
+
+  set statusOfTorCheck(aStatus) {
+    this._statusOfTorCheck = aStatus;
+  },
+
+  createCheckRequest(aAsync) {
+    let req = new XMLHttpRequest();
+    let url = Services.prefs.getCharPref("extensions.torbutton.test_url");
+    req.open("GET", url, aAsync);
+    req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+    req.overrideMimeType("text/xml");
+    req.timeout = 120000; // Wait at most two minutes for a response.
+    return req;
+  },
+
+  parseCheckResponse(aReq) {
+    let ret = 0;
+    if (aReq.status == 200) {
+      if (!aReq.responseXML) {
+        this._logger.log(5, "Check failed! Not text/xml!");
+        ret = 1;
+      } else {
+        let result = aReq.responseXML.getElementById("TorCheckResult");
+
+        if (result === null) {
+          this._logger.log(5, "Test failed! No TorCheckResult element");
+          ret = 2;
+        } else if (
+          typeof result.target == "undefined" ||
+          result.target === null
+        ) {
+          this._logger.log(5, "Test failed! No target");
+          ret = 3;
+        } else if (result.target === "success") {
+          this._logger.log(3, "Test Successful");
+          ret = 4;
+        } else if (result.target === "failure") {
+          this._logger.log(5, "Tor test failed!");
+          ret = 5;
+        } else if (result.target === "unknown") {
+          this._logger.log(5, "Tor test failed. TorDNSEL Failure?");
+          ret = 6;
+        } else {
+          this._logger.log(5, "Tor test failed. Strange target.");
+          ret = 7;
+        }
+      }
+    } else {
+      if (0 == aReq.status) {
+        try {
+          var req = aReq.channel.QueryInterface(Ci.nsIRequest);
+          if (req.status == Cr.NS_ERROR_PROXY_CONNECTION_REFUSED) {
+            this._logger.log(5, "Tor test failed. Proxy connection refused");
+            ret = 8;
+          }
+        } catch (e) {}
+      }
+
+      if (ret == 0) {
+        this._logger.log(5, "Tor test failed. HTTP Error: " + aReq.status);
+        ret = -aReq.status;
+      }
+    }
+
+    return ret;
+  },
+};
+
+// Assign factory to global object.
+const NSGetFactory = XPCOMUtils.generateNSGetFactory
+  ? XPCOMUtils.generateNSGetFactory([TBTorCheckService])
+  : ComponentUtils.generateNSGetFactory([TBTorCheckService]);
diff --git a/toolkit/torbutton/components/torbutton-logger.js b/toolkit/torbutton/components/torbutton-logger.js
new file mode 100644
index 000000000000..2fdcd7e6a753
--- /dev/null
+++ b/toolkit/torbutton/components/torbutton-logger.js
@@ -0,0 +1,185 @@
+// Bug 1506 P1: This is just a handy logger. If you have a better one, toss
+// this in the trash.
+
+/*************************************************************************
+ * TBLogger (JavaScript XPCOM component)
+ *
+ * Allows loglevel-based logging to different logging mechanisms.
+ *
+ *************************************************************************/
+
+// Module specific constants
+const kMODULE_NAME = "Torbutton Logger";
+const kMODULE_CONTRACTID = "@torproject.org/torbutton-logger;1";
+const kMODULE_CID = Components.ID("f36d72c9-9718-4134-b550-e109638331d7");
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
+});
+
+function TorbuttonLogger() {
+  // Register observer
+  Services.prefs.addObserver("extensions.torbutton", this);
+
+  this.loglevel = Services.prefs.getIntPref("extensions.torbutton.loglevel");
+  this.logmethod = Services.prefs.getIntPref("extensions.torbutton.logmethod");
+
+  try {
+    var logMngr = Cc["@mozmonkey.com/debuglogger/manager;1"].getService(
+      Ci.nsIDebugLoggerManager
+    );
+    this._debuglog = logMngr.registerLogger("torbutton");
+  } catch (exErr) {
+    this._debuglog = false;
+  }
+  this._console = Services.console;
+
+  // This JSObject is exported directly to chrome
+  this.wrappedJSObject = this;
+  this.log(3, "Torbutton debug output ready");
+}
+
+/**
+ * JS XPCOM component registration goop:
+ *
+ * Everything below is boring boilerplate and can probably be ignored.
+ */
+
+const nsIClassInfo = Ci.nsIClassInfo;
+
+const logString = { 1: "VERB", 2: "DBUG", 3: "INFO", 4: "NOTE", 5: "WARN" };
+
+function padInt(i) {
+  return i < 10 ? "0" + i : i;
+}
+
+TorbuttonLogger.prototype = {
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]),
+
+  wrappedJSObject: null, // Initialized by constructor
+
+  // make this an nsIClassInfo object
+  flags: nsIClassInfo.DOM_OBJECT,
+
+  // method of nsIClassInfo
+  classDescription: "TorbuttonLogger",
+  classID: kMODULE_CID,
+  contractID: kMODULE_CONTRACTID,
+
+  // method of nsIClassInfo
+  getInterfaces(count) {
+    var interfaceList = [nsIClassInfo];
+    count.value = interfaceList.length;
+    return interfaceList;
+  },
+
+  // method of nsIClassInfo
+  getHelperForLanguage(count) {
+    return null;
+  },
+
+  formatLog(str, level) {
+    var d = new Date();
+    var now =
+      padInt(d.getUTCMonth() + 1) +
+      "-" +
+      padInt(d.getUTCDate()) +
+      " " +
+      padInt(d.getUTCHours()) +
+      ":" +
+      padInt(d.getUTCMinutes()) +
+      ":" +
+      padInt(d.getUTCSeconds());
+    return "[" + now + "] Torbutton " + logString[level] + ": " + str;
+  },
+
+  // error console log
+  eclog(level, str) {
+    switch (this.logmethod) {
+      case 0: // stderr
+        if (this.loglevel <= level) {
+          dump(this.formatLog(str, level) + "\n");
+        }
+        break;
+      default:
+        // errorconsole
+        if (this.loglevel <= level) {
+          this._console.logStringMessage(this.formatLog(str, level));
+        }
+        break;
+    }
+  },
+
+  safe_log(level, str, scrub) {
+    if (this.loglevel < 4) {
+      this.eclog(level, str + scrub);
+    } else {
+      this.eclog(level, str + " [scrubbed]");
+    }
+  },
+
+  log(level, str) {
+    switch (this.logmethod) {
+      case 2: // debuglogger
+        if (this._debuglog) {
+          this._debuglog.log(6 - level, this.formatLog(str, level));
+          break;
+        }
+      // fallthrough
+      case 0: // stderr
+        if (this.loglevel <= level) {
+          dump(this.formatLog(str, level) + "\n");
+        }
+        break;
+      case 1: // errorconsole
+        if (this.loglevel <= level) {
+          this._console.logStringMessage(this.formatLog(str, level));
+        }
+        break;
+      default:
+        dump("Bad log method: " + this.logmethod);
+    }
+  },
+
+  // Pref observer interface implementation
+
+  // topic:   what event occurred
+  // subject: what nsIPrefBranch we're observing
+  // data:    which pref has been changed (relative to subject)
+  observe(subject, topic, data) {
+    if (topic != "nsPref:changed") {
+      return;
+    }
+    switch (data) {
+      case "extensions.torbutton.logmethod":
+        this.logmethod = Services.prefs.getIntPref(
+          "extensions.torbutton.logmethod"
+        );
+        if (this.logmethod === 0) {
+          Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
+        } else if (
+          Services.prefs.getIntPref("extensions.torlauncher.logmethod", 3) !== 0
+        ) {
+          // If Tor Launcher is not available or its log method is not 0
+          // then let's reset the dump pref.
+          Services.prefs.setBoolPref("browser.dom.window.dump.enabled", false);
+        }
+        break;
+      case "extensions.torbutton.loglevel":
+        this.loglevel = Services.prefs.getIntPref(
+          "extensions.torbutton.loglevel"
+        );
+        break;
+    }
+  },
+};
+
+// Assign factory to global object.
+const NSGetFactory = XPCOMUtils.generateNSGetFactory
+  ? XPCOMUtils.generateNSGetFactory([TorbuttonLogger])
+  : ComponentUtils.generateNSGetFactory([TorbuttonLogger]);
diff --git a/toolkit/torbutton/jar.mn b/toolkit/torbutton/jar.mn
new file mode 100644
index 000000000000..5e7a11384cc4
--- /dev/null
+++ b/toolkit/torbutton/jar.mn
@@ -0,0 +1,46 @@
+#filter substitution
+
+torbutton.jar:
+
+% content torbutton %content/
+
+  content/torbutton.js                   (chrome/content/torbutton.js)
+  content/tor-circuit-display.js         (chrome/content/tor-circuit-display.js)
+  content/preferences.xhtml              (chrome/content/preferences.xhtml)
+  content/aboutTor/aboutTor-content.js   (chrome/content/aboutTor/aboutTor-content.js)
+* content/aboutTor/aboutTor.xhtml        (chrome/content/aboutTor/aboutTor.xhtml)
+  content/aboutTor/resources/aboutTor.js (chrome/content/aboutTor/resources/aboutTor.js)
+  content/preferences-mobile.js          (chrome/content/preferences-mobile.js)
+
+  components/    (components/*)
+  modules/       (modules/*)
+  skin/          (chrome/skin/*)
+
+% resource torbutton %
+% resource torbutton-abouttor resource://torbutton/content/aboutTor/resources/ contentaccessible=yes
+% resource torbutton-assets resource://torbutton/skin/ contentaccessible=yes
+
+% skin torbutton classic/1.0 %skin/
+
+# Firefox 4-style component registration
+% component {f605ec27-d867-44b5-ad97-2a29276642c3} %components/dragDropFilter.js
+% contract @torproject.org/torbutton-dragDropFilter;1 {f605ec27-d867-44b5-ad97-2a29276642c3}
+
+% component {3da0269f-fc29-4e9e-a678-c3b1cafcf13f} %components/external-app-blocker.js
+% contract @torproject.org/torbutton-extAppBlocker;1 {3da0269f-fc29-4e9e-a678-c3b1cafcf13f}
+
+% component {06322def-6fde-4c06-aef6-47ae8e799629} %components/startup-observer.js
+% contract @torproject.org/startup-observer;1 {06322def-6fde-4c06-aef6-47ae8e799629}
+
+% component {5d57312b-5d8c-4169-b4af-e80d6a28a72e} %components/torCheckService.js
+% contract @torproject.org/torbutton-torCheckService;1 {5d57312b-5d8c-4169-b4af-e80d6a28a72e}
+
+% component {f36d72c9-9718-4134-b550-e109638331d7} %components/torbutton-logger.js
+% contract @torproject.org/torbutton-logger;1 {f36d72c9-9718-4134-b550-e109638331d7}
+
+% component {e33fd6d4-270f-475f-a96f-ff3140279f68} %components/domain-isolator.js
+% contract @torproject.org/domain-isolator;1 {e33fd6d4-270f-475f-a96f-ff3140279f68}
+
+% category profile-after-change StartupObserver @torproject.org/startup-observer;1
+% category profile-after-change DomainIsolator @torproject.org/domain-isolator;1
+% category profile-after-change DragDropFilter @torproject.org/torbutton-dragDropFilter;1
diff --git a/toolkit/torbutton/modules/tor-control-port.js b/toolkit/torbutton/modules/tor-control-port.js
new file mode 100644
index 000000000000..374ff5fd1bfd
--- /dev/null
+++ b/toolkit/torbutton/modules/tor-control-port.js
@@ -0,0 +1,982 @@
+// A module for TorBrowser that provides an asynchronous controller for
+// Tor, through its ControlPort.
+//
+// This file is written in call stack order (later functions
+// call earlier functions). The file can be processed
+// with docco.js to produce pretty documentation.
+//
+// To import the module, use
+//
+//  let { configureControlPortModule, controller, wait_for_controller } =
+//                Components.utils.import("path/to/tor-control-port.js", {});
+//
+// See the third-to-last function defined in this file:
+//   configureControlPortModule(ipcFile, host, port, password)
+// for usage of the configureControlPortModule function.
+//
+// See the last functions defined in this file:
+//   controller(avoidCache), wait_for_controller(avoidCache)
+// for usage of the controller functions.
+
+/* jshint esnext: true */
+/* jshint -W097 */
+/* global console */
+"use strict";
+
+// ### Import Mozilla Services
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorMonitorService",
+  "resource://gre/modules/TorMonitorService.jsm"
+);
+
+// tor-launcher observer topics
+const TorTopics = Object.freeze({
+  ProcessIsReady: "TorProcessIsReady",
+});
+
+// __log__.
+// Logging function
+let logger = Cc["@torproject.org/torbutton-logger;1"].getService(Ci.nsISupports)
+  .wrappedJSObject;
+let log = x => logger.eclog(3, x.trimRight().replace(/\r\n/g, "\n"));
+
+// ### announce this file
+log("Loading tor-control-port.js\n");
+
+class AsyncSocket {
+  constructor(ipcFile, host, port) {
+    let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+      Ci.nsISocketTransportService
+    );
+    const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
+
+    let socketTransport = ipcFile
+      ? sts.createUnixDomainTransport(ipcFile)
+      : sts.createTransport([], host, port, null, null);
+
+    this.outputStream = socketTransport
+      .openOutputStream(OPEN_UNBUFFERED, 1, 1)
+      .QueryInterface(Ci.nsIAsyncOutputStream);
+    this.outputQueue = [];
+
+    this.inputStream = socketTransport
+      .openInputStream(OPEN_UNBUFFERED, 1, 1)
+      .QueryInterface(Ci.nsIAsyncInputStream);
+    this.scriptableInputStream = Cc[
+      "@mozilla.org/scriptableinputstream;1"
+    ].createInstance(Ci.nsIScriptableInputStream);
+    this.scriptableInputStream.init(this.inputStream);
+    this.inputQueue = [];
+  }
+
+  // asynchronously write string to underlying socket and return number of bytes written
+  async write(str) {
+    return new Promise((resolve, reject) => {
+      // asyncWait next write request
+      const tryAsyncWait = () => {
+        if (this.outputQueue.length) {
+          this.outputStream.asyncWait(
+            this.outputQueue.at(0), // next request
+            0,
+            0,
+            Services.tm.currentThread
+          );
+        }
+      };
+
+      // output stream can only have 1 registered callback at a time, so multiple writes
+      // need to be queued up (see nsIAsyncOutputStream.idl)
+      this.outputQueue.push({
+        // Implement an nsIOutputStreamCallback:
+        onOutputStreamReady: () => {
+          try {
+            let bytesWritten = this.outputStream.write(str, str.length);
+
+            // remove this callback object from queue as it is now completed
+            this.outputQueue.shift();
+
+            // request next wait if there is one
+            tryAsyncWait();
+
+            // finally resolve promise
+            resolve(bytesWritten);
+          } catch (err) {
+            // reject promise on error
+            reject(err);
+          }
+        },
+      });
+
+      // length 1 imples that there is no in-flight asyncWait, so we may immediately
+      // follow through on this write
+      if (this.outputQueue.length == 1) {
+        tryAsyncWait();
+      }
+    });
+  }
+
+  // asynchronously read string from underlying socket and return it
+  async read() {
+    return new Promise((resolve, reject) => {
+      const tryAsyncWait = () => {
+        if (this.inputQueue.length) {
+          this.inputStream.asyncWait(
+            this.inputQueue.at(0), // next input request
+            0,
+            0,
+            Services.tm.currentThread
+          );
+        }
+      };
+
+      this.inputQueue.push({
+        onInputStreamReady: stream => {
+          try {
+            // read our string from input stream
+            let str = this.scriptableInputStream.read(
+              this.scriptableInputStream.available()
+            );
+
+            // remove this callback object from queue now that we have read
+            this.inputQueue.shift();
+
+            // request next wait if there is one
+            tryAsyncWait();
+
+            // finally resolve promise
+            resolve(str);
+          } catch (err) {
+            reject(err);
+          }
+        },
+      });
+
+      // length 1 imples that there is no in-flight asyncWait, so we may immediately
+      // follow through on this read
+      if (this.inputQueue.length == 1) {
+        tryAsyncWait();
+      }
+    });
+  }
+
+  close() {
+    this.outputStream.close();
+    this.inputStream.close();
+  }
+}
+
+class ControlSocket {
+  constructor(asyncSocket) {
+    this.socket = asyncSocket;
+    this._isOpen = true;
+    this.pendingData = "";
+    this.pendingLines = [];
+
+    this.mainDispatcher = io.callbackDispatcher();
+    this.notificationDispatcher = io.callbackDispatcher();
+    // mainDispatcher pushes only async notifications (650) to notificationDispatcher
+    this.mainDispatcher.addCallback(
+      /^650/,
+      this._handleNotification.bind(this)
+    );
+    // callback for handling responses and errors
+    this.mainDispatcher.addCallback(
+      /^[245]\d\d/,
+      this._handleCommandReply.bind(this)
+    );
+
+    this.commandQueue = [];
+
+    this._startMessagePump();
+  }
+
+  // blocks until an entire line is read and returns it
+  // immediately returns next line in queue (pendingLines) if present
+  async _readLine() {
+    // keep reading from socket until we have a full line to return
+    while (!this.pendingLines.length) {
+      // read data from our socket and spit on newline tokens
+      this.pendingData += await this.socket.read();
+      let lines = this.pendingData.split("\r\n");
+
+      // the last line will either be empty string, or a partial read of a response/event
+      // so save it off for the next socket read
+      this.pendingData = lines.pop();
+
+      // copy remaining full lines to our pendingLines list
+      this.pendingLines = this.pendingLines.concat(lines);
+    }
+    return this.pendingLines.shift();
+  }
+
+  // blocks until an entire message is ready and returns it
+  async _readMessage() {
+    // whether we are searching for the end of a multi-line values
+    // See control-spec section 3.9
+    let handlingMultlineValue = false;
+    let endOfMessageFound = false;
+    const message = [];
+
+    do {
+      const line = await this._readLine();
+      message.push(line);
+
+      if (handlingMultlineValue) {
+        // look for end of multiline
+        if (line.match(/^\.$/)) {
+          handlingMultlineValue = false;
+        }
+      } else {
+        // 'Multiline values' are possible. We avoid interrupting one by detecting it
+        // and waiting for a terminating "." on its own line.
+        // (See control-spec section 3.9 and https://trac.torproject.org/16990#comment:28
+        // Ensure this is the first line of a new message
+        // eslint-disable-next-line no-lonely-if
+        if (message.length === 1 && line.match(/^\d\d\d\+.+?=$/)) {
+          handlingMultlineValue = true;
+        }
+        // look for end of message (note the space character at end of the regex)
+        else if (line.match(/^\d\d\d /)) {
+          if (message.length == 1) {
+            endOfMessageFound = true;
+          } else {
+            let firstReplyCode = message[0].substring(0, 3);
+            let lastReplyCode = line.substring(0, 3);
+            if (firstReplyCode == lastReplyCode) {
+              endOfMessageFound = true;
+            }
+          }
+        }
+      }
+    } while (!endOfMessageFound);
+
+    // join our lines back together to form one message
+    return message.join("\r\n");
+  }
+
+  async _startMessagePump() {
+    try {
+      while (true) {
+        let message = await this._readMessage();
+        log("controlPort >> " + message);
+        this.mainDispatcher.pushMessage(message);
+      }
+    } catch (err) {
+      this._isOpen = false;
+      for (const cmd of this.commandQueue) {
+        cmd.reject(err);
+      }
+      this.commandQueue = [];
+    }
+  }
+
+  _writeNextCommand() {
+    let cmd = this.commandQueue[0];
+    log("controlPort << " + cmd.commandString);
+    this.socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
+  }
+
+  async sendCommand(commandString) {
+    if (!this.isOpen()) {
+      throw new Error("ControlSocket not open");
+    }
+
+    // this promise is resolved either in _handleCommandReply, or
+    // in _startMessagePump (on stream error)
+    return new Promise((resolve, reject) => {
+      let command = {
+        commandString,
+        resolve,
+        reject,
+      };
+
+      this.commandQueue.push(command);
+      if (this.commandQueue.length == 1) {
+        this._writeNextCommand();
+      }
+    });
+  }
+
+  _handleCommandReply(message) {
+    let cmd = this.commandQueue.shift();
+    if (message.match(/^2/)) {
+      cmd.resolve(message);
+    } else if (message.match(/^[45]/)) {
+      let myErr = new Error(cmd.commandString + " -> " + message);
+      // Add Tor-specific information to the Error object.
+      let idx = message.indexOf(" ");
+      if (idx > 0) {
+        myErr.torStatusCode = message.substring(0, idx);
+        myErr.torMessage = message.substring(idx);
+      } else {
+        myErr.torStatusCode = message;
+      }
+      cmd.reject(myErr);
+    } else {
+      cmd.reject(
+        new Error(
+          `ControlSocket::_handleCommandReply received unexpected message:\n----\n${message}\n----`
+        )
+      );
+    }
+
+    // send next command if one is available
+    if (this.commandQueue.length) {
+      this._writeNextCommand();
+    }
+  }
+
+  _handleNotification(message) {
+    this.notificationDispatcher.pushMessage(message);
+  }
+
+  close() {
+    this.socket.close();
+    this._isOpen = false;
+  }
+
+  addNotificationCallback(regex, callback) {
+    this.notificationDispatcher.addCallback(regex, callback);
+  }
+
+  isOpen() {
+    return this._isOpen;
+  }
+}
+
+// ## io
+// I/O utilities namespace
+
+let io = {};
+
+// __io.callbackDispatcher()__.
+// Returns dispatcher object with three member functions:
+// dispatcher.addCallback(regex, callback), dispatcher.removeCallback(callback),
+// and dispatcher.pushMessage(message).
+// Pass pushMessage to another function that needs a callback with a single string
+// argument. Whenever dispatcher.pushMessage receives a string, the dispatcher will
+// check for any regex matches and pass the string on to the corresponding callback(s).
+io.callbackDispatcher = function() {
+  let callbackPairs = [],
+    removeCallback = function(aCallback) {
+      callbackPairs = callbackPairs.filter(function([regex, callback]) {
+        return callback !== aCallback;
+      });
+    },
+    addCallback = function(regex, callback) {
+      if (callback) {
+        callbackPairs.push([regex, callback]);
+      }
+      return function() {
+        removeCallback(callback);
+      };
+    },
+    pushMessage = function(message) {
+      for (let [regex, callback] of callbackPairs) {
+        if (message.match(regex)) {
+          callback(message);
+        }
+      }
+    };
+  return {
+    pushMessage,
+    removeCallback,
+    addCallback,
+  };
+};
+
+// __io.controlSocket(ipcFile, host, port, password)__.
+// Instantiates and returns a socket to a tor ControlPort at ipcFile or
+// host:port, authenticating with the given password. Example:
+//
+//     // Open the socket
+//     let socket = await io.controlSocket(undefined, "127.0.0.1", 9151, "MyPassw0rd");
+//     // Send command and receive "250" response reply or error is thrown
+//     await socket.sendCommand(commandText);
+//     // Register or deregister for "650" notifications
+//     // that match regex
+//     socket.addNotificationCallback(regex, callback);
+//     socket.removeNotificationCallback(callback);
+//     // Close the socket permanently
+//     socket.close();
+io.controlSocket = async function(ipcFile, host, port, password) {
+  let socket = new AsyncSocket(ipcFile, host, port);
+  let controlSocket = new ControlSocket(socket);
+
+  // Log in to control port.
+  await controlSocket.sendCommand("authenticate " + (password || ""));
+  // Activate needed events.
+  await controlSocket.sendCommand("setevents stream");
+
+  return controlSocket;
+};
+
+// ## utils
+// A namespace for utility functions
+let utils = {};
+
+// __utils.identity(x)__.
+// Returns its argument unchanged.
+utils.identity = function(x) {
+  return x;
+};
+
+// __utils.isString(x)__.
+// Returns true iff x is a string.
+utils.isString = function(x) {
+  return typeof x === "string" || x instanceof String;
+};
+
+// __utils.capture(string, regex)__.
+// Takes a string and returns an array of capture items, where regex must have a single
+// capturing group and use the suffix /.../g to specify a global search.
+utils.capture = function(string, regex) {
+  let matches = [];
+  // Special trick to use string.replace for capturing multiple matches.
+  string.replace(regex, function(a, captured) {
+    matches.push(captured);
+  });
+  return matches;
+};
+
+// __utils.extractor(regex)__.
+// Returns a function that takes a string and returns an array of regex matches. The
+// regex must use the suffix /.../g to specify a global search.
+utils.extractor = function(regex) {
+  return function(text) {
+    return utils.capture(text, regex);
+  };
+};
+
+// __utils.splitLines(string)__.
+// Splits a string into an array of strings, each corresponding to a line.
+utils.splitLines = function(string) {
+  return string.split(/\r?\n/);
+};
+
+// __utils.splitAtSpaces(string)__.
+// Splits a string into chunks between spaces. Does not split at spaces
+// inside pairs of quotation marks.
+utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
+
+// __utils.splitAtFirst(string, regex)__.
+// Splits a string at the first instance of regex match. If no match is
+// found, returns the whole string.
+utils.splitAtFirst = function(string, regex) {
+  let match = string.match(regex);
+  return match
+    ? [
+        string.substring(0, match.index),
+        string.substring(match.index + match[0].length),
+      ]
+    : string;
+};
+
+// __utils.splitAtEquals(string)__.
+// Splits a string into chunks between equals. Does not split at equals
+// inside pairs of quotation marks.
+utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
+
+// __utils.mergeObjects(arrayOfObjects)__.
+// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
+// Pure function.
+utils.mergeObjects = function(arrayOfObjects) {
+  let result = {};
+  for (let obj of arrayOfObjects) {
+    for (let key in obj) {
+      result[key] = obj[key];
+    }
+  }
+  return result;
+};
+
+// __utils.listMapData(parameterString, listNames)__.
+// Takes a list of parameters separated by spaces, of which the first several are
+// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
+// to the unnamed parameters, and combine them in a map with the named parameters.
+// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
+//
+//     utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
+//                       ["streamID", "event", "circuitID", "IP"])
+//     // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
+//     //      "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
+utils.listMapData = function(parameterString, listNames) {
+  // Split out the space-delimited parameters.
+  let parameters = utils.splitAtSpaces(parameterString),
+    dataMap = {};
+  // Assign listNames to the first n = listNames.length parameters.
+  for (let i = 0; i < listNames.length; ++i) {
+    dataMap[listNames[i]] = parameters[i];
+  }
+  // Read key-value pairs and copy these to the dataMap.
+  for (let i = listNames.length; i < parameters.length; ++i) {
+    let [key, value] = utils.splitAtEquals(parameters[i]);
+    if (key && value) {
+      dataMap[key] = value;
+    }
+  }
+  return dataMap;
+};
+
+// __utils.rejectPromise(errorMessage)__.
+// Returns a rejected promise with the given error message.
+utils.rejectPromise = errorMessage => Promise.reject(new Error(errorMessage));
+
+// ## info
+// A namespace for functions related to tor's GETINFO and GETCONF command.
+let info = {};
+
+// __info.keyValueStringsFromMessage(messageText)__.
+// Takes a message (text) response to GETINFO or GETCONF and provides
+// a series of key-value strings, which are either multiline (with a `250+` prefix):
+//
+//     250+config/defaults=
+//     AccountingMax "0 bytes"
+//     AllowDotExit "0"
+//     .
+//
+// or single-line (with a `250-` or `250 ` prefix):
+//
+//     250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
+info.keyValueStringsFromMessage = utils.extractor(
+  /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
+);
+
+// __info.applyPerLine(transformFunction)__.
+// Returns a function that splits text into lines,
+// and applies transformFunction to each line.
+info.applyPerLine = function(transformFunction) {
+  return function(text) {
+    return utils.splitLines(text.trim()).map(transformFunction);
+  };
+};
+
+// __info.routerStatusParser(valueString)__.
+// Parses a router status entry as, described in
+// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
+// (search for "router status entry")
+info.routerStatusParser = function(valueString) {
+  let lines = utils.splitLines(valueString),
+    objects = [];
+  for (let line of lines) {
+    // Drop first character and grab data following it.
+    let myData = line.substring(2),
+      // Accumulate more maps with data, depending on the first character in the line.
+      dataFun = {
+        r: data =>
+          utils.listMapData(data, [
+            "nickname",
+            "identity",
+            "digest",
+            "publicationDate",
+            "publicationTime",
+            "IP",
+            "ORPort",
+            "DirPort",
+          ]),
+        a: data => ({ IPv6: data }),
+        s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
+        v: data => ({ version: data }),
+        w: data => utils.listMapData(data, []),
+        p: data => ({ portList: data.split(",") }),
+      }[line.charAt(0)];
+    if (dataFun !== undefined) {
+      objects.push(dataFun(myData));
+    }
+  }
+  return utils.mergeObjects(objects);
+};
+
+// __info.circuitStatusParser(line)__.
+// Parse the output of a circuit status line.
+info.circuitStatusParser = function(line) {
+  let data = utils.listMapData(line, ["id", "status", "circuit"]),
+    circuit = data.circuit;
+  // Parse out the individual circuit IDs and names.
+  if (circuit) {
+    data.circuit = circuit.split(",").map(function(x) {
+      return x.split(/~|=/);
+    });
+  }
+  return data;
+};
+
+// __info.streamStatusParser(line)__.
+// Parse the output of a stream status line.
+info.streamStatusParser = function(text) {
+  return utils.listMapData(text, [
+    "StreamID",
+    "StreamStatus",
+    "CircuitID",
+    "Target",
+  ]);
+};
+
+// TODO: fix this parsing logic to handle bridgeLine correctly
+// fingerprint/id is an optional parameter
+// __info.bridgeParser(bridgeLine)__.
+// Takes a single line from a `getconf bridge` result and returns
+// a map containing the bridge's type, address, and ID.
+info.bridgeParser = function(bridgeLine) {
+  let result = {},
+    tokens = bridgeLine.split(/\s+/);
+  // First check if we have a "vanilla" bridge:
+  if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
+    result.type = "vanilla";
+    [result.address, result.ID] = tokens;
+    // Several bridge types have a similar format:
+  } else {
+    result.type = tokens[0];
+    if (
+      [
+        "flashproxy",
+        "fte",
+        "meek",
+        "meek_lite",
+        "obfs3",
+        "obfs4",
+        "scramblesuit",
+        "snowflake",
+      ].includes(result.type)
+    ) {
+      [result.address, result.ID] = tokens.slice(1);
+    }
+  }
+  return result.type ? result : null;
+};
+
+// __info.parsers__.
+// A map of GETINFO and GETCONF keys to parsing function, which convert
+// result strings to JavaScript data.
+info.parsers = {
+  "ns/id/": info.routerStatusParser,
+  "ip-to-country/": utils.identity,
+  "circuit-status": info.applyPerLine(info.circuitStatusParser),
+  bridge: info.bridgeParser,
+  // Currently unused parsers:
+  //  "ns/name/" : info.routerStatusParser,
+  //  "stream-status" : info.applyPerLine(info.streamStatusParser),
+  //  "version" : utils.identity,
+  //  "config-file" : utils.identity,
+};
+
+// __info.getParser(key)__.
+// Takes a key and determines the parser function that should be used to
+// convert its corresponding valueString to JavaScript data.
+info.getParser = function(key) {
+  return (
+    info.parsers[key] ||
+    info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
+  );
+};
+
+// __info.stringToValue(string)__.
+// Converts a key-value string as from GETINFO or GETCONF to a value.
+info.stringToValue = function(string) {
+  // key should look something like `250+circuit-status=` or `250-circuit-status=...`
+  // or `250 circuit-status=...`
+  let matchForKey = string.match(/^250[ +-](.+?)=/),
+    key = matchForKey ? matchForKey[1] : null;
+  if (key === null) {
+    return null;
+  }
+  // matchResult finds a single-line result for `250-` or `250 `,
+  // or a multi-line one for `250+`.
+  let matchResult =
+      string.match(/^250[ -].+?=(.*)$/) ||
+      string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
+    // Retrieve the captured group (the text of the value in the key-value pair)
+    valueString = matchResult ? matchResult[1] : null,
+    // Get the parser function for the key found.
+    parse = info.getParser(key.toLowerCase());
+  if (parse === undefined) {
+    throw new Error("No parser found for '" + key + "'");
+  }
+  // Return value produced by the parser.
+  return parse(valueString);
+};
+
+// __info.getMultipleResponseValues(message)__.
+// Process multiple responses to a GETINFO or GETCONF request.
+info.getMultipleResponseValues = function(message) {
+  return info
+    .keyValueStringsFromMessage(message)
+    .map(info.stringToValue)
+    .filter(utils.identity);
+};
+
+// __info.getInfo(controlSocket, key)__.
+// Sends GETINFO for a single key. Returns a promise with the result.
+info.getInfo = function(aControlSocket, key) {
+  if (!utils.isString(key)) {
+    return utils.rejectPromise("key argument should be a string");
+  }
+  return aControlSocket
+    .sendCommand("getinfo " + key)
+    .then(response => info.getMultipleResponseValues(response)[0]);
+};
+
+// __info.getConf(aControlSocket, key)__.
+// Sends GETCONF for a single key. Returns a promise with the result.
+info.getConf = function(aControlSocket, key) {
+  // GETCONF with a single argument returns results with
+  // one or more lines that look like `250[- ]key=value`.
+  // Any GETCONF lines that contain a single keyword only are currently dropped.
+  // So we can use similar parsing to that for getInfo.
+  if (!utils.isString(key)) {
+    return utils.rejectPromise("key argument should be a string");
+  }
+  return aControlSocket
+    .sendCommand("getconf " + key)
+    .then(info.getMultipleResponseValues);
+};
+
+// ## onionAuth
+// A namespace for functions related to tor's ONION_CLIENT_AUTH_* commands.
+let onionAuth = {};
+
+onionAuth.keyInfoStringsFromMessage = utils.extractor(/^250-CLIENT\s+(.+)$/gim);
+
+onionAuth.keyInfoObjectsFromMessage = function(message) {
+  let keyInfoStrings = onionAuth.keyInfoStringsFromMessage(message);
+  return keyInfoStrings.map(infoStr =>
+    utils.listMapData(infoStr, ["hsAddress", "typeAndKey"])
+  );
+};
+
+// __onionAuth.viewKeys()__.
+// Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private keys.
+// Returns a promise that is fulfilled with an array of key info objects which
+// contain the following properties:
+//   hsAddress
+//   typeAndKey
+//   Flags (e.g., "Permanent")
+onionAuth.viewKeys = function(aControlSocket) {
+  let cmd = "onion_client_auth_view";
+  return aControlSocket
+    .sendCommand(cmd)
+    .then(onionAuth.keyInfoObjectsFromMessage);
+};
+
+// __onionAuth.add(controlSocket, hsAddress, b64PrivateKey, isPermanent)__.
+// Sends a ONION_CLIENT_AUTH_ADD command to add a private key to the
+// Tor configuration.
+onionAuth.add = function(
+  aControlSocket,
+  hsAddress,
+  b64PrivateKey,
+  isPermanent
+) {
+  if (!utils.isString(hsAddress)) {
+    return utils.rejectPromise("hsAddress argument should be a string");
+  }
+
+  if (!utils.isString(b64PrivateKey)) {
+    return utils.rejectPromise("b64PrivateKey argument should be a string");
+  }
+
+  const keyType = "x25519";
+  let cmd = `onion_client_auth_add ${hsAddress} ${keyType}:${b64PrivateKey}`;
+  if (isPermanent) {
+    cmd += " Flags=Permanent";
+  }
+  return aControlSocket.sendCommand(cmd);
+};
+
+// __onionAuth.remove(controlSocket, hsAddress)__.
+// Sends a ONION_CLIENT_AUTH_REMOVE command to remove a private key from the
+// Tor configuration.
+onionAuth.remove = function(aControlSocket, hsAddress) {
+  if (!utils.isString(hsAddress)) {
+    return utils.rejectPromise("hsAddress argument should be a string");
+  }
+
+  let cmd = `onion_client_auth_remove ${hsAddress}`;
+  return aControlSocket.sendCommand(cmd);
+};
+
+// ## event
+// Handlers for events
+
+let event = {};
+
+// __event.parsers__.
+// A map of EVENT keys to parsing functions, which convert result strings to JavaScript
+// data.
+event.parsers = {
+  stream: info.streamStatusParser,
+  // Currently unused:
+  // "circ" : info.circuitStatusParser,
+};
+
+// __event.messageToData(type, message)__.
+// Extract the data from an event. Note, at present
+// we only extract streams that look like `"650" SP...`
+event.messageToData = function(type, message) {
+  let dataText = message.match(/^650 \S+?\s(.*)/m)[1];
+  return dataText && type.toLowerCase() in event.parsers
+    ? event.parsers[type.toLowerCase()](dataText)
+    : null;
+};
+
+// __event.watchEvent(controlSocket, type, filter, onData)__.
+// Watches for a particular type of event. If filter(data) returns true, the event's
+// data is passed to the onData callback. Returns a zero arg function that
+// stops watching the event. Note: we only observe `"650" SP...` events
+// currently (no `650+...` or `650-...` events).
+event.watchEvent = function(controlSocket, type, filter, onData, raw = false) {
+  return controlSocket.addNotificationCallback(
+    new RegExp("^650 " + type),
+    function(message) {
+      let data = event.messageToData(type, message);
+      if (filter === null || filter(data)) {
+        if (raw || !data) {
+          onData(message);
+          return;
+        }
+        onData(data);
+      }
+    }
+  );
+};
+
+// ## tor
+// Things related to the main controller.
+let tor = {};
+
+// __tor.controllerCache__.
+// A map from "unix:socketpath" or "host:port" to controller objects. Prevents
+// redundant instantiation of control sockets.
+tor.controllerCache = new Map();
+
+// __tor.controller(ipcFile, host, port, password)__.
+// Creates a tor controller at the given ipcFile or host and port, with the
+// given password.
+tor.controller = async function(ipcFile, host, port, password) {
+  let socket = await io.controlSocket(ipcFile, host, port, password);
+  return {
+    getInfo: key => info.getInfo(socket, key),
+    getConf: key => info.getConf(socket, key),
+    onionAuthViewKeys: () => onionAuth.viewKeys(socket),
+    onionAuthAdd: (hsAddress, b64PrivateKey, isPermanent) =>
+      onionAuth.add(socket, hsAddress, b64PrivateKey, isPermanent),
+    onionAuthRemove: hsAddress => onionAuth.remove(socket, hsAddress),
+    watchEvent: (type, filter, onData, raw = false) =>
+      event.watchEvent(socket, type, filter, onData, raw),
+    isOpen: () => socket.isOpen(),
+    close: () => {
+      socket.close();
+    },
+    sendCommand: cmd => socket.sendCommand(cmd),
+  };
+};
+
+// ## Export
+
+let controlPortInfo = {};
+
+// __configureControlPortModule(ipcFile, host, port, password)__.
+// Sets Tor control port connection parameters to be used in future calls to
+// the controller() function. Example:
+//     configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
+var configureControlPortModule = function(ipcFile, host, port, password) {
+  controlPortInfo.ipcFile = ipcFile;
+  controlPortInfo.host = host;
+  controlPortInfo.port = port || 9151;
+  controlPortInfo.password = password;
+};
+
+// __controller(avoidCache)__.
+// Instantiates and returns a controller object that is connected and
+// authenticated to a Tor ControlPort using the connection parameters
+// provided in the most recent call to configureControlPortModule(), if
+// the controller doesn't yet exist. Otherwise returns the existing
+// controller to the given ipcFile or host:port. Throws on error.
+//
+// Example:
+//
+//     // Get a new controller
+//     const avoidCache = true;
+//     let c = controller(avoidCache);
+//     // Send command and receive `250` reply or error message in a promise:
+//     let replyPromise = c.getInfo("ip-to-country/16.16.16.16");
+//     // Close the controller permanently
+//     c.close();
+var controller = async function(avoidCache) {
+  if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
+    throw new Error("Please call configureControlPortModule first");
+  }
+
+  const dest = controlPortInfo.ipcFile
+    ? `unix:${controlPortInfo.ipcFile.path}`
+    : `${controlPortInfo.host}:${controlPortInfo.port}`;
+
+  // constructor shorthand
+  const newTorController = async () => {
+    return tor.controller(
+      controlPortInfo.ipcFile,
+      controlPortInfo.host,
+      controlPortInfo.port,
+      controlPortInfo.password
+    );
+  };
+
+  // avoid cache so always return a new controller
+  if (avoidCache) {
+    return newTorController();
+  }
+
+  // first check our cache and see if we already have one
+  let cachedController = tor.controllerCache.get(dest);
+  if (cachedController && cachedController.isOpen()) {
+    return cachedController;
+  }
+
+  // create a new one and store in the map
+  cachedController = await newTorController();
+  // overwrite the close() function to prevent consumers from closing a shared/cached controller
+  cachedController.close = () => {
+    throw new Error("May not close cached Tor Controller as it may be in use");
+  };
+
+  tor.controllerCache.set(dest, cachedController);
+  return cachedController;
+};
+
+// __wait_for_controller(avoidCache)
+// Same as controller() function, but explicitly waits until there is a tor daemon
+// to connect to (either launched by tor-launcher, or if we have an existing system
+// tor daemon)
+var wait_for_controller = function(avoidCache) {
+  // if tor process is running (either ours or system) immediately return controller
+  if (!TorMonitorService.ownsTorDaemon || TorMonitorService.isRunning) {
+    return controller(avoidCache);
+  }
+
+  // otherwise we must wait for tor to finish launching before resolving
+  return new Promise((resolve, reject) => {
+    let observer = {
+      observe: async (subject, topic, data) => {
+        if (topic === TorTopics.ProcessIsReady) {
+          try {
+            resolve(await controller(avoidCache));
+          } catch (err) {
+            reject(err);
+          }
+          Services.obs.removeObserver(observer, TorTopics.ProcessIsReady);
+        }
+      },
+    };
+    Services.obs.addObserver(observer, TorTopics.ProcessIsReady);
+  });
+};
+
+// Export functions for external use.
+var EXPORTED_SYMBOLS = [
+  "configureControlPortModule",
+  "controller",
+  "wait_for_controller",
+];
diff --git a/toolkit/torbutton/modules/utils.js b/toolkit/torbutton/modules/utils.js
new file mode 100644
index 000000000000..30947a737cb9
--- /dev/null
+++ b/toolkit/torbutton/modules/utils.js
@@ -0,0 +1,318 @@
+// # Utils.js
+// Various helpful utility functions.
+
+// ### Import Mozilla Services
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// ## Pref utils
+
+// __prefs__. A shortcut to Mozilla Services.prefs.
+let prefs = Services.prefs;
+
+// __getPrefValue(prefName)__
+// Returns the current value of a preference, regardless of its type.
+var getPrefValue = function(prefName) {
+  switch (prefs.getPrefType(prefName)) {
+    case prefs.PREF_BOOL:
+      return prefs.getBoolPref(prefName);
+    case prefs.PREF_INT:
+      return prefs.getIntPref(prefName);
+    case prefs.PREF_STRING:
+      return prefs.getCharPref(prefName);
+    default:
+      return null;
+  }
+};
+
+// __bindPref(prefName, prefHandler, init)__
+// Applies prefHandler whenever the value of the pref changes.
+// If init is true, applies prefHandler to the current value.
+// Returns a zero-arg function that unbinds the pref.
+var bindPref = function(prefName, prefHandler, init = false) {
+  let update = () => {
+      prefHandler(getPrefValue(prefName));
+    },
+    observer = {
+      observe(subject, topic, data) {
+        if (data === prefName) {
+          update();
+        }
+      },
+    };
+  prefs.addObserver(prefName, observer);
+  if (init) {
+    update();
+  }
+  return () => {
+    prefs.removeObserver(prefName, observer);
+  };
+};
+
+// __bindPrefAndInit(prefName, prefHandler)__
+// Applies prefHandler to the current value of pref specified by prefName.
+// Re-applies prefHandler whenever the value of the pref changes.
+// Returns a zero-arg function that unbinds the pref.
+var bindPrefAndInit = (prefName, prefHandler) =>
+  bindPref(prefName, prefHandler, true);
+
+// ## Observers
+
+// __observe(topic, callback)__.
+// Observe the given topic. When notification of that topic
+// occurs, calls callback(subject, data). Returns a zero-arg
+// function that stops observing.
+var observe = function(topic, callback) {
+  let observer = {
+    observe(aSubject, aTopic, aData) {
+      if (topic === aTopic) {
+        callback(aSubject, aData);
+      }
+    },
+  };
+  Services.obs.addObserver(observer, topic);
+  return () => Services.obs.removeObserver(observer, topic);
+};
+
+// ## Environment variables
+
+// __env__.
+// Provides access to process environment variables.
+let env = Cc["@mozilla.org/process/environment;1"].getService(
+  Ci.nsIEnvironment
+);
+
+// __getEnv(name)__.
+// Reads the environment variable of the given name.
+var getEnv = function(name) {
+  return env.exists(name) ? env.get(name) : undefined;
+};
+
+// __getLocale
+// Returns the app locale to be used in tor-related urls.
+var getLocale = function() {
+  const locale = Services.locale.appLocaleAsBCP47;
+  if (locale === "ja-JP-macos") {
+    // We don't want to distinguish the mac locale.
+    return "ja";
+  }
+  return locale;
+};
+
+// ## Windows
+
+// __dialogsByName__.
+// Map of window names to dialogs.
+let dialogsByName = {};
+
+// __showDialog(parent, url, name, features, arg1, arg2, ...)__.
+// Like window.openDialog, but if the window is already
+// open, just focuses it instead of opening a new one.
+var showDialog = function(parent, url, name, features) {
+  let existingDialog = dialogsByName[name];
+  if (existingDialog && !existingDialog.closed) {
+    existingDialog.focus();
+    return existingDialog;
+  }
+  let newDialog = parent.openDialog.apply(parent, Array.slice(arguments, 1));
+  dialogsByName[name] = newDialog;
+  return newDialog;
+};
+
+// ## Tor control protocol utility functions
+
+let _torControl = {
+  // Unescape Tor Control string aStr (removing surrounding "" and \ escapes).
+  // Based on Vidalia's src/common/stringutil.cpp:string_unescape().
+  // Returns the unescaped string. Throws upon failure.
+  // Within Tor Launcher, the file components/tl-protocol.js also contains a
+  // copy of _strUnescape().
+  _strUnescape(aStr) {
+    if (!aStr) {
+      return aStr;
+    }
+
+    var len = aStr.length;
+    if (len < 2 || '"' != aStr.charAt(0) || '"' != aStr.charAt(len - 1)) {
+      return aStr;
+    }
+
+    const kHexRE = /[0-9A-Fa-f]{2}/;
+    const kOctalRE = /[0-7]{3}/;
+    var rv = "";
+    var i = 1;
+    var lastCharIndex = len - 2;
+    while (i <= lastCharIndex) {
+      var c = aStr.charAt(i);
+      if ("\\" == c) {
+        if (++i > lastCharIndex) {
+          throw new Error("missing character after \\");
+        }
+
+        c = aStr.charAt(i);
+        if ("n" == c) {
+          rv += "\n";
+        } else if ("r" == c) {
+          rv += "\r";
+        } else if ("t" == c) {
+          rv += "\t";
+        } else if ("x" == c) {
+          if (i + 2 > lastCharIndex) {
+            throw new Error("not enough hex characters");
+          }
+
+          let s = aStr.substr(i + 1, 2);
+          if (!kHexRE.test(s)) {
+            throw new Error("invalid hex characters");
+          }
+
+          let val = parseInt(s, 16);
+          rv += String.fromCharCode(val);
+          i += 3;
+        } else if (this._isDigit(c)) {
+          let s = aStr.substr(i, 3);
+          if (i + 2 > lastCharIndex) {
+            throw new Error("not enough octal characters");
+          }
+
+          if (!kOctalRE.test(s)) {
+            throw new Error("invalid octal characters");
+          }
+
+          let val = parseInt(s, 8);
+          rv += String.fromCharCode(val);
+          i += 3;
+        } // "\\" and others
+        else {
+          rv += c;
+          ++i;
+        }
+      } else if ('"' == c) {
+        throw new Error('unescaped " within string');
+      } else {
+        rv += c;
+        ++i;
+      }
+    }
+
+    // Convert from UTF-8 to Unicode. TODO: is UTF-8 always used in protocol?
+    return decodeURIComponent(escape(rv));
+  }, // _strUnescape()
+
+  // Within Tor Launcher, the file components/tl-protocol.js also contains a
+  // copy of _isDigit().
+  _isDigit(aChar) {
+    const kRE = /^\d$/;
+    return aChar && kRE.test(aChar);
+  },
+}; // _torControl
+
+// __unescapeTorString(str, resultObj)__.
+// Unescape Tor Control string str (removing surrounding "" and \ escapes).
+// Returns the unescaped string. Throws upon failure.
+var unescapeTorString = function(str) {
+  return _torControl._strUnescape(str);
+};
+
+var getFPDFromHost = hostname => {
+  try {
+    return Services.eTLD.getBaseDomainFromHost(hostname);
+  } catch (e) {
+    if (
+      e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+      e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
+    ) {
+      return hostname;
+    }
+  }
+  return null;
+};
+
+// Assuming this is called with gBrowser.selectedBrowser
+var getDomainForBrowser = browser => {
+  let fpd = browser.contentPrincipal.originAttributes.firstPartyDomain;
+  // Bug 31562: For neterror or certerror, get the original URL from
+  // browser.currentURI and use it to calculate the firstPartyDomain.
+  let knownErrors = ["about:neterror", "about:certerror"];
+  let documentURI = browser.documentURI;
+  if (
+    documentURI &&
+    documentURI.schemeIs("about") &&
+    knownErrors.some(x => documentURI.spec.startsWith(x))
+  ) {
+    let knownSchemes = ["http", "https", "ftp"];
+    let currentURI = browser.currentURI;
+    if (currentURI && knownSchemes.some(x => currentURI.schemeIs(x))) {
+      fpd = getFPDFromHost(currentURI.host) || fpd;
+    }
+  }
+  return fpd;
+};
+
+var m_tb_torlog = Cc["@torproject.org/torbutton-logger;1"].getService(
+  Ci.nsISupports
+).wrappedJSObject;
+
+var m_tb_string_bundle = torbutton_get_stringbundle();
+
+function torbutton_safelog(nLevel, sMsg, scrub) {
+  m_tb_torlog.safe_log(nLevel, sMsg, scrub);
+  return true;
+}
+
+function torbutton_log(nLevel, sMsg) {
+  m_tb_torlog.log(nLevel, sMsg);
+
+  // So we can use it in boolean expressions to determine where the
+  // short-circuit is..
+  return true;
+}
+
+// load localization strings
+function torbutton_get_stringbundle() {
+  var o_stringbundle = false;
+
+  try {
+    var oBundle = Services.strings;
+    o_stringbundle = oBundle.createBundle(
+      "chrome://torbutton/locale/torbutton.properties"
+    );
+  } catch (err) {
+    o_stringbundle = false;
+  }
+  if (!o_stringbundle) {
+    torbutton_log(5, "ERROR (init): failed to find torbutton-bundle");
+  }
+
+  return o_stringbundle;
+}
+
+function torbutton_get_property_string(propertyname) {
+  try {
+    if (!m_tb_string_bundle) {
+      m_tb_string_bundle = torbutton_get_stringbundle();
+    }
+
+    return m_tb_string_bundle.GetStringFromName(propertyname);
+  } catch (e) {
+    torbutton_log(4, "Unlocalized string " + propertyname);
+  }
+
+  return propertyname;
+}
+
+// Export utility functions for external use.
+let EXPORTED_SYMBOLS = [
+  "bindPref",
+  "bindPrefAndInit",
+  "getEnv",
+  "getLocale",
+  "getDomainForBrowser",
+  "getPrefValue",
+  "observe",
+  "showDialog",
+  "show_torbrowser_manual",
+  "unescapeTorString",
+  "torbutton_safelog",
+  "torbutton_log",
+  "torbutton_get_property_string",
+];
diff --git a/toolkit/torbutton/moz.build b/toolkit/torbutton/moz.build
new file mode 100644
index 000000000000..376c99765de4
--- /dev/null
+++ b/toolkit/torbutton/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+JAR_MANIFESTS += ['jar.mn']
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
index c24943e9dee3..de0091ae8d4d 100644
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
@@ -83,7 +83,11 @@ function getGlobalScriptIncludes(scriptPath) {
           "browser/components/screenshots/content/"
         )
         .replace("chrome://browser/content/", "browser/base/content/")
-        .replace("chrome://global/content/", "toolkit/content/");
+        .replace("chrome://global/content/", "toolkit/content/")
+        .replace(
+          "chrome://torbutton/content/",
+          "toolkit/torproject/torbutton/chrome/content/"
+        );
 
       for (let mapping of Object.getOwnPropertyNames(MAPPINGS)) {
         if (sourceFile.includes(mapping)) {

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the tbb-commits mailing list