Merge m-c to graphics
authorKartikaya Gupta <kgupta@mozilla.com>
Mon, 23 Jan 2017 09:45:48 -0500
changeset 342226 69800debbe54f118693ad697b823ad7266232957
parent 342225 d295888faea067587708534300861dcba5a38928 (current diff)
parent 330638 36486fdc3813ef7943ae5b07b4128866d1938a6c (diff)
child 342227 229013509fdf2cdccb52de2932e59f1c4a7ed51a
push id37261
push userkwierso@gmail.com
push dateFri, 10 Feb 2017 23:42:51 +0000
treeherderautoland@779d10ed78f5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to graphics MozReview-Commit-ID: 98wqjDW1RVK
browser/base/content/browser-captivePortal.js
browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
browser/base/content/test/general/browser_fxa_oauth.html
browser/base/content/test/general/browser_fxa_oauth.js
browser/base/content/test/general/browser_fxa_oauth_with_keys.html
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/file_clearplugindata.html
browser/installer/package-manifest.in
browser/modules/CaptivePortalWatcher.jsm
browser/modules/test/browser_CaptivePortalWatcher.js
config/rules.mk
devtools/client/debugger/new/test/mochitest/examples/bundle.js
devtools/client/debugger/new/test/mochitest/examples/bundle.js.map
gfx/layers/ipc/CompositorBridgeParent.cpp
gfx/layers/ipc/CompositorBridgeParent.h
gfx/layers/ipc/CrossProcessCompositorBridgeParent.h
gfx/layers/ipc/PCompositorBridge.ipdl
gfx/layers/moz.build
gfx/thebes/gfxPlatform.cpp
js/src/jit/BaselineCacheIRCompiler.cpp
layout/reftests/transform-3d/reftest.list
modules/freetype2/src/gzip/ftzconf.h
modules/freetype2/src/gzip/zconf.h
modules/libpref/init/all.js
security/nss/fuzz/nssfuzz.cc
security/nss/fuzz/quickder_targets.cc
security/nss/fuzz/registry.h
security/nss/gtests/common/common.gyp
security/nss/lib/freebl/ecl/tests/ec_naft.c
security/nss/lib/freebl/ecl/tests/ecp_test.c
security/nss/lib/freebl/os2_rand.c
services/fxaccounts/FxAccountsOAuthClient.jsm
services/fxaccounts/tests/xpcshell/test_oauth_client.js
taskcluster/docker/desktop1604-test/Dockerfile
taskcluster/docker/lint/Dockerfile
taskcluster/docker/lint/system-setup.sh
taskcluster/docker/recipes/ubuntu1604-test-system-setup.sh
toolkit/library/gtest/rust/Cargo.toml
toolkit/library/rust/Cargo.toml
toolkit/library/rust/shared/Cargo.toml
toolkit/moz.configure
tools/fuzzing/libfuzzer/FuzzerCustomMain.cpp
tools/profiler/public/GeckoProfilerFunc.h
tools/profiler/public/GeckoProfilerImpl.h
tools/profiler/public/GeckoProfilerTypes.h
xpcom/glue/standalone/nsXPCOMGlue.h
--- a/accessible/generic/DocAccessible.h
+++ b/accessible/generic/DocAccessible.h
@@ -347,17 +347,17 @@ public:
   void ContentRemoved(Accessible* aContainer, nsIContent* aChildNode)
   {
     // Update the whole tree of this document accessible when the container is
     // null (document element is removed).
     UpdateTreeOnRemoval((aContainer ? aContainer : this), aChildNode);
   }
   void ContentRemoved(nsIContent* aContainerNode, nsIContent* aChildNode)
   {
-    ContentRemoved(GetAccessibleOrContainer(aContainerNode), aChildNode);
+    ContentRemoved(AccessibleOrTrueContainer(aContainerNode), aChildNode);
   }
 
   /**
    * Updates accessible tree when rendered text is changed.
    */
   void UpdateText(nsIContent* aTextNode);
 
   /**
--- a/accessible/tests/mochitest/events.js
+++ b/accessible/tests/mochitest/events.js
@@ -1766,16 +1766,29 @@ function nofocusChecker(aID)
 function textChangeChecker(aID, aStart, aEnd, aTextOrFunc, aIsInserted, aFromUser)
 {
   this.target = getNode(aID);
   this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED;
   this.startOffset = aStart;
   this.endOffset = aEnd;
   this.textOrFunc = aTextOrFunc;
 
+  this.match = function stextChangeChecker_match(aEvent)
+  {
+    if (!(aEvent instanceof nsIAccessibleTextChangeEvent) ||
+        aEvent.accessible !== getAccessible(this.target)) {
+      return false;
+    }
+
+    let tcEvent = aEvent.QueryInterface(nsIAccessibleTextChangeEvent);
+    let modifiedText = (typeof this.textOrFunc === "function") ?
+      this.textOrFunc() : this.textOrFunc;
+    return modifiedText === tcEvent.modifiedText;
+  };
+
   this.check = function textChangeChecker_check(aEvent)
   {
     aEvent.QueryInterface(nsIAccessibleTextChangeEvent);
 
     var modifiedText = (typeof this.textOrFunc == "function") ?
       this.textOrFunc() : this.textOrFunc;
     var modifiedTextLen =
       (this.endOffset == -1) ? modifiedText.length : aEnd - aStart;
--- a/accessible/tests/mochitest/treeupdate/test_select.html
+++ b/accessible/tests/mochitest/treeupdate/test_select.html
@@ -1,12 +1,12 @@
 <!DOCTYPE html>
 <html>
 <head>
-  <title>Add select options test</title>
+  <title>HTML select options test</title>
   <link rel="stylesheet" type="text/css"
         href="chrome://mochikit/content/tests/SimpleTest/test.css" />
 
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
 
   <script type="application/javascript"
           src="../common.js"></script>
@@ -86,45 +86,65 @@
       }
 
       this.getID = function removeptions_getID()
       {
         return "test elements removal from a select";
       }
     }
 
-    //gA11yEventDumpID = "debug";
+    /**
+     * Setting @href on option makes the accessible to recreate.
+     */
+    function setHrefOnOption()
+    {
+      this.eventSeq = [
+        new invokerChecker(EVENT_HIDE, 's2_o'),
+        new invokerChecker(EVENT_SHOW, 's2_o'),
+      ];
+
+      this.invoke = function setHrefOnOption_setHref()
+      {
+        getNode('s2_o').setAttribute('href', '1');
+      }
+
+      this.finalCheck = function() {
+        var tree =
+          { COMBOBOX: [
+            { COMBOBOX_LIST: [
+              { COMBOBOX_OPTION: [ ] }
+            ] }
+          ] };
+        testAccessibleTree('s2', tree);
+      }
+
+      this.getID = function removeptions_getID()
+      {
+        return "setting @href on select option";
+      }
+    }
 
     function doTest()
     {
       gQueue = new eventQueue();
 
       gQueue.push(new addOptions("select"));
       gQueue.push(new removeOptions("select"));
+      gQueue.push(new setHrefOnOption());
 
       gQueue.invoke(); // Will call SimpleTest.finish();
 
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   </script>
 </head>
 <body>
-
-  <a target="_blank"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=616452"
-     title="Bug 616452 - Dynamically inserted select options aren't reflected in accessible tree">
-    Mozilla Bug 616452</a>
-  <a target="_blank"
-     href="https://bugzilla.mozilla.org/show_bug.cgi?id=616940"
-     title="Removed select option accessibles aren't removed until hide event is fired">
-    Mozilla Bug 616940</a>
   <p id="display"></p>
   <div id="content" style="display: none"></div>
   <pre id="test">
   </pre>
 
   <select id="select"></select>
-
-  <div id="debug"/>
+  <select id="s2"><option id="s2_o"></option></select>
 </body>
 </html>
--- a/browser/app/moz.build
+++ b/browser/app/moz.build
@@ -29,17 +29,20 @@ LOCAL_INCLUDES += [
     '/xpcom/build',
 ]
 
 USE_LIBS += [
     'mozglue',
 ]
 
 if CONFIG['LIBFUZZER']:
-  USE_LIBS += [ 'fuzzer' ]
+    USE_LIBS += [ 'fuzzer' ]
+    LOCAL_INCLUDES += [
+        '/tools/fuzzing/libfuzzer',
+    ]
 
 if CONFIG['_MSC_VER']:
     # Always enter a Windows program through wmain, whether or not we're
     # a console application.
     WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup']
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     RCINCLUDE = 'splash.rc'
--- a/browser/app/nsBrowserApp.cpp
+++ b/browser/app/nsBrowserApp.cpp
@@ -1,17 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "nsXULAppAPI.h"
 #include "mozilla/XREAppData.h"
 #include "application.ini.h"
-#include "nsXPCOMGlue.h"
+#include "mozilla/Bootstrap.h"
 #if defined(XP_WIN)
 #include <windows.h>
 #include <stdlib.h>
 #elif defined(XP_UNIX)
 #include <sys/resource.h>
 #include <unistd.h>
 #endif
 
@@ -38,16 +38,20 @@
 #include "BinaryPath.h"
 
 #include "nsXPCOMPrivate.h" // for MAXPATHLEN and XPCOM_DLL
 
 #include "mozilla/Sprintf.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/WindowsDllBlocklist.h"
 
+#ifdef LIBFUZZER
+#include "FuzzerDefs.h"
+#endif
+
 #ifdef MOZ_LINUX_32_SSE2_STARTUP_ERROR
 #include <cpuid.h>
 #include "mozilla/Unused.h"
 
 static bool
 IsSSE2Available()
 {
   // The rest of the app has been compiled to assume that SSE2 is present
@@ -158,27 +162,16 @@ static bool IsArg(const char* arg, const
     return !strcasecmp(++arg, s);
 #endif
 
   return false;
 }
 
 Bootstrap::UniquePtr gBootstrap;
 
-#ifdef LIBFUZZER
-int libfuzzer_main(int argc, char **argv);
-
-/* This wrapper is used by the libFuzzer main to call into libxul */
-
-void libFuzzerGetFuncs(const char* moduleName, LibFuzzerInitFunc* initFunc,
-                       LibFuzzerTestingFunc* testingFunc) {
-  return gBootstrap->XRE_LibFuzzerGetFuncs(moduleName, initFunc, testingFunc);
-}
-#endif
-
 static int do_main(int argc, char* argv[], char* envp[])
 {
   // Allow firefox.exe to launch XULRunner apps via -app <application.ini>
   // Note that -app must be the *first* argument.
   const char *appDataFile = getenv("XUL_APP_FILE");
   if ((!appDataFile || !*appDataFile) &&
       (argc > 1 && IsArg(argv[1], "app"))) {
     if (argc == 2) {
@@ -230,59 +223,32 @@ static int do_main(int argc, char* argv[
     return 255;
   }
 #endif
   config.sandboxBrokerServices = brokerServices;
 #endif
 
 #ifdef LIBFUZZER
   if (getenv("LIBFUZZER"))
-    gBootstrap->XRE_LibFuzzerSetMain(argc, argv, libfuzzer_main);
+    gBootstrap->XRE_LibFuzzerSetDriver(fuzzer::FuzzerDriver);
 #endif
 
   return gBootstrap->XRE_main(argc, argv, config);
 }
 
-static bool
-FileExists(const char *path)
-{
-#ifdef XP_WIN
-  wchar_t wideDir[MAX_PATH];
-  MultiByteToWideChar(CP_UTF8, 0, path, -1, wideDir, MAX_PATH);
-  DWORD fileAttrs = GetFileAttributesW(wideDir);
-  return fileAttrs != INVALID_FILE_ATTRIBUTES;
-#else
-  return access(path, R_OK) == 0;
-#endif
-}
-
 static nsresult
 InitXPCOMGlue(const char *argv0)
 {
-  char exePath[MAXPATHLEN];
-
-  nsresult rv = mozilla::BinaryPath::Get(argv0, exePath);
-  if (NS_FAILED(rv)) {
+  UniqueFreePtr<char> exePath = BinaryPath::Get(argv0);
+  if (!exePath) {
     Output("Couldn't find the application directory.\n");
-    return rv;
-  }
-
-  char *lastSlash = strrchr(exePath, XPCOM_FILE_PATH_SEPARATOR[0]);
-  if (!lastSlash ||
-      (size_t(lastSlash - exePath) > MAXPATHLEN - sizeof(XPCOM_DLL) - 1))
-    return NS_ERROR_FAILURE;
-
-  strcpy(lastSlash + 1, XPCOM_DLL);
-
-  if (!FileExists(exePath)) {
-    Output("Could not find the Mozilla runtime.\n");
     return NS_ERROR_FAILURE;
   }
 
-  gBootstrap = mozilla::GetBootstrap(exePath);
+  gBootstrap = mozilla::GetBootstrap(exePath.get());
   if (!gBootstrap) {
     Output("Couldn't load XPCOM.\n");
     return NS_ERROR_FAILURE;
   }
 
   // This will set this thread as the main thread.
   gBootstrap->NS_LogInit();
 
@@ -290,24 +256,16 @@ InitXPCOMGlue(const char *argv0)
 }
 
 int main(int argc, char* argv[], char* envp[])
 {
   mozilla::TimeStamp start = mozilla::TimeStamp::Now();
 
 #ifdef HAS_DLL_BLOCKLIST
   DllBlocklist_Initialize();
-
-#ifdef DEBUG
-  // In order to be effective against AppInit DLLs, the blocklist must be
-  // initialized before user32.dll is loaded into the process (bug 932100).
-  if (GetModuleHandleA("user32.dll")) {
-    fprintf(stderr, "DLL blocklist was unable to intercept AppInit DLLs.\n");
-  }
-#endif
 #endif
 
 #ifdef MOZ_BROWSER_CAN_BE_CONTENTPROC
   // We are launching as a content process, delegate to the appropriate
   // main
   if (argc > 1 && IsArg(argv[1], "contentproc")) {
 #if defined(XP_WIN) && defined(MOZ_SANDBOX)
     // We need to initialize the sandbox TargetServices before InitXPCOMGlue
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -850,20 +850,28 @@ pref("places.frecency.thirdBucketWeight"
 pref("places.frecency.fourthBucketWeight", 30);
 pref("places.frecency.defaultBucketWeight", 10);
 
 // bonus (in percent) for visit transition types for frecency calculations
 pref("places.frecency.embedVisitBonus", 0);
 pref("places.frecency.framedLinkVisitBonus", 0);
 pref("places.frecency.linkVisitBonus", 100);
 pref("places.frecency.typedVisitBonus", 2000);
+// The bookmarks bonus is always added on top of any other bonus, including
+// the redirect source and the typed ones.
 pref("places.frecency.bookmarkVisitBonus", 75);
+// The redirect source bonus overwrites any transition bonus.
+// 0 would hide these pages, instead we want them low ranked.  Thus we use
+// linkVisitBonus - bookmarkVisitBonus, so that a bookmarked source is in par
+// with a common link.
+pref("places.frecency.redirectSourceVisitBonus", 25);
 pref("places.frecency.downloadVisitBonus", 0);
-pref("places.frecency.permRedirectVisitBonus", 0);
-pref("places.frecency.tempRedirectVisitBonus", 0);
+// The perm/temp redirects here relate to redirect targets, not sources.
+pref("places.frecency.permRedirectVisitBonus", 50);
+pref("places.frecency.tempRedirectVisitBonus", 40);
 pref("places.frecency.reloadVisitBonus", 0);
 pref("places.frecency.defaultVisitBonus", 0);
 
 // bonus (in percent) for place types for frecency calculations
 pref("places.frecency.unvisitedBookmarkBonus", 140);
 pref("places.frecency.unvisitedTypedBonus", 200);
 
 // Controls behavior of the "Add Exception" dialog launched from SSL error pages
@@ -1569,9 +1577,9 @@ pref("services.sync.validation.enabled",
 
 // Preferences for the form autofill system extension
 pref("browser.formautofill.experimental", false);
 
 // Enable safebrowsing v4 tables (suffixed by "-proto") update.
 #ifdef NIGHTLY_BUILD
 pref("urlclassifier.malwareTable", "goog-malware-shavar,goog-unwanted-shavar,goog-malware-proto,goog-unwanted-proto,test-malware-simple,test-unwanted-simple");
 pref("urlclassifier.phishTable", "goog-phish-shavar,goog-phish-proto,test-phish-simple");
-#endif
\ No newline at end of file
+#endif
--- a/browser/base/content/aboutNetError.xhtml
+++ b/browser/base/content/aboutNetError.xhtml
@@ -590,17 +590,21 @@
 
         <!-- Short Description -->
         <div id="errorShortDesc">
           <p id="errorShortDescText" />
         </div>
         <p id="badStsCertExplanation" hidden="true">&certerror.whatShouldIDo.badStsCertExplanation;</p>
 
         <div id="wrongSystemTimePanel" style="display: none;">
-          &certerror.wrongSystemTime;
+          &certerror.wrongSystemTime2;
+        </div>
+
+        <div id="wrongSystemTimeWithoutReferencePanel" style="display: none;">
+          &certerror.wrongSystemTimeWithoutReference;
         </div>
 
         <!-- Long Description (Note: See netError.dtd for used XHTML tags) -->
         <div id="errorLongDesc" />
 
         <div id="learnMoreContainer">
           <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new">&errorReporting.learnMore;</a></p>
         </div>
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -511,43 +511,51 @@ const gExtensionsNotifications = {
     }
 
     let container = document.getElementById("PanelUI-footer-addons");
 
     while (container.firstChild) {
       container.firstChild.remove();
     }
 
-    // Strings below to be properly localized in bug 1316996
     const DEFAULT_EXTENSION_ICON =
       "chrome://mozapps/skin/extensions/extensionGeneric.svg";
     let items = 0;
     for (let update of updates) {
       if (++items > 4) {
         break;
       }
+
       let button = document.createElement("toolbarbutton");
-      button.setAttribute("label", `"${update.addon.name}" requires new permissions`);
+      let text = gNavigatorBundle.getFormattedString("webextPerms.updateMenuItem", [update.addon.name]);
+      button.setAttribute("label", text);
 
       let icon = update.addon.iconURL || DEFAULT_EXTENSION_ICON;
       button.setAttribute("image", icon);
 
       button.addEventListener("click", evt => {
         ExtensionsUI.showUpdate(gBrowser, update);
       });
 
       container.appendChild(button);
     }
 
+    let appName;
     for (let addon of sideloaded) {
       if (++items > 4) {
         break;
       }
+      if (!appName) {
+        let brandBundle = document.getElementById("bundle_brand");
+        appName = brandBundle.getString("brandShortName");
+      }
+
       let button = document.createElement("toolbarbutton");
-      button.setAttribute("label", `"${addon.name}" added to Firefox`);
+      let text = gNavigatorBundle.getFormattedString("webextPerms.sideloadMenuItem", [addon.name, appName]);
+      button.setAttribute("label", text);
 
       let icon = addon.iconURL || DEFAULT_EXTENSION_ICON;
       button.setAttribute("image", icon);
 
       button.addEventListener("click", evt => {
         ExtensionsUI.showSideloaded(gBrowser, addon);
       });
 
rename from browser/modules/CaptivePortalWatcher.jsm
rename to browser/base/content/browser-captivePortal.js
--- a/browser/modules/CaptivePortalWatcher.jsm
+++ b/browser/base/content/browser-captivePortal.js
@@ -1,89 +1,90 @@
 /* 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/. */
 
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-this.EXPORTED_SYMBOLS = [ "CaptivePortalWatcher" ];
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Timer.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/RecentWindow.jsm");
-
 XPCOMUtils.defineLazyServiceGetter(this, "cps",
                                    "@mozilla.org/network/captive-portal-service;1",
                                    "nsICaptivePortalService");
 
-this.CaptivePortalWatcher = {
+var CaptivePortalWatcher = {
   /**
    * This constant is chosen to be large enough for a portal recheck to complete,
    * and small enough that the delay in opening a tab isn't too noticeable.
    * Please see comments for _delayedCaptivePortalDetected for more details.
    */
-  PORTAL_RECHECK_DELAY_MS: 150,
+  PORTAL_RECHECK_DELAY_MS: Preferences.get("captivedetect.portalRecheckDelayMS", 500),
 
   // This is the value used to identify the captive portal notification.
   PORTAL_NOTIFICATION_VALUE: "captive-portal-detected",
 
   // This holds a weak reference to the captive portal tab so that we
   // don't leak it if the user closes it.
   _captivePortalTab: null,
 
-  // This holds a weak reference to the captive portal notification.
-  _captivePortalNotification: null,
-
-  _initialized: false,
-
   /**
    * If a portal is detected when we don't have focus, we first wait for focus
    * and then add the tab if, after a recheck, the portal is still active. This
    * is set to true while we wait so that in the unlikely event that we receive
    * another notification while waiting, we don't do things twice.
    */
   _delayedCaptivePortalDetectedInProgress: false,
 
   // In the situation above, this is set to true while we wait for the recheck.
   // This flag exists so that tests can appropriately simulate a recheck.
   _waitingForRecheck: false,
 
+  get _captivePortalNotification() {
+    let nb = document.getElementById("high-priority-global-notificationbox");
+    return nb.getNotificationWithValue(this.PORTAL_NOTIFICATION_VALUE);
+  },
+
   get canonicalURL() {
     return Services.prefs.getCharPref("captivedetect.canonicalURL");
   },
 
+  get _browserBundle() {
+    delete this._browserBundle;
+    return this._browserBundle =
+      Services.strings.createBundle("chrome://browser/locale/browser.properties");
+  },
+
   init() {
     Services.obs.addObserver(this, "captive-portal-login", false);
     Services.obs.addObserver(this, "captive-portal-login-abort", false);
     Services.obs.addObserver(this, "captive-portal-login-success", false);
-    this._initialized = true;
 
     if (cps.state == cps.LOCKED_PORTAL) {
       // A captive portal has already been detected.
       this._captivePortalDetected();
-      return;
+
+      // Automatically open a captive portal tab if there's no other browser window.
+      let windows = Services.wm.getEnumerator("navigator:browser");
+      if (windows.getNext() == window && !windows.hasMoreElements()) {
+        this.ensureCaptivePortalTab();
+      }
     }
 
     cps.recheckCaptivePortal();
   },
 
   uninit() {
-    if (!this._initialized) {
-      return;
-    }
     Services.obs.removeObserver(this, "captive-portal-login");
     Services.obs.removeObserver(this, "captive-portal-login-abort");
     Services.obs.removeObserver(this, "captive-portal-login-success");
+
+
+    if (this._delayedCaptivePortalDetectedInProgress) {
+      Services.obs.removeObserver(this, "xul-window-visible");
+    }
   },
 
-  observe(subject, topic, data) {
-    switch (topic) {
+  observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
       case "captive-portal-login":
         this._captivePortalDetected();
         break;
       case "captive-portal-login-abort":
       case "captive-portal-login-success":
         this._captivePortalGone();
         break;
       case "xul-window-visible":
@@ -93,43 +94,27 @@ this.CaptivePortalWatcher = {
   },
 
   _captivePortalDetected() {
     if (this._delayedCaptivePortalDetectedInProgress) {
       return;
     }
 
     let win = RecentWindow.getMostRecentBrowserWindow();
-    // If there's no browser window or none have focus, open and show the
-    // tab when we regain focus. This is so that if a different application was
-    // focused, when the user (re-)focuses a browser window, we open the tab
-    // immediately in that window so they can login before continuing to browse.
-    if (!win || win != Services.ww.activeWindow) {
+    // If no browser window has focus, open and show the tab when we regain focus.
+    // This is so that if a different application was focused, when the user
+    // (re-)focuses a browser window, we open the tab immediately in that window
+    // so they can log in before continuing to browse.
+    if (win != Services.ww.activeWindow) {
       this._delayedCaptivePortalDetectedInProgress = true;
       Services.obs.addObserver(this, "xul-window-visible", false);
       return;
     }
 
-    this._showNotification(win);
-  },
-
-  _ensureCaptivePortalTab(win) {
-    let tab;
-    if (this._captivePortalTab) {
-      tab = this._captivePortalTab.get();
-    }
-
-    // If the tab is gone or going, we need to open a new one.
-    if (!tab || tab.closing || !tab.parentNode) {
-      tab = win.gBrowser.addTab(this.canonicalURL,
-                                { ownerTab: win.gBrowser.selectedTab });
-      this._captivePortalTab = Cu.getWeakReference(tab);
-    }
-
-    win.gBrowser.selectedTab = tab;
+    this._showNotification();
   },
 
   /**
    * Called after we regain focus if we detect a portal while a browser window
    * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
    * the tab if needed after a short delay to allow the recheck to complete.
    */
   _delayedCaptivePortalDetected() {
@@ -138,141 +123,137 @@ this.CaptivePortalWatcher = {
     }
 
     let win = RecentWindow.getMostRecentBrowserWindow();
     if (win != Services.ww.activeWindow) {
       // The window that got focused was not a browser window.
       return;
     }
     Services.obs.removeObserver(this, "xul-window-visible");
+    this._delayedCaptivePortalDetectedInProgress = false;
 
+    if (win != window) {
+      // Some other browser window got focus, we don't have to do anything.
+      return;
+    }
     // Trigger a portal recheck. The user may have logged into the portal via
     // another client, or changed networks.
     cps.recheckCaptivePortal();
     this._waitingForRecheck = true;
     let requestTime = Date.now();
 
     let self = this;
     Services.obs.addObserver(function observer() {
       let time = Date.now() - requestTime;
       Services.obs.removeObserver(observer, "captive-portal-check-complete");
       self._waitingForRecheck = false;
-      self._delayedCaptivePortalDetectedInProgress = false;
       if (cps.state != cps.LOCKED_PORTAL) {
         // We're free of the portal!
         return;
       }
 
-      self._showNotification(win);
+      self._showNotification();
       if (time <= self.PORTAL_RECHECK_DELAY_MS) {
         // The amount of time elapsed since we requested a recheck (i.e. since
         // the browser window was focused) was small enough that we can add and
         // focus a tab with the login page with no noticeable delay.
-        self._ensureCaptivePortalTab(win);
+        self.ensureCaptivePortalTab();
       }
     }, "captive-portal-check-complete", false);
   },
 
   _captivePortalGone() {
     if (this._delayedCaptivePortalDetectedInProgress) {
       Services.obs.removeObserver(this, "xul-window-visible");
       this._delayedCaptivePortalDetectedInProgress = false;
     }
 
     this._removeNotification();
-
-    if (!this._captivePortalTab) {
-      return;
-    }
-
-    let tab = this._captivePortalTab.get();
-    // In all the cases below, we want to stop treating the tab as a
-    // captive portal tab.
-    this._captivePortalTab = null;
-
-    // Check parentNode in case the object hasn't been gc'd yet.
-    if (!tab || tab.closing || !tab.parentNode) {
-      // User has closed the tab already.
-      return;
-    }
-
-    let tabbrowser = tab.ownerGlobal.gBrowser;
-
-    // If after the login, the captive portal has redirected to some other page,
-    // leave it open if the tab has focus.
-    if (tab.linkedBrowser.currentURI.spec != this.canonicalURL &&
-        tabbrowser.selectedTab == tab) {
-      return;
-    }
-
-    // Remove the tab.
-    tabbrowser.removeTab(tab);
-  },
-
-  get _browserBundle() {
-    delete this._browserBundle;
-    return this._browserBundle =
-      Services.strings.createBundle("chrome://browser/locale/browser.properties");
   },
 
   handleEvent(aEvent) {
     if (aEvent.type != "TabSelect" || !this._captivePortalTab || !this._captivePortalNotification) {
       return;
     }
 
     let tab = this._captivePortalTab.get();
-    let n = this._captivePortalNotification.get();
+    let n = this._captivePortalNotification;
     if (!tab || !n) {
       return;
     }
 
     let doc = tab.ownerDocument;
     let button = n.querySelector("button.notification-button");
     if (doc.defaultView.gBrowser.selectedTab == tab) {
       button.style.visibility = "hidden";
     } else {
       button.style.visibility = "visible";
     }
   },
 
-  _showNotification(win) {
+  _showNotification() {
     let buttons = [
       {
         label: this._browserBundle.GetStringFromName("captivePortal.showLoginPage"),
         callback: () => {
-          this._ensureCaptivePortalTab(win);
+          this.ensureCaptivePortalTab();
 
           // Returning true prevents the notification from closing.
           return true;
         },
         isDefault: true,
       },
     ];
 
     let message = this._browserBundle.GetStringFromName("captivePortal.infoMessage2");
 
     let closeHandler = (aEventName) => {
       if (aEventName != "removed") {
         return;
       }
-      win.gBrowser.tabContainer.removeEventListener("TabSelect", this);
+      gBrowser.tabContainer.removeEventListener("TabSelect", this);
     };
 
-    let nb = win.document.getElementById("high-priority-global-notificationbox");
-    let n = nb.appendNotification(message, this.PORTAL_NOTIFICATION_VALUE, "",
-                                  nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler);
+    let nb = document.getElementById("high-priority-global-notificationbox");
+    nb.appendNotification(message, this.PORTAL_NOTIFICATION_VALUE, "",
+                          nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler);
 
-    this._captivePortalNotification = Cu.getWeakReference(n);
-
-    win.gBrowser.tabContainer.addEventListener("TabSelect", this);
+    gBrowser.tabContainer.addEventListener("TabSelect", this);
   },
 
   _removeNotification() {
-    if (!this._captivePortalNotification)
-      return;
-    let n = this._captivePortalNotification.get();
-    this._captivePortalNotification = null;
+    let n = this._captivePortalNotification;
     if (!n || !n.parentNode) {
       return;
     }
     n.close();
   },
+
+  ensureCaptivePortalTab() {
+    let tab;
+    if (this._captivePortalTab) {
+      tab = this._captivePortalTab.get();
+    }
+
+    // If the tab is gone or going, we need to open a new one.
+    if (!tab || tab.closing || !tab.parentNode) {
+      tab = gBrowser.addTab(this.canonicalURL, { ownerTab: gBrowser.selectedTab });
+      this._captivePortalTab = Cu.getWeakReference(tab);
+    }
+
+    gBrowser.selectedTab = tab;
+
+    let canonicalURI = makeURI(this.canonicalURL);
+
+    // When we are no longer captive, close the tab if it's at the canonical URL.
+    let tabCloser = () => {
+      Services.obs.removeObserver(tabCloser, "captive-portal-login-abort");
+      Services.obs.removeObserver(tabCloser, "captive-portal-login-success");
+      if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser ||
+          !tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)) {
+        return;
+      }
+      gBrowser.removeTab(tab);
+    }
+    Services.obs.addObserver(tabCloser, "captive-portal-login-abort", false);
+    Services.obs.addObserver(tabCloser, "captive-portal-login-success", false);
+  },
 };
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1476,16 +1476,20 @@ var BookmarkingUI = {
 
     let options = PlacesUtils.history.getNewQueryOptions();
     options.excludeQueries = true;
     options.queryType = options.QUERY_TYPE_BOOKMARKS;
     options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING;
     options.maxResults = kMaxResults;
     let query = PlacesUtils.history.getNewQuery();
 
+    let sh = Cc["@mozilla.org/network/serialization-helper;1"]
+               .getService(Ci.nsISerializationHelper);
+    let loadingPrincipal = sh.serializeToString(document.nodePrincipal);
+
     let fragment = document.createDocumentFragment();
     let root = PlacesUtils.history.executeQuery(query, options).root;
     root.containerOpen = true;
     for (let i = 0; i < root.childCount; i++) {
       let node = root.getChild(i);
       let uri = node.uri;
       let title = node.title;
       let icon = node.icon;
@@ -1495,16 +1499,17 @@ var BookmarkingUI = {
                                  "menuitem");
       item.setAttribute("label", title || uri);
       item.setAttribute("targetURI", uri);
       item.setAttribute("simulated-places-node", true);
       item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " +
                                  aExtraCSSClass);
       if (icon) {
         item.setAttribute("image", icon);
+        item.setAttribute("loadingprincipal", loadingPrincipal);
       }
       item._placesNode = node;
       fragment.appendChild(item);
     }
     root.containerOpen = false;
     aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling);
   },
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -106,16 +106,19 @@ if (AppConstants.MOZ_CRASHREPORTER) {
 ].forEach(([name, cc, ci]) => XPCOMUtils.defineLazyServiceGetter(this, name, cc, ci));
 
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
                                      "@mozilla.org/xre/app-info;1",
                                      "nsICrashReporter");
 }
 
+XPCOMUtils.defineLazyServiceGetter(this, "gSerializationHelper",
+                                   "@mozilla.org/network/serialization-helper;1",
+                                   "nsISerializationHelper");
 
 XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function() {
   let tmp = {};
   Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", tmp);
   return tmp.BrowserToolboxProcess;
 });
 
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
@@ -837,22 +840,38 @@ function gKeywordURIFixup({ target: brow
     // Do nothing if the URL is invalid (we don't want to show a notification in that case).
     if (ex.result != Cr.NS_ERROR_UNKNOWN_HOST) {
       // ... otherwise, report:
       Cu.reportError(ex);
     }
   }
 }
 
+function serializeInputStream(aStream) {
+  let data = {
+    content: NetUtil.readInputStreamToString(aStream, aStream.available()),
+  };
+
+  if (aStream instanceof Ci.nsIMIMEInputStream) {
+    data.headers = new Map();
+    aStream.visitHeaders((name, value) => {
+      data.headers.set(name, value);
+    });
+  }
+
+  return data;
+}
+
 // A shared function used by both remote and non-remote browser XBL bindings to
 // load a URI or redirect it to the correct process.
 function _loadURIWithFlags(browser, uri, params) {
   if (!uri) {
     uri = "about:blank";
   }
+  let triggeringPrincipal = params.triggeringPrincipal || null;
   let flags = params.flags || 0;
   let referrer = params.referrerURI;
   let referrerPolicy = ("referrerPolicy" in params ? params.referrerPolicy :
                         Ci.nsIHttpChannel.REFERRER_POLICY_UNSET);
   let postData = params.postData;
 
   let currentRemoteType = browser.remoteType;
   let requiredRemoteType =
@@ -866,30 +885,33 @@ function _loadURIWithFlags(browser, uri,
   try {
     if (!mustChangeProcess) {
       if (params.userContextId) {
         browser.webNavigation.setOriginAttributesBeforeLoading({ userContextId: params.userContextId });
       }
 
       browser.webNavigation.loadURIWithOptions(uri, flags,
                                                referrer, referrerPolicy,
-                                               postData, null, null);
+                                               postData, null, null, triggeringPrincipal);
     } else {
       // Check if the current browser is allowed to unload.
       let {permitUnload, timedOut} = browser.permitUnload();
       if (!timedOut && !permitUnload) {
         return;
       }
 
       if (postData) {
-        postData = NetUtil.readInputStreamToString(postData, postData.available());
+        postData = serializeInputStream(postData);
       }
 
       let loadParams = {
         uri,
+        triggeringPrincipal: triggeringPrincipal
+          ? gSerializationHelper.serializePrincipal(triggeringPrincipal)
+          : null,
         flags,
         referrer: referrer ? referrer.spec : null,
         referrerPolicy,
         postData
       }
 
       if (params.userContextId) {
         loadParams.userContextId = params.userContextId;
@@ -907,17 +929,17 @@ function _loadURIWithFlags(browser, uri,
       Cu.reportError(e);
       gBrowser.updateBrowserRemotenessByURL(browser, uri);
 
       if (params.userContextId) {
         browser.webNavigation.setOriginAttributesBeforeLoading({ userContextId: params.userContextId });
       }
 
       browser.webNavigation.loadURIWithOptions(uri, flags, referrer, referrerPolicy,
-                                               postData, null, null);
+                                               postData, null, null, triggeringPrincipal);
     } else {
       throw e;
     }
   } finally {
     if (!requiredRemoteType) {
       browser.inLoadURI = false;
     }
   }
@@ -1004,16 +1026,17 @@ var gBrowserInit = {
     gPageStyleMenu.init();
     LanguageDetectionListener.init();
     BrowserOnClick.init();
     FeedHandler.init();
     CompactTheme.init();
     AboutPrivateBrowsingListener.init();
     TrackingProtection.init();
     RefreshBlocker.init();
+    CaptivePortalWatcher.init();
 
     let mm = window.getGroupMessageManager("browsers");
     mm.loadFrameScript("chrome://browser/content/tab-content.js", true);
     mm.loadFrameScript("chrome://browser/content/content.js", true);
     mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
     mm.loadFrameScript("chrome://global/content/manifestMessages.js", true);
 
     // initialize observers and listeners
@@ -1531,16 +1554,18 @@ var gBrowserInit = {
     FeedHandler.uninit();
 
     CompactTheme.uninit();
 
     TrackingProtection.uninit();
 
     RefreshBlocker.uninit();
 
+    CaptivePortalWatcher.uninit();
+
     gMenuButtonUpdateBadge.uninit();
 
     gMenuButtonBadgeManager.uninit();
 
     SidebarUI.uninit();
 
     // Now either cancel delayedStartup, or clean up the services initialized from
     // it.
@@ -2847,17 +2872,17 @@ var BrowserOnClick = {
   receiveMessage(msg) {
     switch (msg.name) {
       case "Browser:CertExceptionError":
         this.onCertError(msg.target, msg.data.elementId,
                          msg.data.isTopFrame, msg.data.location,
                          msg.data.securityInfoAsString);
       break;
       case "Browser:OpenCaptivePortalPage":
-        this.onOpenCaptivePortalPage();
+        CaptivePortalWatcher.ensureCaptivePortalTab();
       break;
       case "Browser:SiteBlockedError":
         this.onAboutBlocked(msg.data.elementId, msg.data.reason,
                             msg.data.isTopFrame, msg.data.location);
       break;
       case "Browser:EnableOnlineMode":
         if (Services.io.offline) {
           // Reset network state and refresh the page.
@@ -2983,38 +3008,16 @@ var BrowserOnClick = {
         let detailedInfo = getDetailedCertErrorInfo(location,
                                                     securityInfo);
         gClipboardHelper.copyString(detailedInfo);
         break;
 
     }
   },
 
-  onOpenCaptivePortalPage() {
-    // Open a new tab with the canonical URL that we use to check for a captive portal.
-    // It will be redirected to the login page.
-    let canonicalURL = Services.prefs.getCharPref("captivedetect.canonicalURL");
-    let tab = gBrowser.addTab(canonicalURL);
-    let canonicalURI = makeURI(canonicalURL);
-    gBrowser.selectedTab = tab;
-
-    // When we are no longer captive, close the tab if it's at the canonical URL.
-    let tabCloser = () => {
-      Services.obs.removeObserver(tabCloser, "captive-portal-login-abort");
-      Services.obs.removeObserver(tabCloser, "captive-portal-login-success");
-      if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser ||
-          !tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)) {
-        return;
-      }
-      gBrowser.removeTab(tab);
-    }
-    Services.obs.addObserver(tabCloser, "captive-portal-login-abort", false);
-    Services.obs.addObserver(tabCloser, "captive-portal-login-success", false);
-  },
-
   onAboutBlocked(elementId, reason, isTopFrame, location) {
     // Depending on what page we are displaying here (malware/phishing/unwanted)
     // use the right strings and links for each.
     let bucketName = "";
     let sendTelemetry = false;
     if (reason === "malware") {
       sendTelemetry = true;
       bucketName = "WARNING_MALWARE_PAGE_";
@@ -4955,17 +4958,17 @@ var TabsProgressListener = {
 function nsBrowserAccess() { }
 
 nsBrowserAccess.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]),
 
   _openURIInNewTab(aURI, aReferrer, aReferrerPolicy, aIsPrivate,
                              aIsExternal, aForceNotRemote = false,
                              aUserContextId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID,
-                             aOpener = null) {
+                             aOpener = null, aTriggeringPrincipal = null) {
     let win, needToFocusWin;
 
     // try the current window.  if we're in a popup, fall back on the most recent browser window
     if (window.toolbar.visible)
       win = window;
     else {
       win = RecentWindow.getMostRecentBrowserWindow({private: aIsPrivate});
       needToFocusWin = true;
@@ -4980,16 +4983,17 @@ nsBrowserAccess.prototype = {
       win.BrowserOpenTab(); // this also focuses the location bar
       win.focus();
       return win.gBrowser.selectedBrowser;
     }
 
     let loadInBackground = gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground");
 
     let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", {
+                                      triggeringPrincipal: aTriggeringPrincipal,
                                       referrerURI: aReferrer,
                                       referrerPolicy: aReferrerPolicy,
                                       userContextId: aUserContextId,
                                       fromExternal: aIsExternal,
                                       inBackground: loadInBackground,
                                       forceNotRemote: aForceNotRemote,
                                       opener: aOpener,
                                       });
@@ -5028,19 +5032,21 @@ nsBrowserAccess.prototype = {
       if (isExternal &&
           gPrefService.prefHasUserValue("browser.link.open_newwindow.override.external"))
         aWhere = gPrefService.getIntPref("browser.link.open_newwindow.override.external");
       else
         aWhere = gPrefService.getIntPref("browser.link.open_newwindow");
     }
 
     let referrer = aOpener ? makeURI(aOpener.location.href) : null;
+    let triggeringPrincipal = null;
     let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_UNSET;
     if (aOpener && aOpener.document) {
       referrerPolicy = aOpener.document.referrerPolicy;
+      triggeringPrincipal = aOpener.document.nodePrincipal;
     }
     let isPrivate = aOpener
                   ? PrivateBrowsingUtils.isContentWindowPrivate(aOpener)
                   : PrivateBrowsingUtils.isWindowPrivate(window);
 
     switch (aWhere) {
       case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW :
         // FIXME: Bug 408379. So how come this doesn't send the
@@ -5064,27 +5070,28 @@ nsBrowserAccess.prototype = {
         let forceNotRemote = !!aOpener;
         let userContextId = aOpener && aOpener.document
                               ? aOpener.document.nodePrincipal.originAttributes.userContextId
                               : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
         let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener;
         let browser = this._openURIInNewTab(aURI, referrer, referrerPolicy,
                                             isPrivate, isExternal,
                                             forceNotRemote, userContextId,
-                                            openerWindow);
+                                            openerWindow, triggeringPrincipal);
         if (browser)
           newWindow = browser.contentWindow;
         break;
       default : // OPEN_CURRENTWINDOW or an illegal value
         newWindow = content;
         if (aURI) {
           let loadflags = isExternal ?
                             Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
                             Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
           gBrowser.loadURIWithFlags(aURI.spec, {
+                                    triggeringPrincipal,
                                     flags: loadflags,
                                     referrerURI: referrer,
                                     referrerPolicy,
                                     });
         }
         if (!gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground"))
           window.focus();
     }
@@ -5104,17 +5111,18 @@ nsBrowserAccess.prototype = {
                           ? aParams.openerOriginAttributes.userContextId
                           : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID
 
     let referrer = aParams.referrer ? makeURI(aParams.referrer) : null;
     let browser = this._openURIInNewTab(aURI, referrer,
                                         aParams.referrerPolicy,
                                         aParams.isPrivate,
                                         isExternal, false,
-                                        userContextId);
+                                        userContextId, null,
+                                        aParams.triggeringPrincipal);
     if (browser)
       return browser.QueryInterface(Ci.nsIFrameLoaderOwner);
 
     return null;
   },
 
   isTabContentWindow(aWindow) {
     return gBrowser.browsers.some(browser => browser.contentWindow == aWindow);
@@ -7865,17 +7873,20 @@ var TabContextMenu = {
     // Hide "Bookmark All Tabs" for a pinned tab.  Update its state if visible.
     let bookmarkAllTabs = document.getElementById("context_bookmarkAllTabs");
     bookmarkAllTabs.hidden = this.contextTab.pinned;
     if (!bookmarkAllTabs.hidden)
       PlacesCommandHook.updateBookmarkAllTabsCommand();
 
     // Adjust the state of the toggle mute menu item.
     let toggleMute = document.getElementById("context_toggleMuteTab");
-    if (this.contextTab.hasAttribute("muted")) {
+    if (this.contextTab.hasAttribute("blocked")) {
+      toggleMute.label = gNavigatorBundle.getString("playTab.label");
+      toggleMute.accessKey = gNavigatorBundle.getString("playTab.accesskey");
+    } else if (this.contextTab.hasAttribute("muted")) {
       toggleMute.label = gNavigatorBundle.getString("unmuteTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("unmuteTab.accesskey");
     } else {
       toggleMute.label = gNavigatorBundle.getString("muteTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey");
     }
 
     this.contextTab.toggleMuteMenuItem = toggleMute;
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -300,46 +300,74 @@ var AboutNetAndCertErrorListener = {
     let learnMoreLink = content.document.getElementById("learnMoreLink");
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
 
     switch (msg.data.code) {
       case SEC_ERROR_UNKNOWN_ISSUER:
         learnMoreLink.href = baseURL + "security-error";
         break;
 
-      // in case the certificate expired we make sure the system clock
-      // matches settings server (kinto) time
+      // In case the certificate expired we make sure the system clock
+      // matches the blocklist ping (Kinto) time and is not before the build date.
       case SEC_ERROR_EXPIRED_CERTIFICATE:
       case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE:
       case SEC_ERROR_OCSP_FUTURE_RESPONSE:
       case SEC_ERROR_OCSP_OLD_RESPONSE:
       case MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE:
       case MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE:
 
-        // use blocklist stats if available
+        // We check against Kinto time first if available, because that allows us
+        // to give the user an approximation of what the correct time is.
+        let difference = 0;
         if (Services.prefs.getPrefType(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS)) {
-          let difference = Services.prefs.getIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS);
+          difference = Services.prefs.getIntPref(PREF_BLOCKLIST_CLOCK_SKEW_SECONDS);
+        }
+
+        // If the difference is more than a day.
+        if (Math.abs(difference) > 60 * 60 * 24) {
+          let formatter = new Intl.DateTimeFormat();
+          let systemDate = formatter.format(new Date());
+          // negative difference means local time is behind server time
+          let actualDate = formatter.format(new Date(Date.now() - difference * 1000));
+
+          content.document.getElementById("wrongSystemTime_URL")
+            .textContent = content.document.location.hostname;
+          content.document.getElementById("wrongSystemTime_systemDate")
+            .textContent = systemDate;
+          content.document.getElementById("wrongSystemTime_actualDate")
+            .textContent = actualDate;
 
-          // if the difference is more than a day
-          if (Math.abs(difference) > 60 * 60 * 24) {
+          content.document.getElementById("errorShortDesc")
+            .style.display = "none";
+          content.document.getElementById("wrongSystemTimePanel")
+            .style.display = "block";
+
+        // If there is no clock skew with Kinto servers, check against the build date.
+        // (The Kinto ping could have happened when the time was still right, or not at all)
+        } else {
+          let appBuildID = Services.appinfo.appBuildID;
+
+          let year = parseInt(appBuildID.substr(0, 4), 10);
+          let month = parseInt(appBuildID.substr(4, 2), 10) - 1;
+          let day = parseInt(appBuildID.substr(6, 2), 10);
+
+          let buildDate = new Date(year, month, day);
+          let systemDate = new Date();
+
+          if (buildDate > systemDate) {
             let formatter = new Intl.DateTimeFormat();
-            let systemDate = formatter.format(new Date());
-            // negative difference means local time is behind server time
-            let actualDate = formatter.format(new Date(Date.now() - difference * 1000));
 
-            content.document.getElementById("wrongSystemTime_URL")
+            content.document.getElementById("wrongSystemTimeWithoutReference_URL")
               .textContent = content.document.location.hostname;
-            content.document.getElementById("wrongSystemTime_systemDate")
-              .textContent = systemDate;
-            content.document.getElementById("wrongSystemTime_actualDate")
-              .textContent = actualDate;
+            content.document.getElementById("wrongSystemTimeWithoutReference_systemDate")
+              .textContent = formatter.format(systemDate);
 
             content.document.getElementById("errorShortDesc")
               .style.display = "none";
-            content.document.getElementById("wrongSystemTimePanel")
+            content.document.getElementById("wrongSystemTimeWithoutReferencePanel")
               .style.display = "block";
           }
         }
         learnMoreLink.href = baseURL + "time-errors";
         break;
     }
   },
 
@@ -479,16 +507,17 @@ var ClickEventHandler = {
         referrerPolicy = referrerAttrValue;
       }
     }
 
     let json = { button: event.button, shiftKey: event.shiftKey,
                  ctrlKey: event.ctrlKey, metaKey: event.metaKey,
                  altKey: event.altKey, href: null, title: null,
                  bookmark: false, referrerPolicy,
+                 triggeringPrincipal: principal,
                  originAttributes: principal ? principal.originAttributes : {},
                  isContentWindowPrivate: PrivateBrowsingUtils.isContentWindowPrivate(ownerDoc.defaultView)};
 
     if (href) {
       try {
         BrowserUtils.urlSecurityCheck(href, principal);
       } catch (e) {
         return;
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -6,16 +6,17 @@
 <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
 <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
 <script type="application/javascript" src="chrome://browser/content/places/browserPlacesViews.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser.js"/>
 <script type="application/javascript" src="chrome://browser/content/customizableui/panelUI.js"/>
 <script type="application/javascript" src="chrome://global/content/viewSourceUtils.js"/>
 
 <script type="application/javascript" src="chrome://browser/content/browser-addons.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-captivePortal.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-ctrlTab.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-customization.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-compacttheme.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-feeds.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-fullScreenAndPointerLock.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-fullZoom.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-gestureSupport.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-media.js"/>
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -75,8 +75,15 @@
     <popupnotification id="addon-webext-permissions-notification" hidden="true">
       <popupnotificationcontent orient="vertical">
         <description id="addon-webext-perm-header" class="addon-webext-perm-header"/>
         <description id="addon-webext-perm-text" class="addon-webext-perm-text"/>
         <label id="addon-webext-perm-intro" class="addon-webext-perm-text"/>
         <html:ul id="addon-webext-perm-list" class="addon-webext-perm-list"/>
       </popupnotificationcontent>
     </popupnotification>
+
+    <popupnotification id="addon-installed-notification" hidden="true">
+      <popupnotificationcontent orient="vertical">
+        <description id="addon-installed-notification-header"/>
+        <description id="addon-installed-notification-message"/>
+      </popupnotificationcontent>
+    </popupnotification>
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1500,32 +1500,34 @@
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <parameter name="aPostData"/>
         <parameter name="aLoadInBackground"/>
         <parameter name="aAllowThirdPartyFixup"/>
         <parameter name="aIsPrerendered"/>
         <body>
           <![CDATA[
+            var aTriggeringPrincipal;
             var aReferrerPolicy;
             var aFromExternal;
             var aRelatedToCurrent;
             var aAllowMixedContent;
             var aSkipAnimation;
             var aForceNotRemote;
             var aPreferredRemoteType;
             var aNoReferrer;
             var aUserContextId;
             var aRelatedBrowser;
             var aOriginPrincipal;
             var aOpener;
             if (arguments.length == 2 &&
                 typeof arguments[1] == "object" &&
                 !(arguments[1] instanceof Ci.nsIURI)) {
               let params = arguments[1];
+              aTriggeringPrincipal  = params.triggeringPrincipal
               aReferrerURI          = params.referrerURI;
               aReferrerPolicy       = params.referrerPolicy;
               aCharset              = params.charset;
               aPostData             = params.postData;
               aLoadInBackground     = params.inBackground;
               aAllowThirdPartyFixup = params.allowThirdPartyFixup;
               aFromExternal         = params.fromExternal;
               aRelatedToCurrent     = params.relatedToCurrent;
@@ -1539,17 +1541,19 @@
               aOriginPrincipal      = params.originPrincipal;
               aOpener               = params.opener;
               aIsPrerendered        = params.isPrerendered;
             }
 
             var bgLoad = (aLoadInBackground != null) ? aLoadInBackground :
                          Services.prefs.getBoolPref("browser.tabs.loadInBackground");
             var owner = bgLoad ? null : this.selectedTab;
+
             var tab = this.addTab(aURI, {
+                                  triggeringPrincipal: aTriggeringPrincipal,
                                   referrerURI: aReferrerURI,
                                   referrerPolicy: aReferrerPolicy,
                                   charset: aCharset,
                                   postData: aPostData,
                                   ownerTab: owner,
                                   allowThirdPartyFixup: aAllowThirdPartyFixup,
                                   fromExternal: aFromExternal,
                                   relatedToCurrent: aRelatedToCurrent,
@@ -2136,16 +2140,17 @@
         <parameter name="aOwner"/>
         <parameter name="aAllowThirdPartyFixup"/>
         <parameter name="aIsPrerendered"/>
         <body>
           <![CDATA[
             "use strict";
 
             const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+            var aTriggeringPrincipal;
             var aReferrerPolicy;
             var aFromExternal;
             var aRelatedToCurrent;
             var aSkipAnimation;
             var aAllowMixedContent;
             var aForceNotRemote;
             var aPreferredRemoteType;
             var aNoReferrer;
@@ -2154,16 +2159,17 @@
             var aRelatedBrowser;
             var aOriginPrincipal;
             var aDisallowInheritPrincipal;
             var aOpener;
             if (arguments.length == 2 &&
                 typeof arguments[1] == "object" &&
                 !(arguments[1] instanceof Ci.nsIURI)) {
               let params = arguments[1];
+              aTriggeringPrincipal      = params.triggeringPrincipal;
               aReferrerURI              = params.referrerURI;
               aReferrerPolicy           = params.referrerPolicy;
               aCharset                  = params.charset;
               aPostData                 = params.postData;
               aOwner                    = params.ownerTab;
               aAllowThirdPartyFixup     = params.allowThirdPartyFixup;
               aFromExternal             = params.fromExternal;
               aRelatedToCurrent         = params.relatedToCurrent;
@@ -2301,16 +2307,17 @@
                 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
               if (aAllowMixedContent)
                 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
               if (aDisallowInheritPrincipal)
                 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
               try {
                 b.loadURIWithFlags(aURI, {
                   flags,
+                  triggeringPrincipal: aTriggeringPrincipal,
                   referrerURI: aNoReferrer ? null : aReferrerURI,
                   referrerPolicy: aReferrerPolicy,
                   charset: aCharset,
                   postData: aPostData,
                 });
               } catch (ex) {
                 Cu.reportError(ex);
               }
@@ -3086,17 +3093,21 @@
           ]]>
         </body>
       </method>
 
       <method name="reloadTab">
         <parameter name="aTab"/>
         <body>
           <![CDATA[
-            this.getBrowserForTab(aTab).reload();
+            let browser = this.getBrowserForTab(aTab);
+            // Reset temporary permissions on the current tab. This is done here
+            // because we only want to reset permissions on user reload.
+            SitePermissions.clearTemporaryPermissions(browser);
+            browser.reload();
           ]]>
         </body>
       </method>
 
       <method name="addProgressListener">
         <parameter name="aListener"/>
         <body>
           <![CDATA[
@@ -6987,16 +6998,22 @@
       </property>
 
       <property name="soundPlaying" readonly="true">
         <getter>
           return this.getAttribute("soundplaying") == "true";
         </getter>
       </property>
 
+      <property name="soundBlocked" readonly="true">
+        <getter>
+          return this.getAttribute("blocked") == "true";
+        </getter>
+      </property>
+
       <property name="lastAccessed">
         <getter>
           return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
         </getter>
       </property>
       <method name="updateLastAccessed">
         <parameter name="aDate"/>
         <body><![CDATA[
--- a/browser/base/content/test/captivePortal/browser.ini
+++ b/browser/base/content/test/captivePortal/browser.ini
@@ -1,5 +1,7 @@
 [DEFAULT]
 support-files =
   head.js
 
-[browser_captivePortal_certErrorUI.js]
\ No newline at end of file
+[browser_CaptivePortalWatcher.js]
+skip-if = os == "win" # Bug 1313894
+[browser_captivePortal_certErrorUI.js]
rename from browser/modules/test/browser_CaptivePortalWatcher.js
rename to browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
--- a/browser/modules/test/browser_CaptivePortalWatcher.js
+++ b/browser/base/content/test/captivePortal/browser_CaptivePortalWatcher.js
@@ -14,30 +14,33 @@ const CANONICAL_URL = "data:text/plain;c
 const CANONICAL_URL_REDIRECTED = "data:text/plain;charset=utf-8,redirected";
 const PORTAL_NOTIFICATION_VALUE = "captive-portal-detected";
 
 add_task(function* setup() {
   yield SpecialPowers.pushPrefEnv({
     set: [["captivedetect.canonicalURL", CANONICAL_URL],
           ["captivedetect.canonicalContent", CANONICAL_CONTENT]],
   });
+  // We need to test behavior when a portal is detected when there is no browser
+  // window, but we can't close the default window opened by the test harness.
+  // Instead, we deactivate CaptivePortalWatcher in the default window and
+  // exclude it from RecentWindow.getMostRecentBrowserWindow in an attempt to
+  // mask its presence.
+  window.CaptivePortalWatcher.uninit();
+  RecentWindow._getMostRecentBrowserWindowCopy = RecentWindow.getMostRecentBrowserWindow;
+  let defaultWindow = window;
+  RecentWindow.getMostRecentBrowserWindow = () => {
+    let win = RecentWindow._getMostRecentBrowserWindowCopy();
+    if (win == defaultWindow) {
+      return null;
+    }
+    return win;
+  };
 });
 
-/**
- * We can't close the original window opened by mochitest without failing, so
- * override RecentWindow.getMostRecentBrowserWindow to make CaptivePortalWatcher
- * think there's no window.
- */
-function* portalDetectedNoBrowserWindow() {
-  let getMostRecentBrowserWindow = RecentWindow.getMostRecentBrowserWindow;
-  RecentWindow.getMostRecentBrowserWindow = () => {};
-  yield portalDetected();
-  RecentWindow.getMostRecentBrowserWindow = getMostRecentBrowserWindow;
-}
-
 function* portalDetected() {
   Services.obs.notifyObservers(null, "captive-portal-login", null);
   yield BrowserTestUtils.waitForCondition(() => {
     return cps.state == cps.LOCKED_PORTAL;
   }, "Waiting for Captive Portal Service to update state after portal detected.");
 }
 
 function* freePortal(aSuccess) {
@@ -51,24 +54,24 @@ function* freePortal(aSuccess) {
 function* openWindowAndWaitForPortalUI(aLongRecheck) {
   // CaptivePortalWatcher triggers a recheck when a window gains focus. If
   // the time taken for the check to complete is under PORTAL_RECHECK_DELAY_MS,
   // a tab with the login page is opened and selected. If it took longer,
   // no tab is opened. It's not reliable to time things in an async test,
   // so use a delay threshold of -1 to simulate a long recheck (so that any
   // amount of time is considered excessive), and a very large threshold to
   // simulate a short recheck.
-  CaptivePortalWatcher.PORTAL_RECHECK_DELAY_MS = aLongRecheck ? -1 : 1000000;
+  Preferences.set("captivedetect.portalRecheckDelayMS", aLongRecheck ? -1 : 1000000);
 
-  let win = yield BrowserTestUtils.openNewBrowserWindow();
+  let win = yield openWindowAndWaitForFocus();
 
   // After a new window is opened, CaptivePortalWatcher asks for a recheck, and
   // waits for it to complete. We need to manually tell it a recheck completed.
   yield BrowserTestUtils.waitForCondition(() => {
-    return CaptivePortalWatcher._waitingForRecheck;
+    return win.CaptivePortalWatcher._waitingForRecheck;
   }, "Waiting for CaptivePortalWatcher to trigger a recheck.");
   Services.obs.notifyObservers(null, "captive-portal-check-complete", null);
 
   let notification = ensurePortalNotification(win);
 
   if (aLongRecheck) {
     ensureNoPortalTab(win);
     testShowLoginPageButtonVisibility(notification, "visible");
@@ -143,95 +146,112 @@ function waitForXulWindowVisible() {
 }
 
 function* closeWindowAndWaitForXulWindowVisible(win) {
   let p = waitForXulWindowVisible();
   yield BrowserTestUtils.closeWindow(win);
   yield p;
 }
 
+/**
+ * BrowserTestUtils.openNewBrowserWindow() does not guarantee the newly
+ * opened window has received focus when the promise resolves, so we
+ * have to manually wait every time.
+ */
+function* openWindowAndWaitForFocus() {
+  let win = yield BrowserTestUtils.openNewBrowserWindow();
+  yield SimpleTest.promiseFocus(win);
+  return win;
+}
+
 // Each of the test cases below is run twice: once for login-success and once
 // for login-abort (aSuccess set to true and false respectively).
 let testCasesForBothSuccessAndAbort = [
   /**
    * A portal is detected when there's no browser window, then a browser
    * window is opened, then the portal is freed.
    * The portal tab should be added and focused when the window is
    * opened, and closed automatically when the success event is fired.
    * The captive portal notification should be shown when the window is
    * opened, and closed automatically when the success event is fired.
    */
   function* test_detectedWithNoBrowserWindow_Open(aSuccess) {
-    yield portalDetectedNoBrowserWindow();
+    yield portalDetected();
     let win = yield openWindowAndWaitForPortalUI();
     yield freePortal(aSuccess);
     ensureNoPortalTab(win);
     ensureNoPortalNotification(win);
     yield closeWindowAndWaitForXulWindowVisible(win);
   },
 
   /**
    * A portal is detected when there's no browser window, then a browser
    * window is opened, then the portal is freed.
    * The recheck triggered when the browser window is opened takes a
    * long time. No portal tab should be added.
    * The captive portal notification should be shown when the window is
    * opened, and closed automatically when the success event is fired.
    */
   function* test_detectedWithNoBrowserWindow_LongRecheck(aSuccess) {
-    yield portalDetectedNoBrowserWindow();
+    yield portalDetected();
     let win = yield openWindowAndWaitForPortalUI(true);
     yield freePortal(aSuccess);
     ensureNoPortalTab(win);
     ensureNoPortalNotification(win);
     yield closeWindowAndWaitForXulWindowVisible(win);
   },
 
   /**
    * A portal is detected when there's no browser window, and the
    * portal is freed before a browser window is opened. No portal
    * UI should be shown when a browser window is opened.
    */
   function* test_detectedWithNoBrowserWindow_GoneBeforeOpen(aSuccess) {
-    yield portalDetectedNoBrowserWindow();
+    yield portalDetected();
     yield freePortal(aSuccess);
-    let win = yield BrowserTestUtils.openNewBrowserWindow();
+    let win = yield openWindowAndWaitForFocus();
     // Wait for a while to make sure no UI is shown.
     yield new Promise(resolve => {
       setTimeout(resolve, 1000);
     });
     ensureNoPortalTab(win);
     ensureNoPortalNotification(win);
     yield closeWindowAndWaitForXulWindowVisible(win);
   },
 
   /**
    * A portal is detected when a browser window has focus. No portal tab should
-   * be opened. A notification bar should be displayed in the focused window.
+   * be opened. A notification bar should be displayed in all browser windows.
    */
   function* test_detectedWithFocus(aSuccess) {
-    let win = RecentWindow.getMostRecentBrowserWindow();
+    let win1 = yield openWindowAndWaitForFocus();
+    let win2 = yield openWindowAndWaitForFocus();
     yield portalDetected();
-    ensureNoPortalTab(win);
-    ensurePortalNotification(win);
+    ensureNoPortalTab(win1);
+    ensureNoPortalTab(win2);
+    ensurePortalNotification(win1);
+    ensurePortalNotification(win2);
     yield freePortal(aSuccess);
-    ensureNoPortalNotification(win);
+    ensureNoPortalNotification(win1);
+    ensureNoPortalNotification(win2);
+    yield closeWindowAndWaitForXulWindowVisible(win2);
+    yield closeWindowAndWaitForXulWindowVisible(win1);
   },
 ];
 
 let singleRunTestCases = [
   /**
    * A portal is detected when there's no browser window,
    * then a browser window is opened, and the portal is logged into
    * and redirects to a different page. The portal tab should be added
    * and focused when the window is opened, and left open after login
    * since it redirected.
    */
   function* test_detectedWithNoBrowserWindow_Redirect() {
-    yield portalDetectedNoBrowserWindow();
+    yield portalDetected();
     let win = yield openWindowAndWaitForPortalUI();
     let browser = win.gBrowser.selectedTab.linkedBrowser;
     let loadPromise =
       BrowserTestUtils.browserLoaded(browser, false, CANONICAL_URL_REDIRECTED);
     BrowserTestUtils.loadURI(browser, CANONICAL_URL_REDIRECTED);
     yield loadPromise;
     yield freePortal(true);
     ensurePortalTab(win);
@@ -241,17 +261,17 @@ let singleRunTestCases = [
 
   /**
    * Test the various expected behaviors of the "Show Login Page" button
    * in the captive portal notification. The button should be visible for
    * all tabs except the captive portal tab, and when clicked, should
    * ensure a captive portal tab is open and select it.
    */
   function* test_showLoginPageButton() {
-    let win = RecentWindow.getMostRecentBrowserWindow();
+    let win = yield openWindowAndWaitForFocus();
     yield portalDetected();
     let notification = ensurePortalNotification(win);
     testShowLoginPageButtonVisibility(notification, "visible");
 
     function testPortalTabSelectedAndButtonNotVisible() {
       is(win.gBrowser.selectedTab, tab, "The captive portal tab should be selected.");
       testShowLoginPageButtonVisibility(notification, "hidden");
     }
@@ -292,19 +312,26 @@ let singleRunTestCases = [
     win.gBrowser.selectedTab = anotherTab;
     testShowLoginPageButtonVisibility(notification, "visible");
     tab = yield clickButtonAndExpectNewPortalTab();
 
     yield BrowserTestUtils.removeTab(anotherTab);
     yield freePortal(true);
     ensureNoPortalTab(win);
     ensureNoPortalNotification(win);
+    yield closeWindowAndWaitForXulWindowVisible(win);
   },
 ];
 
 for (let testcase of testCasesForBothSuccessAndAbort) {
   add_task(testcase.bind(null, true));
   add_task(testcase.bind(null, false));
 }
 
 for (let testcase of singleRunTestCases) {
   add_task(testcase);
 }
+
+add_task(function* cleanUp() {
+  RecentWindow.getMostRecentBrowserWindow = RecentWindow._getMostRecentBrowserWindowCopy;
+  delete RecentWindow._getMostRecentBrowserWindowCopy;
+  window.CaptivePortalWatcher.init();
+});
--- a/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
+++ b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
@@ -47,16 +47,29 @@ add_task(function* checkCaptivePortalCer
 
     info("Clicking the Open Login Page button.");
     doc.getElementById("openPortalLoginPageButton").click();
   });
 
   let portalTab = yield portalTabPromise;
   is(gBrowser.selectedTab, portalTab, "Login page should be open in a new foreground tab.");
 
+  // Make sure clicking the "Open Login Page" button again focuses the existing portal tab.
+  yield BrowserTestUtils.switchTab(gBrowser, errorTab);
+  // Passing an empty function to BrowserTestUtils.switchTab lets us wait for an arbitrary
+  // tab switch.
+  portalTabPromise = BrowserTestUtils.switchTab(gBrowser, () => {});
+  yield ContentTask.spawn(browser, null, () => {
+    info("Clicking the Open Login Page button.");
+    content.document.getElementById("openPortalLoginPageButton").click();
+  });
+
+  let portalTab2 = yield portalTabPromise;
+  is(portalTab2, portalTab, "The existing portal tab should be focused.");
+
   let portalTabRemoved = BrowserTestUtils.removeTab(portalTab, {dontRemove: true});
   let errorTabReloaded = waitForCertErrorLoad(browser);
 
   Services.obs.notifyObservers(null, "captive-portal-login-success", null);
   yield portalTabRemoved;
 
   info("Waiting for error tab to be reloaded after the captive portal was freed.");
   yield errorTabReloaded;
--- a/browser/base/content/test/chrome/test_aboutCrashed.xul
+++ b/browser/base/content/test/chrome/test_aboutCrashed.xul
@@ -11,76 +11,72 @@
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
   <iframe type="content" id="frame1"/>
   <iframe type="content" id="frame2" onload="doTest()"/>
   <script type="application/javascript"><![CDATA[
     const Ci = Components.interfaces;
     const Cu = Components.utils;
 
     Cu.import("resource://gre/modules/Services.jsm");
-    Cu.import("resource://gre/modules/Task.jsm");
-    Cu.import("resource://gre/modules/Promise.jsm");
     Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
     SimpleTest.waitForExplicitFinish();
 
     // Load error pages do not fire "load" events, so let's use a progressListener.
     function waitForErrorPage(frame) {
-      let errorPageDeferred = Promise.defer();
-
-      let progressListener = {
-        onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
-          if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
-            frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIWebProgress)
-                          .removeProgressListener(progressListener,
-                                                  Ci.nsIWebProgress.NOTIFY_LOCATION);
+      return new Promise(resolve => {
+        let progressListener = {
+          onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+            if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+              frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIWebProgress)
+                            .removeProgressListener(progressListener,
+                                                    Ci.nsIWebProgress.NOTIFY_LOCATION);
 
-            errorPageDeferred.resolve();
-          }
-        },
+              resolve();
+            }
+          },
 
-        QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
-                                               Ci.nsISupportsWeakReference])
-      };
+          QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                                 Ci.nsISupportsWeakReference])
+        };
 
-      frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                    .getInterface(Ci.nsIWebProgress)
-                    .addProgressListener(progressListener,
-                                         Ci.nsIWebProgress.NOTIFY_LOCATION);
-
-      return errorPageDeferred.promise;
+        frame.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIWebProgress)
+                      .addProgressListener(progressListener,
+                                           Ci.nsIWebProgress.NOTIFY_LOCATION);
+      });
     }
 
   function doTest() {
-    Task.spawn(function test_aboutCrashed() {
+    (async function testBody() {
       let frame1 = document.getElementById("frame1");
       let frame2 = document.getElementById("frame2");
       let uri1 = Services.io.newURI("http://www.example.com/1", null, null);
       let uri2 = Services.io.newURI("http://www.example.com/2", null, null);
 
       let errorPageReady = waitForErrorPage(frame1);
       frame1.docShell.chromeEventHandler.setAttribute("crashedPageTitle", "pageTitle");
       frame1.docShell.displayLoadError(Components.results.NS_ERROR_CONTENT_CRASHED, uri1, null);
 
-      yield errorPageReady;
+      await errorPageReady;
       frame1.docShell.chromeEventHandler.removeAttribute("crashedPageTitle");
 
       SimpleTest.is(frame1.contentDocument.documentURI,
                     "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/1&c=UTF-8&f=regular&d=pageTitle",
                     "Correct about:tabcrashed displayed for page with title.");
 
       errorPageReady = waitForErrorPage(frame2);
       frame2.docShell.displayLoadError(Components.results.NS_ERROR_CONTENT_CRASHED, uri2, null);
 
-      yield errorPageReady;
+      await errorPageReady;
 
       SimpleTest.is(frame2.contentDocument.documentURI,
                     "about:tabcrashed?e=tabcrashed&u=http%3A//www.example.com/2&c=UTF-8&f=regular&d=%20",
                     "Correct about:tabcrashed displayed for page with no title.");
 
       SimpleTest.finish();
-  });
+    })().catch(ex => SimpleTest.ok(false, ex));
   }
   ]]></script>
 
   <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" />
 </window>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -6,18 +6,16 @@ support-files =
   app_bug575561.html
   app_subframe_bug575561.html
   aboutHome_content_script.js
   audio.ogg
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
   browser_bug970746.xhtml
-  browser_fxa_oauth.html
-  browser_fxa_oauth_with_keys.html
   browser_fxa_web_channel.html
   browser_registerProtocolHandler_notification.html
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_web_channel.html
   browser_web_channel_iframe.html
   bug1262648_string_with_newlines.dtd
   bug592338.html
@@ -315,17 +313,16 @@ skip-if = true # browser_drag.js is disa
 [browser_findbarClose.js]
 [browser_focusonkeydown.js]
 [browser_fullscreen-window-open.js]
 tags = fullscreen
 skip-if = os == "linux" # Linux: Intermittent failures - bug 941575.
 [browser_fxaccounts.js]
 support-files = fxa_profile_handler.sjs
 [browser_fxa_migrate.js]
-[browser_fxa_oauth.js]
 [browser_fxa_web_channel.js]
 [browser_gestureSupport.js]
 skip-if = e10s # Bug 863514 - no gesture support.
 [browser_getshortcutoruri.js]
 [browser_hide_removing.js]
 [browser_homeDrop.js]
 [browser_identity_UI.js]
 [browser_insecureLoginForms.js]
--- a/browser/base/content/test/general/browser_aboutCertError.js
+++ b/browser/base/content/test/general/browser_aboutCertError.js
@@ -100,16 +100,29 @@ add_task(function* checkBadStsCert() {
     let exceptionButton = doc.getElementById("exceptionDialogButton");
     return exceptionButton.hidden;
   });
   ok(exceptionButtonHidden, "Exception button is hidden");
 
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
+// This checks that the appinfo.appBuildID starts with a date string,
+// which is required for the misconfigured system time check.
+add_task(function* checkAppBuildIDIsDate() {
+  let appBuildID = Services.appinfo.appBuildID;
+  let year = parseInt(appBuildID.substr(0, 4), 10);
+  let month = parseInt(appBuildID.substr(4, 2), 10);
+  let day = parseInt(appBuildID.substr(6, 2), 10);
+
+  ok(year >= 2016 && year <= 2100, "appBuildID contains a valid year");
+  ok(month >= 1 && month <= 12, "appBuildID contains a valid month");
+  ok(day >= 1 && day <= 31, "appBuildID contains a valid day");
+});
+
 const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
 
 add_task(function* checkWrongSystemTimeWarning() {
   function* setUpPage() {
     let browser;
     let certErrorLoaded;
     yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
       gBrowser.selectedTab = gBrowser.addTab(BAD_CERT);
@@ -146,17 +159,17 @@ add_task(function* checkWrongSystemTimeW
 
   let skew = Math.floor((Date.now() - serverDate.getTime()) / 1000);
   yield SpecialPowers.pushPrefEnv({set: [[PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, skew]]});
 
   info("Loading a bad cert page with a skewed clock");
   let message = yield Task.spawn(setUpPage);
 
   isnot(message.divDisplay, "none", "Wrong time message information is visible");
-  ok(message.text.includes("because your clock appears to show the wrong time"),
+  ok(message.text.includes("clock appears to show the wrong time"),
      "Correct error message found");
   ok(message.text.includes("expired.example.com"), "URL found in error message");
   ok(message.systemDate.includes(localDateFmt), "correct local date displayed");
   ok(message.actualDate.includes(serverDateFmt), "correct server date displayed");
   ok(message.learnMoreLink.includes("time-errors"), "time-errors in the Learn More URL");
 
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
@@ -167,17 +180,17 @@ add_task(function* checkWrongSystemTimeW
 
   skew = Math.floor((Date.now() - serverDate.getTime()) / 1000);
   yield SpecialPowers.pushPrefEnv({set: [[PREF_BLOCKLIST_CLOCK_SKEW_SECONDS, skew]]});
 
   info("Loading a bad cert page with a skewed clock");
   message = yield Task.spawn(setUpPage);
 
   isnot(message.divDisplay, "none", "Wrong time message information is visible");
-  ok(message.text.includes("because your clock appears to show the wrong time"),
+  ok(message.text.includes("clock appears to show the wrong time"),
      "Correct error message found");
   ok(message.text.includes("expired.example.com"), "URL found in error message");
   ok(message.systemDate.includes(localDateFmt), "correct local date displayed");
   ok(message.actualDate.includes(serverDateFmt), "correct server date displayed");
 
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
   // pretend we only have a slightly skewed system time, four hours
--- a/browser/base/content/test/general/browser_e10s_about_page_triggeringprincipal.js
+++ b/browser/base/content/test/general/browser_e10s_about_page_triggeringprincipal.js
@@ -5,17 +5,17 @@ registerCleanupFunction(function() {
   Services.ppmm.broadcastAsyncMessage("AboutPrincipalTest:Unregister");
   BrowserTestUtils.waitForMessage(Services.ppmm, "AboutPrincipalTest:Unregistered").then(
     Services.ppmm.removeDelayedProcessScript(
       "chrome://mochitests/content/browser/browser/base/content/test/general/file_register_about_page.js"
     )
   );
 });
 
-add_task(function* test_principal() {
+add_task(function* test_principal_click() {
   Services.ppmm.loadProcessScript(
     "chrome://mochitests/content/browser/browser/base/content/test/general/file_register_about_page.js",
     true
   );
 
   yield BrowserTestUtils.withNewTab("about:test-about-principal-parent", function*(browser) {
     let loadPromise = BrowserTestUtils.browserLoaded(browser, false, "about:test-about-principal-child");
     let myLink = browser.contentDocument.getElementById("aboutchildprincipal");
@@ -37,8 +37,88 @@ add_task(function* test_principal() {
         "sanity check - loading a top level document");
 
       let loadingPrincipal = channel.loadInfo.loadingPrincipal;
       is(loadingPrincipal, null,
          "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal");
     });
   });
 });
+
+add_task(function* test_principal_ctrl_click() {
+  yield SpecialPowers.pushPrefEnv({
+    "set": [["security.sandbox.content.level", 1]],
+  });
+
+  yield BrowserTestUtils.withNewTab("about:test-about-principal-parent", function*(browser) {
+    let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:test-about-principal-child");
+    // simulate ctrl+click
+    BrowserTestUtils.synthesizeMouseAtCenter("#aboutchildprincipal",
+                                             { ctrlKey: true, metaKey: true },
+                                             gBrowser.selectedBrowser);
+    let tab = yield loadPromise;
+    gBrowser.selectTabAtIndex(2);
+
+    yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+      let channel = content.document.docShell.currentDocumentChannel;
+      is(channel.originalURI.asciiSpec,
+         "about:test-about-principal-child",
+         "sanity check - make sure we test the principal for the correct URI");
+
+      let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+      ok(Services.scriptSecurityManager.isSystemPrincipal(triggeringPrincipal),
+         "loading about: from privileged page must have a triggering of System");
+
+      let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+      is(contentPolicyType, Ci.nsIContentPolicy.TYPE_DOCUMENT,
+        "sanity check - loading a top level document");
+
+      let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+      is(loadingPrincipal, null,
+         "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal");
+    });
+    yield BrowserTestUtils.removeTab(tab);
+  });
+});
+
+add_task(function* test_principal_right_click_open_link_in_new_tab() {
+  yield SpecialPowers.pushPrefEnv({
+    "set": [["security.sandbox.content.level", 1]],
+  });
+
+  yield BrowserTestUtils.withNewTab("about:test-about-principal-parent", function*(browser) {
+    let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:test-about-principal-child");
+
+    // simulate right-click open link in tab
+    BrowserTestUtils.waitForEvent(document, "popupshown", false, event => {
+      // These are operations that must be executed synchronously with the event.
+      document.getElementById("context-openlinkintab").doCommand();
+      event.target.hidePopup();
+      return true;
+    });
+    BrowserTestUtils.synthesizeMouseAtCenter("#aboutchildprincipal",
+                                             { type: "contextmenu", button: 2 },
+                                             gBrowser.selectedBrowser);
+
+    let tab = yield loadPromise;
+    gBrowser.selectTabAtIndex(2);
+
+    yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+      let channel = content.document.docShell.currentDocumentChannel;
+      is(channel.originalURI.asciiSpec,
+         "about:test-about-principal-child",
+         "sanity check - make sure we test the principal for the correct URI");
+
+      let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+      ok(Services.scriptSecurityManager.isSystemPrincipal(triggeringPrincipal),
+         "loading about: from privileged page must have a triggering of System");
+
+      let contentPolicyType = channel.loadInfo.externalContentPolicyType;
+      is(contentPolicyType, Ci.nsIContentPolicy.TYPE_DOCUMENT,
+        "sanity check - loading a top level document");
+
+      let loadingPrincipal = channel.loadInfo.loadingPrincipal;
+      is(loadingPrincipal, null,
+         "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal");
+    });
+    yield BrowserTestUtils.removeTab(tab);
+  });
+});
deleted file mode 100644
--- a/browser/base/content/test/general/browser_fxa_oauth.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>fxa_oauth_test</title>
-</head>
-<body>
-<script>
-  window.onload = function() {
-    var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      // Note: This intentionally sends an object instead of a string, to ensure both work
-      // (see browser_fxa_oauth_with_keys.html for the other test)
-      detail: {
-        id: "oauth_client_id",
-        message: {
-          command: "oauth_complete",
-          data: {
-            state: "state",
-            code: "code1",
-            closeWindow: "signin",
-          },
-        },
-      },
-    });
-
-    window.dispatchEvent(event);
-  };
-</script>
-</body>
-</html>
deleted file mode 100644
--- a/browser/base/content/test/general/browser_fxa_oauth.js
+++ /dev/null
@@ -1,327 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-//
-// Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed.
-//
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
-
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthClient",
-  "resource://gre/modules/FxAccountsOAuthClient.jsm");
-
-const HTTP_PATH = "http://example.com";
-const HTTP_ENDPOINT = "/browser/browser/base/content/test/general/browser_fxa_oauth.html";
-const HTTP_ENDPOINT_WITH_KEYS = "/browser/browser/base/content/test/general/browser_fxa_oauth_with_keys.html";
-
-var gTests = [
-  {
-    desc: "FxA OAuth - should open a new tab, complete OAuth flow",
-    run() {
-      return new Promise(function(resolve, reject) {
-        let tabOpened = false;
-        let properURL = "http://example.com/browser/browser/base/content/test/general/browser_fxa_oauth.html";
-        let queryStrings = [
-          "action=signin",
-          "client_id=client_id",
-          "scope=",
-          "state=state",
-          "webChannelId=oauth_client_id",
-        ];
-        queryStrings.sort();
-
-        waitForTab(function(tab) {
-          Assert.ok("Tab successfully opened");
-          Assert.ok(gBrowser.currentURI.spec.split("?")[0], properURL, "Check URL without params");
-          let actualURL = new URL(gBrowser.currentURI.spec);
-          let actualQueryStrings = actualURL.search.substring(1).split("&");
-          actualQueryStrings.sort();
-          Assert.equal(actualQueryStrings.length, queryStrings.length, "Check number of params");
-
-          for (let i = 0; i < queryStrings.length; i++) {
-            Assert.equal(actualQueryStrings[i], queryStrings[i], "Check parameter " + i);
-          }
-
-          tabOpened = true;
-        });
-
-        let client = new FxAccountsOAuthClient({
-          parameters: {
-            state: "state",
-            client_id: "client_id",
-            oauth_uri: HTTP_PATH,
-            content_uri: HTTP_PATH,
-          },
-          authorizationEndpoint: HTTP_ENDPOINT
-        });
-
-        client.onComplete = function(tokenData) {
-          Assert.ok(tabOpened);
-          Assert.equal(tokenData.code, "code1");
-          Assert.equal(tokenData.state, "state");
-          resolve();
-        };
-
-        client.onError = reject;
-
-        client.launchWebFlow();
-      });
-    }
-  },
-  {
-    desc: "FxA OAuth - should open a new tab, complete OAuth flow when forcing auth",
-    run() {
-      return new Promise(function(resolve, reject) {
-        let tabOpened = false;
-        let properURL = "http://example.com/browser/browser/base/content/test/general/browser_fxa_oauth.html";
-        let queryStrings = [
-          "action=force_auth",
-          "client_id=client_id",
-          "scope=",
-          "state=state",
-          "webChannelId=oauth_client_id",
-          "email=test%40invalid.com",
-        ];
-        queryStrings.sort();
-
-        waitForTab(function(tab) {
-          Assert.ok("Tab successfully opened");
-          Assert.ok(gBrowser.currentURI.spec.split("?")[0], properURL, "Check URL without params");
-
-          let actualURL = new URL(gBrowser.currentURI.spec);
-          let actualQueryStrings = actualURL.search.substring(1).split("&");
-          actualQueryStrings.sort();
-          Assert.equal(actualQueryStrings.length, queryStrings.length, "Check number of params");
-
-          for (let i = 0; i < queryStrings.length; i++) {
-            Assert.equal(actualQueryStrings[i], queryStrings[i], "Check parameter " + i);
-          }
-
-          tabOpened = true;
-        });
-
-        let client = new FxAccountsOAuthClient({
-          parameters: {
-            state: "state",
-            client_id: "client_id",
-            oauth_uri: HTTP_PATH,
-            content_uri: HTTP_PATH,
-            action: "force_auth",
-            email: "test@invalid.com"
-          },
-          authorizationEndpoint: HTTP_ENDPOINT
-        });
-
-        client.onComplete = function(tokenData) {
-          Assert.ok(tabOpened);
-          Assert.equal(tokenData.code, "code1");
-          Assert.equal(tokenData.state, "state");
-          resolve();
-        };
-
-        client.onError = reject;
-
-        client.launchWebFlow();
-      });
-    }
-  },
-  {
-    desc: "FxA OAuth - should receive an error when there's a state mismatch",
-    run() {
-      return new Promise(function(resolve, reject) {
-        let tabOpened = false;
-
-        waitForTab(function(tab) {
-          Assert.ok("Tab successfully opened");
-
-          // It should have passed in the expected non-matching state value.
-          let queryString = gBrowser.currentURI.spec.split("?")[1];
-          Assert.ok(queryString.indexOf("state=different-state") >= 0);
-
-          tabOpened = true;
-        });
-
-        let client = new FxAccountsOAuthClient({
-          parameters: {
-            state: "different-state",
-            client_id: "client_id",
-            oauth_uri: HTTP_PATH,
-            content_uri: HTTP_PATH,
-          },
-          authorizationEndpoint: HTTP_ENDPOINT
-        });
-
-        client.onComplete = reject;
-
-        client.onError = function(err) {
-          Assert.ok(tabOpened);
-          Assert.equal(err.message, "OAuth flow failed. State doesn't match");
-          resolve();
-        };
-
-        client.launchWebFlow();
-      });
-    }
-  },
-  {
-    desc: "FxA OAuth - should be able to request keys during OAuth flow",
-    run() {
-      return new Promise(function(resolve, reject) {
-        let tabOpened = false;
-
-        waitForTab(function(tab) {
-          Assert.ok("Tab successfully opened");
-
-          // It should have asked for keys.
-          let queryString = gBrowser.currentURI.spec.split("?")[1];
-          Assert.ok(queryString.indexOf("keys=true") >= 0);
-
-          tabOpened = true;
-        });
-
-        let client = new FxAccountsOAuthClient({
-          parameters: {
-            state: "state",
-            client_id: "client_id",
-            oauth_uri: HTTP_PATH,
-            content_uri: HTTP_PATH,
-            keys: true,
-          },
-          authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
-        });
-
-        client.onComplete = function(tokenData, keys) {
-          Assert.ok(tabOpened);
-          Assert.equal(tokenData.code, "code1");
-          Assert.equal(tokenData.state, "state");
-          Assert.deepEqual(keys.kAr, {k: "kAr"});
-          Assert.deepEqual(keys.kBr, {k: "kBr"});
-          resolve();
-        };
-
-        client.onError = reject;
-
-        client.launchWebFlow();
-      });
-    }
-  },
-  {
-    desc: "FxA OAuth - should not receive keys if not explicitly requested",
-    run() {
-      return new Promise(function(resolve, reject) {
-        let tabOpened = false;
-
-        waitForTab(function(tab) {
-          Assert.ok("Tab successfully opened");
-
-          // It should not have asked for keys.
-          let queryString = gBrowser.currentURI.spec.split("?")[1];
-          Assert.ok(queryString.indexOf("keys=true") == -1);
-
-          tabOpened = true;
-        });
-
-        let client = new FxAccountsOAuthClient({
-          parameters: {
-            state: "state",
-            client_id: "client_id",
-            oauth_uri: HTTP_PATH,
-            content_uri: HTTP_PATH
-          },
-          // This endpoint will cause the completion message to contain keys.
-          authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
-        });
-
-        client.onComplete = function(tokenData, keys) {
-          Assert.ok(tabOpened);
-          Assert.equal(tokenData.code, "code1");
-          Assert.equal(tokenData.state, "state");
-          Assert.strictEqual(keys, undefined);
-          resolve();
-        };
-
-        client.onError = reject;
-
-        client.launchWebFlow();
-      });
-    }
-  },
-  {
-    desc: "FxA OAuth - should receive an error if keys could not be obtained",
-    run() {
-      return new Promise(function(resolve, reject) {
-        let tabOpened = false;
-
-        waitForTab(function(tab) {
-          Assert.ok("Tab successfully opened");
-
-          // It should have asked for keys.
-          let queryString = gBrowser.currentURI.spec.split("?")[1];
-          Assert.ok(queryString.indexOf("keys=true") >= 0);
-
-          tabOpened = true;
-        });
-
-        let client = new FxAccountsOAuthClient({
-          parameters: {
-            state: "state",
-            client_id: "client_id",
-            oauth_uri: HTTP_PATH,
-            content_uri: HTTP_PATH,
-            keys: true,
-          },
-          // This endpoint will cause the completion message not to contain keys.
-          authorizationEndpoint: HTTP_ENDPOINT
-        });
-
-        client.onComplete = reject;
-
-        client.onError = function(err) {
-          Assert.ok(tabOpened);
-          Assert.equal(err.message, "OAuth flow failed. Keys were not returned");
-          resolve();
-        };
-
-        client.launchWebFlow();
-      });
-    }
-  }
-]; // gTests
-
-function waitForTab(aCallback) {
-  let container = gBrowser.tabContainer;
-  container.addEventListener("TabOpen", function tabOpener(event) {
-    container.removeEventListener("TabOpen", tabOpener);
-    gBrowser.addEventListener("load", function listener() {
-      gBrowser.removeEventListener("load", listener, true);
-      let tab = event.target;
-      aCallback(tab);
-    }, true);
-  });
-}
-
-function test() {
-  waitForExplicitFinish();
-
-  Task.spawn(function* () {
-    const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
-    let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
-    let newWhitelist = origWhitelist + " http://example.com";
-    Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
-    try {
-      for (let testCase of gTests) {
-        info("Running: " + testCase.desc);
-        yield testCase.run();
-      }
-    } finally {
-      Services.prefs.clearUserPref(webchannelWhitelistPref);
-    }
-  }).then(finish, ex => {
-    Assert.ok(false, "Unexpected Exception: " + ex);
-    finish();
-  });
-}
deleted file mode 100644
--- a/browser/base/content/test/general/browser_fxa_oauth_with_keys.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <title>fxa_oauth_test</title>
-</head>
-<body>
-<script>
-  window.onload = function() {
-    var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      // Note: This intentionally sends a string instead of an object, to ensure both work
-      // (see browser_fxa_oauth.html for the other test)
-      detail: JSON.stringify({
-        id: "oauth_client_id",
-        message: {
-          command: "oauth_complete",
-          data: {
-            state: "state",
-            code: "code1",
-            closeWindow: "signin",
-            // Keys normally contain more information, but this is enough
-            // to keep Loop's tests happy.
-            keys: { kAr: { k: "kAr" }, kBr: { k: "kBr" }},
-          },
-        },
-      }),
-    });
-
-    window.dispatchEvent(event);
-  };
-</script>
-</body>
-</html>
--- a/browser/base/content/test/general/browser_misused_characters_in_strings.js
+++ b/browser/base/content/test/general/browser_misused_characters_in_strings.js
@@ -25,17 +25,21 @@ let gWhitelist = [{
     key: "weakCryptoAdvanced.override",
     type: "single-quote"
   }, {
     file: "netError.dtd",
     key: "inadequateSecurityError.longDesc",
     type: "single-quote"
   }, {
     file: "netError.dtd",
-    key: "certerror.wrongSystemTime",
+    key: "certerror.wrongSystemTime2",
+    type: "single-quote"
+  }, {
+    file: "netError.dtd",
+    key: "certerror.wrongSystemTimeWithoutReference",
     type: "single-quote"
   }, {
     file: "phishing-afterload-warning-message.dtd",
     key: "safeb.blocked.malwarePage.shortDesc",
     type: "single-quote"
   }, {
     file: "phishing-afterload-warning-message.dtd",
     key: "safeb.blocked.unwantedPage.shortDesc",
--- a/browser/base/content/test/general/browser_temporary_permissions_navigation.js
+++ b/browser/base/content/test/general/browser_temporary_permissions_navigation.js
@@ -30,19 +30,46 @@ add_task(function* testTempPermissionOnR
       return reloadButton.disabled == false;
     });
 
     Assert.deepEqual(SitePermissions.get(uri, id, browser), {
       state: SitePermissions.BLOCK,
       scope: SitePermissions.SCOPE_TEMPORARY,
     });
 
+    reloaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+
     // Reload as a user (should remove the temp permission).
     EventUtils.synthesizeMouseAtCenter(reloadButton, {});
 
+    yield reloaded;
+
+    Assert.deepEqual(SitePermissions.get(uri, id, browser), {
+      state: SitePermissions.UNKNOWN,
+      scope: SitePermissions.SCOPE_PERSISTENT,
+    });
+
+    // Set the permission again.
+    SitePermissions.set(uri, id, SitePermissions.BLOCK, SitePermissions.SCOPE_TEMPORARY, browser);
+
+    // Open the tab context menu.
+    let contextMenu = document.getElementById("tabContextMenu");
+    let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+    EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, {type: "contextmenu", button: 2});
+    yield popupShownPromise;
+
+    let reloadMenuItem = document.getElementById("context_reloadTab");
+
+    reloaded = BrowserTestUtils.browserLoaded(browser, false, uri.spec);
+
+    // Reload as a user through the context menu (should remove the temp permission).
+    EventUtils.synthesizeMouseAtCenter(reloadMenuItem, {});
+
+    yield reloaded;
+
     Assert.deepEqual(SitePermissions.get(uri, id, browser), {
       state: SitePermissions.UNKNOWN,
       scope: SitePermissions.SCOPE_PERSISTENT,
     });
 
     SitePermissions.remove(uri, id, browser);
   });
 });
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
@@ -174,41 +174,55 @@ var tests = [
     }
   },
   // Test that persistent panels are still open after switching to another
   // window and back.
   { id: "Test#7",
     *run() {
       this.oldSelectedTab = gBrowser.selectedTab;
       yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+      let firstTab = gBrowser.selectedTab;
+
+      yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
 
       let shown = waitForNotificationPanel();
       let notifyObj = new BasicNotification(this.id);
       notifyObj.options.persistent = true;
       this.notification = showNotification(notifyObj);
       yield shown;
 
-      ok(notifyObj.shownCallbackTriggered, "Should have triggered the shown callback");
+      ok(notifyObj.shownCallbackTriggered, "Should have triggered the shown event");
+      ok(notifyObj.showingCallbackTriggered, "Should have triggered the showing event");
+      // Reset to false so that we can ensure these are not fired a second time.
+      notifyObj.shownCallbackTriggered = false;
+      notifyObj.showingCallbackTriggered = false;
 
-      yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
       let promiseWin = BrowserTestUtils.waitForNewWindow();
-      gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+      gBrowser.replaceTabWithWindow(firstTab);
       let win = yield promiseWin;
 
       let anchor = win.document.getElementById("default-notification-icon");
       win.PopupNotifications._reshowNotifications(anchor);
       ok(win.PopupNotifications.panel.childNodes.length == 0,
          "no notification displayed in new window");
 
       yield BrowserTestUtils.closeWindow(win);
       yield waitForWindowReadyForPopupNotifications(window);
 
       let id = PopupNotifications.panel.firstChild.getAttribute("popupid");
       ok(id.endsWith("Test#7"), "Should have found the notification from Test7");
-      ok(PopupNotifications.isPanelOpen, "Should have shown the popup again after getting back to the window");
+      ok(PopupNotifications.isPanelOpen,
+         "Should have kept the popup on the first window");
+      ok(!notifyObj.dismissalCallbackTriggered,
+         "Should not have triggered a dismissed event");
+      ok(!notifyObj.shownCallbackTriggered,
+         "Should not have triggered a second shown event");
+      ok(!notifyObj.showingCallbackTriggered,
+         "Should not have triggered a second showing event");
+
       this.notification.remove();
       gBrowser.removeTab(gBrowser.selectedTab);
       gBrowser.selectedTab = this.oldSelectedTab;
 
       goNext();
     }
   },
   // Test that only the first persistent notification is shown on update
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -45,9 +45,30 @@ var tests = [
     onHidden(popup) {
       ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
       ok(!this.notifyObj.secondaryActionClicked, "secondaryAction was not clicked");
       ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered");
       ok(!this.notifyObj.removedCallbackTriggered, "removed callback was not triggered");
       this.notification.remove();
     }
   },
+  // Test that the space key on an anchor element focuses an active notification
+  { id: "Test#3",
+    *run() {
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.anchorID = "geo-notification-icon";
+      this.notifyObj.addOptions({
+        persistent: true
+      });
+      this.notification = showNotification(this.notifyObj);
+    },
+    *onShown(popup) {
+      checkPopup(popup, this.notifyObj);
+      let anchor = document.getElementById(this.notifyObj.anchorID);
+      anchor.focus();
+      is(document.activeElement, anchor);
+      EventUtils.synthesizeKey(" ", {});
+      is(document.activeElement, popup.childNodes[0].button);
+      this.notification.remove();
+    },
+    onHidden(popup) { }
+  },
 ];
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -373,16 +373,17 @@ function openLinkIn(url, where, params) 
       flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV;
     }
 
     if (aForceAboutBlankViewerInCurrent) {
       targetBrowser.createAboutBlankContentViewer(aPrincipal);
     }
 
     targetBrowser.loadURIWithFlags(url, {
+      triggeringPrincipal: aPrincipal,
       flags,
       referrerURI: aNoReferrer ? null : aReferrerURI,
       referrerPolicy: aReferrerPolicy,
       postData: aPostData,
       userContextId: aUserContextId
     });
     break;
   case "tabshifted":
@@ -397,16 +398,17 @@ function openLinkIn(url, where, params) 
       inBackground: loadInBackground,
       allowThirdPartyFixup: aAllowThirdPartyFixup,
       relatedToCurrent: aRelatedToCurrent,
       skipAnimation: aSkipTabAnimation,
       allowMixedContent: aAllowMixedContent,
       noReferrer: aNoReferrer,
       userContextId: aUserContextId,
       originPrincipal: aPrincipal,
+      triggeringPrincipal: aPrincipal,
     });
     targetBrowser = tabUsedForLoad.linkedBrowser;
     break;
   }
 
   // Focus the content, but only if the browser used for the load is selected.
   if (targetBrowser == w.gBrowser.selectedBrowser) {
     targetBrowser.focus();
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -61,16 +61,17 @@ browser.jar:
         content/browser/aboutProviderDirectory.xhtml  (content/aboutProviderDirectory.xhtml)
         content/browser/aboutTabCrashed.css           (content/aboutTabCrashed.css)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
         content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
         content/browser/browser-addons.js             (content/browser-addons.js)
+        content/browser/browser-captivePortal.js      (content/browser-captivePortal.js)
         content/browser/browser-ctrlTab.js            (content/browser-ctrlTab.js)
         content/browser/browser-customization.js      (content/browser-customization.js)
         content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
         content/browser/browser-compacttheme.js       (content/browser-compacttheme.js)
         content/browser/browser-feeds.js              (content/browser-feeds.js)
         content/browser/browser-fullScreenAndPointerLock.js  (content/browser-fullScreenAndPointerLock.js)
         content/browser/browser-fullZoom.js           (content/browser-fullZoom.js)
         content/browser/browser-fxaccounts.js         (content/browser-fxaccounts.js)
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -33,17 +33,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 const kSpecialWidgetPfx = "customizableui-special-";
 
 const kPrefCustomizationState        = "browser.uiCustomization.state";
 const kPrefCustomizationAutoAdd      = "browser.uiCustomization.autoAdd";
 const kPrefCustomizationDebug        = "browser.uiCustomization.debug";
 const kPrefDrawInTitlebar            = "browser.tabs.drawInTitlebar";
-const kPrefSelectedThemeID           = "lightweightThemes.selectedThemeID";
 const kPrefWebIDEInNavbar            = "devtools.webide.widget.inNavbarByDefault";
 
 const kExpectedWindowURL = "chrome://browser/content/browser.xul";
 
 /**
  * The keys are the handlers that are fired when the event type (the value)
  * is fired on the subview. A widget that provides a subview has the option
  * of providing onViewShowing and onViewHiding event handlers.
@@ -220,16 +219,22 @@ var CustomizableUIInternal = {
     let showCharacterEncoding = Services.prefs.getComplexValue(
       "browser.menu.showCharacterEncoding",
       Ci.nsIPrefLocalizedString
     ).data;
     if (showCharacterEncoding == "true") {
       panelPlacements.push("characterencoding-button");
     }
 
+    if (AppConstants.NIGHTLY_BUILD) {
+      if (Services.prefs.getBoolPref("extensions.webcompat-reporter.enabled")) {
+        panelPlacements.push("webcompat-reporter-button");
+      }
+    }
+
     this.registerArea(CustomizableUI.AREA_PANEL, {
       anchor: "PanelUI-menu-button",
       type: CustomizableUI.TYPE_MENU_PANEL,
       defaultPlacements: panelPlacements
     }, true);
     PanelWideWidgetTracker.init();
 
     let navbarPlacements = [
@@ -1916,17 +1921,25 @@ var CustomizableUIInternal = {
   // Note that this does not populate gPlacements, which is done lazily so that
   // the legacy state can be migrated, which is only available once a browser
   // window is openned.
   // The panel area is an exception here, since it has no legacy state and is
   // built lazily - and therefore wouldn't otherwise result in restoring its
   // state immediately when a browser window opens, which is important for
   // other consumers of this API.
   loadSavedState() {
-    let state = Services.prefs.getCharPref(kPrefCustomizationState);
+    let state = null;
+    try {
+      state = Services.prefs.getCharPref(kPrefCustomizationState);
+    } catch (e) {
+      log.debug("No saved state found");
+      // This will fail if nothing has been customized, so silently fall back to
+      // the defaults.
+    }
+
     if (!state) {
       return;
     }
     try {
       gSavedState = JSON.parse(state);
       if (typeof gSavedState != "object" || gSavedState === null) {
         throw "Invalid saved state";
       }
@@ -2502,28 +2515,28 @@ var CustomizableUIInternal = {
     if (gSeenWidgets.size) {
       gDirty = true;
     }
 
     gResetting = false;
   },
 
   _resetUIState() {
+    try {
+      gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
+      gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
+      gUIStateBeforeReset.currentTheme = LightweightThemeManager.currentTheme;
+    } catch (e) { }
+
     this._resetExtraToolbars();
 
-    gUIStateBeforeReset.selectedThemeID = Services.prefs.getCharPref(kPrefSelectedThemeID);
-    let selectedThemeID = Services.prefs.getDefaultBranch("").getCharPref(kPrefSelectedThemeID);
-    LightweightThemeManager.currentTheme =
-      selectedThemeID ? LightweightThemeManager.getUsedTheme(selectedThemeID) : null;
-
-    gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
+    Services.prefs.clearUserPref(kPrefCustomizationState);
     Services.prefs.clearUserPref(kPrefDrawInTitlebar);
-
-    gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
-    Services.prefs.clearUserPref(kPrefCustomizationState);
+    LightweightThemeManager.currentTheme = null;
+    log.debug("State reset");
 
     // Reset placements to make restoring default placements possible.
     gPlacements = new Map();
     gDirtyAreaCache = new Set();
     gSeenWidgets = new Set();
     // Clear the saved state to ensure that defaults will be used.
     gSavedState = null;
     // Restore the state for each area to its defaults
@@ -2581,26 +2594,25 @@ var CustomizableUIInternal = {
     if (gUIStateBeforeReset.uiCustomizationState == null ||
         gUIStateBeforeReset.drawInTitlebar == null) {
       return;
     }
     gUndoResetting = true;
 
     let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
     let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
-    let selectedThemeID = gUIStateBeforeReset.selectedThemeID;
+    let currentTheme = gUIStateBeforeReset.currentTheme;
 
     // Need to clear the previous state before setting the prefs
     // because pref observers may check if there is a previous UI state.
     this._clearPreviousUIState();
 
     Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
     Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
-    LightweightThemeManager.currentTheme =
-      selectedThemeID ? LightweightThemeManager.getUsedTheme(selectedThemeID) : null;
+    LightweightThemeManager.currentTheme = currentTheme;
     this.loadSavedState();
     // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
     // and we don't need to do anything else here:
     if (gSavedState) {
       for (let areaId of Object.keys(gSavedState.placements)) {
         let placements = gSavedState.placements[areaId];
         gPlacements.set(areaId, placements);
       }
@@ -2769,19 +2781,18 @@ var CustomizableUIInternal = {
       }
     }
 
     if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
       log.debug(kPrefDrawInTitlebar + " pref is non-default");
       return false;
     }
 
-    if (Services.prefs.getDefaultBranch("").getCharPref(kPrefSelectedThemeID) !=
-        Services.prefs.getCharPref(kPrefSelectedThemeID)) {
-      log.debug(kPrefSelectedThemeID + " pref is non-default");
+    if (LightweightThemeManager.currentTheme) {
+      log.debug(LightweightThemeManager.currentTheme + " theme is non-default");
       return false;
     }
 
     return true;
   },
 
   setToolbarVisibility(aToolbarId, aIsVisible) {
     // We only persist the attribute the first time.
--- a/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js
+++ b/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js
@@ -25,26 +25,31 @@ add_task(function* testWrapUnwrap() {
 });
 
 // Creating and destroying a widget should correctly deal with panel placeholders
 add_task(function* testPanelPlaceholders() {
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   // The value of expectedPlaceholders depends on the default palette layout.
   // Bug 1229236 is for these tests to be smarter so the test doesn't need to
   // change when the default placements change.
-  let expectedPlaceholders = 1 + (isInDevEdition() ? 1 : 0);
+  let expectedPlaceholders = 1;
+  if (isInDevEdition()) {
+    expectedPlaceholders += 1;
+  } else if (isInNightly()) {
+    expectedPlaceholders += 2;
+  }
   is(panel.querySelectorAll(".panel-customization-placeholder").length, expectedPlaceholders, "The number of placeholders should be correct.");
   CustomizableUI.createWidget({id: kTestWidget2, label: "Pretty label", tooltiptext: "Pretty tooltip", defaultArea: CustomizableUI.AREA_PANEL});
   let elem = document.getElementById(kTestWidget2);
   let wrapper = document.getElementById("wrapper-" + kTestWidget2);
   ok(elem, "There should be an item");
   ok(wrapper, "There should be a wrapper");
   is(wrapper.firstChild.id, kTestWidget2, "Wrapper should have test widget");
   is(wrapper.parentNode, panel, "Wrapper should be in panel");
-  expectedPlaceholders = isInDevEdition() ? 1 : 3;
+  expectedPlaceholders = (expectedPlaceholders - 1) || 3;
   is(panel.querySelectorAll(".panel-customization-placeholder").length, expectedPlaceholders, "The number of placeholders should be correct.");
   CustomizableUI.destroyWidget(kTestWidget2);
   wrapper = document.getElementById("wrapper-" + kTestWidget2);
   ok(!wrapper, "There should be a wrapper");
   let item = document.getElementById(kTestWidget2);
   ok(!item, "There should no longer be an item");
   yield endCustomizing();
 });
--- a/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
+++ b/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
@@ -19,18 +19,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(zoomControls, printButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
@@ -47,18 +48,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(zoomControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
 });
 
 
 // Dragging the zoom controls to be before the new-window button should not move any widgets.
 add_task(function*() {
@@ -73,18 +75,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(zoomControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the zoom controls to be before the history-panelmenu should move the zoom-controls in to the row higher than the history-panelmenu.
 add_task(function*() {
   yield startCustomizing();
@@ -98,18 +101,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(zoomControls, historyPanelMenu);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
@@ -127,18 +131,19 @@ add_task(function*() {
                              "history-panelmenu",
                              "fullscreen-button",
                              "zoom-controls",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(zoomControls, preferencesButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
@@ -156,18 +161,19 @@ add_task(function*() {
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
                                "developer-button",
                                "sync-button",
+                               "webcompat-reporter-button"
                               ];
-  removeDeveloperButtonIfDevEdition(placementsAfterInsert);
+  removeNonReleaseButtons(placementsAfterInsert);
   simulateItemDrag(openFileButton, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
   is(feedButton.parentNode, feedWrapper,
@@ -197,18 +203,19 @@ add_task(function*() {
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
                                "developer-button",
                                "sync-button",
+                               "webcompat-reporter-button"
                               ];
-  removeDeveloperButtonIfDevEdition(placementsAfterInsert);
+  removeNonReleaseButtons(placementsAfterInsert);
   simulateItemDrag(openFileButton, editControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
   is(feedButton.parentNode, feedWrapper,
@@ -235,18 +242,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(editControls, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to be before the new-window-button should
 // move the zoom-controls before the edit-controls.
 add_task(function*() {
@@ -261,18 +269,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(editControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to be before the privatebrowsing-button
@@ -290,18 +299,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(editControls, privateBrowsingButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to be before the save-page-button
@@ -319,18 +329,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button"
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(editControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to the panel itself should append
@@ -348,21 +359,27 @@ add_task(function*() {
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
                              "developer-button",
                              "sync-button",
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
+  if (isInNightly()) {
+    CustomizableUI.removeWidgetFromArea("webcompat-reporter-button");
+  }
   simulateItemDrag(editControls, panel);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
+  if (isInNightly()) {
+    CustomizableUI.addWidgetToArea("webcompat-reporter-button", CustomizableUI.AREA_PANEL);
+  }
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to the customization-palette and
 // back should work.
 add_task(function*() {
   yield startCustomizing();
   let editControls = document.getElementById("edit-controls");
@@ -374,18 +391,19 @@ add_task(function*() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button",
                              "sync-button",
+                             "webcompat-reporter-button",
                             ];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+  removeNonReleaseButtons(placementsAfterMove);
   let paletteChildElementCount = palette.childElementCount;
   simulateItemDrag(editControls, palette);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   is(paletteChildElementCount + 1, palette.childElementCount,
      "The palette should have a new child, congratulations!");
   is(editControls.parentNode.id, "wrapper-edit-controls",
      "The edit-controls should be properly wrapped.");
   is(editControls.parentNode.getAttribute("place"), "palette",
@@ -401,42 +419,44 @@ add_task(function*() {
 // should append the edit-controls to the bottom of the panel.
 add_task(function*() {
   yield startCustomizing();
   let editControls = document.getElementById("edit-controls");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let numPlaceholders = 2;
   for (let i = 0; i < numPlaceholders; i++) {
     // This test relies on there being a specific number of widgets in the
-    // panel. The addition of sync-button screwed this up, so we remove it
-    // here. We should either fix the tests to not rely on the specific layout,
-    // or fix bug 1007910 which would change the placeholder logic in different
-    // ways. Bug 1229236 is for these tests to be smarter.
-    CustomizableUI.removeWidgetFromArea("sync-button");
+    // panel. The addition of sync-button and webcompat-reporter-button screwed
+    // this up, so we remove them here. We should either fix the tests to not
+    // rely on the specific layout, or fix bug 1007910 which would change the
+    // placeholder logic in different ways. Bug 1229236 is for these tests to
+    // be smarter.
+    removeNonOriginalButtons();
     // NB: We can't just iterate over all of the placeholders
     // because each drag-drop action recreates them.
     let placeholder = panel.getElementsByClassName("panel-customization-placeholder")[i];
     let placementsAfterMove = ["zoom-controls",
                                "new-window-button",
                                "privatebrowsing-button",
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
                                "edit-controls",
-                               "developer-button"];
-    removeDeveloperButtonIfDevEdition(placementsAfterMove);
+                               "developer-button",
+                              ];
+    removeNonReleaseButtons(placementsAfterMove);
     simulateItemDrag(editControls, placeholder);
     assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
     let zoomControls = document.getElementById("zoom-controls");
     simulateItemDrag(editControls, zoomControls);
-    CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+    restoreNonOriginalButtons();
     ok(CustomizableUI.inDefaultState, "Should still be in default state.");
   }
 });
 
 // Dragging the open-file-button back on to itself should work.
 add_task(function*() {
   yield startCustomizing();
   let openFileButton = document.getElementById("open-file-button");
@@ -449,49 +469,51 @@ add_task(function*() {
   is(editControls.parentNode.tagName, "toolbarpaletteitem",
      "edit-controls should be wrapped by a toolbarpaletteitem");
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging a small button onto the last big button should work.
 add_task(function*() {
   // Bug 1007910 requires there be a placeholder on the final row for this
-  // test to work as written. The addition of sync-button meant that's not true
-  // so we remove it from here. Bug 1229236 is for these tests to be smarter.
-  CustomizableUI.removeWidgetFromArea("sync-button");
+  // test to work as written. The addition of sync-button and
+  // webcompat-reporter-button meant that's not true so we remove them from
+  // here. Bug 1229236 is for these tests to be smarter.
+  removeNonOriginalButtons();
   yield startCustomizing();
   let editControls = document.getElementById("edit-controls");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let target = panel.getElementsByClassName("panel-customization-placeholder")[0];
   let placementsAfterMove = ["zoom-controls",
                              "new-window-button",
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
-                             "developer-button"];
-  removeDeveloperButtonIfDevEdition(placementsAfterMove);
+                             "developer-button",
+                            ];
+  removeNonReleaseButtons(placementsAfterMove);
   simulateItemDrag(editControls, target);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let itemToDrag = "email-link-button"; // any button in the palette by default.
   let button = document.getElementById(itemToDrag);
   placementsAfterMove.splice(11, 0, itemToDrag);
   simulateItemDrag(button, editControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
 
   // Put stuff back:
   let palette = document.getElementById("customization-palette");
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(button, palette);
   simulateItemDrag(editControls, zoomControls);
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  restoreNonOriginalButtons();
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 add_task(function* asyncCleanup() {
   yield endCustomizing();
   yield resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
+++ b/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
@@ -16,47 +16,47 @@ add_task(function*() {
   }
   if (!isInDevEdition()) {
     ok(CustomizableUI.inDefaultState, "Should be in default state.");
   } else {
     ok(!CustomizableUI.inDefaultState, "Should not be in default state if on DevEdition.");
   }
 
   // This test relies on an exact number of widgets being in the panel.
-  // Remove the sync-button to satisfy that. (bug 1229236)
-  CustomizableUI.removeWidgetFromArea("sync-button");
+  // Remove the buttons to satisfy that. (bug 1229236)
+  removeNonOriginalButtons();
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders after re-entering");
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  restoreNonOriginalButtons();
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // Two orphaned items should have one placeholder next to them (case 1).
 add_task(function*() {
   yield startCustomizing();
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
   }
 
   // This test relies on an exact number of widgets being in the panel.
-  // Remove the sync-button to satisfy that. (bug 1229236)
-  CustomizableUI.removeWidgetFromArea("sync-button");
+  // Remove the button to satisfy that. (bug 1229236)
+  removeNonOriginalButtons()
 
   let btn = document.getElementById("open-file-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
   let placementsAfterAppend = placements;
 
   placementsAfterAppend = placements.concat(["open-file-button"]);
   simulateItemDrag(btn, panel);
@@ -76,30 +76,30 @@ add_task(function*() {
 
   btn = document.getElementById("open-file-button");
   simulateItemDrag(btn, palette);
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  restoreNonOriginalButtons();
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // Two orphaned items should have one placeholder next to them (case 2).
 add_task(function*() {
   yield startCustomizing();
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
   }
   // This test relies on an exact number of widgets being in the panel.
-  // Remove the sync-button to satisfy that. (bug 1229236)
-  CustomizableUI.removeWidgetFromArea("sync-button");
+  // Remove the buttons to satisfy that. (bug 1229236)
+  removeNonOriginalButtons();
 
   let btn = document.getElementById("add-ons-button");
   let btn2 = document.getElementById("developer-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   let placementsAfterAppend = placements.filter(p => p != btn.id && p != btn2.id);
@@ -118,31 +118,31 @@ add_task(function*() {
   simulateItemDrag(btn2, panel);
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  restoreNonOriginalButtons();
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // A wide widget at the bottom of the panel should have three placeholders after it.
 add_task(function*() {
   yield startCustomizing();
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
   }
 
   // This test relies on an exact number of widgets being in the panel.
-  // Remove the sync-button to satisfy that. (bug 1229236)
-  CustomizableUI.removeWidgetFromArea("sync-button");
+  // Remove the buttons to satisfy that. (bug 1229236)
+  removeNonOriginalButtons();
 
   let btn = document.getElementById("edit-controls");
   let btn2 = document.getElementById("developer-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   placements.pop();
@@ -162,17 +162,17 @@ add_task(function*() {
 
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(btn, zoomControls);
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  restoreNonOriginalButtons();
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // The default placements should have two placeholders at the bottom (or 1 in win8).
 add_task(function*() {
   yield startCustomizing();
   let numPlaceholders = -1;
 
@@ -181,26 +181,26 @@ add_task(function*() {
   } else {
     numPlaceholders = 2;
   }
 
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
 
   // This test relies on an exact number of widgets being in the panel.
-  // Remove the sync-button to satisfy that. (bug 1229236)
-  CustomizableUI.removeWidgetFromArea("sync-button");
+  // Remove the buttons to satisfy that. (bug 1229236)
+  removeNonOriginalButtons();
 
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders after re-entering");
 
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  restoreNonOriginalButtons();
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 add_task(function* asyncCleanup() {
   yield endCustomizing();
   yield resetCustomization();
 });
 
--- a/browser/components/customizableui/test/browser_967000_button_sync.js
+++ b/browser/components/customizableui/test/browser_967000_button_sync.js
@@ -57,19 +57,16 @@ add_task(function* setup() {
   });
 });
 
 // The test expects the about:preferences#sync page to open in the current tab
 function* openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
   info("Check Sync button functionality");
   Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", "http://example.com/");
 
-  // add the Sync button to the panel
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
-
   // check the button's functionality
   yield PanelUI.show();
 
   if (entryPoint == "uitour") {
     UITour.tourBrowsersByWindow.set(window, new Set());
     UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser);
   }
 
@@ -147,18 +144,16 @@ add_task(function* () {
 
   mockedInternal.getTabClients = () => [];
   mockedInternal.syncTabs = () => Promise.resolve();
 
   document.getElementById("sync-reauth-state").hidden = true;
   document.getElementById("sync-setup-state").hidden = true;
   document.getElementById("sync-syncnow-state").hidden = false;
 
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
-
   let syncPanel = document.getElementById("PanelUI-remotetabs");
   let links = syncPanel.querySelectorAll(".remotetabs-promo-link");
 
   is(links.length, 2, "found 2 links as expected");
 
   // test each link and left and middle mouse buttons
   for (let link of links) {
     for (let button = 0; button < 2; button++) {
@@ -209,18 +204,16 @@ add_task(function* () {
     return Promise.resolve();
   }
 
   // configure our broadcasters so we are in the right state.
   document.getElementById("sync-reauth-state").hidden = true;
   document.getElementById("sync-setup-state").hidden = true;
   document.getElementById("sync-syncnow-state").hidden = false;
 
-  // add the Sync button to the panel
-  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
   yield PanelUI.show();
   document.getElementById("sync-button").click();
   let syncPanel = document.getElementById("PanelUI-remotetabs");
   ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
 
   let subpanel = document.getElementById("PanelUI-remotetabs-main")
   ok(!subpanel.hidden, "main pane is visible");
   let deck = document.getElementById("PanelUI-remotetabs-deck");
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -113,20 +113,42 @@ function getToolboxCustomToolbarId(toolb
 function resetCustomization() {
   return CustomizableUI.reset();
 }
 
 function isInDevEdition() {
   return AppConstants.MOZ_DEV_EDITION;
 }
 
-function removeDeveloperButtonIfDevEdition(areaPanelPlacements) {
+function isInNightly() {
+  return AppConstants.NIGHTLY_BUILD;
+}
+
+function removeNonReleaseButtons(areaPanelPlacements) {
   if (isInDevEdition()) {
     areaPanelPlacements.splice(areaPanelPlacements.indexOf("developer-button"), 1);
   }
+
+  if (!isInNightly()) {
+    areaPanelPlacements.splice(areaPanelPlacements.indexOf("webcompat-reporter-button"), 1);
+  }
+}
+
+function removeNonOriginalButtons() {
+  CustomizableUI.removeWidgetFromArea("sync-button");
+  if (isInNightly()) {
+    CustomizableUI.removeWidgetFromArea("webcompat-reporter-button");
+  }
+}
+
+function restoreNonOriginalButtons() {
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  if (isInNightly()) {
+    CustomizableUI.addWidgetToArea("webcompat-reporter-button", CustomizableUI.AREA_PANEL);
+  }
 }
 
 function assertAreaPlacements(areaId, expectedPlacements) {
   let actualPlacements = getAreaWidgetIds(areaId);
   placementArraysEqual(areaId, actualPlacements, expectedPlacements);
 }
 
 function placementArraysEqual(areaId, actualPlacements, expectedPlacements) {
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -1156,17 +1156,19 @@ DownloadsPlacesView.prototype = {
     return aCommand == "downloadsCmd_clearDownloads" ||
            document.activeElement == this._richlistbox;
   },
 
   // nsIController
   isCommandEnabled(aCommand) {
     switch (aCommand) {
       case "cmd_copy":
-        return this._richlistbox.selectedItems.length > 0;
+      case "downloadsCmd_openReferrer":
+      case "downloadShowMenuItem":
+        return this._richlistbox.selectedItems.length == 1;
       case "cmd_selectAll":
         return true;
       case "cmd_paste":
         return this._canDownloadClipboardURL();
       case "downloadsCmd_clearDownloads":
         return this._canClearDownloads();
       default:
         return Array.every(this._richlistbox.selectedItems,
--- a/browser/components/downloads/test/browser/browser_libraryDrop.js
+++ b/browser/components/downloads/test/browser/browser_libraryDrop.js
@@ -12,27 +12,27 @@ registerCleanupFunction(function*() {
 });
 
 add_task(function* test_indicatorDrop() {
   let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
       getService(Ci.mozIJSSubScriptLoader);
   let EventUtils = {};
   scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
 
-  function task_drop(win, urls) {
+  async function drop(win, urls) {
     let dragData = [[{type: "text/plain", data: urls.join("\n")}]];
 
     let listBox = win.document.getElementById("downloadsRichListBox");
     ok(listBox, "download list box present");
 
-    let list = yield Downloads.getList(Downloads.ALL);
+    let list = await Downloads.getList(Downloads.ALL);
 
     let added = new Set();
     let succeeded = new Set();
-    yield new Promise(function(resolve) {
+    await new Promise(resolve => {
       let view = {
         onDownloadAdded: function(download) {
           added.add(download.source.url);
         },
         onDownloadChanged: function(download) {
           if (!added.has(download.source.url))
             return;
           if (!download.succeeded)
@@ -60,13 +60,13 @@ add_task(function* test_indicatorDrop() 
 
   startServer();
 
   let win = yield openLibrary("Downloads");
   registerCleanupFunction(function() {
     win.close();
   });
 
-  yield* task_drop(win, [httpUrl("file1.txt")]);
-  yield* task_drop(win, [httpUrl("file1.txt"),
-                         httpUrl("file2.txt"),
-                         httpUrl("file3.txt")]);
+  yield drop(win, [httpUrl("file1.txt")]);
+  yield drop(win, [httpUrl("file1.txt"),
+                   httpUrl("file2.txt"),
+                   httpUrl("file3.txt")]);
 });
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -1,16 +1,187 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
                                   "resource:///modules/Sanitizer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+                                  "resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+                                   "@mozilla.org/serviceworkers/manager;1",
+                                   "nsIServiceWorkerManager");
+
+/**
+* A number of iterations after which to yield time back
+* to the system.
+*/
+const YIELD_PERIOD = 10;
+
+const PREF_DOMAIN = "privacy.cpd.";
+
+XPCOMUtils.defineLazyGetter(this, "sanitizer", () => {
+  let sanitizer = new Sanitizer();
+  sanitizer.prefDomain = PREF_DOMAIN;
+  return sanitizer;
+});
+
+function makeRange(options) {
+  return (options.since == null) ?
+    null :
+    [PlacesUtils.toPRTime(options.since), PlacesUtils.toPRTime(Date.now())];
+}
+
+function clearCache() {
+  // Clearing the cache does not support timestamps.
+  return sanitizer.items.cache.clear();
+}
+
+let clearCookies = Task.async(function* (options) {
+  let cookieMgr = Services.cookies;
+  // This code has been borrowed from sanitize.js.
+  let yieldCounter = 0;
+
+  if (options.since) {
+    // Iterate through the cookies and delete any created after our cutoff.
+    let cookiesEnum = cookieMgr.enumerator;
+    while (cookiesEnum.hasMoreElements()) {
+      let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
+
+      if (cookie.creationTime >= PlacesUtils.toPRTime(options.since)) {
+        // This cookie was created after our cutoff, clear it.
+        cookieMgr.remove(cookie.host, cookie.name, cookie.path,
+                         false, cookie.originAttributes);
+
+        if (++yieldCounter % YIELD_PERIOD == 0) {
+          yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+        }
+      }
+    }
+  } else {
+    // Remove everything.
+    cookieMgr.removeAll();
+  }
+});
+
+function clearDownloads(options) {
+  return sanitizer.items.downloads.clear(makeRange(options));
+}
+
+function clearFormData(options) {
+  return sanitizer.items.formdata.clear(makeRange(options));
+}
+
+function clearHistory(options) {
+  return sanitizer.items.history.clear(makeRange(options));
+}
+
+let clearPasswords = Task.async(function* (options) {
+  let loginManager = Services.logins;
+  let yieldCounter = 0;
+
+  if (options.since) {
+    // Iterate through the logins and delete any updated after our cutoff.
+    let logins = loginManager.getAllLogins();
+    for (let login of logins) {
+      login.QueryInterface(Ci.nsILoginMetaInfo);
+      if (login.timePasswordChanged >= options.since) {
+        loginManager.removeLogin(login);
+        if (++yieldCounter % YIELD_PERIOD == 0) {
+          yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+        }
+      }
+    }
+  } else {
+    // Remove everything.
+    loginManager.removeAllLogins();
+  }
+});
+
+function clearPluginData(options) {
+  return sanitizer.items.pluginData.clear(makeRange(options));
+}
+
+let clearServiceWorkers = Task.async(function* () {
+  // Clearing service workers does not support timestamps.
+  let yieldCounter = 0;
+
+  // Iterate through the service workers and remove them.
+  let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+  for (let i = 0; i < serviceWorkers.length; i++) {
+    let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+    let host = sw.principal.URI.host;
+    serviceWorkerManager.removeAndPropagate(host);
+    if (++yieldCounter % YIELD_PERIOD == 0) {
+      yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+    }
+  }
+});
+
+function doRemoval(options, dataToRemove, extension) {
+  if (options.originTypes &&
+      (options.originTypes.protectedWeb || options.originTypes.extension)) {
+    return Promise.reject(
+      {message: "Firefox does not support protectedWeb or extension as originTypes."});
+  }
+
+  let removalPromises = [];
+  let invalidDataTypes = [];
+  for (let dataType in dataToRemove) {
+    if (dataToRemove[dataType]) {
+      switch (dataType) {
+        case "cache":
+          removalPromises.push(clearCache());
+          break;
+        case "cookies":
+          removalPromises.push(clearCookies(options));
+          break;
+        case "downloads":
+          removalPromises.push(clearDownloads(options));
+          break;
+        case "formData":
+          removalPromises.push(clearFormData(options));
+          break;
+        case "history":
+          removalPromises.push(clearHistory(options));
+          break;
+        case "passwords":
+          removalPromises.push(clearPasswords(options));
+          break;
+        case "pluginData":
+          removalPromises.push(clearPluginData(options));
+          break;
+        case "serviceWorkers":
+          removalPromises.push(clearServiceWorkers());
+          break;
+        default:
+          invalidDataTypes.push(dataType);
+      }
+    }
+  }
+  if (extension && invalidDataTypes.length) {
+    extension.logger.warn(
+      `Firefox does not support dataTypes: ${invalidDataTypes.toString()}.`);
+  }
+  return Promise.all(removalPromises);
+}
 
 extensions.registerSchemaAPI("browsingData", "addon_parent", context => {
+  let {extension} = context;
   return {
     browsingData: {
       settings() {
         const PREF_DOMAIN = "privacy.cpd.";
         // The following prefs are the only ones in Firefox that match corresponding
         // values used by Chrome when rerturning settings.
         const PREF_LIST = ["cache", "cookies", "history", "formdata", "downloads"];
 
@@ -29,11 +200,35 @@ extensions.registerSchemaAPI("browsingDa
           dataRemovalPermitted[item] = true;
         }
         // formData has a different case than the pref formdata.
         dataToRemove.formData = Preferences.get(`${PREF_DOMAIN}formdata`);
         dataRemovalPermitted.formData = true;
 
         return Promise.resolve({options, dataToRemove, dataRemovalPermitted});
       },
+      remove(options, dataToRemove) {
+        return doRemoval(options, dataToRemove, extension);
+      },
+      removeCache(options) {
+        return doRemoval(options, {cache: true});
+      },
+      removeCookies(options) {
+        return doRemoval(options, {cookies: true});
+      },
+      removeDownloads(options) {
+        return doRemoval(options, {downloads: true});
+      },
+      removeFormData(options) {
+        return doRemoval(options, {formData: true});
+      },
+      removeHistory(options) {
+        return doRemoval(options, {history: true});
+      },
+      removePasswords(options) {
+        return doRemoval(options, {passwords: true});
+      },
+      removePluginData(options) {
+        return doRemoval(options, {pluginData: true});
+      },
     },
   };
 });
--- a/browser/components/extensions/schemas/browsing_data.json
+++ b/browser/components/extensions/schemas/browsing_data.json
@@ -151,17 +151,16 @@
           }
         ]
       },
       {
         "name": "remove",
         "description": "Clears various types of browsing data stored in a user's profile.",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "dataToRemove",
             "$ref": "DataTypeSet",
@@ -196,17 +195,16 @@
           }
         ]
       },
       {
         "name": "removeCache",
         "description": "Clears the browser's cache.",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
@@ -216,17 +214,16 @@
           }
         ]
       },
       {
         "name": "removeCookies",
         "description": "Clears the browser's cookies and server-bound certificates modified within a particular timeframe.",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
@@ -236,17 +233,16 @@
           }
         ]
       },
       {
         "name": "removeDownloads",
         "description": "Clears the browser's list of downloaded files (<em>not</em> the downloaded files themselves).",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
@@ -276,17 +272,16 @@
           }
         ]
       },
       {
         "name": "removeFormData",
         "description": "Clears the browser's stored form data (autofill).",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
@@ -296,17 +291,16 @@
           }
         ]
       },
       {
         "name": "removeHistory",
         "description": "Clears the browser's history.",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
@@ -356,17 +350,16 @@
           }
         ]
       },
       {
         "name": "removePluginData",
         "description": "Clears plugins' data.",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
@@ -376,17 +369,16 @@
           }
         ]
       },
       {
         "name": "removePasswords",
         "description": "Clears the browser's stored passwords.",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -2,37 +2,44 @@
 support-files =
   head.js
   head_pageAction.js
   head_sessions.js
   context.html
   ctxmenu-image.png
   context_tabs_onUpdated_page.html
   context_tabs_onUpdated_iframe.html
+  file_clearplugindata.html
   file_popup_api_injection_a.html
   file_popup_api_injection_b.html
   file_iframe_document.html
   file_iframe_document.sjs
   file_bypass_cache.sjs
   file_language_fr_en.html
   file_language_ja.html
   file_language_tlh.html
   file_dummy.html
+  file_serviceWorker.html
+  serviceWorker.js
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
   ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js
 
 [browser_ext_browserAction_context.js]
 [browser_ext_browserAction_disabled.js]
 [browser_ext_browserAction_pageAction_icon.js]
 [browser_ext_browserAction_pageAction_icon_permissions.js]
 [browser_ext_browserAction_popup.js]
 [browser_ext_browserAction_popup_preload.js]
 [browser_ext_browserAction_popup_resize.js]
 [browser_ext_browserAction_simple.js]
+[browser_ext_browsingData_formData.js]
+[browser_ext_browsingData_history.js]
+[browser_ext_browsingData_pluginData.js]
+[browser_ext_browsingData_serviceWorkers.js]
 [browser_ext_commands_execute_browser_action.js]
 [browser_ext_commands_execute_page_action.js]
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_contentscript_connect.js]
 [browser_ext_contextMenus.js]
 [browser_ext_contextMenus_checkboxes.js]
 [browser_ext_contextMenus_chrome.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_formData.js
@@ -0,0 +1,138 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+
+const REFERENCE_DATE = Date.now();
+
+function countEntries(fieldname, message, expected) {
+  return new Promise((resolve, reject) => {
+    let callback = {
+      handleResult: result => {
+        is(result, expected, message);
+        resolve();
+      },
+      handleError: reject,
+    };
+
+    FormHistory.count({fieldname}, callback);
+  });
+}
+
+async function setupFormHistory() {
+  function searchEntries(terms, params) {
+    return new Promise((resolve, reject) => {
+      let callback = {
+        handleResult: resolve,
+        handleError: reject,
+      };
+
+      FormHistory.search(terms, params, callback);
+    });
+  }
+
+  function update(changes) {
+    return new Promise((resolve, reject) => {
+      let callback = {
+        handleError: reject,
+        handleCompletion: resolve,
+      };
+      FormHistory.update(changes, callback);
+    });
+  }
+
+  // Make sure we've got a clean DB to start with, then add the entries we'll be testing.
+  await update([
+    {op: "remove"},
+    {
+      op: "add",
+      fieldname: "reference",
+      value: "reference",
+    }, {
+      op: "add",
+      fieldname: "10secondsAgo",
+      value: "10s",
+    }, {
+      op: "add",
+      fieldname: "10minutesAgo",
+      value: "10m",
+    }]);
+
+  // Age the entries to the proper vintage.
+  let timestamp = PlacesUtils.toPRTime(REFERENCE_DATE);
+  let result = await searchEntries(["guid"], {fieldname: "reference"});
+  await update({op: "update", firstUsed: timestamp, guid: result.guid});
+
+  timestamp = PlacesUtils.toPRTime(REFERENCE_DATE - 10000);
+  result = await searchEntries(["guid"], {fieldname: "10secondsAgo"});
+  await update({op: "update", firstUsed: timestamp, guid: result.guid});
+
+  timestamp = PlacesUtils.toPRTime(REFERENCE_DATE - 10000 * 60);
+  result = await searchEntries(["guid"], {fieldname: "10minutesAgo"});
+  await update({op: "update", firstUsed: timestamp, guid: result.guid});
+
+  // Sanity check.
+  await countEntries("reference", "Checking for 10minutes form history entry creation", 1);
+  await countEntries("10secondsAgo", "Checking for 1hour form history entry creation", 1);
+  await countEntries("10minutesAgo", "Checking for 1hour10minutes form history entry creation", 1);
+}
+
+add_task(async function testFormData() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, options) => {
+      if (msg == "removeFormData") {
+        await browser.browsingData.removeFormData(options);
+      } else {
+        await browser.browsingData.remove(options, {formData: true});
+      }
+      browser.test.sendMessage("formDataRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Clear form data with no since value.
+    await setupFormHistory();
+    extension.sendMessage(method, {});
+    await extension.awaitMessage("formDataRemoved");
+
+    await countEntries("reference", "reference form entry should be deleted.", 0);
+    await countEntries("10secondsAgo", "10secondsAgo form entry should be deleted.", 0);
+    await countEntries("10minutesAgo", "10minutesAgo form entry should be deleted.", 0);
+
+    // Clear form data with recent since value.
+    await setupFormHistory();
+    extension.sendMessage(method, {since: REFERENCE_DATE});
+    await extension.awaitMessage("formDataRemoved");
+
+    await countEntries("reference", "reference form entry should be deleted.", 0);
+    await countEntries("10secondsAgo", "10secondsAgo form entry should still exist.", 1);
+    await countEntries("10minutesAgo", "10minutesAgo form entry should still exist.", 1);
+
+    // Clear form data with old since value.
+    await setupFormHistory();
+    extension.sendMessage(method, {since: REFERENCE_DATE - 1000000});
+    await extension.awaitMessage("formDataRemoved");
+
+    await countEntries("reference", "reference form entry should be deleted.", 0);
+    await countEntries("10secondsAgo", "10secondsAgo form entry should be deleted.", 0);
+    await countEntries("10minutesAgo", "10minutesAgo form entry should be deleted.", 0);
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removeFormData");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_history.js
@@ -0,0 +1,91 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+                                  "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+
+const OLD_URL = "http://example.com/";
+const RECENT_URL = "http://example.com/2/";
+const REFERENCE_DATE = new Date();
+
+// pages/visits to add via History.insert
+const PAGE_INFOS = [
+  {
+    url: RECENT_URL,
+    title: `test visit for ${RECENT_URL}`,
+    visits: [
+      {date: REFERENCE_DATE},
+    ],
+  },
+  {
+    url: OLD_URL,
+    title: `test visit for ${OLD_URL}`,
+    visits: [
+      {date: new Date(Number(REFERENCE_DATE) - 1000)},
+      {date: new Date(Number(REFERENCE_DATE) - 2000)},
+    ],
+  },
+];
+
+async function setupHistory() {
+  await PlacesTestUtils.clearHistory();
+  await PlacesUtils.history.insertMany(PAGE_INFOS);
+  is((await PlacesTestUtils.visitsInDB(RECENT_URL)), 1, "Expected number of visits found in history database.");
+  is((await PlacesTestUtils.visitsInDB(OLD_URL)), 2, "Expected number of visits found in history database.");
+}
+
+add_task(async function testHistory() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, options) => {
+      if (msg == "removeHistory") {
+        await browser.browsingData.removeHistory(options);
+      } else {
+        await browser.browsingData.remove(options, {history: true});
+      }
+      browser.test.sendMessage("historyRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Clear history with no since value.
+    await setupHistory();
+    extension.sendMessage(method, {});
+    await extension.awaitMessage("historyRemoved");
+
+    is((await PlacesTestUtils.visitsInDB(RECENT_URL)), 0, "Expected number of visits removed from history database.");
+    is((await PlacesTestUtils.visitsInDB(OLD_URL)), 0, "Expected number of visits removed from history database.");
+
+    // Clear history with recent since value.
+    await setupHistory();
+    extension.sendMessage(method, {since: REFERENCE_DATE - 1000});
+    await extension.awaitMessage("historyRemoved");
+
+    is((await PlacesTestUtils.visitsInDB(RECENT_URL)), 0, "Expected number of visits removed from history database.");
+    is((await PlacesTestUtils.visitsInDB(OLD_URL)), 1, "Expected number of visits removed from history database.");
+
+    // Clear history with old since value.
+    await setupHistory();
+    extension.sendMessage(method, {since: REFERENCE_DATE - 100000});
+    await extension.awaitMessage("historyRemoved");
+
+    is((await PlacesTestUtils.visitsInDB(RECENT_URL)), 0, "Expected number of visits removed from history database.");
+    is((await PlacesTestUtils.visitsInDB(OLD_URL)), 0, "Expected number of visits removed from history database.");
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removeHistory");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_pluginData.js
@@ -0,0 +1,136 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(this, "pluginHost",
+                                   "@mozilla.org/plugin/host;1",
+                                   "nsIPluginHost");
+
+// Returns the chrome side nsIPluginTag for the test plugin.
+function getTestPlugin() {
+  let tags = pluginHost.getPluginTags();
+  let plugin = tags.find(tag => tag.name == "Test Plug-in");
+  if (!plugin) {
+    ok(false, "Unable to find plugin");
+  }
+  return plugin;
+}
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+const TEST_URL = TEST_ROOT + "file_clearplugindata.html";
+const REFERENCE_DATE = Date.now();
+const PLUGIN_TAG = getTestPlugin();
+
+/* Due to layout being async, "PluginBindAttached" may trigger later. This
+   returns a Promise that resolves once we've forced a layout flush, which
+   triggers the PluginBindAttached event to fire. This trick only works if
+   there is some sort of plugin in the page.
+ */
+function promiseUpdatePluginBindings(browser) {
+  return ContentTask.spawn(browser, {}, function* () {
+    let doc = content.document;
+    let elems = doc.getElementsByTagName("embed");
+    if (elems && elems.length > 0) {
+      elems[0].clientTop; // eslint-disable-line no-unused-expressions
+    }
+  });
+}
+
+function stored(needles) {
+  let something = pluginHost.siteHasData(PLUGIN_TAG, null);
+  if (!needles) {
+    return something;
+  }
+
+  if (!something) {
+    return false;
+  }
+
+  if (needles.every(value => pluginHost.siteHasData(PLUGIN_TAG, value))) {
+    return true;
+  }
+
+  return false;
+}
+
+add_task(async function testPluginData() {
+  function background() {
+    browser.test.onMessage.addListener(async(msg, options) => {
+      if (msg == "removePluginData") {
+        await browser.browsingData.removePluginData(options);
+      } else {
+        await browser.browsingData.remove(options, {pluginData: true});
+      }
+      browser.test.sendMessage("pluginDataRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Clear plugin data with no since value.
+
+    // Load page to set data for the plugin.
+    let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+    await promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+    ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
+       "Data stored for sites");
+
+    extension.sendMessage(method, {});
+    await extension.awaitMessage("pluginDataRemoved");
+
+    ok(!stored(null), "All data cleared");
+    await BrowserTestUtils.removeTab(tab);
+
+    // Clear history with recent since value.
+
+    // Load page to set data for the plugin.
+    tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+    await promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+    ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
+       "Data stored for sites");
+
+    extension.sendMessage(method, {since: REFERENCE_DATE - 20000});
+    await extension.awaitMessage("pluginDataRemoved");
+
+    ok(stored(["bar.com", "qux.com"]), "Data stored for sites");
+    ok(!stored(["foo.com"]), "Data cleared for foo.com");
+    ok(!stored(["baz.com"]), "Data cleared for baz.com");
+    await BrowserTestUtils.removeTab(tab);
+
+    // Clear history with old since value.
+
+    // Load page to set data for the plugin.
+    tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+    await promiseUpdatePluginBindings(gBrowser.selectedBrowser);
+
+    ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
+       "Data stored for sites");
+
+    extension.sendMessage(method, {since: REFERENCE_DATE - 1000000});
+    await extension.awaitMessage("pluginDataRemoved");
+
+    ok(!stored(null), "All data cleared");
+    await BrowserTestUtils.removeTab(tab);
+  }
+
+  Services.prefs.setBoolPref("plugins.click_to_play", true);
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("plugins.click_to_play");
+  });
+  PLUGIN_TAG.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+
+  await extension.startup();
+
+  await testRemovalMethod("removePluginData");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_serviceWorkers.js
@@ -0,0 +1,89 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({
+    set: [["dom.serviceWorkers.exemptFromPerDomainMax", true],
+         ["dom.serviceWorkers.enabled", true],
+         ["dom.serviceWorkers.testing.enabled", true]],
+  });
+});
+
+add_task(function* testServiceWorkers() {
+  function background() {
+    const PAGE = "/browser/browser/components/extensions/test/browser/file_serviceWorker.html";
+
+    browser.runtime.onMessage.addListener(msg => {
+      browser.test.sendMessage("serviceWorkerRegistered");
+    });
+
+    browser.test.onMessage.addListener(async (msg) => {
+      await browser.browsingData.remove({}, {serviceWorkers: true});
+      browser.test.sendMessage("serviceWorkersRemoved");
+    });
+
+    // Create two serviceWorkers.
+    browser.tabs.create({url: `http://mochi.test:8888${PAGE}`});
+    browser.tabs.create({url: `http://example.com${PAGE}`});
+  }
+
+  function contentScript() {
+    window.addEventListener("message", msg => { // eslint-disable-line mozilla/balanced-listeners
+      if (msg.data == "serviceWorkerRegistered") {
+        browser.runtime.sendMessage("serviceWorkerRegistered");
+      }
+    }, true);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData", "tabs"],
+      "content_scripts": [{
+        "matches": [
+          "http://mochi.test/*/file_serviceWorker.html",
+          "http://example.com/*/file_serviceWorker.html",
+        ],
+        "js": ["script.js"],
+        "run_at": "document_start",
+      }],
+    },
+    files: {
+      "script.js": contentScript,
+    },
+  });
+
+  let serviceWorkerManager = SpecialPowers.Cc["@mozilla.org/serviceworkers/manager;1"]
+    .getService(SpecialPowers.Ci.nsIServiceWorkerManager);
+
+  let win = yield BrowserTestUtils.openNewBrowserWindow();
+  yield focusWindow(win);
+
+  yield extension.startup();
+  yield extension.awaitMessage("serviceWorkerRegistered");
+  yield extension.awaitMessage("serviceWorkerRegistered");
+
+  let serviceWorkers = [];
+  // Even though we await the registrations by waiting for the messages,
+  // sometimes the serviceWorkers are still not registered at this point.
+  while (serviceWorkers.length < 2) {
+    serviceWorkers = serviceWorkerManager.getAllRegistrations();
+    yield new Promise(resolve => setTimeout(resolve, 1));
+  }
+  is(serviceWorkers.length, 2, "ServiceWorkers have been registered.");
+
+  extension.sendMessage();
+
+  yield extension.awaitMessage("serviceWorkersRemoved");
+
+  // The serviceWorkers and not necessarily removed immediately.
+  while (serviceWorkers.length > 0) {
+    serviceWorkers = serviceWorkerManager.getAllRegistrations();
+    yield new Promise(resolve => setTimeout(resolve, 1));
+  }
+  is(serviceWorkers.length, 0, "ServiceWorkers have been removed.");
+
+  yield extension.unload();
+  yield BrowserTestUtils.closeWindow(win);
+});
copy from browser/base/content/test/plugins/browser_clearplugindata.html
copy to browser/components/extensions/test/browser/file_clearplugindata.html
--- a/browser/base/content/test/plugins/browser_clearplugindata.html
+++ b/browser/components/extensions/test/browser/file_clearplugindata.html
@@ -3,27 +3,28 @@
   http://creativecommons.org/publicdomain/zero/1.0/
 -->
 <html>
   <head>
     <title>Plugin Clear Site Data sanitize test</title>
 
     <embed id="plugin1" type="application/x-test" width="200" height="200"></embed>
 
-    <script type="application/javascript">
-      function testSteps() {
-        // Make sure clearing by timerange is supported.
-        var p = document.getElementById("plugin1");
-        p.setSitesWithDataCapabilities(true);
-
-        p.setSitesWithData(
-          "foo.com:0:5," +
-          "bar.com:0:100," +
-          "baz.com:1:5," +
-          "qux.com:1:100"
-        );
-      }
-    </script>
   </head>
 
-  <body onload="testSteps();"></body>
+  <body></body>
+
+  <script type="application/javascript">
+    "use strict";
+
+    // Make sure clearing by timerange is supported.
+    let p = document.getElementById("plugin1");
+    p.setSitesWithDataCapabilities(true);
+
+    p.setSitesWithData(
+      "foo.com:0:5," +
+      "bar.com:0:100," +
+      "baz.com:1:5," +
+      "qux.com:1:100"
+    );
+  </script>
 
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_serviceWorker.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <script>
+      "use strict";
+
+      navigator.serviceWorker.register("serviceWorker.js").then(() => {
+        window.postMessage("serviceWorkerRegistered", "*");
+      });
+    </script>
+  </head>
+  <body>
+    This is a test page.
+  </body>
+<html>
new file mode 100644
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -1,14 +1,15 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-/* exported createHttpServer */
+/* exported createHttpServer, promiseConsoleOutput  */
 
+Components.utils.import("resource://gre/modules/Task.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
                                   "resource://gre/modules/Extension.jsm");
@@ -21,16 +22,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TestUtils",
+                                  "resource://testing-common/TestUtils.jsm");
 
 ExtensionTestUtils.init(this);
 
 
 /**
  * Creates a new HttpServer for testing, and begins listening on the
  * specified port. Automatically shuts down the server when the test
  * unit ends.
@@ -48,8 +51,37 @@ function createHttpServer(port = -1) {
   do_register_cleanup(() => {
     return new Promise(resolve => {
       server.stop(resolve);
     });
   });
 
   return server;
 }
+
+var promiseConsoleOutput = Task.async(function* (task) {
+  const DONE = `=== console listener ${Math.random()} done ===`;
+
+  let listener;
+  let messages = [];
+  let awaitListener = new Promise(resolve => {
+    listener = msg => {
+      if (msg == DONE) {
+        resolve();
+      } else {
+        void (msg instanceof Ci.nsIConsoleMessage);
+        messages.push(msg);
+      }
+    };
+  });
+
+  Services.console.registerListener(listener);
+  try {
+    let result = yield task();
+
+    Services.console.logStringMessage(DONE);
+    yield awaitListener;
+
+    return {messages, result};
+  } finally {
+    Services.console.unregisterListener(listener);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData.js
@@ -0,0 +1,66 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testInvalidArguments() {
+  async function background() {
+    const UNSUPPORTED_DATA_TYPES = ["appcache", "fileSystems", "webSQL"];
+
+    await browser.test.assertRejects(
+      browser.browsingData.remove({originTypes: {protectedWeb: true}}, {cookies: true}),
+      "Firefox does not support protectedWeb or extension as originTypes.",
+      "Expected error received when using protectedWeb originType.");
+
+    await browser.test.assertRejects(
+      browser.browsingData.removeCookies({originTypes: {extension: true}}),
+      "Firefox does not support protectedWeb or extension as originTypes.",
+      "Expected error received when using extension originType.");
+
+    for (let dataType of UNSUPPORTED_DATA_TYPES) {
+      let dataTypes = {};
+      dataTypes[dataType] = true;
+      browser.test.assertThrows(
+        () => browser.browsingData.remove({}, dataTypes),
+        /Type error for parameter dataToRemove/,
+        `Expected error received when using ${dataType} dataType.`
+      );
+    }
+
+    browser.test.notifyPass("invalidArguments");
+  }
+
+  let extensionData = {
+    background: background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  await extension.awaitFinish("invalidArguments");
+  await extension.unload();
+});
+
+add_task(async function testUnimplementedDataType() {
+  function background() {
+    browser.browsingData.remove({}, {localStorage: true});
+    browser.test.sendMessage("finished");
+  }
+
+  let {messages} = await promiseConsoleOutput(async function() {
+    let extension = ExtensionTestUtils.loadExtension({
+      background: background,
+      manifest: {
+        permissions: ["browsingData"],
+      },
+    });
+
+    await extension.startup();
+    await extension.awaitMessage("finished");
+    await extension.unload();
+  });
+
+  let warningObserved = messages.find(line => /Firefox does not support dataTypes: localStorage/.test(line));
+  ok(warningObserved, "Warning issued when calling remove with an unimplemented dataType.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_cookies_cache.js
@@ -0,0 +1,172 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+                                  "resource://gre/modules/Timer.jsm");
+
+const COOKIE = {
+  host: "example.com",
+  name: "test_cookie",
+  path: "/",
+};
+let since, oldCookie;
+
+function addCookie(cookie) {
+  Services.cookies.add(cookie.host, cookie.path, cookie.name, "test", false, false, false, Date.now() / 1000 + 10000);
+  ok(Services.cookies.cookieExists(cookie), `Cookie ${cookie.name} was created.`);
+}
+
+async function setUpCookies() {
+  // Add a cookie which will end up with an older creationTime.
+  oldCookie = Object.assign({}, COOKIE, {name: Date.now()});
+  addCookie(oldCookie);
+  await new Promise(resolve => setTimeout(resolve, 10));
+  since = Date.now();
+  await new Promise(resolve => setTimeout(resolve, 10));
+
+  // Add a cookie which will end up with a more recent creationTime.
+  addCookie(COOKIE);
+}
+
+add_task(async function testCache() {
+  function background() {
+    browser.test.onMessage.addListener(async msg => {
+      if (msg == "removeCache") {
+        await browser.browsingData.removeCache({});
+      } else {
+        await browser.browsingData.remove({}, {cache: true});
+      }
+      browser.test.sendMessage("cacheRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // We can assume the notification works properly, so we only need to observe
+    // the notification to know the cache was cleared.
+    let awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
+    extension.sendMessage(method);
+    await awaitNotification;
+    await extension.awaitMessage("cacheRemoved");
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removeCache");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
+
+add_task(async function testCookies() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, options) => {
+      if (msg == "removeCookies") {
+        await browser.browsingData.removeCookies(options);
+      } else {
+        await browser.browsingData.remove(options, {cookies: true});
+      }
+      browser.test.sendMessage("cookiesRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Clear cookies with a recent since value.
+    await setUpCookies();
+    extension.sendMessage(method, {since});
+    await extension.awaitMessage("cookiesRemoved");
+
+    ok(Services.cookies.cookieExists(oldCookie), "Old cookie was not removed.");
+    ok(!Services.cookies.cookieExists(COOKIE), "Recent cookie was removed.");
+
+    // Clear cookies with an old since value.
+    await setUpCookies();
+    addCookie(COOKIE);
+    extension.sendMessage(method, {since: since - 100000});
+    await extension.awaitMessage("cookiesRemoved");
+
+    ok(!Services.cookies.cookieExists(oldCookie), "Old cookie was removed.");
+    ok(!Services.cookies.cookieExists(COOKIE), "Recent cookie was removed.");
+
+    // Clear cookies with no since value and valid originTypes.
+    await setUpCookies();
+    extension.sendMessage(
+      method,
+      {originTypes: {unprotectedWeb: true, protectedWeb: false}});
+    await extension.awaitMessage("cookiesRemoved");
+
+    ok(!Services.cookies.cookieExists(COOKIE), `Cookie ${COOKIE.name}  was removed.`);
+    ok(!Services.cookies.cookieExists(oldCookie), `Cookie ${oldCookie.name}  was removed.`);
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removeCookies");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
+
+add_task(async function testCacheAndCookies() {
+  function background() {
+    browser.test.onMessage.addListener(async options => {
+      await browser.browsingData.remove(options, {cache: true, cookies: true});
+      browser.test.sendMessage("cacheAndCookiesRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  await extension.startup();
+
+  // Clear cache and cookies with a recent since value.
+  await setUpCookies();
+  let awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
+  extension.sendMessage({since});
+  await awaitNotification;
+  await extension.awaitMessage("cacheAndCookiesRemoved");
+
+  ok(Services.cookies.cookieExists(oldCookie), "Old cookie was not removed.");
+  ok(!Services.cookies.cookieExists(COOKIE), "Recent cookie was removed.");
+
+  // Clear cache and cookies with an old since value.
+  await setUpCookies();
+  awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
+  extension.sendMessage({since: since - 100000});
+  await awaitNotification;
+  await extension.awaitMessage("cacheAndCookiesRemoved");
+
+  ok(!Services.cookies.cookieExists(oldCookie), "Old cookie was removed.");
+  ok(!Services.cookies.cookieExists(COOKIE), "Recent cookie was removed.");
+
+  // Clear cache and cookies with no since value.
+  await setUpCookies();
+  awaitNotification = TestUtils.topicObserved("cacheservice:empty-cache");
+  extension.sendMessage({});
+  await awaitNotification;
+  await extension.awaitMessage("cacheAndCookiesRemoved");
+
+  ok(!Services.cookies.cookieExists(COOKIE), `Cookie ${COOKIE.name}  was removed.`);
+  ok(!Services.cookies.cookieExists(oldCookie), `Cookie ${oldCookie.name}  was removed.`);
+
+  await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js
@@ -0,0 +1,110 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+
+const OLD_NAMES = {[Downloads.PUBLIC]: "old-public", [Downloads.PRIVATE]: "old-private"};
+const RECENT_NAMES = {[Downloads.PUBLIC]: "recent-public", [Downloads.PRIVATE]: "recent-private"};
+const REFERENCE_DATE = new Date();
+const OLD_DATE = new Date(Number(REFERENCE_DATE) - 10000);
+
+async function downloadExists(list, path) {
+  let listArray = await list.getAll();
+  return listArray.some(i => i.target.path == path);
+}
+
+async function checkDownloads(expectOldExists = true, expectRecentExists = true) {
+  for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+    let downloadsList = await Downloads.getList(listType);
+    equal(
+      (await downloadExists(downloadsList, OLD_NAMES[listType])),
+      expectOldExists,
+      `Fake old download ${(expectOldExists) ? "was found" : "was removed"}.`);
+    equal(
+      (await downloadExists(downloadsList, RECENT_NAMES[listType])),
+      expectRecentExists,
+      `Fake recent download ${(expectRecentExists) ? "was found" : "was removed"}.`);
+  }
+}
+
+async function setupDownloads() {
+  let downloadsList = await Downloads.getList(Downloads.ALL);
+  await downloadsList.removeFinished();
+
+  for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+    downloadsList = await Downloads.getList(listType);
+    let download = await Downloads.createDownload({
+      source: {
+        url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303",
+        isPrivate: listType == Downloads.PRIVATE},
+      target: OLD_NAMES[listType],
+    });
+    download.startTime = OLD_DATE;
+    download.canceled = true;
+    await downloadsList.add(download);
+
+    download = await Downloads.createDownload({
+      source: {
+        url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303",
+        isPrivate: listType == Downloads.PRIVATE},
+      target: RECENT_NAMES[listType],
+    });
+    download.startTime = REFERENCE_DATE;
+    download.canceled = true;
+    await downloadsList.add(download);
+  }
+
+  // Confirm everything worked.
+  downloadsList = await Downloads.getList(Downloads.ALL);
+  equal((await downloadsList.getAll()).length, 4, "4 fake downloads added.");
+  checkDownloads();
+}
+
+add_task(async function testDownloads() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, options) => {
+      if (msg == "removeDownloads") {
+        await browser.browsingData.removeDownloads(options);
+      } else {
+        await browser.browsingData.remove(options, {downloads: true});
+      }
+      browser.test.sendMessage("downloadsRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Clear downloads with no since value.
+    await setupDownloads();
+    extension.sendMessage(method, {});
+    await extension.awaitMessage("downloadsRemoved");
+    await checkDownloads(false, false);
+
+    // Clear downloads with recent since value.
+    await setupDownloads();
+    extension.sendMessage(method, {since: REFERENCE_DATE});
+    await extension.awaitMessage("downloadsRemoved");
+    await checkDownloads(true, false);
+
+    // Clear downloads with old since value.
+    await setupDownloads();
+    extension.sendMessage(method, {since: REFERENCE_DATE - 100000});
+    await extension.awaitMessage("downloadsRemoved");
+    await checkDownloads(false, false);
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removeDownloads");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js
@@ -0,0 +1,92 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(this, "loginManager",
+                                   "@mozilla.org/login-manager;1",
+                                   "nsILoginManager");
+
+const REFERENCE_DATE = Date.now();
+const LOGIN_USERNAME = "username";
+const LOGIN_PASSWORD = "password";
+const LOGIN_USERNAME_FIELD = "username_field";
+const LOGIN_PASSWORD_FIELD = "password_field";
+const OLD_HOST = "http://mozilla.org";
+const NEW_HOST = "http://mozilla.com";
+
+function checkLoginExists(host, shouldExist) {
+  let count = {value: 0};
+  loginManager.findLogins(count, host, "", null);
+  equal(count.value, shouldExist ? 1 : 0, `Login was ${shouldExist ? "" : "not "} found.`);
+}
+
+function addLogin(host, timestamp) {
+  checkLoginExists(host, false);
+  let login = Cc["@mozilla.org/login-manager/loginInfo;1"]
+              .createInstance(Ci.nsILoginInfo);
+  login.init(host, "", null, LOGIN_USERNAME, LOGIN_PASSWORD,
+             LOGIN_USERNAME_FIELD, LOGIN_PASSWORD_FIELD);
+  login.QueryInterface(Ci.nsILoginMetaInfo);
+  login.timePasswordChanged = timestamp;
+  loginManager.addLogin(login);
+  checkLoginExists(host, true);
+}
+
+async function setupPasswords() {
+  loginManager.removeAllLogins();
+  addLogin(NEW_HOST, REFERENCE_DATE);
+  addLogin(OLD_HOST, REFERENCE_DATE - 10000);
+}
+
+add_task(async function testPasswords() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, options) => {
+      if (msg == "removeHistory") {
+        await browser.browsingData.removePasswords(options);
+      } else {
+        await browser.browsingData.remove(options, {passwords: true});
+      }
+      browser.test.sendMessage("passwordsRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Clear passwords with no since value.
+    await setupPasswords();
+    extension.sendMessage(method, {});
+    await extension.awaitMessage("passwordsRemoved");
+
+    checkLoginExists(OLD_HOST, false);
+    checkLoginExists(NEW_HOST, false);
+
+    // Clear passwords with recent since value.
+    await setupPasswords();
+    extension.sendMessage(method, {since: REFERENCE_DATE - 1000});
+    await extension.awaitMessage("passwordsRemoved");
+
+    checkLoginExists(OLD_HOST, true);
+    checkLoginExists(NEW_HOST, false);
+
+    // Clear passwords with old since value.
+    await setupPasswords();
+    extension.sendMessage(method, {since: REFERENCE_DATE - 20000});
+    await extension.awaitMessage("passwordsRemoved");
+
+    checkLoginExists(OLD_HOST, false);
+    checkLoginExists(NEW_HOST, false);
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removePasswords");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell.ini
@@ -1,11 +1,15 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 tags = webextensions
 
 [test_ext_bookmarks.js]
+[test_ext_browsingData.js]
+[test_ext_browsingData_cookies_cache.js]
+[test_ext_browsingData_downloads.js]
+[test_ext_browsingData_passwords.js]
 [test_ext_browsingData_settings.js]
 [test_ext_history.js]
 [test_ext_manifest_commands.js]
 [test_ext_manifest_omnibox.js]
 [test_ext_manifest_permissions.js]
--- a/browser/components/migration/tests/unit/head_migration.js
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -15,17 +15,18 @@ Cu.import("resource://gre/modules/Promis
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://testing-common/TestUtils.jsm");
 Cu.import("resource://testing-common/PlacesTestUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
-
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+                                  "resource://gre/modules/Sqlite.jsm");
 // Initialize profile.
 var gProfD = do_get_profile();
 
 Cu.import("resource://testing-common/AppInfo.jsm"); /* globals updateAppInfo */
 updateAppInfo();
 
 /**
  * Migrates the requested resource and waits for the migration to be complete.
--- a/browser/components/migration/tests/unit/test_Chrome_passwords.js
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js
@@ -75,41 +75,22 @@ const TEST_LOGINS = [
     timesUsed: 1,
   },
 ];
 
 var crypto = new OSCrypto();
 var dbConn;
 
 function promiseSetPassword(login) {
-  return new Promise((resolve, reject) => {
-    let stmt = dbConn.createAsyncStatement(`
-      UPDATE logins
-      SET password_value = :password_value
-      WHERE rowid = :rowid
-    `);
-    let passwordValue = crypto.stringToArray(crypto.encryptData(login.password));
-    stmt.bindBlobByName("password_value", passwordValue, passwordValue.length);
-    stmt.params.rowid = login.id;
-
-    stmt.executeAsync({
-      handleError(aError) {
-        reject("Error with the query: " + aError.message);
-      },
-
-      handleCompletion(aReason) {
-        if (aReason === Ci.mozIStorageStatementCallback.REASON_FINISHED) {
-          resolve();
-        } else {
-          reject("Query has failed: " + aReason);
-        }
-      },
-    });
-    stmt.finalize();
-  });
+  let passwordValue = crypto.stringToArray(crypto.encryptData(login.password));
+  return dbConn.execute(`UPDATE logins
+                         SET password_value = :password_value
+                         WHERE rowid = :rowid
+                        `, { password_value: passwordValue,
+                             rowid: login.id });
 }
 
 function checkLoginsAreEqual(passwordManagerLogin, chromeLogin, id) {
   passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
 
   Assert.equal(passwordManagerLogin.username, chromeLogin.username,
                "The two logins ID " + id + " have the same username");
   Assert.equal(passwordManagerLogin.password, chromeLogin.password,
@@ -142,23 +123,23 @@ function generateDifferentLogin(login) {
   newLogin.timeCreated = login.timeCreated + 1;
   newLogin.timePasswordChanged = login.timePasswordChanged + 1;
   newLogin.timesUsed = login.timesUsed + 1;
   return newLogin;
 }
 
 add_task(function* setup() {
   let loginDataFile = do_get_file("AppData/Local/Google/Chrome/User Data/Default/Login Data");
-  dbConn = Services.storage.openUnsharedDatabase(loginDataFile);
+  dbConn = yield Sqlite.openConnection({ path: loginDataFile.path });
   registerFakePath("LocalAppData", do_get_file("AppData/Local/"));
 
   do_register_cleanup(() => {
     Services.logins.removeAllLogins();
-    dbConn.asyncClose();
     crypto.finalize();
+    return dbConn.close();
   });
 });
 
 add_task(function* test_importIntoEmptyDB() {
   for (let login of TEST_LOGINS) {
     yield promiseSetPassword(login);
   }
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -17,19 +17,19 @@ Cu.import("resource://gre/modules/AsyncP
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils", "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-service;1", "nsIAlertsService");
 
 // lazy module getters
 
 /* global AboutHome:false, AboutNewTab:false, AddonManager:false, AddonWatcher:false,
           AsyncShutdown:false, AutoCompletePopup:false, BookmarkHTMLUtils:false,
           BookmarkJSONUtils:false, BrowserUITelemetry:false, BrowserUsageTelemetry:false,
-          CaptivePortalWatcher:false, ContentClick:false, ContentPrefServiceParent:false,
-          ContentSearch:false, DateTimePickerHelper:false, DirectoryLinksProvider:false,
-          Feeds:false, FileUtils:false, FormValidationHandler:false, Integration:false,
+          ContentClick:false, ContentPrefServiceParent:false, ContentSearch:false,
+          DateTimePickerHelper:false, DirectoryLinksProvider:false, Feeds:false,
+          FileUtils:false, FormValidationHandler:false, Integration:false,
           LightweightThemeManager:false, LoginHelper:false, LoginManagerParent:false,
           NetUtil:false, NewTabMessages:false, NewTabUtils:false, OS:false,
           PageThumbs:false, PdfJs:false, PermissionUI:false, PlacesBackups:false,
           PlacesUtils:false, PluralForm:false, PrivateBrowsingUtils:false,
           ProcessHangMonitor:false, ReaderParent:false, RecentWindow:false,
           RemotePrompt:false, SelfSupportBackend:false, SessionStore:false,
           ShellService:false, SimpleServiceDiscovery:false, TabCrashHandler:false,
           Task:false, UITour:false, URLBarZoom:false, WebChannel:false,
@@ -46,17 +46,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
   ["AddonManager", "resource://gre/modules/AddonManager.jsm"],
   ["AddonWatcher", "resource://gre/modules/AddonWatcher.jsm"],
   ["AsyncShutdown", "resource://gre/modules/AsyncShutdown.jsm"],
   ["AutoCompletePopup", "resource://gre/modules/AutoCompletePopup.jsm"],
   ["BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"],
   ["BookmarkJSONUtils", "resource://gre/modules/BookmarkJSONUtils.jsm"],
   ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
   ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
-  ["CaptivePortalWatcher", "resource:///modules/CaptivePortalWatcher.jsm"],
   ["ContentClick", "resource:///modules/ContentClick.jsm"],
   ["ContentPrefServiceParent", "resource://gre/modules/ContentPrefServiceParent.jsm"],
   ["ContentSearch", "resource:///modules/ContentSearch.jsm"],
   ["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"],
   ["DirectoryLinksProvider", "resource:///modules/DirectoryLinksProvider.jsm"],
   ["ExtensionsUI", "resource:///modules/ExtensionsUI.jsm"],
   ["Feeds", "resource:///modules/Feeds.jsm"],
   ["FileUtils", "resource://gre/modules/FileUtils.jsm"],
@@ -975,18 +974,16 @@ BrowserGlue.prototype = {
         if (removalSuccessful && uninstalledValue == "True") {
           this._resetProfileNotification("uninstall");
         }
       }
     }
 
     this._checkForOldBuildUpdates();
 
-    CaptivePortalWatcher.init();
-
     AutoCompletePopup.init();
     DateTimePickerHelper.init();
 
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
   },
 
   /**
@@ -1005,17 +1002,16 @@ BrowserGlue.prototype = {
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     BrowserUsageTelemetry.uninit();
     SelfSupportBackend.uninit();
     NewTabMessages.uninit();
-    CaptivePortalWatcher.uninit();
     AboutNewTab.uninit();
     webrtcUI.uninit();
     FormValidationHandler.uninit();
     AutoCompletePopup.uninit();
     DateTimePickerHelper.uninit();
     if (AppConstants.NIGHTLY_BUILD) {
       AddonWatcher.uninit();
     }
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -204,49 +204,49 @@
     <tabpanel id="dataChoicesPanel" orient="vertical">
 #ifdef MOZ_TELEMETRY_REPORTING
       <groupbox>
         <caption>
           <checkbox id="submitHealthReportBox" label="&enableHealthReport.label;"
                     accesskey="&enableHealthReport.accesskey;"/>
         </caption>
         <vbox>
-          <hbox class="indent">
-            <label>&healthReportDesc.label;</label>
-            <label id="FHRLearnMore"
+          <hbox class="indent" flex="1">
+            <label flex="1">&healthReportDesc.label;</label>
+            <label id="FHRLearnMore" flex="1"
                    class="learnMore text-link">&healthReportLearnMore.label;</label>
           </hbox>
           <hbox class="indent">
             <groupbox flex="1">
               <caption>
                 <checkbox id="submitTelemetryBox" preference="toolkit.telemetry.enabled"
                           label="&enableTelemetryData.label;"
                           accesskey="&enableTelemetryData.accesskey;"/>
               </caption>
-              <hbox class="indent">
-                <label id="telemetryDataDesc">&telemetryDesc.label;</label>
-                <label id="telemetryLearnMore"
+              <hbox class="indent" flex="1">
+                <label id="telemetryDataDesc" flex="1">&telemetryDesc.label;</label>
+                <label id="telemetryLearnMore" flex="1"
                        class="learnMore text-link">&telemetryLearnMore.label;</label>
               </hbox>
             </groupbox>
           </hbox>
         </vbox>
       </groupbox>
 #endif
 #ifdef MOZ_CRASHREPORTER
       <groupbox>
         <caption>
           <checkbox id="automaticallySubmitCrashesBox"
                     preference="browser.crashReports.unsubmittedCheck.autoSubmit"
                     label="&alwaysSubmitCrashReports.label;"
                     accesskey="&alwaysSubmitCrashReports.accesskey;"/>
         </caption>
-        <hbox class="indent">
-          <label>&crashReporterDesc2.label;</label>
-          <label id="crashReporterLearnMore"
+        <hbox class="indent" flex="1">
+          <label flex="1">&crashReporterDesc2.label;</label>
+          <label id="crashReporterLearnMore" flex="1"
                  class="learnMore text-link">&crashReporterLearnMore.label;</label>
         </hbox>
       </groupbox>
 #endif
     </tabpanel>
 #endif
 
     <!-- Network -->
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js
@@ -2,63 +2,50 @@
 /* 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/. */
 
 var gTests;
 function test() {
   waitForExplicitFinish();
   requestLongerTimeout(2);
-  gTests = runTest();
-  gTests.next();
+  runTest().catch(ex => ok(false, ex));
 }
 
 /*
  * ================
  * Helper functions
  * ================
  */
 
-function moveAlong(aResult) {
-  try {
-    gTests.send(aResult);
-  } catch (x if x instanceof StopIteration) {
-    finish();
-  }
-}
-
 function createWindow(aOptions) {
-  whenNewWindowLoaded(aOptions, function(win) {
-    moveAlong(win);
-  });
+  return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve));
 }
 
 function getFile(downloadLastDir, aURI) {
-  downloadLastDir.getFileAsync(aURI, function(result) {
-    moveAlong(result);
-  });
+  return new Promise(resolve => downloadLastDir.getFileAsync(aURI, resolve));
 }
 
 function setFile(downloadLastDir, aURI, aValue) {
   downloadLastDir.setFile(aURI, aValue);
-  executeSoon(moveAlong);
+  return new Promise(resolve => executeSoon(resolve));
 }
 
 function clearHistoryAndWait() {
   clearHistory();
-  executeSoon(() => executeSoon(moveAlong));
+  return new Promise(resolve => executeSoon(_ => executeSoon(resolve)));
 }
 
 /*
  * ===================
  * Function with tests
  * ===================
  */
 
-function runTest() {
+async function runTest() {
   let FileUtils =
     Cu.import("resource://gre/modules/FileUtils.jsm", {}).FileUtils;
   let DownloadLastDir =
     Cu.import("resource://gre/modules/DownloadLastDir.jsm", {}).DownloadLastDir;
 
   let tmpDir = FileUtils.getDir("TmpD", [], true);
   let dir1 = newDirectory();
   let dir2 = newDirectory();
@@ -76,207 +63,209 @@ function runTest() {
     [dir1, dir2, dir3].forEach(dir => dir.remove(true));
     win.close();
     pbWin.close();
   });
 
   function checkDownloadLastDir(gDownloadLastDir, aLastDir) {
     is(gDownloadLastDir.file.path, aLastDir.path,
        "gDownloadLastDir should point to the expected last directory");
-    getFile(gDownloadLastDir, uri1);
+    return getFile(gDownloadLastDir, uri1);
   }
 
   function checkDownloadLastDirNull(gDownloadLastDir) {
     is(gDownloadLastDir.file, null, "gDownloadLastDir should be null");
-    getFile(gDownloadLastDir, uri1);
+    return getFile(gDownloadLastDir, uri1);
   }
 
   /*
    * ================================
    * Create a regular and a PB window
    * ================================
    */
 
-  let win = yield createWindow({private: false});
-  let pbWin = yield createWindow({private: true});
+  let win = await createWindow({private: false});
+  let pbWin = await createWindow({private: true});
 
   let downloadLastDir = new DownloadLastDir(win);
   let pbDownloadLastDir = new DownloadLastDir(pbWin);
 
   /*
    * ==================
    * Beginning of tests
    * ==================
    */
 
   is(typeof downloadLastDir, "object",
      "downloadLastDir should be a valid object");
   is(downloadLastDir.file, null,
      "LastDir pref should be null to start with");
 
   // set up last dir
-  yield setFile(downloadLastDir, null, tmpDir);
+  await setFile(downloadLastDir, null, tmpDir);
   is(downloadLastDir.file.path, tmpDir.path,
      "LastDir should point to the tmpDir");
   isnot(downloadLastDir.file, tmpDir,
         "downloadLastDir.file should not be pointing to tmpDir");
 
   // set uri1 to dir1, all should now return dir1
   // also check that a new object is returned
-  yield setFile(downloadLastDir, uri1, dir1);
+  await setFile(downloadLastDir, uri1, dir1);
   is(downloadLastDir.file.path, dir1.path,
      "downloadLastDir should return dir1");
   isnot(downloadLastDir.file, dir1,
         "downloadLastDir.file should not return dir1");
-  is((yield getFile(downloadLastDir, uri1)).path, dir1.path,
+  is((await getFile(downloadLastDir, uri1)).path, dir1.path,
      "uri1 should return dir1"); // set in CPS
-  isnot((yield getFile(downloadLastDir, uri1)), dir1,
+  isnot((await getFile(downloadLastDir, uri1)), dir1,
         "getFile on uri1 should not return dir1");
-  is((yield getFile(downloadLastDir, uri2)).path, dir1.path,
+  is((await getFile(downloadLastDir, uri2)).path, dir1.path,
      "uri2 should return dir1"); // fallback
-  isnot((yield getFile(downloadLastDir, uri2)), dir1,
+  isnot((await getFile(downloadLastDir, uri2)), dir1,
         "getFile on uri2 should not return dir1");
-  is((yield getFile(downloadLastDir, uri3)).path, dir1.path,
+  is((await getFile(downloadLastDir, uri3)).path, dir1.path,
      "uri3 should return dir1"); // fallback
-  isnot((yield getFile(downloadLastDir, uri3)), dir1,
+  isnot((await getFile(downloadLastDir, uri3)), dir1,
         "getFile on uri3 should not return dir1");
-  is((yield getFile(downloadLastDir, uri4)).path, dir1.path,
+  is((await getFile(downloadLastDir, uri4)).path, dir1.path,
      "uri4 should return dir1"); // fallback
-  isnot((yield getFile(downloadLastDir, uri4)), dir1,
+  isnot((await getFile(downloadLastDir, uri4)), dir1,
         "getFile on uri4 should not return dir1");
 
   // set uri2 to dir2, all except uri1 should now return dir2
-  yield setFile(downloadLastDir, uri2, dir2);
+  await setFile(downloadLastDir, uri2, dir2);
   is(downloadLastDir.file.path, dir2.path,
      "downloadLastDir should point to dir2");
-  is((yield getFile(downloadLastDir, uri1)).path, dir1.path,
+  is((await getFile(downloadLastDir, uri1)).path, dir1.path,
      "uri1 should return dir1"); // set in CPS
-  is((yield getFile(downloadLastDir, uri2)).path, dir2.path,
+  is((await getFile(downloadLastDir, uri2)).path, dir2.path,
      "uri2 should return dir2"); // set in CPS
-  is((yield getFile(downloadLastDir, uri3)).path, dir2.path,
+  is((await getFile(downloadLastDir, uri3)).path, dir2.path,
      "uri3 should return dir2"); // fallback
-  is((yield getFile(downloadLastDir, uri4)).path, dir2.path,
+  is((await getFile(downloadLastDir, uri4)).path, dir2.path,
      "uri4 should return dir2"); // fallback
 
   // set uri3 to dir3, all except uri1 and uri2 should now return dir3
-  yield setFile(downloadLastDir, uri3, dir3);
+  await setFile(downloadLastDir, uri3, dir3);
   is(downloadLastDir.file.path, dir3.path,
      "downloadLastDir should point to dir3");
-  is((yield getFile(downloadLastDir, uri1)).path, dir1.path,
+  is((await getFile(downloadLastDir, uri1)).path, dir1.path,
      "uri1 should return dir1"); // set in CPS
-  is((yield getFile(downloadLastDir, uri2)).path, dir2.path,
+  is((await getFile(downloadLastDir, uri2)).path, dir2.path,
      "uri2 should return dir2"); // set in CPS
-  is((yield getFile(downloadLastDir, uri3)).path, dir3.path,
+  is((await getFile(downloadLastDir, uri3)).path, dir3.path,
      "uri3 should return dir3"); // set in CPS
-  is((yield getFile(downloadLastDir, uri4)).path, dir3.path,
+  is((await getFile(downloadLastDir, uri4)).path, dir3.path,
      "uri4 should return dir4"); // fallback
 
   // set uri1 to dir2, all except uri3 should now return dir2
-  yield setFile(downloadLastDir, uri1, dir2);
+  await setFile(downloadLastDir, uri1, dir2);
   is(downloadLastDir.file.path, dir2.path,
      "downloadLastDir should point to dir2");
-  is((yield getFile(downloadLastDir, uri1)).path, dir2.path,
+  is((await getFile(downloadLastDir, uri1)).path, dir2.path,
      "uri1 should return dir2"); // set in CPS
-  is((yield getFile(downloadLastDir, uri2)).path, dir2.path,
+  is((await getFile(downloadLastDir, uri2)).path, dir2.path,
      "uri2 should return dir2"); // set in CPS
-  is((yield getFile(downloadLastDir, uri3)).path, dir3.path,
+  is((await getFile(downloadLastDir, uri3)).path, dir3.path,
      "uri3 should return dir3"); // set in CPS
-  is((yield getFile(downloadLastDir, uri4)).path, dir2.path,
+  is((await getFile(downloadLastDir, uri4)).path, dir2.path,
      "uri4 should return dir2"); // fallback
 
-  yield clearHistoryAndWait();
+  await clearHistoryAndWait();
 
   // check clearHistory removes all data
   is(downloadLastDir.file, null, "clearHistory removes all data");
   //is(Services.contentPrefs.hasPref(uri1, "browser.download.lastDir", null),
   //   false, "LastDir preference should be absent");
-  is((yield getFile(downloadLastDir, uri1)), null, "uri1 should point to null");
-  is((yield getFile(downloadLastDir, uri2)), null, "uri2 should point to null");
-  is((yield getFile(downloadLastDir, uri3)), null, "uri3 should point to null");
-  is((yield getFile(downloadLastDir, uri4)), null, "uri4 should point to null");
+  is((await getFile(downloadLastDir, uri1)), null, "uri1 should point to null");
+  is((await getFile(downloadLastDir, uri2)), null, "uri2 should point to null");
+  is((await getFile(downloadLastDir, uri3)), null, "uri3 should point to null");
+  is((await getFile(downloadLastDir, uri4)), null, "uri4 should point to null");
 
-  yield setFile(downloadLastDir, null, tmpDir);
+  await setFile(downloadLastDir, null, tmpDir);
 
   // check data set outside PB mode is remembered
-  is((yield checkDownloadLastDir(pbDownloadLastDir, tmpDir)).path, tmpDir.path, "uri1 should return the expected last directory");
-  is((yield checkDownloadLastDir(downloadLastDir, tmpDir)).path, tmpDir.path, "uri1 should return the expected last directory");
-  yield clearHistoryAndWait();
+  is((await checkDownloadLastDir(pbDownloadLastDir, tmpDir)).path, tmpDir.path, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDir(downloadLastDir, tmpDir)).path, tmpDir.path, "uri1 should return the expected last directory");
+  await clearHistoryAndWait();
 
-  yield setFile(downloadLastDir, uri1, dir1);
+  await setFile(downloadLastDir, uri1, dir1);
 
   // check data set using CPS outside PB mode is remembered
-  is((yield checkDownloadLastDir(pbDownloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
-  is((yield checkDownloadLastDir(downloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
-  yield clearHistoryAndWait();
+  is((await checkDownloadLastDir(pbDownloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDir(downloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
+  await clearHistoryAndWait();
 
   // check data set inside PB mode is forgotten
-  yield setFile(pbDownloadLastDir, null, tmpDir);
+  await setFile(pbDownloadLastDir, null, tmpDir);
 
-  is((yield checkDownloadLastDir(pbDownloadLastDir, tmpDir)).path, tmpDir.path, "uri1 should return the expected last directory");
-  is((yield checkDownloadLastDirNull(downloadLastDir)), null, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDir(pbDownloadLastDir, tmpDir)).path, tmpDir.path, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDirNull(downloadLastDir)), null, "uri1 should return the expected last directory");
 
-  yield clearHistoryAndWait();
+  await clearHistoryAndWait();
 
   // check data set using CPS inside PB mode is forgotten
-  yield setFile(pbDownloadLastDir, uri1, dir1);
+  await setFile(pbDownloadLastDir, uri1, dir1);
 
-  is((yield checkDownloadLastDir(pbDownloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
-  is((yield checkDownloadLastDirNull(downloadLastDir)), null, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDir(pbDownloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDirNull(downloadLastDir)), null, "uri1 should return the expected last directory");
 
   // check data set outside PB mode but changed inside is remembered correctly
-  yield setFile(downloadLastDir, uri1, dir1);
-  yield setFile(pbDownloadLastDir, uri1, dir2);
-  is((yield checkDownloadLastDir(pbDownloadLastDir, dir2)).path, dir2.path, "uri1 should return the expected last directory");
-  is((yield checkDownloadLastDir(downloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
+  await setFile(downloadLastDir, uri1, dir1);
+  await setFile(pbDownloadLastDir, uri1, dir2);
+  is((await checkDownloadLastDir(pbDownloadLastDir, dir2)).path, dir2.path, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDir(downloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
 
   /*
    * ====================
    * Create new PB window
    * ====================
    */
 
   // check that the last dir store got cleared in a new PB window
   pbWin.close();
   // And give it time to close
-  executeSoon(moveAlong);
-  yield;
-  pbWin = yield createWindow({private: true});
+  await new Promise(resolve => executeSoon(resolve));
+
+  pbWin = await createWindow({private: true});
   pbDownloadLastDir = new DownloadLastDir(pbWin);
 
-  is((yield checkDownloadLastDir(pbDownloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDir(pbDownloadLastDir, dir1)).path, dir1.path, "uri1 should return the expected last directory");
 
-  yield clearHistoryAndWait();
+  await clearHistoryAndWait();
 
   // check clearHistory inside PB mode clears data outside PB mode
-  yield setFile(pbDownloadLastDir, uri1, dir2);
+  await setFile(pbDownloadLastDir, uri1, dir2);
 
-  yield clearHistoryAndWait();
+  await clearHistoryAndWait();
 
-  is((yield checkDownloadLastDirNull(downloadLastDir)), null, "uri1 should return the expected last directory");
-  is((yield checkDownloadLastDirNull(pbDownloadLastDir)), null, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDirNull(downloadLastDir)), null, "uri1 should return the expected last directory");
+  is((await checkDownloadLastDirNull(pbDownloadLastDir)), null, "uri1 should return the expected last directory");
 
   // check that disabling CPS works
   Services.prefs.setBoolPref("browser.download.lastDir.savePerSite", false);
 
-  yield setFile(downloadLastDir, uri1, dir1);
+  await setFile(downloadLastDir, uri1, dir1);
   is(downloadLastDir.file.path, dir1.path, "LastDir should be set to dir1");
-  is((yield getFile(downloadLastDir, uri1)).path, dir1.path, "uri1 should return dir1");
-  is((yield getFile(downloadLastDir, uri2)).path, dir1.path, "uri2 should return dir1");
-  is((yield getFile(downloadLastDir, uri3)).path, dir1.path, "uri3 should return dir1");
-  is((yield getFile(downloadLastDir, uri4)).path, dir1.path, "uri4 should return dir1");
+  is((await getFile(downloadLastDir, uri1)).path, dir1.path, "uri1 should return dir1");
+  is((await getFile(downloadLastDir, uri2)).path, dir1.path, "uri2 should return dir1");
+  is((await getFile(downloadLastDir, uri3)).path, dir1.path, "uri3 should return dir1");
+  is((await getFile(downloadLastDir, uri4)).path, dir1.path, "uri4 should return dir1");
 
   downloadLastDir.setFile(uri2, dir2);
   is(downloadLastDir.file.path, dir2.path, "LastDir should be set to dir2");
-  is((yield getFile(downloadLastDir, uri1)).path, dir2.path, "uri1 should return dir2");
-  is((yield getFile(downloadLastDir, uri2)).path, dir2.path, "uri2 should return dir2");
-  is((yield getFile(downloadLastDir, uri3)).path, dir2.path, "uri3 should return dir2");
-  is((yield getFile(downloadLastDir, uri4)).path, dir2.path, "uri4 should return dir2");
+  is((await getFile(downloadLastDir, uri1)).path, dir2.path, "uri1 should return dir2");
+  is((await getFile(downloadLastDir, uri2)).path, dir2.path, "uri2 should return dir2");
+  is((await getFile(downloadLastDir, uri3)).path, dir2.path, "uri3 should return dir2");
+  is((await getFile(downloadLastDir, uri4)).path, dir2.path, "uri4 should return dir2");
 
   Services.prefs.clearUserPref("browser.download.lastDir.savePerSite");
 
   // check that passing null to setFile clears the stored value
-  yield setFile(downloadLastDir, uri3, dir3);
-  is((yield getFile(downloadLastDir, uri3)).path, dir3.path, "LastDir should be set to dir3");
-  yield setFile(downloadLastDir, uri3, null);
-  is((yield getFile(downloadLastDir, uri3)), null, "uri3 should return null");
+  await setFile(downloadLastDir, uri3, dir3);
+  is((await getFile(downloadLastDir, uri3)).path, dir3.path, "LastDir should be set to dir3");
+  await setFile(downloadLastDir, uri3, null);
+  is((await getFile(downloadLastDir, uri3)), null, "uri3 should return null");
 
-  yield clearHistoryAndWait();
+  await clearHistoryAndWait();
+
+  finish();
 }
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js
@@ -12,17 +12,17 @@
 // Step 4: load a page in the tab from step 2 that checks the value of test is value and the total count in private storage is 1
 
 add_task(function* setup() {
   yield SpecialPowers.pushPrefEnv({
     set: [["dom.ipc.processCount", 1]]
   });
 });
 
-add_task(function test() {
+add_task(function* test() {
   let prefix = 'http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent_page.html';
 
   function getElts(browser) {
     return browser.contentTitle.split('|');
   };
 
   // Step 1
   let non_private_browser = gBrowser.selectedBrowser;
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_crh.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_crh.js
@@ -1,16 +1,16 @@
 /* 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/. */
 
 // This test makes sure that the Clear Recent History menu item and command 
 // is disabled inside the private browsing mode.
 
-add_task(function test() {
+add_task(function* test() {
   function checkDisableOption(aPrivateMode, aWindow) {
     let crhCommand = aWindow.document.getElementById("Tools:Sanitize");
     ok(crhCommand, "The clear recent history command should exist");
 
     is(PrivateBrowsingUtils.isWindowPrivate(aWindow), aPrivateMode,
       "PrivateBrowsingUtils should report the correct per-window private browsing status");
     is(crhCommand.hasAttribute("disabled"), aPrivateMode,
       "Clear Recent History command should be disabled according to the private browsing mode");
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_toggle.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_toggle.js
@@ -70,36 +70,34 @@ add_task(function* test_downloads_last_d
  *        expectedDir (nsIFile, expectedDir):
  *          An nsIFile for what we expect the last download directory
  *          should be. The nsIFile is not compared directly - only
  *          paths are compared. If expectedDir is not set, then the
  *          last download directory is expected to be null.
  *
  * @returns Promise
  */
-function testHelper(options) {
-  return new Task.spawn(function() {
-    let win = yield BrowserTestUtils.openNewBrowserWindow(options);
-    let gDownloadLastDir = new DownloadLastDir(win);
+async function testHelper(options) {
+  let win = await BrowserTestUtils.openNewBrowserWindow(options);
+  let gDownloadLastDir = new DownloadLastDir(win);
 
-    if (options.clearHistory) {
-      clearHistory();
-    }
+  if (options.clearHistory) {
+    clearHistory();
+  }
 
-    if (options.setDir) {
-      gDownloadLastDir.file = options.setDir;
-    }
+  if (options.setDir) {
+    gDownloadLastDir.file = options.setDir;
+  }
 
-    let expectedDir = options.expectedDir;
+  let expectedDir = options.expectedDir;
 
-    if (expectedDir) {
-      is(gDownloadLastDir.file.path, expectedDir.path,
-         "gDownloadLastDir should point to the expected last directory");
-      isnot(gDownloadLastDir.file, expectedDir,
-            "gDownloadLastDir.file should not be pointing to the last directory");
-    } else {
-      is(gDownloadLastDir.file, null, "gDownloadLastDir should be null");
-    }
+  if (expectedDir) {
+    is(gDownloadLastDir.file.path, expectedDir.path,
+       "gDownloadLastDir should point to the expected last directory");
+    isnot(gDownloadLastDir.file, expectedDir,
+          "gDownloadLastDir.file should not be pointing to the last directory");
+  } else {
+    is(gDownloadLastDir.file, null, "gDownloadLastDir should be null");
+  }
 
-    gDownloadLastDir.cleanupPrivateFile();
-    yield BrowserTestUtils.closeWindow(win);
-  });
+  gDownloadLastDir.cleanupPrivateFile();
+  await BrowserTestUtils.closeWindow(win);
 }
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage.js
@@ -1,13 +1,13 @@
 /* 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/. */
 
- add_task(function test() {
+ add_task(function* test() {
   requestLongerTimeout(2);
   const page1 = 'http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/' +
                 'browser_privatebrowsing_localStorage_page1.html'
 
   let win = yield BrowserTestUtils.openNewBrowserWindow({private: true});
 
   let tab = win.gBrowser.selectedTab = win.gBrowser.addTab(page1);
   let browser = win.gBrowser.selectedBrowser;
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after.js
@@ -5,17 +5,17 @@
 // Ensure that a storage instance used by both private and public sessions at different times does not
 // allow any data to leak due to cached values.
 
 // Step 1: Load browser_privatebrowsing_localStorage_before_after_page.html in a private tab, causing a storage
 //   item to exist. Close the tab.
 // Step 2: Load the same page in a non-private tab, ensuring that the storage instance reports only one item
 //   existing.
 
-add_task(function test() {
+add_task(function* test() {
   let testURI = "about:blank";
   let prefix = 'http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/';
 
   // Step 1.
   let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true});
   let privateBrowser = privateWin.gBrowser.addTab(
     prefix + 'browser_privatebrowsing_localStorage_before_after_page.html').linkedBrowser;
   yield BrowserTestUtils.browserLoaded(privateBrowser);
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js
@@ -1,16 +1,16 @@
 /* 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/. */
 
 // This test makes sure that the window title changes correctly while switching
 // from and to private browsing mode.
 
-add_task(function test() {
+add_task(function* test() {
   const testPageURL = "http://mochi.test:8888/browser/" +
     "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html";
   requestLongerTimeout(2);
 
   // initialization of expected titles
   let test_title = "Test title";
   let app_name = document.documentElement.getAttribute("title");
   const isOSX = ("nsILocalFileMac" in Ci);
@@ -32,46 +32,46 @@ add_task(function test() {
     page_with_title = test_title + " - " + app_name;
     page_without_title = app_name;
     about_pb_title = "Open a private window?" + " - " + app_name;
     pb_page_with_title = test_title + " - " + app_name + " (Private Browsing)";
     pb_page_without_title = app_name + " (Private Browsing)";
     pb_about_pb_title = "Private Browsing - " + app_name + " (Private Browsing)";
   }
 
-  function* testTabTitle(aWindow, url, insidePB, expected_title) {
-    let tab = (yield BrowserTestUtils.openNewForegroundTab(aWindow.gBrowser));
-    yield BrowserTestUtils.loadURI(tab.linkedBrowser, url);
-    yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  async function testTabTitle(aWindow, url, insidePB, expected_title) {
+    let tab = (await BrowserTestUtils.openNewForegroundTab(aWindow.gBrowser));
+    await BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+    await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
-    yield BrowserTestUtils.waitForCondition(() => {
+    await BrowserTestUtils.waitForCondition(() => {
       return aWindow.document.title === expected_title;
     }, `Window title should be ${expected_title}, got ${aWindow.document.title}`);
 
     is(aWindow.document.title, expected_title, "The window title for " + url +
        " is correct (" + (insidePB ? "inside" : "outside") +
        " private browsing mode)");
 
     let win = aWindow.gBrowser.replaceTabWithWindow(tab);
-    yield BrowserTestUtils.waitForEvent(win, "load", false);
+    await BrowserTestUtils.waitForEvent(win, "load", false);
 
-    yield BrowserTestUtils.waitForCondition(() => {
+    await BrowserTestUtils.waitForCondition(() => {
       return win.document.title === expected_title;
     }, `Window title should be ${expected_title}, got ${aWindow.document.title}`);
 
     is(win.document.title, expected_title, "The window title for " + url +
        " detached tab is correct (" + (insidePB ? "inside" : "outside") +
        " private browsing mode)");
 
-    yield Promise.all([ BrowserTestUtils.closeWindow(win),
+    await Promise.all([ BrowserTestUtils.closeWindow(win),
                         BrowserTestUtils.closeWindow(aWindow) ]);
   }
 
   function openWin(private) {
     return BrowserTestUtils.openNewBrowserWindow({ private });
   }
-  yield Task.spawn(testTabTitle((yield openWin(false)), "about:blank", false, page_without_title));
-  yield Task.spawn(testTabTitle((yield openWin(false)), testPageURL, false, page_with_title));
-  yield Task.spawn(testTabTitle((yield openWin(false)), "about:privatebrowsing", false, about_pb_title));
-  yield Task.spawn(testTabTitle((yield openWin(true)), "about:blank", true, pb_page_without_title));
-  yield Task.spawn(testTabTitle((yield openWin(true)), testPageURL, true, pb_page_with_title));
-  yield Task.spawn(testTabTitle((yield openWin(true)), "about:privatebrowsing", true, pb_about_pb_title));
+  yield testTabTitle((yield openWin(false)), "about:blank", false, page_without_title);
+  yield testTabTitle((yield openWin(false)), testPageURL, false, page_with_title);
+  yield testTabTitle((yield openWin(false)), "about:privatebrowsing", false, about_pb_title);
+  yield testTabTitle((yield openWin(true)), "about:blank", true, pb_page_without_title);
+  yield testTabTitle((yield openWin(true)), testPageURL, true, pb_page_with_title);
+  yield testTabTitle((yield openWin(true)), "about:privatebrowsing", true, pb_about_pb_title);
 });
--- a/browser/components/sessionstore/SessionMigration.jsm
+++ b/browser/components/sessionstore/SessionMigration.jsm
@@ -61,38 +61,38 @@ var SessionMigrationInternal = {
     let url = "about:welcomeback";
     let formdata = {id: {sessionData: state}, url};
     return {windows: [{tabs: [{entries: [{url}], formdata}]}]};
   },
   /**
    * Asynchronously read session restore state (JSON) from a path
    */
   readState: function(aPath) {
-    return Task.spawn(function() {
+    return Task.spawn(function*() {
       let bytes = yield OS.File.read(aPath);
       let text = gDecoder.decode(bytes);
       let state = JSON.parse(text);
-      throw new Task.Result(state);
+      return state;
     });
   },
   /**
    * Asynchronously write session restore state as JSON to a path
    */
   writeState: function(aPath, aState) {
     let bytes = gEncoder.encode(JSON.stringify(aState));
     return OS.File.writeAtomic(aPath, bytes, {tmpPath: aPath + ".tmp"});
   }
 }
 
 var SessionMigration = {
   /**
    * Migrate a limited set of session data from one path to another.
    */
   migrate: function(aFromPath, aToPath) {
-    return Task.spawn(function() {
+    return Task.spawn(function*() {
       let inState = yield SessionMigrationInternal.readState(aFromPath);
       let outState = SessionMigrationInternal.convertState(inState);
       // Unfortunately, we can't use SessionStore's own SessionFile to
       // write out the data because it has a dependency on the profile dir
       // being known. When the migration runs, there is no guarantee that
       // that's true.
       yield SessionMigrationInternal.writeState(aToPath, outState);
     });
--- a/browser/components/sessionstore/SessionStorage.jsm
+++ b/browser/components/sessionstore/SessionStorage.jsm
@@ -117,17 +117,17 @@ var SessionStorageInternal = {
       }
 
       let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager);
       let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
 
       // There is no need to pass documentURI, it's only used to fill documentURI property of
       // domstorage event, which in this case has no consumer. Prevention of events in case
       // of missing documentURI will be solved in a followup bug to bug 600307.
-      let storage = storageManager.createStorage(window, principal, "");
+      let storage = storageManager.createStorage(window, principal, "", aDocShell.usePrivateBrowsing);
 
       for (let key of Object.keys(data)) {
         try {
           storage.setItem(key, data[key]);
         } catch (e) {
           // throws e.g. for URIs that can't have sessionStorage
           console.error(e);
         }
--- a/browser/components/sessionstore/test/browser_393716.js
+++ b/browser/components/sessionstore/test/browser_393716.js
@@ -3,17 +3,17 @@
 
 "use strict";
 
 const URL = "about:config";
 
 /**
  * Bug 393716 - Basic tests for getTabState(), setTabState(), and duplicateTab().
  */
-add_task(function test_set_tabstate() {
+add_task(function* test_set_tabstate() {
   let key = "Unique key: " + Date.now();
   let value = "Unique value: " + Math.random();
 
   // create a new tab
   let tab = gBrowser.addTab(URL);
   ss.setTabValue(tab, key, value);
   yield promiseBrowserLoaded(tab.linkedBrowser);
 
@@ -30,17 +30,17 @@ add_task(function test_set_tabstate() {
      "Got the expected state object (test URL)");
   ok(state.extData && state.extData[key] == value,
      "Got the expected state object (test manually set tab value)");
 
   // clean up
   gBrowser.removeTab(tab);
 });
 
-add_task(function test_set_tabstate_and_duplicate() {
+add_task(function* test_set_tabstate_and_duplicate() {
   let key2 = "key2";
   let value2 = "Value " + Math.random();
   let value3 = "Another value: " + Date.now();
   let state = { entries: [{ url: URL }], extData: { key2: value2 } };
 
   // create a new tab
   let tab = gBrowser.addTab();
   // set the tab's state
--- a/browser/components/sessionstore/test/browser_456342.js
+++ b/browser/components/sessionstore/test/browser_456342.js
@@ -3,17 +3,17 @@
 
 "use strict";
 
 const URL = ROOT + "browser_456342_sample.xhtml";
 
 /**
  * Bug 456342 - Restore values from non-standard input field types.
  */
-add_task(function test_restore_nonstandard_input_values() {
+add_task(function* test_restore_nonstandard_input_values() {
   // Add tab with various non-standard input field types.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Fill in form values.
   let expectedValue = Math.random();
   yield setFormElementValues(browser, {value: expectedValue});
--- a/browser/components/sessionstore/test/browser_463205.js
+++ b/browser/components/sessionstore/test/browser_463205.js
@@ -5,17 +5,17 @@
 
 const URL = ROOT + "browser_463205_sample.html";
 
 /**
  * Bug 463205 - Check URLs before restoring form data to make sure a malicious
  * website can't modify frame URLs and make us inject form data into the wrong
  * web pages.
  */
-add_task(function test_check_urls_before_restoring() {
+add_task(function* test_check_urls_before_restoring() {
   // Add a blank tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Restore form data with a valid URL.
   yield promiseTabState(tab, getState(URL));
 
--- a/browser/components/sessionstore/test/browser_466937.js
+++ b/browser/components/sessionstore/test/browser_466937.js
@@ -3,17 +3,17 @@
 
 "use strict";
 
 const URL = ROOT + "browser_466937_sample.html";
 
 /**
  * Bug 466937 - Prevent file stealing with sessionstore.
  */
-add_task(function test_prevent_file_stealing() {
+add_task(function* test_prevent_file_stealing() {
   // Add a tab with some file input fields.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Generate a path to a 'secret' file.
   let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
   file.append("466937_test.file");
--- a/browser/components/sessionstore/test/browser_467409-backslashplosion.js
+++ b/browser/components/sessionstore/test/browser_467409-backslashplosion.js
@@ -25,17 +25,17 @@ const STATE3 = createEntries(JSON.string
 
 function createEntries(sessionData) {
   return {
     entries: [{url: "about:sessionrestore"}],
     formdata: {id: {sessionData: sessionData}, url: "about:sessionrestore"}
   };
 }
 
-add_task(function test_nested_about_sessionrestore() {
+add_task(function* test_nested_about_sessionrestore() {
   // Prepare a blank tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // test 1
   yield promiseTabState(tab, STATE);
   yield checkState("test1", tab);
--- a/browser/components/sessionstore/test/browser_485482.js
+++ b/browser/components/sessionstore/test/browser_485482.js
@@ -4,17 +4,17 @@
 "use strict";
 
 const URL = ROOT + "browser_485482_sample.html";
 
 /**
  * Bug 485482 - Make sure that we produce valid XPath expressions even for very
  * weird HTML documents.
  */
-add_task(function test_xpath_exp_for_strange_documents() {
+add_task(function* test_xpath_exp_for_strange_documents() {
   // Load a page with weird tag names.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Fill in some values.
   let uniqueValue = Math.random();
   yield setInputValue(browser, {selector: "input[type=text]", value: uniqueValue});
--- a/browser/components/sessionstore/test/browser_528776.js
+++ b/browser/components/sessionstore/test/browser_528776.js
@@ -6,16 +6,16 @@ function browserWindowsCount(expected) {
       ++count;
   }
   is(count, expected,
      "number of open browser windows according to nsIWindowMediator");
   is(JSON.parse(ss.getBrowserState()).windows.length, expected,
      "number of open browser windows according to getBrowserState");
 }
 
-add_task(function() {
+add_task(function*() {
   browserWindowsCount(1);
 
   let win = yield BrowserTestUtils.openNewBrowserWindow();
   browserWindowsCount(2);
   yield BrowserTestUtils.closeWindow(win);
   browserWindowsCount(1);
 });
--- a/browser/components/sessionstore/test/browser_broadcast.js
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -4,33 +4,33 @@
 "use strict";
 
 const INITIAL_VALUE = "browser_broadcast.js-initial-value-" + Date.now();
 
 /**
  * This test ensures we won't lose tab data queued in the content script when
  * closing a tab.
  */
-add_task(function flush_on_tabclose() {
+add_task(function* flush_on_tabclose() {
   let tab = yield createTabWithStorageData(["http://example.com"]);
   let browser = tab.linkedBrowser;
 
   yield modifySessionStorage(browser, {test: "on-tab-close"});
   yield promiseRemoveTab(tab);
 
   let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://example.com"].test, "on-tab-close",
     "sessionStorage data has been flushed on TabClose");
 });
 
 /**
  * This test ensures we won't lose tab data queued in the content script when
  * duplicating a tab.
  */
-add_task(function flush_on_duplicate() {
+add_task(function* flush_on_duplicate() {
   let tab = yield createTabWithStorageData(["http://example.com"]);
   let browser = tab.linkedBrowser;
 
   yield modifySessionStorage(browser, {test: "on-duplicate"});
   let tab2 = ss.duplicateTab(window, tab);
   yield promiseTabRestored(tab2);
 
   yield promiseRemoveTab(tab2);
@@ -40,34 +40,34 @@ add_task(function flush_on_duplicate() {
 
   gBrowser.removeTab(tab);
 });
 
 /**
  * This test ensures we won't lose tab data queued in the content script when
  * a window is closed.
  */
-add_task(function flush_on_windowclose() {
+add_task(function* flush_on_windowclose() {
   let win = yield promiseNewWindow();
   let tab = yield createTabWithStorageData(["http://example.com"], win);
   let browser = tab.linkedBrowser;
 
   yield modifySessionStorage(browser, {test: "on-window-close"});
   yield BrowserTestUtils.closeWindow(win);
 
   let [{tabs: [_, {storage}]}] = JSON.parse(ss.getClosedWindowData());
   is(storage["http://example.com"].test, "on-window-close",
     "sessionStorage data has been flushed when closing a window");
 });
 
 /**
  * This test ensures that stale tab data is ignored when reusing a tab
  * (via e.g. setTabState) and does not overwrite the new data.
  */
-add_task(function flush_on_settabstate() {
+add_task(function* flush_on_settabstate() {
   let tab = yield createTabWithStorageData(["http://example.com"]);
   let browser = tab.linkedBrowser;
 
   // Flush to make sure our tab state is up-to-date.
   yield TabStateFlusher.flush(browser);
 
   let state = ss.getTabState(tab);
   yield modifySessionStorage(browser, {test: "on-set-tab-state"});
@@ -85,17 +85,17 @@ add_task(function flush_on_settabstate()
   gBrowser.removeTab(tab);
 });
 
 /**
  * This test ensures that we won't lose tab data that has been sent
  * asynchronously just before closing a tab. Flushing must re-send all data
  * that hasn't been received by chrome, yet.
  */
-add_task(function flush_on_tabclose_racy() {
+add_task(function* flush_on_tabclose_racy() {
   let tab = yield createTabWithStorageData(["http://example.com"]);
   let browser = tab.linkedBrowser;
 
   // Flush to make sure we start with an empty queue.
   yield TabStateFlusher.flush(browser);
 
   yield modifySessionStorage(browser, {test: "on-tab-close-racy"});
 
@@ -110,22 +110,20 @@ add_task(function flush_on_tabclose_racy
 });
 
 function promiseNewWindow() {
   let deferred = Promise.defer();
   whenNewWindowLoaded({private: false}, deferred.resolve);
   return deferred.promise;
 }
 
-function createTabWithStorageData(urls, win = window) {
-  return Task.spawn(function task() {
-    let tab = win.gBrowser.addTab();
-    let browser = tab.linkedBrowser;
+async function createTabWithStorageData(urls, win = window) {
+  let tab = win.gBrowser.addTab();
+  let browser = tab.linkedBrowser;
 
-    for (let url of urls) {
-      browser.loadURI(url);
-      yield promiseBrowserLoaded(browser);
-      yield modifySessionStorage(browser, {test: INITIAL_VALUE});
-    }
+  for (let url of urls) {
+    browser.loadURI(url);
+    await promiseBrowserLoaded(browser);
+    await modifySessionStorage(browser, {test: INITIAL_VALUE});
+  }
 
-    throw new Task.Result(tab);
-  });
+  return tab;
 }
--- a/browser/components/sessionstore/test/browser_capabilities.js
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -3,17 +3,17 @@
 
 "use strict";
 
 /**
  * These tests ensures that disabling features by flipping nsIDocShell.allow*
  * properties are (re)stored as disabled. Disallowed features must be
  * re-enabled when the tab is re-used by another tab restoration.
  */
-add_task(function docshell_capabilities() {
+add_task(function* docshell_capabilities() {
   let tab = yield createTab();
   let browser = tab.linkedBrowser;
   let docShell = browser.docShell;
 
   // Get the list of capabilities for docShells.
   let flags = Object.keys(docShell).filter(k => k.startsWith("allow"));
 
   // Check that everything is allowed by default for new tabs.
@@ -64,13 +64,14 @@ add_task(function docshell_capabilities(
   ok(disallow.has("Images"), "images not allowed anymore");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
   is(disallow.size, 2, "two capabilities disallowed");
 
   // Clean up after ourselves.
   gBrowser.removeTab(tab);
 });
 
-function createTab() {
+async function createTab() {
   let tab = gBrowser.addTab("about:mozilla");
   let browser = tab.linkedBrowser;
-  return promiseBrowserLoaded(browser).then(() => tab);
+  await promiseBrowserLoaded(browser);
+  return tab;
 }
--- a/browser/components/sessionstore/test/browser_cookies.js
+++ b/browser/components/sessionstore/test/browser_cookies.js
@@ -79,17 +79,17 @@ add_task(function* test_run() {
   });
 });
 
 /**
  * Generic test function to check sessionstore's cookie collection module with
  * different cookie domains given in the Set-Cookie header. See above for some
  * usage examples.
  */
-var testCookieCollection = Task.async(function (params) {
+var testCookieCollection = async function (params) {
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
 
   let urlParams = new URLSearchParams();
   let value = Math.random();
   urlParams.append("value", value);
 
   if (params.domain) {
@@ -97,60 +97,60 @@ var testCookieCollection = Task.async(fu
   }
 
   // Construct request URI.
   let uri = `${params.host}${PATH}browser_cookies.sjs?${urlParams}`;
 
   // Wait for the browser to load and the cookie to be set.
   // These two events can probably happen in no particular order,
   // so let's wait for them in parallel.
-  yield Promise.all([
+  await Promise.all([
     waitForNewCookie(),
     replaceCurrentURI(browser, uri)
   ]);
 
   // Check all URIs for which the cookie should be collected.
   for (let uri of params.cookieURIs || []) {
-    yield replaceCurrentURI(browser, uri);
+    await replaceCurrentURI(browser, uri);
 
     // Check the cookie.
     let cookie = getCookie();
     is(cookie.host, params.cookieHost, "cookie host is correct");
     is(cookie.path, PATH, "cookie path is correct");
     is(cookie.name, "foobar", "cookie name is correct");
     is(cookie.value, value, "cookie value is correct");
   }
 
   // Check all URIs for which the cookie should NOT be collected.
   for (let uri of params.noCookieURIs || []) {
-    yield replaceCurrentURI(browser, uri);
+    await replaceCurrentURI(browser, uri);
 
     // Cookie should be ignored.
     ok(!getCookie(), "no cookie collected");
   }
 
   // Clean up.
   gBrowser.removeTab(tab);
   Services.cookies.removeAll();
-});
+};
 
 /**
  * Replace the current URI of the given browser by loading a new URI. The
  * browser's session history will be completely replaced. This function ensures
  * that the parent process has the lastest shistory data before resolving.
  */
-var replaceCurrentURI = Task.async(function* (browser, uri) {
+var replaceCurrentURI = async function(browser, uri) {
   // Replace the tab's current URI with the parent domain.
   let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
   browser.loadURIWithFlags(uri, flags);
-  yield promiseBrowserLoaded(browser);
+  await promiseBrowserLoaded(browser);
 
   // Ensure the tab's session history is up-to-date.
-  yield TabStateFlusher.flush(browser);
-});
+  await TabStateFlusher.flush(browser);
+};
 
 /**
  * Waits for a new "*example.com" cookie to be added.
  */
 function waitForNewCookie() {
   return new Promise(resolve => {
     Services.obs.addObserver(function observer(subj, topic, data) {
       let cookie = subj.QueryInterface(Ci.nsICookie2);
--- a/browser/components/sessionstore/test/browser_crashedTabs.js
+++ b/browser/components/sessionstore/test/browser_crashedTabs.js
@@ -120,17 +120,17 @@ function promiseTabCrashedReady(browser)
     }, false, true);
   });
 }
 
 /**
  * Checks that if a tab crashes, that information about the tab crashed
  * page does not get added to the tab history.
  */
-add_task(function test_crash_page_not_in_history() {
+add_task(function* test_crash_page_not_in_history() {
   let newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   let browser = newTab.linkedBrowser;
   ok(browser.isRemoteBrowser, "Should be a remote browser");
   yield promiseBrowserLoaded(browser);
 
   browser.loadURI(PAGE_1);
   yield promiseBrowserLoaded(browser);
@@ -149,17 +149,17 @@ add_task(function test_crash_page_not_in
   gBrowser.removeTab(newTab);
 });
 
 /**
  * Checks that if a tab crashes, that when we browse away from that page
  * to a non-blacklisted site (so the browser becomes remote again), that
  * we record history for that new visit.
  */
-add_task(function test_revived_history_from_remote() {
+add_task(function* test_revived_history_from_remote() {
   let newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   let browser = newTab.linkedBrowser;
   ok(browser.isRemoteBrowser, "Should be a remote browser");
   yield promiseBrowserLoaded(browser);
 
   browser.loadURI(PAGE_1);
   yield promiseBrowserLoaded(browser);
@@ -188,17 +188,17 @@ add_task(function test_revived_history_f
   gBrowser.removeTab(newTab);
 });
 
 /**
  * Checks that if a tab crashes, that when we browse away from that page
  * to a blacklisted site (so the browser stays non-remote), that
  * we record history for that new visit.
  */
-add_task(function test_revived_history_from_non_remote() {
+add_task(function* test_revived_history_from_non_remote() {
   let newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   let browser = newTab.linkedBrowser;
   ok(browser.isRemoteBrowser, "Should be a remote browser");
   yield promiseBrowserLoaded(browser);
 
   browser.loadURI(PAGE_1);
   yield promiseBrowserLoaded(browser);
@@ -226,17 +226,17 @@ add_task(function test_revived_history_f
 
   gBrowser.removeTab(newTab);
 });
 
 /**
  * Checks that we can revive a crashed tab back to the page that
  * it was on when it crashed.
  */
-add_task(function test_revive_tab_from_session_store() {
+add_task(function* test_revive_tab_from_session_store() {
   let newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   let browser = newTab.linkedBrowser;
   ok(browser.isRemoteBrowser, "Should be a remote browser");
   yield promiseBrowserLoaded(browser);
 
   browser.loadURI(PAGE_1);
   yield promiseBrowserLoaded(browser);
@@ -279,17 +279,17 @@ add_task(function test_revive_tab_from_s
   gBrowser.removeTab(newTab);
   gBrowser.removeTab(newTab2);
 });
 
 /**
  * Checks that we can revive multiple crashed tabs back to the pages
  * that they were on when they crashed.
  */
-add_task(function test_revive_all_tabs_from_session_store() {
+add_task(function* test_revive_all_tabs_from_session_store() {
   let newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   let browser = newTab.linkedBrowser;
   ok(browser.isRemoteBrowser, "Should be a remote browser");
   yield promiseBrowserLoaded(browser);
 
   browser.loadURI(PAGE_1);
   yield promiseBrowserLoaded(browser);
@@ -339,17 +339,17 @@ add_task(function test_revive_all_tabs_f
 
   yield BrowserTestUtils.closeWindow(win2);
   gBrowser.removeTab(newTab);
 });
 
 /**
  * Checks that about:tabcrashed can close the current tab
  */
-add_task(function test_close_tab_after_crash() {
+add_task(function* test_close_tab_after_crash() {
   let newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
   let browser = newTab.linkedBrowser;
   ok(browser.isRemoteBrowser, "Should be a remote browser");
   yield promiseBrowserLoaded(browser);
 
   browser.loadURI(PAGE_1);
   yield promiseBrowserLoaded(browser);
--- a/browser/components/sessionstore/test/browser_dynamic_frames.js
+++ b/browser/components/sessionstore/test/browser_dynamic_frames.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
  * Ensure that static frames of framesets are serialized but dynamically
  * inserted iframes are ignored.
  */
-add_task(function () {
+add_task(function*() {
   // This URL has the following frames:
   //  + data:text/html,A (static)
   //  + data:text/html,B (static)
   //  + data:text/html,C (dynamic iframe)
   const URL = "data:text/html;charset=utf-8," +
               "<frameset cols=50%25,50%25><frame src='data:text/html,A'>" +
               "<frame src='data:text/html,B'></frameset>" +
               "<script>var i=document.createElement('iframe');" +
@@ -40,17 +40,17 @@ add_task(function () {
   gBrowser.removeTab(tab);
 });
 
 /**
  * Ensure that iframes created by the network parser are serialized but
  * dynamically inserted iframes are ignored. Navigating a subframe should
  * create a second root entry that doesn't contain any dynamic children either.
  */
-add_task(function () {
+add_task(function*() {
   // This URL has the following frames:
   //  + data:text/html,A (static)
   //  + data:text/html,C (dynamic iframe)
   const URL = "data:text/html;charset=utf-8," +
               "<iframe name=t src='data:text/html,A'></iframe>" +
               "<a id=lnk href='data:text/html,B' target=t>clickme</a>" +
               "<script>var i=document.createElement('iframe');" +
               "i.setAttribute('src', 'data:text/html,C');" +
--- a/browser/components/sessionstore/test/browser_form_restore_events.js
+++ b/browser/components/sessionstore/test/browser_form_restore_events.js
@@ -4,17 +4,17 @@
 "use strict";
 
 const URL = ROOT + "browser_form_restore_events_sample.html";
 
 /**
  * Originally a test for Bug 476161, but then expanded to include all input
  * types in bug 640136.
  */
-add_task(function () {
+add_task(function*() {
   // Load a page with some form elements.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // text fields
   yield setInputValue(browser, {id: "modify01", value: Math.random()});
   yield setInputValue(browser, {id: "modify02", value: Date.now()});
--- a/browser/components/sessionstore/test/browser_formdata.js
+++ b/browser/components/sessionstore/test/browser_formdata.js
@@ -4,71 +4,69 @@
 "use strict";
 
 requestLongerTimeout(2);
 
 /**
  * This test ensures that form data collection respects the privacy level as
  * set by the user.
  */
-add_task(function test_formdata() {
+add_task(function* test_formdata() {
   const URL = "http://mochi.test:8888/browser/browser/components/" +
               "sessionstore/test/browser_formdata_sample.html";
 
   const OUTER_VALUE = "browser_formdata_" + Math.random();
   const INNER_VALUE = "browser_formdata_" + Math.random();
 
   // Creates a tab, loads a page with some form fields,
   // modifies their values and closes the tab.
-  function createAndRemoveTab() {
-    return Task.spawn(function () {
-      // Create a new tab.
-      let tab = gBrowser.addTab(URL);
-      let browser = tab.linkedBrowser;
-      yield promiseBrowserLoaded(browser);
+  function* createAndRemoveTab() {
+    // Create a new tab.
+    let tab = gBrowser.addTab(URL);
+    let browser = tab.linkedBrowser;
+    yield promiseBrowserLoaded(browser);
 
-      // Modify form data.
-      yield setInputValue(browser, {id: "txt", value: OUTER_VALUE});
-      yield setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
+    // Modify form data.
+    yield setInputValue(browser, {id: "txt", value: OUTER_VALUE});
+    yield setInputValue(browser, {id: "txt", value: INNER_VALUE, frame: 0});
 
-      // Remove the tab.
-      yield promiseRemoveTab(tab);
-    });
+    // Remove the tab.
+    yield promiseRemoveTab(tab);
   }
 
-  yield createAndRemoveTab();
+  yield* createAndRemoveTab();
   let [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
   is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
   is(formdata.children[0].id.txt, INNER_VALUE, "inner value is correct");
 
   // Disable saving data for encrypted sites.
   Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
 
-  yield createAndRemoveTab();
+  yield* createAndRemoveTab();
   [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
   is(formdata.id.txt, OUTER_VALUE, "outer value is correct");
   ok(!formdata.children, "inner value was *not* stored");
 
   // Disable saving data for any site.
   Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
 
-  yield createAndRemoveTab();
+  yield* createAndRemoveTab();
   [{state: {formdata}}] = JSON.parse(ss.getClosedTabData(window));
   ok(!formdata, "form data has *not* been stored");
 
   // Restore the default privacy level.
   Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
 });
 
 /**
  * This test ensures that a malicious website can't trick us into restoring
  * form data into a wrong website and that we always check the stored URL
  * before doing so.
  */
-add_task(function test_url_check() {
+add_task(function* test_url_check() {
   const URL = "data:text/html;charset=utf-8,<input%20id=input>";
   const VALUE = "value-" + Math.random();
 
   // Create a tab with an iframe containing an input field.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
@@ -93,17 +91,17 @@ add_task(function test_url_check() {
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * This test ensures that collecting form data works as expected when having
  * nested frame sets.
  */
-add_task(function test_nested() {
+add_task(function* test_nested() {
   const URL = "data:text/html;charset=utf-8," +
               "<iframe src='data:text/html;charset=utf-8," +
               "<input autofocus=true>'/>";
 
   const FORM_DATA = {
     children: [{
       xpath: {"/xhtml:html/xhtml:body/xhtml:input": "M"},
       url: "data:text/html;charset=utf-8,<input%20autofocus=true>"
@@ -138,17 +136,17 @@ add_task(function test_nested() {
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * This test ensures that collecting form data for documents with
  * designMode=on works as expected.
  */
-add_task(function test_design_mode() {
+add_task(function* test_design_mode() {
   const URL = "data:text/html;charset=utf-8,<h1>mozilla</h1>" +
               "<script>document.designMode='on'</script>";
 
   // Load a tab with an editable document.
   let tab = gBrowser.selectedTab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
--- a/browser/components/sessionstore/test/browser_formdata_format.js
+++ b/browser/components/sessionstore/test/browser_formdata_format.js
@@ -48,66 +48,57 @@ function test() {
     [ "", "value16" ],
     [ "value18", "" ],
     [ "value20", "value21" ],
     [ "", "value23" ],
     [ "value26", "" ],
     [ "value29", "value30" ],
     [ "", "value33" ]
   ];
-  let testTabCount = 0;
-  let callback = function() {
-    testTabCount--;
-    if (testTabCount == 0) {
-      finish();
-    }
-  };
 
+  let promises = [];
   for (let i = 0; i < formData.length; i++) {
-    testTabCount++;
-    testTabRestoreData(formData[i], expectedValues[i], callback);
+    promises.push(testTabRestoreData(formData[i], expectedValues[i]));
   }
+
+  Promise.all(promises).then(() => finish(), ex => ok(false, ex));
 }
 
-function testTabRestoreData(aFormData, aExpectedValue, aCallback) {
+async function testTabRestoreData(aFormData, aExpectedValue) {
   let URL = ROOT + "browser_formdata_format_sample.html";
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
 
   aFormData.url = URL;
   let tabState = { entries: [{ url: URL }], formdata: aFormData };
 
-  Task.spawn(function () {
-    yield promiseBrowserLoaded(tab.linkedBrowser);
-    yield promiseTabState(tab, tabState);
+  await promiseBrowserLoaded(tab.linkedBrowser);
+  await promiseTabState(tab, tabState);
 
-    yield TabStateFlusher.flush(tab.linkedBrowser);
-    let restoredTabState = JSON.parse(ss.getTabState(tab));
-    let restoredFormData = restoredTabState.formdata;
+  await TabStateFlusher.flush(tab.linkedBrowser);
+  let restoredTabState = JSON.parse(ss.getTabState(tab));
+  let restoredFormData = restoredTabState.formdata;
 
-    if (restoredFormData) {
-      let doc = tab.linkedBrowser.contentDocument;
-      let input1 = doc.getElementById("input1");
-      let input2 = doc.querySelector("input[name=input2]");
+  if (restoredFormData) {
+    let doc = tab.linkedBrowser.contentDocument;
+    let input1 = doc.getElementById("input1");
+    let input2 = doc.querySelector("input[name=input2]");
 
-      // test format
-      ok("id" in restoredFormData || "xpath" in restoredFormData,
-        "FormData format is valid: " + restoredFormData);
-      // validate that there are no old keys
-      for (let key of Object.keys(restoredFormData)) {
-        if (["id", "xpath", "url"].indexOf(key) === -1) {
-          ok(false, "FormData format is invalid.");
-        }
+    // test format
+    ok("id" in restoredFormData || "xpath" in restoredFormData,
+       "FormData format is valid: " + restoredFormData);
+    // validate that there are no old keys
+    for (let key of Object.keys(restoredFormData)) {
+      if (["id", "xpath", "url"].indexOf(key) === -1) {
+        ok(false, "FormData format is invalid.");
       }
-      // test id
-      is(input1.value, aExpectedValue[0],
-        "FormData by 'id' has been restored correctly");
-      // test xpath
-      is(input2.value, aExpectedValue[1],
-        "FormData by 'xpath' has been restored correctly");
     }
-
-    // clean up
-    gBrowser.removeTab(tab);
+    // test id
+    is(input1.value, aExpectedValue[0],
+       "FormData by 'id' has been restored correctly");
+    // test xpath
+    is(input2.value, aExpectedValue[1],
+       "FormData by 'xpath' has been restored correctly");
+  }
 
-  // This test might time out if the task fails.
-  }).then(aCallback);
+  // clean up
+  gBrowser.removeTab(tab);
 }
--- a/browser/components/sessionstore/test/browser_formdata_xpath.js
+++ b/browser/components/sessionstore/test/browser_formdata_xpath.js
@@ -33,17 +33,17 @@ const FIELDS = {
   "//select[@multiple]":        [1, 3],
   "//textarea[1]":              "",
   "//textarea[2]":              "Some text... " + Math.random(),
   "//textarea[3]":              "Some more text\n" + new Date(),
   "//input[@type='file'][1]":   [FILE1],
   "//input[@type='file'][2]":   [FILE1, FILE2]
 };
 
-add_task(function test_form_data_restoration() {
+add_task(function* test_form_data_restoration() {
   // Load page with some input fields.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Fill in some values.
   for (let xpath of Object.keys(FIELDS)) {
     yield setFormValue(browser, xpath);
--- a/browser/components/sessionstore/test/browser_frame_history.js
+++ b/browser/components/sessionstore/test/browser_frame_history.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  Ensure that frameset history works properly when restoring a tab,
  provided that the frameset is static.
  */
 
 // Loading a toplevel frameset
-add_task(function() {
+add_task(function*() {
   let testURL = getRootDirectory(gTestPath) + "browser_frame_history_index.html";
   let tab = gBrowser.addTab(testURL);
   gBrowser.selectedTab = tab;
 
   info("Opening a page with three frames, 4 loads should take place");
   yield waitForLoadsInBrowser(tab.linkedBrowser, 4);
 
   let browser_b = tab.linkedBrowser.contentDocument.getElementsByTagName("frame")[1];
@@ -46,17 +46,17 @@ add_task(function() {
     is(frames[i].contentDocument.location,
        getRootDirectory(gTestPath) + "browser_frame_history_" + expectedURLEnds[i],
        "frame " + i + " has the right url");
   }
   gBrowser.removeTab(newTab);
 });
 
 // Loading the frameset inside an iframe
-add_task(function() {
+add_task(function*() {
   let testURL = getRootDirectory(gTestPath) + "browser_frame_history_index2.html";
   let tab = gBrowser.addTab(testURL);
   gBrowser.selectedTab = tab;
 
   info("iframe: Opening a page with an iframe containing three frames, 5 loads should take place");
   yield waitForLoadsInBrowser(tab.linkedBrowser, 5);
 
   let browser_b = tab.linkedBrowser.contentDocument.
@@ -93,17 +93,17 @@ add_task(function() {
     is(frames[i].contentDocument.location,
        getRootDirectory(gTestPath) + "browser_frame_history_" + expectedURLEnds[i],
        "frame " + i + " has the right url");
   }
   gBrowser.removeTab(newTab);
 });
 
 // Now, test that we don't record history if the iframe is added dynamically
-add_task(function() {
+add_task(function*() {
   // Start with an empty history
     let blankState = JSON.stringify({
       windows: [{
         tabs: [{ entries: [{ url: "about:blank" }] }],
         _closedTabs: []
       }],
       _closedWindows: []
     });
--- a/browser/components/sessionstore/test/browser_frametree.js
+++ b/browser/components/sessionstore/test/browser_frametree.js
@@ -8,17 +8,17 @@ const URL_FRAMESET = HTTPROOT + "browser
 
 /**
  * This ensures that loading a page normally, aborting a page load, reloading
  * a page, navigating using the bfcache, and ignoring frames that were
  * created dynamically work as expect. We expect the frame tree to be reset
  * when a page starts loading and we also expect a valid frame tree to exist
  * when it has stopped loading.
  */
-add_task(function test_frametree() {
+add_task(function* test_frametree() {
   const FRAME_TREE_SINGLE = { href: URL };
   const FRAME_TREE_FRAMESET = {
     href: URL_FRAMESET,
     children: [{href: URL}, {href: URL}, {href: URL}]
   };
 
   // Create a tab with a single frame.
   let tab = gBrowser.addTab(URL);
@@ -60,17 +60,17 @@ add_task(function test_frametree() {
   gBrowser.removeTab(tab);
 });
 
 /**
  * This test ensures that we ignore frames that were created dynamically at or
  * after the load event. SessionStore can't handle these and will not restore
  * or collect any data for them.
  */
-add_task(function test_frametree_dynamic() {
+add_task(function* test_frametree_dynamic() {
   // The frame tree as expected. The first two frames are static
   // and the third one was created on DOMContentLoaded.
   const FRAME_TREE = {
     href: URL_FRAMESET,
     children: [{href: URL}, {href: URL}, {href: URL}]
   };
   const FRAME_TREE_REMOVED = {
     href: URL_FRAMESET,
--- a/browser/components/sessionstore/test/browser_history_persist.js
+++ b/browser/components/sessionstore/test/browser_history_persist.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 /**
  * Ensure that history entries that should not be persisted are restored in the
  * same state.
  */
-add_task(function check_history_not_persisted() {
+add_task(function* check_history_not_persisted() {
   // Create an about:blank tab
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Retrieve the tab state.
   yield TabStateFlusher.flush(browser);
   let state = JSON.parse(ss.getTabState(tab));
@@ -39,17 +39,17 @@ add_task(function check_history_not_pers
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * Check that entries default to being persisted when the attribute doesn't
  * exist
  */
-add_task(function check_history_default_persisted() {
+add_task(function* check_history_default_persisted() {
   // Create an about:blank tab
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Retrieve the tab state.
   yield TabStateFlusher.flush(browser);
   let state = JSON.parse(ss.getTabState(tab));
--- a/browser/components/sessionstore/test/browser_label_and_icon.js
+++ b/browser/components/sessionstore/test/browser_label_and_icon.js
@@ -15,17 +15,17 @@ add_task(function setup() {
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
   });
 });
 
 /**
  * Ensure that a pending tab has label and icon correctly set.
  */
-add_task(function test_label_and_icon() {
+add_task(function* test_label_and_icon() {
   // Create a new tab.
   let tab = gBrowser.addTab("about:robots");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Retrieve the tab state.
   yield TabStateFlusher.flush(browser);
   let state = ss.getTabState(tab);
--- a/browser/components/sessionstore/test/browser_merge_closed_tabs.js
+++ b/browser/components/sessionstore/test/browser_merge_closed_tabs.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * This test ensures that closed tabs are merged when restoring
  * a window state without overwriting tabs.
  */
-add_task(function () {
+add_task(function* () {
   const initialState = {
     windows: [{
       tabs: [
         { entries: [{ url: "about:blank" }] }
       ],
       _closedTabs: [
         { state: { entries: [{ ID: 1000, url: "about:blank" }]} },
         { state: { entries: [{ ID: 1001, url: "about:blank" }]} }
--- a/browser/components/sessionstore/test/browser_pageStyle.js
+++ b/browser/components/sessionstore/test/browser_pageStyle.js
@@ -4,17 +4,17 @@
 "use strict";
 
 const URL = getRootDirectory(gTestPath) + "browser_pageStyle_sample.html";
 const URL_NESTED = getRootDirectory(gTestPath) + "browser_pageStyle_sample_nested.html";
 
 /**
  * This test ensures that page style information is correctly persisted.
  */
-add_task(function page_style() {
+add_task(function* page_style() {
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
   let sheets = yield getStyleSheets(browser);
 
   // Enable all style sheets one by one.
   for (let [title, disabled] of sheets) {
     yield enableStyleSheetsForSet(browser, title);
@@ -48,17 +48,17 @@ add_task(function page_style() {
   gBrowser.removeTab(tab);
   gBrowser.removeTab(tab2);
 });
 
 /**
  * This test ensures that page style notification from nested documents are
  * received and the page style is persisted correctly.
  */
-add_task(function nested_page_style() {
+add_task(function* nested_page_style() {
   let tab = gBrowser.addTab(URL_NESTED);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   yield enableSubDocumentStyleSheetsForSet(browser, "alternate");
   yield promiseRemoveTab(tab);
 
   let [{state: {pageStyle}}] = JSON.parse(ss.getClosedTabData(window));
--- a/browser/components/sessionstore/test/browser_privatetabs.js
+++ b/browser/components/sessionstore/test/browser_privatetabs.js
@@ -3,17 +3,17 @@
 
 add_task(function cleanup() {
   info("Forgetting closed tabs");
   while (ss.getClosedTabCount(window)) {
     ss.forgetClosedTab(window, 0);
   }
 });
 
-add_task(function() {
+add_task(function*() {
   let URL_PUBLIC = "http://example.com/public/" + Math.random();
   let URL_PRIVATE = "http://example.com/private/" + Math.random();
   let tab1, tab2;
   try {
     // Setup a public tab and a private tab
     info("Setting up public tab");
     tab1 = gBrowser.addTab(URL_PUBLIC);
     yield promiseBrowserLoaded(tab1.linkedBrowser);
@@ -54,17 +54,17 @@ add_task(function() {
       gBrowser.removeTab(tab1);
     }
     if (tab2) {
       gBrowser.removeTab(tab2);
     }
   }
 });
 
-add_task(function () {
+add_task(function* () {
   const FRAME_SCRIPT = "data:," +
     "docShell.QueryInterface%28Components.interfaces.nsILoadContext%29.usePrivateBrowsing%3Dtrue";
 
   // Clear the list of closed windows.
   forgetClosedWindows();
 
   // Create a new window to attach our frame script to.
   let win = yield promiseNewWindowLoaded();
@@ -96,17 +96,17 @@ add_task(function () {
   ok(state.isPrivate, "tab considered private");
 
   // Check that all private tabs are removed when the non-private
   // window is closed and we don't save windows without any tabs.
   yield BrowserTestUtils.closeWindow(win);
   is(ss.getClosedWindowCount(), 0, "no windows to restore");
 });
 
-add_task(function () {
+add_task(function* () {
   // Clear the list of closed windows.
   forgetClosedWindows();
 
   // Create a new window to attach our frame script to.
   let win = yield promiseNewWindowLoaded({private: true});
 
   // Create a new tab in the new window that will load the frame script.
   let tab = win.gBrowser.addTab("about:mozilla");
--- a/browser/components/sessionstore/test/browser_restore_redirect.js
+++ b/browser/components/sessionstore/test/browser_restore_redirect.js
@@ -1,17 +1,17 @@
 "use strict";
 
 const BASE = "http://example.com/browser/browser/components/sessionstore/test/";
 const TARGET = BASE + "restore_redirect_target.html";
 
 /**
  * Ensure that a http redirect leaves a working tab.
  */
-add_task(function check_http_redirect() {
+add_task(function* check_http_redirect() {
   let state = {
     entries: [{ url: BASE + "restore_redirect_http.html" }]
   };
 
   // Open a new tab to restore into.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseTabState(tab, state);
@@ -27,17 +27,17 @@ add_task(function check_http_redirect() 
 
   // Cleanup.
   yield promiseRemoveTab(tab);
 });
 
 /**
  * Ensure that a js redirect leaves a working tab.
  */
-add_task(function check_js_redirect() {
+add_task(function* check_js_redirect() {
   let state = {
     entries: [{ url: BASE + "restore_redirect_js.html" }]
   };
 
   let loadPromise = new Promise(resolve => {
     function listener(msg) {
       if (msg.data.url.endsWith("restore_redirect_target.html")) {
         window.messageManager.removeMessageListener("ss-test:loadEvent", listener);
--- a/browser/components/sessionstore/test/browser_scrollPositions.js
+++ b/browser/components/sessionstore/test/browser_scrollPositions.js
@@ -17,17 +17,17 @@ const SCROLL2_Y = Math.round(400 * (1 + 
 const SCROLL2_STR = SCROLL2_X + "," + SCROLL2_Y;
 
 requestLongerTimeout(2);
 
 /**
  * This test ensures that we properly serialize and restore scroll positions
  * for an average page without any frames.
  */
-add_task(function test_scroll() {
+add_task(function* test_scroll() {
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Scroll down a little.
   yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL_X, y: SCROLL_Y});
   yield checkScroll(tab, {scroll: SCROLL_STR}, "scroll is fine");
 
@@ -60,17 +60,17 @@ add_task(function test_scroll() {
   yield promiseRemoveTab(tab);
   yield promiseRemoveTab(tab2);
 });
 
 /**
  * This tests ensures that we properly serialize and restore scroll positions
  * for multiple frames of pages with framesets.
  */
-add_task(function test_scroll_nested() {
+add_task(function* test_scroll_nested() {
   let tab = gBrowser.addTab(URL_FRAMESET);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Scroll the first child frame down a little.
   yield sendMessage(browser, "ss-test:setScrollPosition", {x: SCROLL_X, y: SCROLL_Y, frame: 0});
   yield checkScroll(tab, {children: [{scroll: SCROLL_STR}]}, "scroll is fine");
 
@@ -104,17 +104,17 @@ add_task(function test_scroll_nested() {
   yield promiseRemoveTab(tab);
   yield promiseRemoveTab(tab2);
 });
 
 /**
  * Test that scroll positions persist after restoring background tabs in
  * a restored window (bug 1228518).
  */
-add_task(function test_scroll_background_tabs() {
+add_task(function* test_scroll_background_tabs() {
   pushPrefs(["browser.sessionstore.restore_on_demand", true]);
 
   let newWin = yield BrowserTestUtils.openNewBrowserWindow();
   let tab = newWin.gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield BrowserTestUtils.browserLoaded(browser);
 
   // Scroll down a little.
--- a/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js
+++ b/browser/components/sessionstore/test/browser_scrollPositionsReaderMode.js
@@ -12,17 +12,17 @@ const SCROLL_READER_MODE_Y = Math.round(
 const SCROLL_READER_MODE_STR = "0," + SCROLL_READER_MODE_Y;
 
 requestLongerTimeout(2);
 
 /**
  * Test that scroll positions of about reader page after restoring background
  * tabs in a restored window (bug 1153393).
  */
-add_task(function test_scroll_background_about_reader_tabs() {
+add_task(function* test_scroll_background_about_reader_tabs() {
   pushPrefs(["browser.sessionstore.restore_on_demand", true]);
 
   let newWin = yield BrowserTestUtils.openNewBrowserWindow();
   let tab = newWin.gBrowser.addTab(READER_MODE_URL);
   let browser = tab.linkedBrowser;
   yield Promise.all([
     BrowserTestUtils.browserLoaded(browser),
     BrowserTestUtils.waitForContentEvent(browser, "AboutReaderContentReady")
--- a/browser/components/sessionstore/test/browser_sessionHistory.js
+++ b/browser/components/sessionstore/test/browser_sessionHistory.js
@@ -3,17 +3,17 @@
 
 "use strict";
 
 requestLongerTimeout(2);
 
 /**
  * Ensure that starting a load invalidates shistory.
  */
-add_task(function test_load_start() {
+add_task(function* test_load_start() {
   // Create a new tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Load a new URI.
   yield BrowserTestUtils.loadURI(browser, "about:mozilla");
 
@@ -31,17 +31,17 @@ add_task(function test_load_start() {
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * Ensure that anchor navigation invalidates shistory.
  */
-add_task(function test_hashchange() {
+add_task(function* test_hashchange() {
   const URL = "data:text/html;charset=utf-8,<a id=a href=%23>clickme</a>";
 
   // Create a new tab.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Check that we start with a single shistory entry.
@@ -60,17 +60,17 @@ add_task(function test_hashchange() {
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * Ensure that loading pages from the bfcache invalidates shistory.
  */
-add_task(function test_pageshow() {
+add_task(function* test_pageshow() {
   const URL = "data:text/html;charset=utf-8,<h1>first</h1>";
   const URL2 = "data:text/html;charset=utf-8,<h1>second</h1>";
 
   // Create a new tab.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
@@ -90,17 +90,17 @@ add_task(function test_pageshow() {
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * Ensure that subframe navigation invalidates shistory.
  */
-add_task(function test_subframes() {
+add_task(function* test_subframes() {
   const URL = "data:text/html;charset=utf-8," +
               "<iframe src=http%3A//example.com/ name=t></iframe>" +
               "<a id=a1 href=http%3A//example.com/1 target=t>clickme</a>" +
               "<a id=a2 href=http%3A//example.com/%23 target=t>clickme</a>";
 
   // Create a new tab.
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
@@ -138,17 +138,17 @@ add_task(function test_subframes() {
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * Ensure that navigating from an about page invalidates shistory.
  */
-add_task(function test_about_page_navigate() {
+add_task(function* test_about_page_navigate() {
   // Create a new tab.
   let tab = gBrowser.addTab("about:blank");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Check that we have a single shistory entry.
   yield TabStateFlusher.flush(browser);
   let {entries} = JSON.parse(ss.getTabState(tab));
@@ -166,17 +166,17 @@ add_task(function test_about_page_naviga
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * Ensure that history.pushState and history.replaceState invalidate shistory.
  */
-add_task(function test_pushstate_replacestate() {
+add_task(function* test_pushstate_replacestate() {
   // Create a new tab.
   let tab = gBrowser.addTab("http://example.com/1");
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Check that we have a single shistory entry.
   yield TabStateFlusher.flush(browser);
   let {entries} = JSON.parse(ss.getTabState(tab));
@@ -205,17 +205,17 @@ add_task(function test_pushstate_replace
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
 
 /**
  * Ensure that slow loading subframes will invalidate shistory.
  */
-add_task(function test_slow_subframe_load() {
+add_task(function* test_slow_subframe_load() {
   const SLOW_URL = "http://mochi.test:8888/browser/browser/components/" +
                    "sessionstore/test/browser_sessionHistory_slow.sjs";
 
   const URL = "data:text/html;charset=utf-8," +
               "<frameset cols=50%25,50%25>" +
               "<frame src='" + SLOW_URL + "'>" +
               "</frameset>";
 
--- a/browser/components/sessionstore/test/browser_sessionStorage.js
+++ b/browser/components/sessionstore/test/browser_sessionStorage.js
@@ -10,17 +10,17 @@ const URL = "http://mochi.test:8888/brow
 
 const OUTER_VALUE = "outer-value-" + RAND;
 const INNER_VALUE = "inner-value-" + RAND;
 
 /**
  * This test ensures that setting, modifying and restoring sessionStorage data
  * works as expected.
  */
-add_task(function session_storage() {
+add_task(function* session_storage() {
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Flush to make sure chrome received all data.
   yield TabStateFlusher.flush(browser);
 
   let {storage} = JSON.parse(ss.getTabState(tab));
@@ -97,17 +97,17 @@ add_task(function session_storage() {
   yield promiseRemoveTab(tab);
   yield promiseRemoveTab(tab2);
 });
 
 /**
  * This test ensures that purging domain data also purges data from the
  * sessionStorage data collected for tabs.
  */
-add_task(function purge_domain() {
+add_task(function* purge_domain() {
   let tab = gBrowser.addTab(URL);
   let browser = tab.linkedBrowser;
   yield promiseBrowserLoaded(browser);
 
   // Purge data for "mochi.test".
   yield purgeDomainData(browser, "mochi.test");
 
   // Flush to make sure chrome received all data.
@@ -121,17 +121,17 @@ add_task(function purge_domain() {
 
   yield promiseRemoveTab(tab);
 });
 
 /**
  * This test ensures that collecting sessionStorage data respects the privacy
  * levels as set by the user.
  */
-add_task(function respect_privacy_level() {
+add_task(function* respect_privacy_level() {
   let tab = gBrowser.addTab(URL + "&secure");
   yield promiseBrowserLoaded(tab.linkedBrowser);
   yield promiseRemoveTab(tab);
 
   let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
   is(storage["http://mochi.test:8888"].test, OUTER_VALUE,
     "http sessionStorage data has been saved");
   is(storage["https://example.com"].test, INNER_VALUE,
--- a/browser/config/mozconfigs/linux64/stylo
+++ b/browser/config/mozconfigs/linux64/stylo
@@ -1,3 +1,5 @@
 . "$topsrcdir/browser/config/mozconfigs/linux64/nightly"
 
+export LLVM_CONFIG="${TOOLTOOL_DIR}/clang/bin/llvm-config"
+
 ac_add_options --enable-stylo
--- a/browser/config/mozconfigs/linux64/stylo-debug
+++ b/browser/config/mozconfigs/linux64/stylo-debug
@@ -1,3 +1,5 @@
 . "$topsrcdir/browser/config/mozconfigs/linux64/debug"
 
+export LLVM_CONFIG="${TOOLTOOL_DIR}/clang/bin/llvm-config"
+
 ac_add_options --enable-stylo
--- a/browser/config/tooltool-manifests/linux64/releng.manifest
+++ b/browser/config/tooltool-manifests/linux64/releng.manifest
@@ -26,10 +26,18 @@
 {
 "version": "sccache rev b21198a7183a2fe226ff49348b1c0b51bae9f4f8",
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "sccache2.tar.xz",
 "unpack": true,
 "digest": "b89c40dbf28c2bd54fadf017c15a8789f6e7611252a623cc3a1507e3dd6fc9e5a50d746e81776ba856e33fdc99b4a6413ba7c3ac0aed5f4835705da2b758ef22",
 "size": 1020700
+},
+{
+"version": "clang + llvm 3.9.0, built from SVN r290136",
+"size": 151724092,
+"digest": "4ab5ff2131e4ce4888d38c17feb192c19bc6ede83abef55af7d2f29e2446f6335dc860377fa25cbb0283b3958c0a3d377a3cfdc7705a85d4843e3ab357ddca7f",
+"algorithm": "sha512",
+"filename": "clang.tar.xz",
+"unpack": true
 }
 ]
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -22,8 +22,14 @@ if not CONFIG['RELEASE_OR_BETA']:
         'presentation',
     ]
 
 # Only include mortar system add-ons if we locally enable it
 if CONFIG['MOZ_MORTAR']:
     DIRS += [
         'mortar',
     ]
+
+# Nightly-only system add-ons
+if CONFIG['NIGHTLY_BUILD']:
+    DIRS += [
+        'webcompat-reporter',
+    ]
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.6.454
+Current extension version is: 1.6.467
--- a/browser/extensions/pdfjs/content/PdfJs.jsm
+++ b/browser/extensions/pdfjs/content/PdfJs.jsm
@@ -192,21 +192,21 @@ var PdfJs = {
       this._ensureRegistered();
     } else {
       this._ensureUnregistered();
     }
   },
 
   uninit: function uninit() {
     if (this._initialized) {
-      Services.prefs.removeObserver(PREF_DISABLED, this, false);
-      Services.prefs.removeObserver(PREF_DISABLED_PLUGIN_TYPES, this, false);
-      Services.obs.removeObserver(this, TOPIC_PDFJS_HANDLER_CHANGED, false);
-      Services.obs.removeObserver(this, TOPIC_PLUGINS_LIST_UPDATED, false);
-      Services.obs.removeObserver(this, TOPIC_PLUGIN_INFO_UPDATED, false);
+      Services.prefs.removeObserver(PREF_DISABLED, this);
+      Services.prefs.removeObserver(PREF_DISABLED_PLUGIN_TYPES, this);
+      Services.obs.removeObserver(this, TOPIC_PDFJS_HANDLER_CHANGED);
+      Services.obs.removeObserver(this, TOPIC_PLUGINS_LIST_UPDATED);
+      Services.obs.removeObserver(this, TOPIC_PLUGIN_INFO_UPDATED);
       this._initialized = false;
     }
     this._ensureUnregistered();
   },
 
   _migrate: function migrate() {
     const VERSION = 2;
     var currentVersion = getIntPref(PREF_MIGRATION_VERSION, 0);
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
+++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
@@ -166,17 +166,17 @@ PdfDataListener.prototype = {
       newBuffer.set(buffer);
       newBuffer.set(chunk, buffer.length);
       this.buffer = newBuffer;
     }
     this.loaded += chunk.length;
     if (this.length >= 0 && this.length < this.loaded) {
       this.length = -1; // reset the length, server is giving incorrect one
     }
-    this.onprogress(this.loaded, this.length >= 0 ? this.length : void(0));
+    this.onprogress(this.loaded, this.length >= 0 ? this.length : void 0);
   },
   readData: function PdfDataListener_readData() {
     var result = this.buffer;
     this.buffer = null;
     return result;
   },
   finish: function PdfDataListener_finish() {
     this.isDataReady = true;
--- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
@@ -105,17 +105,17 @@ var PdfjsChromeUtils = {
                                        this);
 
       this._mmg.removeMessageListener('PDFJS:Parent:displayWarning', this);
 
       this._mmg.removeMessageListener('PDFJS:Parent:addEventListener', this);
       this._mmg.removeMessageListener('PDFJS:Parent:removeEventListener', this);
       this._mmg.removeMessageListener('PDFJS:Parent:updateControlState', this);
 
-      Services.obs.removeObserver(this, 'quit-application', false);
+      Services.obs.removeObserver(this, 'quit-application');
 
       this._mmg = null;
       this._ppmm = null;
     }
   },
 
   /*
    * Called by the main module when preference changes are picked up
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -18,18 +18,18 @@
   define('pdfjs-dist/build/pdf', ['exports'], factory);
  } else if (typeof exports !== 'undefined') {
   factory(exports);
  } else {
   factory(root['pdfjsDistBuildPdf'] = {});
  }
 }(this, function (exports) {
  'use strict';
- var pdfjsVersion = '1.6.454';
- var pdfjsBuild = 'b8cd1433';
+ var pdfjsVersion = '1.6.467';
+ var pdfjsBuild = '54d55e8b';
  var pdfjsFilePath = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : null;
  var pdfjsLibs = {};
  (function pdfjsWrapper() {
   (function (root, factory) {
    factory(root.pdfjsSharedUtil = {});
   }(this, function (exports) {
    var globalScope = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : this;
    var FONT_IDENTITY_MATRIX = [
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -18,18 +18,18 @@
   define('pdfjs-dist/build/pdf.worker', ['exports'], factory);
  } else if (typeof exports !== 'undefined') {
   factory(exports);
  } else {
   factory(root['pdfjsDistBuildPdfWorker'] = {});
  }
 }(this, function (exports) {
  'use strict';
- var pdfjsVersion = '1.6.454';
- var pdfjsBuild = 'b8cd1433';
+ var pdfjsVersion = '1.6.467';
+ var pdfjsBuild = '54d55e8b';
  var pdfjsFilePath = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : null;
  var pdfjsLibs = {};
  (function pdfjsWrapper() {
   (function (root, factory) {
    factory(root.pdfjsCoreArithmeticDecoder = {});
   }(this, function (exports) {
    var ArithmeticDecoder = function ArithmeticDecoderClosure() {
     var QeTable = [
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/bootstrap.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const WEBCOMPATREPORTER_JSM = "chrome://webcompat-reporter/content/WebCompatReporter.jsm";
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebCompatReporter",
+  WEBCOMPATREPORTER_JSM);
+
+const PREF_WC_REPORTER_ENABLED = "extensions.webcompat-reporter.enabled";
+
+let prefObserver = function(aSubject, aTopic, aData) {
+  let enabled = Services.prefs.getBoolPref(PREF_WC_REPORTER_ENABLED);
+  if (enabled) {
+    WebCompatReporter.init();
+  } else {
+    WebCompatReporter.uninit();
+  }
+};
+
+function startup(aData, aReason) {
+  // Observe pref changes and enable/disable as necessary.
+  Services.prefs.addObserver(PREF_WC_REPORTER_ENABLED, prefObserver, false);
+
+  // Only initialize if pref is enabled.
+  let enabled = Services.prefs.getBoolPref(PREF_WC_REPORTER_ENABLED);
+  if (enabled) {
+    WebCompatReporter.init();
+  }
+}
+
+function shutdown(aData, aReason) {
+  if (aReason === APP_SHUTDOWN) {
+    return;
+  }
+
+  Cu.unload(WEBCOMPATREPORTER_JSM);
+}
+
+function install(aData, aReason) {}
+function uninstall(aData, aReason) {}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/TabListener.jsm
@@ -0,0 +1,63 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["TabListener"];
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+ "resource:///modules/CustomizableUI.jsm");
+
+const WIDGET_ID = "webcompat-reporter-button";
+
+// Class that watches for url/location/tab changes and enables or disables
+// the Report Site Issue button accordingly
+class TabListener {
+  constructor(win) {
+    this.win = win;
+    this.browser = win.gBrowser;
+    this.addListeners();
+  }
+
+  addListeners() {
+    this.browser.addTabsProgressListener(this);
+    this.browser.tabContainer.addEventListener("TabSelect", this);
+  }
+
+  removeListeners() {
+    this.browser.removeTabsProgressListener(this);
+    this.browser.tabContainer.removeEventListener("TabSelect", this);
+  }
+
+  handleEvent(e) {
+    switch (e.type) {
+      case "TabSelect":
+        this.setButtonState(e.target.linkedBrowser.currentURI.scheme);
+        break;
+    }
+  }
+
+  onLocationChange(browser, webProgress, request, uri, flags) {
+    this.setButtonState(uri.scheme);
+  }
+
+  static isReportableScheme(scheme) {
+    return ["http", "https"].some((prefix) => scheme.startsWith(prefix));
+  }
+
+  setButtonState(scheme) {
+    // Bail early if the button is in the palette.
+    if (!CustomizableUI.getPlacementOfWidget(WIDGET_ID)) {
+      return;
+    }
+
+    if (TabListener.isReportableScheme(scheme)) {
+      CustomizableUI.getWidget(WIDGET_ID).forWindow(this.win).node.removeAttribute("disabled");
+    } else {
+      CustomizableUI.getWidget(WIDGET_ID).forWindow(this.win).node.setAttribute("disabled", true);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/WebCompatReporter.jsm
@@ -0,0 +1,163 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["WebCompatReporter"];
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+  "resource:///modules/CustomizableUI.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "wcStrings", function() {
+  return Services.strings.createBundle(
+    "chrome://webcompat-reporter/locale/webcompat.properties");
+});
+
+XPCOMUtils.defineLazyGetter(this, "wcStyleURI", function() {
+  return Services.io.newURI("chrome://webcompat-reporter/skin/lightbulb.css");
+});
+
+const WIDGET_ID = "webcompat-reporter-button";
+const TABLISTENER_JSM = "chrome://webcompat-reporter/content/TabListener.jsm";
+
+let WebCompatReporter = {
+  get endpoint() {
+    return Services.urlFormatter.formatURLPref(
+      "extensions.webcompat-reporter.newIssueEndpoint");
+  },
+
+  init() {
+    Cu.import(TABLISTENER_JSM);
+
+    let styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
+      .getService(Ci.nsIStyleSheetService);
+    this._sheetType = styleSheetService.AUTHOR_SHEET;
+    this._cachedSheet = styleSheetService.preloadSheet(wcStyleURI,
+                                                       this._sheetType);
+
+    CustomizableUI.createWidget({
+      id: WIDGET_ID,
+      label: wcStrings.GetStringFromName("wc-reporter.label"),
+      tooltiptext: wcStrings.GetStringFromName("wc-reporter.tooltip"),
+      defaultArea: CustomizableUI.AREA_PANEL,
+      disabled: true,
+      onCommand: (e) => this.reportIssue(e.target.ownerDocument),
+    });
+
+    for (let win of CustomizableUI.windows) {
+      this.onWindowOpened(win);
+    }
+
+    CustomizableUI.addListener(this);
+  },
+
+  onWindowOpened(win) {
+    // Attach stylesheet for the button icon.
+    win.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindowUtils)
+      .addSheet(this._cachedSheet, this._sheetType);
+    // Attach listeners to new window.
+    win._webcompatReporterTabListener = new TabListener(win);
+  },
+
+  onWindowClosed(win) {
+    if (win._webcompatReporterTabListener) {
+      win._webcompatReporterTabListener.removeListeners();
+      delete win._webcompatReporterTabListener;
+    }
+  },
+
+  uninit() {
+    CustomizableUI.destroyWidget(WIDGET_ID);
+
+    for (let win of CustomizableUI.windows) {
+      this.onWindowClosed(win);
+
+      win.QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIDOMWindowUtils)
+        .removeSheet(wcStyleURI, this._sheetType);
+    }
+
+    CustomizableUI.removeListener(this);
+    Cu.unload(TABLISTENER_JSM);
+  },
+
+  // This method injects a framescript that should send back a screenshot blob
+  // of the top-level window of the currently selected tab, resolved as a
+  // Promise.
+  getScreenshot(gBrowser) {
+    const FRAMESCRIPT = "chrome://webcompat-reporter/content/tab-frame.js";
+    const TABDATA_MESSAGE = "WebCompat:SendTabData";
+
+    return new Promise((resolve) => {
+      let mm = gBrowser.selectedBrowser.messageManager;
+      mm.loadFrameScript(FRAMESCRIPT, false);
+
+      mm.addMessageListener(TABDATA_MESSAGE, function receiveFn(message) {
+        mm.removeMessageListener(TABDATA_MESSAGE, receiveFn);
+        resolve([gBrowser, message.json]);
+      });
+    });
+  },
+
+  // This should work like so:
+  // 1) set up listeners for a new webcompat.com tab, and open it, passing
+  //    along the current URI
+  // 2) if we successfully got a screenshot from getScreenshot,
+  //    inject a frame script that will postMessage it to webcompat.com
+  //    so it can show a preview to the user and include it in FormData
+  // Note: openWebCompatTab arguments are passed in as an array because they
+  // are the result of a promise resolution.
+  openWebCompatTab([gBrowser, tabData]) {
+    const SCREENSHOT_MESSAGE = "WebCompat:SendScreenshot";
+    const FRAMESCRIPT = "chrome://webcompat-reporter/content/wc-frame.js";
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    const WEBCOMPAT_ORIGIN = new win.URL(WebCompatReporter.endpoint).origin;
+
+    let tab = gBrowser.loadOneTab(
+      `${WebCompatReporter.endpoint}?url=${encodeURIComponent(tabData.url)}&src=desktop-reporter`,
+      {inBackground: false});
+
+    // If we successfully got a screenshot blob, add a listener to know when
+    // the new tab is loaded before sending it over.
+    if (tabData && tabData.blob) {
+      let browser = gBrowser.getBrowserForTab(tab);
+      let loadedListener = {
+        QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener",
+          "nsISupportsWeakReference"]),
+        onStateChange(webProgress, request, flags, status) {
+          let isStopped = flags & Ci.nsIWebProgressListener.STATE_STOP;
+          let isNetwork = flags & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+          if (isStopped && isNetwork && webProgress.isTopLevel) {
+            let location;
+            try {
+              location = request.QueryInterface(Ci.nsIChannel).URI;
+            } catch (ex) {}
+
+            if (location && location.prePath === WEBCOMPAT_ORIGIN) {
+              let mm = gBrowser.selectedBrowser.messageManager;
+              mm.loadFrameScript(FRAMESCRIPT, false);
+              mm.sendAsyncMessage(SCREENSHOT_MESSAGE, {
+                screenshot: tabData.blob,
+                origin: WEBCOMPAT_ORIGIN
+              });
+
+              browser.removeProgressListener(this);
+            }
+          }
+        }
+      };
+
+      browser.addProgressListener(loadedListener);
+    }
+  },
+
+  reportIssue(xulDoc) {
+    this.getScreenshot(xulDoc.defaultView.gBrowser).then(this.openWebCompatTab)
+                                                   .catch(Cu.reportError);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/tab-frame.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+let { utils: Cu } = Components;
+
+const TABDATA_MESSAGE = "WebCompat:SendTabData";
+
+let getScreenshot = function(win) {
+  return new Promise(resolve => {
+    let url = win.location.href;
+    try {
+      let dpr = win.devicePixelRatio;
+      let canvas = win.document.createElement("canvas");
+      let ctx = canvas.getContext("2d");
+      let x = win.document.documentElement.scrollLeft;
+      let y = win.document.documentElement.scrollTop;
+      let w = win.innerWidth;
+      let h = win.innerHeight;
+      canvas.width = dpr * w;
+      canvas.height = dpr * h;
+      ctx.scale(dpr, dpr);
+      ctx.drawWindow(win, x, y, w, h, "#fff");
+      canvas.toBlob(blob => {
+        resolve({url, blob});
+      });
+    } catch (ex) {
+      // CanvasRenderingContext2D.drawWindow can fail depending on memory or
+      // surface size. Rather than reject, resolve the URL so the user can
+      // file an issue without a screenshot.
+      Cu.reportError(`WebCompatReporter: getting a screenshot failed: ${ex}`);
+      resolve({url});
+    }
+  });
+};
+
+getScreenshot(content).then(data => sendAsyncMessage(TABDATA_MESSAGE, data));
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/content/wc-frame.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+let { utils: Cu } = Components;
+
+const SCREENSHOT_MESSAGE = "WebCompat:SendScreenshot";
+
+addMessageListener(SCREENSHOT_MESSAGE, function handleMessage(message) {
+  removeMessageListener(SCREENSHOT_MESSAGE, handleMessage);
+  // postMessage the screenshot blob from a content Sandbox so message event.origin
+  // is what we expect on the client-side (i.e., https://webcompat.com)
+  try {
+    let sb = new Cu.Sandbox(content.document.nodePrincipal);
+    sb.win = content;
+    sb.screenshotBlob = Cu.cloneInto(message.data.screenshot, content);
+    sb.wcOrigin = Cu.cloneInto(message.data.origin, content);
+    Cu.evalInSandbox("win.postMessage(screenshotBlob, wcOrigin);", sb);
+    Cu.nukeSandbox(sb);
+  } catch (ex) {
+    Cu.reportError(`WebCompatReporter: sending a screenshot failed: ${ex}`);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/install.rdf.in
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>webcompat-reporter@mozilla.org</em:id>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+    <em:name>WebCompat Reporter</em:name>
+    <em:description>Report site compatibility issues on webcompat.com.</em:description>
+
+    <em:version>1.0.0</em:version>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/jar.mn
@@ -0,0 +1,9 @@
+# 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/.
+
+[features/webcompat-reporter@mozilla.org] chrome.jar:
+% content webcompat-reporter %content/
+% skin webcompat-reporter classic/1.0 %skin/
+  content/  (content/*)
+  skin/  (skin/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/locale/en-US/webcompat.properties
@@ -0,0 +1,2 @@
+wc-reporter.label=Report Site Issue
+wc-reporter.tooltip=Report a site compatibility issue
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/locale/jar.mn
@@ -0,0 +1,8 @@
+#filter substitution
+# 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/.
+
+[features/webcompat-reporter@mozilla.org] @AB_CD@.jar:
+% locale webcompat-reporter @AB_CD@ %locale/@AB_CD@/
+  locale/@AB_CD@/                    (en-US/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/locale/moz.build
@@ -0,0 +1,7 @@
+# -*- 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']
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/moz.build
@@ -0,0 +1,22 @@
+# -*- 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/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+DIRS += ['locale']
+
+FINAL_TARGET_FILES.features['webcompat-reporter@mozilla.org'] += [
+  'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['webcompat-reporter@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/skin/lightbulb.css
@@ -0,0 +1,6 @@
+/* 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/. */
+#webcompat-reporter-button {
+  list-style-image: url("chrome://webcompat-reporter/skin/lightbulb.svg");
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/skin/lightbulb.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 85 128">
+  <path d="M85.23,34.52v-7L43.34,35.21v6.94Zm0-16L43.34,26.28v6.94L85.23,25.5Zm-2.86-1.47v-.26A15,15,0,0,0,81.58,12h0a1.23,1.23,0,0,1-.17-.52,3.62,3.62,0,0,0-.35-.87.3.3,0,0,0-.09-.17h0A18.33,18.33,0,0,0,64.15,0C55.91,0,49,5,46.72,12h25.5L45.94,16.82l-2.6.43v7l4.08-.78Z" transform="translate(-23.46)" fill-opacity="0.6"/>
+  <path d="M28.65,92.41c1.7,18,14.52,30.59,35.83,30.59s34.13-12.61,35.83-30.59c1.33-18.21-4.72-23.88-12.75-35.61a36.8,36.8,0,0,1-5.23-11.57s-24.25.29-24.33,0H46.71A36.8,36.8,0,0,1,41.47,56.8C33.36,68.52,27.32,74.2,28.65,92.41Z" transform="translate(-23.46)" fill="none" stroke="#000" stroke-opacity="0.6" stroke-width="10"/>
+  <path d="M82.11,86.19c-.17-.26-.35-.43-.61-2L70.83,51.58c-1.13.17-.69.17-1.13.17h-.78c-.52.17-.35.78-.17,1.3L78.81,83.67a11.18,11.18,0,0,0-8-1,7.69,7.69,0,0,0-1.47.69l-4.25-1.3a8.07,8.07,0,0,0-4.94,1.65,11.54,11.54,0,0,0-2.6-1,10.73,10.73,0,0,0-7.46.52L60,53.14a3.83,3.83,0,0,0,.26-1.39c-2.25-.09-2.25.09-2.17.69L47.24,85.58a1.2,1.2,0,0,0-.26.35,1,1,0,1,0,1.56,1.21c2-2.52,4.86-3.3,8.5-2.34a5.55,5.55,0,0,1,1.65.61,5.19,5.19,0,0,0-.87,3.12c.09,2.25.87,3.82,2.08,4.42a2.16,2.16,0,0,0,2.17-.09c1-.69,1.56-2.34,1.39-4.42a5.24,5.24,0,0,0-1.65-3.38,6.35,6.35,0,0,1,6-.17,6.65,6.65,0,0,0-1.13,3.56c-.09,2,.52,3.56,1.65,4.25a2.48,2.48,0,0,0,2.34.09c1.13-.69,1.82-2.25,1.73-4.25A5.78,5.78,0,0,0,71,84.88c.17-.09.26-.09.43-.17,3-1,7.46.78,9.11,2.78a1,1,0,0,0,1.39.17A1.09,1.09,0,0,0,82.11,86.19ZM60.86,91.13h-.09a3.58,3.58,0,0,1-1-2.69,4,4,0,0,1,.52-2,3,3,0,0,1,1,2.08C61.55,90.09,61.2,91,60.86,91.13ZM69.62,91h-.09a.27.27,0,0,1-.17-.09c-.26-.17-.78-.87-.69-2.43A5.31,5.31,0,0,1,69.36,86a3.51,3.51,0,0,1,1,2.34C70.31,90.09,69.88,90.78,69.62,91Z" transform="translate(-23.46)" fill="none" stroke="#000" stroke-opacity="0.6" stroke-width="5"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "../../../../../testing/mochitest/browser.eslintrc.js"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+  head.js
+  test.html
+  webcompat.html
+
+[browser_disabled_cleanup.js]
+[browser_button_state.js]
+[browser_report_site_issue.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
@@ -0,0 +1,28 @@
+const REPORTABLE_PAGE = "http://example.com/";
+const REPORTABLE_PAGE2 = "https://example.com/";
+const NONREPORTABLE_PAGE = "about:blank";
+
+/* Test that the Report Site Issue button is enabled for http and https tabs,
+   on page load, or TabSelect, and disabled for everything else. */
+add_task(function* test_button_state_disabled() {
+  let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE);
+  yield PanelUI.show();
+  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
+
+  let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, NONREPORTABLE_PAGE);
+  is(isButtonDisabled(), true, "Check that button is disabled for non-reportable schemes on tab load");
+
+  let tab3 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE2);
+  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
+
+
+  yield BrowserTestUtils.switchTab(gBrowser, tab2);
+  is(isButtonDisabled(), true, "Check that button is disabled for non-reportable schemes on TabSelect");
+
+  yield BrowserTestUtils.switchTab(gBrowser, tab1);
+  is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on TabSelect");
+
+  yield BrowserTestUtils.removeTab(tab1);
+  yield BrowserTestUtils.removeTab(tab2);
+  yield BrowserTestUtils.removeTab(tab3);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_disabled_cleanup.js
@@ -0,0 +1,9 @@
+// Test the addon is cleaning up after itself when disabled.
+add_task(function* test_disabled() {
+  yield SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENABLED, false]]});
+
+  yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function() {
+    is(typeof window._webCompatReporterTabListener, "undefined", "TabListener expando does not exist.");
+    is(document.getElementById("webcompat-reporter-button"), null, "Report Site Issue button does not exist.");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
@@ -0,0 +1,32 @@
+/* Test that clicking on the Report Site Issue button opens a new tab
+   and sends a postMessaged blob at it.
+   testing/profiles/prefs_general.js sets the value for
+   "extensions.webcompat-reporter.newIssueEndpoint" */
+add_task(function* test_screenshot() {
+  yield SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENDPOINT, NEW_ISSUE_PAGE]]});
+
+  let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+  yield PanelUI.show();
+
+  let webcompatButton = document.getElementById("webcompat-reporter-button");
+  ok(webcompatButton, "Report Site Issue button exists.");
+
+  let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+  webcompatButton.click();
+  let tab2 = yield newTabPromise;
+
+  yield BrowserTestUtils.waitForContentEvent(tab2.linkedBrowser, "ScreenshotReceived", false, null, true);
+
+  yield ContentTask.spawn(tab2.linkedBrowser, {TEST_PAGE}, function(args) {
+    let doc = content.document;
+    let urlParam = doc.getElementById("url").innerText;
+    let preview = doc.getElementById("screenshot-preview");
+    is(urlParam, args.TEST_PAGE, "Reported page is correctly added to the url param");
+
+    is(preview.innerText, "Pass", "A Blob object was successfully transferred to the test page.")
+    ok(preview.style.backgroundImage.startsWith("url(\"data:image/png;base64,iVBOR"), "A green screenshot was successfully postMessaged");
+  });
+
+  yield BrowserTestUtils.removeTab(tab2);
+  yield BrowserTestUtils.removeTab(tab1);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/head.js
@@ -0,0 +1,10 @@
+const PREF_WC_REPORTER_ENABLED = "extensions.webcompat-reporter.enabled";
+const PREF_WC_REPORTER_ENDPOINT = "extensions.webcompat-reporter.newIssueEndpoint";
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+const TEST_PAGE = TEST_ROOT + "test.html";
+const NEW_ISSUE_PAGE = TEST_ROOT + "webcompat.html";
+
+function isButtonDisabled() {
+  return document.getElementById("webcompat-reporter-button").disabled;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/test.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+  body {background: rgb(0, 128, 0);}
+</style>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat-reporter/test/browser/webcompat.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+ #screenshot-preview {width: 200px; height: 200px;}
+</style>
+<div id="url"></div>
+<div id="screenshot-preview">Fail</div>
+<script>
+let params = new URL(location.href).searchParams;
+let preview = document.getElementById("screenshot-preview");
+let url = document.getElementById("url");
+url.innerText = params.get("url");
+
+function getBlobAsDataURL(blob) {
+  return new Promise((resolve, reject) => {
+    let reader = new FileReader();
+
+    reader.addEventListener("error", (e) => {
+      reject(`There was an error reading the blob: ${e.type}`);
+    });
+
+    reader.addEventListener("load", (e) => {
+      resolve(e.target.result);
+    });
+
+    reader.readAsDataURL(blob);
+  });
+}
+
+function setPreviewBG(backgroundData) {
+  return new Promise((resolve) => {
+    preview.style.background = `url(${backgroundData})`;
+    resolve();
+  });
+}
+
+function sendReceivedEvent() {
+  window.dispatchEvent(new CustomEvent("ScreenshotReceived", {bubbles:true}));
+}
+
+window.addEventListener("message", function(event) {
+  if (event.data instanceof Blob) {
+    preview.innerText = "Pass";
+  }
+
+  getBlobAsDataURL(event.data).then(setPreviewBG).then(sendReceivedEvent);
+});
+</script>
\ No newline at end of file
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -439,16 +439,18 @@
 @RESPATH@/components/nsLivemarkService.js
 @RESPATH@/components/nsTaggingService.js
 @RESPATH@/components/UnifiedComplete.js
 @RESPATH@/components/nsPlacesExpiration.js
 @RESPATH@/components/PageIconProtocolHandler.js
 @RESPATH@/components/PlacesCategoriesStarter.js
 @RESPATH@/components/ColorAnalyzer.js
 @RESPATH@/components/PageThumbsProtocol.js
+@RESPATH@/components/mozProtocolHandler.js
+@RESPATH@/components/mozProtocolHandler.manifest
 @RESPATH@/components/nsDefaultCLH.manifest
 @RESPATH@/components/nsDefaultCLH.js
 @RESPATH@/components/nsContentPrefService.manifest
 @RESPATH@/components/nsContentPrefService.js
 @RESPATH@/components/nsContentDispatchChooser.manifest
 @RESPATH@/components/nsContentDispatchChooser.js
 @RESPATH@/components/nsHandlerService.manifest
 @RESPATH@/components/nsHandlerService.js
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -101,16 +101,19 @@ libs-%:
 ifndef RELEASE_OR_BETA
 	@$(MAKE) -C ../extensions/presentation/locale AB_CD=$* XPI_NAME=locale-$*
 endif
 	@$(MAKE) -C ../../intl/locales AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) -C ../../devtools/client/locales AB_CD=$* XPI_NAME=locale-$* XPI_ROOT_APPID='$(XPI_ROOT_APPID)'
 	@$(MAKE) -B searchplugins AB_CD=$* XPI_NAME=locale-$*
 	@$(MAKE) libs AB_CD=$* XPI_NAME=locale-$* PREF_DIR=$(PREF_DIR)
 	@$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY)/locales AB_CD=$* XPI_NAME=locale-$*
+ifdef NIGHTLY_BUILD
+	@$(MAKE) -C ../extensions/webcompat-reporter/locale AB_CD=$* XPI_NAME=locale-$*
+endif
 
 repackage-win32-installer: WIN32_INSTALLER_OUT=$(ABS_DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe
 repackage-win32-installer: $(call ESCAPE_WILDCARD,$(WIN32_INSTALLER_IN)) $(SUBMAKEFILES) libs-$(AB_CD)
 	@echo 'Repackaging $(WIN32_INSTALLER_IN) into $(WIN32_INSTALLER_OUT).'
 	$(MAKE) -C $(DEPTH)/$(MOZ_BRANDING_DIRECTORY) export
 	$(MAKE) -C ../installer/windows CONFIG_DIR=l10ngen l10ngen/setup.exe l10ngen/7zSD.sfx
 	$(MAKE) repackage-zip \
 	  AB_CD=$(AB_CD) \
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -28,16 +28,117 @@ xpinstallPromptAllowButton=Allow
 # Be sure you do not choose an accesskey that is used elsewhere in the active context (e.g. main menu bar, submenu of the warning popup button)
 # See http://www.mozilla.org/access/keyboard/accesskey for details
 xpinstallPromptAllowButton.accesskey=A
 xpinstallDisabledMessageLocked=Software installation has been disabled by your system administrator.
 xpinstallDisabledMessage=Software installation is currently disabled. Click Enable and try again.
 xpinstallDisabledButton=Enable
 xpinstallDisabledButton.accesskey=n
 
+# LOCALIZATION NOTE (webextPerms.header)
+# This string is used as a header in the webextension permissions dialog,
+# %S is replaced with the localized name of the extension being installed.
+# See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612
+# for an example of the full dialog.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+webextPerms.header=Add %S?
+
+# LOCALIZATION NOTE (webextPerms.listIntro)
+# This string will be followed by a list of permissions requested
+# by the webextension.
+webextPerms.listIntro=It requires your permission to:
+webextPerms.add.label=Add
+webextPerms.add.accessKey=A
+webextPerms.cancel.label=Cancel
+webextPerms.cancel.accessKey=C
+
+# LOCALIZATION NOTE (webextPerms.sideloadMenuItem)
+# %1$S will be replaced with the localized name of the sideloaded add-on.
+# %2$S will be replace with the name of the application (e.g., Firefox, Nightly)
+webextPerms.sideloadMenuItem=%1$S added to %2$S
+
+# LOCALIZATION NOTE (webextPerms.sideloadHeader)
+# This string is used as a header in the webextension permissions dialog
+# when the extension is side-loaded.
+# %S is replaced with the localized name of the extension being installed.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+webextPerms.sideloadHeader=%S added
+webextPerms.sideloadText=Another program on your computer installed an add-on that may affect your browser. Please review this add-on’s permissions requests and choose to Enable or Disable.
+
+webextPerms.sideloadEnable.label=Enable
+webextPerms.sideloadEnable.accessKey=E
+webextPerms.sideloadDisable.label=Disable
+webextPerms.sideloadDisable.accessKey=D
+
+# LOCALIZATION NOTE (webextPerms.updateMenuItem)
+# %S will be replaced with the localized name of the extension which
+# has been updated.
+webextPerms.updateMenuItem=%S requires new permissions
+
+# LOCALIZATION NOTE (webextPerms.updateText)
+# %S is replaced with the localized name of the updated extension.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+webextPerms.updateText=%S has been updated. You must approve new permissions before the updated version will install. Choosing “Cancel” will maintain your current add-on version.
+
+webextPerms.updateAccept.label=Update
+webextPerms.updateAccept.accessKey=U
+
+webextPerms.description.bookmarks=Read and modify bookmarks
+webextPerms.description.downloads=Download files and read and modify the browser’s download history
+webextPerms.description.history=Access browsing history
+# LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
+# %S will be replaced with the name of the application
+webextPerms.description.nativeMessaging=Exchange messages with programs other than %S
+webextPerms.description.notifications=Display notifications to you
+webextPerms.description.sessions=Access browser recently closed tabs
+webextPerms.description.tabs=Access browser tabs
+webextPerms.description.topSites=Access browsing history
+webextPerms.description.webNavigation=Access browser activity during navigation
+
+webextPerms.hostDescription.allUrls=Access your data for all websites
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.wildcard)
+# %S will be replaced by the DNS domain for which a webextension
+# is requesting access (e.g., mozilla.org)
+webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.tooManyWildcards):
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 will be replaced by an integer indicating the number of additional
+# domains for which this webextension is requesting permission.
+webextPerms.hostDescription.tooManyWildcards=Access your data in #1 other domain;Access your data in #1 other domains
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.oneSite)
+# %S will be replaced by the DNS host name for which a webextension
+# is requesting access (e.g., www.mozilla.org)
+webextPerms.hostDescription.oneSite=Access your data for %S
+
+# LOCALIZATION NOTE (webextPerms.hostDescription.tooManySites)
+# Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 will be replaced by an integer indicating the number of additional
+# hosts for which this webextension is requesting permission.
+webextPerms.hostDescription.tooManySites=Access your data on #1 other site;Access your data on #1 other sites
+
+# LOCALIZATION NOTE (addonPostInstall.message)
+# %1$S is replaced with the localized named of the extension that was
+# just installed.
+# %2$S is replaced with the localized name of the application.
+addonPostInstall.message1=%1$S has been added to %2$S.
+
+# LOCALIZATION NOTE (addonPostInstall.message2)
+# %1$S is replaced with the localized name of the extension.
+# %2$S is replaced with the icon for the add-ons menu.
+# %3$S is replaced with the icon for the toolbar menu.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+addonPostInstall.message2=Manage %1$S by clicking %2$S in the %3$S menu.
+addonPostInstall.okay.label=OK
+addonPostInstall.okay.key=O
+
 # LOCALIZATION NOTE (addonDownloadingAndVerifying):
 # Semicolon-separated list of plural forms. See:
 # http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # Also see https://bugzilla.mozilla.org/show_bug.cgi?id=570012 for mockups
 addonDownloadingAndVerifying=Downloading and verifying add-on…;Downloading and verifying #1 add-ons…
 addonDownloadVerifying=Verifying
 
 addonInstall.unsigned=(Unverified)
@@ -709,16 +810,18 @@ userContext.aboutPage.label = Manage con
 userContext.aboutPage.accesskey = O
 
 userContextOpenLink.label = Open Link in New %S Tab
 
 muteTab.label = Mute Tab
 muteTab.accesskey = M
 unmuteTab.label = Unmute Tab
 unmuteTab.accesskey = M
+playTab.label = Play Tab
+playTab.accesskey = P
 
 # LOCALIZATION NOTE (weakCryptoOverriding.message): %S is brandShortName
 weakCryptoOverriding.message = %S recommends that you don’t enter your password, credit card and other personal information on this website.
 revokeOverride.label = Don’t Trust This Website
 revokeOverride.accesskey = D
 
 # LOCALIZATION NOTE (certErrorDetails*.label): These are text strings that
 # appear in the about:certerror page, so that the user can copy and send them to
--- a/browser/locales/en-US/chrome/overrides/netError.dtd
+++ b/browser/locales/en-US/chrome/overrides/netError.dtd
@@ -192,19 +192,21 @@ was trying to connect. -->
 <!ENTITY weakCryptoUsed.title "Your connection is not secure">
 <!-- LOCALIZATION NOTE (weakCryptoUsed.longDesc2) - Do not translate
      "SSL_ERROR_NO_CYPHER_OVERLAP". -->
 <!ENTITY weakCryptoUsed.longDesc2 "Advanced info: SSL_ERROR_NO_CYPHER_OVERLAP">
 <!ENTITY weakCryptoAdvanced.title "Advanced">
 <!ENTITY weakCryptoAdvanced.longDesc "<span class='hostname'></span> uses security technology that is outdated and vulnerable to attack. An attacker could easily reveal information which you thought to be safe.">
 <!ENTITY weakCryptoAdvanced.override "(Not secure) Try loading <span class='hostname'></span> using outdated security">
 
-<!-- LOCALIZATION NOTE (certerror.wrongSystemTime) - The <span id='..' /> tags will be injected with actual values,
-     please leave them unchanged. -->
-<!ENTITY certerror.wrongSystemTime "<p>A secure connection to <span id='wrongSystemTime_URL'/> isn’t possible because your clock appears to show the wrong time.</p> <p>Your computer thinks it is <span id='wrongSystemTime_systemDate'/>, when it should be <span id='wrongSystemTime_actualDate'/>. To fix this problem, change your date and time settings to match the correct time.</p>">
+<!-- LOCALIZATION NOTE (certerror.wrongSystemTime2,
+                        certerror.wrongSystemTimeWithoutReference) - The <span id='..' />
+     tags will be injected with actual values, please leave them unchanged. -->
+<!ENTITY certerror.wrongSystemTime2 "<p> &brandShortName; did not connect to <span id='wrongSystemTime_URL'/> because your computer’s clock appears to show the wrong time and this is preventing a secure connection.</p> <p>Your computer is set to <span id='wrongSystemTime_systemDate'/>, when it should be <span id='wrongSystemTime_actualDate'/>. To fix this problem, change your date and time settings to match the correct time.</p>">
+<!ENTITY certerror.wrongSystemTimeWithoutReference "<p>&brandShortName; did not connect to <span id='wrongSystemTimeWithoutReference_URL'/> because your computer’s clock appears to show the wrong time and this is preventing a secure connection.</p> <p>Your computer is set to <span id='wrongSystemTimeWithoutReference_systemDate'/>. To fix this problem, change your date and time settings to match the correct time.</p>">
 
 <!ENTITY certerror.pagetitle1  "Insecure Connection">
 <!ENTITY certerror.whatShouldIDo.badStsCertExplanation "This site uses HTTP
 Strict Transport Security (HSTS) to specify that &brandShortName; may only connect
 to it securely. As a result, it is not possible to add an exception for this
 certificate.">
 <!ENTITY certerror.copyToClipboard.label "Copy text to clipboard">
 
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -6,16 +6,18 @@
 
 this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"];
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+  "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
   "resource://gre/modules/UITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
   "resource:///modules/UITour.jsm");
@@ -74,16 +76,22 @@ XPCOMUtils.defineLazyGetter(this, "DEFAU
   let showCharacterEncoding = Services.prefs.getComplexValue(
     "browser.menu.showCharacterEncoding",
     Ci.nsIPrefLocalizedString
   ).data;
   if (showCharacterEncoding == "true") {
     result["PanelUI-contents"].push("characterencoding-button");
   }
 
+  if (AppConstants.NIGHTLY_BUILD) {
+    if (Services.prefs.getBoolPref("extensions.webcompat-reporter.enabled")) {
+      result["PanelUI-contents"].push("webcompat-reporter-button");
+    }
+  }
+
   return result;
 });
 
 XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() {
   return Object.keys(DEFAULT_AREA_PLACEMENTS);
 });
 
 XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() {
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -7,35 +7,38 @@ const {classes: Cc, interfaces: Ci, resu
 
 this.EXPORTED_SYMBOLS = ["ExtensionsUI"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://devtools/shared/event-emitter.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+                                  "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
                                       "extensions.webextPermissionPrompts", false);
 
-const DEFAULT_EXENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+const DEFAULT_EXTENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 this.ExtensionsUI = {
   sideloaded: new Set(),
   updates: new Set(),
 
   init() {
     Services.obs.addObserver(this, "webextension-permission-prompt", false);
     Services.obs.addObserver(this, "webextension-update-permissions", false);
+    Services.obs.addObserver(this, "webextension-install-notify", false);
 
     this._checkForSideloaded();
   },
 
   _checkForSideloaded() {
     AddonManager.getAllAddons(addons => {
       // Check for any side-loaded addons that the user is allowed
       // to enable.
@@ -134,139 +137,156 @@ this.ExtensionsUI = {
         reply(true);
       } else {
         info.permissions = perms;
         this.showPermissionsPrompt(target, info).then(reply);
       }
     } else if (topic == "webextension-update-permissions") {
       this.updates.add(subject.wrappedJSObject);
       this.emit("change");
+    } else if (topic == "webextension-install-notify") {
+      let {target, addon} = subject.wrappedJSObject;
+      this.showInstallNotification(target, addon);
     }
   },
 
   showPermissionsPrompt(target, info) {
     let perms = info.permissions;
     if (!perms) {
       return Promise.resolve();
     }
 
     let win = target.ownerGlobal;
 
     let name = info.addon.name;
     if (name.length > 50) {
       name = name.slice(0, 49) + "…";
     }
-
-    // The strings below are placeholders, they will switch over to the
-    // bundle.get*String() calls as part of bug 1316996.
+    name = name.replace(/&/g, "&amp;")
+               .replace(/</g, "&lt;")
+               .replace(/>/g, "&gt;");
 
-    // let bundle = win.gNavigatorBundle;
-    // let header = bundle.getFormattedString("webextPerms.header", [name])
-    // let listHeader = bundle.getString("webextPerms.listHeader");
-    let header = "Add ADDON?".replace("ADDON", name);
+    let addonLabel = `<label class="addon-webext-name">${name}</label>`;
+    let bundle = win.gNavigatorBundle;
+
+    let header = bundle.getFormattedString("webextPerms.header", [addonLabel]);
     let text = "";
-    let listHeader = "It can:";
+    let listIntro = bundle.getString("webextPerms.listIntro");
 
-    // let acceptText = bundle.getString("webextPerms.accept.label");
-    // let acceptKey = bundle.getString("webextPerms.accept.accessKey");
-    // let cancelText = bundle.getString("webextPerms.cancel.label");
-    // let cancelKey = bundle.getString("webextPerms.cancel.accessKey");
-    let acceptText = "Add extension";
-    let acceptKey = "A";
-    let cancelText = "Cancel";
-    let cancelKey = "C";
+    let acceptText = bundle.getString("webextPerms.add.label");
+    let acceptKey = bundle.getString("webextPerms.add.accessKey");
+    let cancelText = bundle.getString("webextPerms.cancel.label");
+    let cancelKey = bundle.getString("webextPerms.cancel.accessKey");
 
     if (info.type == "sideload") {
-      header = `${name} added`;
-      text = "Another program on your computer installed an add-on that may affect your browser.  Please review this add-on's permission requests and choose to Enable or Disable";
-      acceptText = "Enable";
-      acceptKey = "E";
-      cancelText = "Disable";
-      cancelKey = "D";
+      header = bundle.getFormattedString("webextPerms.sideloadHeader", [addonLabel]);
+      text = bundle.getString("webextPerms.sideloadText");
+      acceptText = bundle.getString("webextPerms.sideloadEnable.label");
+      acceptKey = bundle.getString("webextPerms.sideloadEnable.accessKey");
+      cancelText = bundle.getString("webextPerms.sideloadDisable.label");
+      cancelKey = bundle.getString("webextPerms.sideloadDisable.accessKey");
     } else if (info.type == "update") {
       header = "";
-      text = `${name} has been updated.  You must approve new permissions before the updated version will install.`;
-      acceptText = "Update";
-      acceptKey = "U";
+      text = bundle.getFormattedString("webextPerms.updateText", [addonLabel]);
+      acceptText = bundle.getString("webextPerms.updateAccept.label");
+      acceptKey = bundle.getString("webextPerms.updateAccept.accessKey");
     }
 
-    let formatPermission = perm => {
-      try {
-        // return bundle.getString(`webextPerms.description.${perm}`);
-        return `localized description of permission ${perm}`;
-      } catch (err) {
-        // return bundle.getFormattedString("webextPerms.description.unknown",
-        //                                  [perm]);
-        return `localized description of unknown permission ${perm}`;
+    let msgs = [];
+    for (let permission of perms.permissions) {
+      let key = `webextPerms.description.${permission}`;
+      if (permission == "nativeMessaging") {
+        let brandBundle = win.document.getElementById("bundle_brand");
+        let appName = brandBundle.getString("brandShortName");
+        msgs.push(bundle.getFormattedString(key, [appName]));
+      } else {
+        try {
+          msgs.push(bundle.getString(key));
+        } catch (err) {
+          // We deliberately do not include all permissions in the prompt.
+          // So if we don't find one then just skip it.
+        }
       }
-    };
+    }
 
-    let formatHostPermission = perm => {
-      if (perm == "<all_urls>") {
-        // return bundle.getString("webextPerms.hostDescription.allUrls");
-        return "localized description of <all_urls> host permission";
+    let allUrls = false, wildcards = [], sites = [];
+    for (let permission of perms.hosts) {
+      if (permission == "<all_urls>") {
+        allUrls = true;
+        break;
       }
-      let match = /^[htps*]+:\/\/([^/]+)\//.exec(perm);
+      let match = /^[htps*]+:\/\/([^/]+)\//.exec(permission);
       if (!match) {
         throw new Error("Unparseable host permission");
       }
-      if (match[1].startsWith("*.")) {
-        let domain = match[1].slice(2);
-        // return bundle.getFormattedString("webextPerms.hostDescription.wildcard", [domain]);
-        return `localized description of wildcard host permission for ${domain}`;
+      if (match[1] == "*") {
+        allUrls = true;
+      } else if (match[1].startsWith("*.")) {
+        wildcards.push(match[1].slice(2));
+      } else {
+        sites.push(match[1]);
+      }
+    }
+
+    if (allUrls) {
+      msgs.push(bundle.getString("webextPerms.hostDescription.allUrls"));
+    } else {
+      // Formats a list of host permissions.  If we have 4 or fewer, display
+      // them all, otherwise display the first 3 followed by an item that
+      // says "...plus N others"
+      function format(list, itemKey, moreKey) {
+        function formatItems(items) {
+          msgs.push(...items.map(item => bundle.getFormattedString(itemKey, [item])));
+        }
+        if (list.length < 5) {
+          formatItems(list);
+        } else {
+          formatItems(list.slice(0, 3));
+
+          let remaining = list.length - 3;
+          msgs.push(PluralForm.get(remaining, bundle.getString(moreKey))
+                              .replace("#1", remaining));
+        }
       }
 
-      //  return bundle.getFormattedString("webextPerms.hostDescription.oneSite", [match[1]]);
-      return `localized description of single host permission for ${match[1]}`;
-    };
+      format(wildcards, "webextPerms.hostDescription.wildcard",
+             "webextPerms.hostDescription.tooManyWildcards");
+      format(sites, "webextPerms.hostDescription.oneSite",
+             "webextPerms.hostDescription.tooManySites");
+    }
 
-    let msgs = [
-      ...perms.permissions.map(formatPermission),
-      ...perms.hosts.map(formatHostPermission),
-    ];
-
-    let rendered = false;
     let popupOptions = {
       hideClose: true,
       popupIconURL: info.icon,
       persistent: true,
 
       eventCallback(topic) {
         if (topic == "showing") {
-          // This check can be removed when bug 1325223 is resolved.
-          if (rendered) {
-            return false;
+          let doc = this.browser.ownerDocument;
+          doc.getElementById("addon-webext-perm-header").innerHTML = header;
+
+          if (text) {
+            doc.getElementById("addon-webext-perm-text").innerHTML = text;
           }
 
-          let doc = this.browser.ownerDocument;
-          doc.getElementById("addon-webext-perm-header").textContent = header;
+          let listIntroEl = doc.getElementById("addon-webext-perm-intro");
+          listIntroEl.value = listIntro;
+          listIntroEl.hidden = (msgs.length == 0);
 
           let list = doc.getElementById("addon-webext-perm-list");
           while (list.firstChild) {
             list.firstChild.remove();
           }
 
-          if (text) {
-            doc.getElementById("addon-webext-perm-text").textContent = text;
-          }
-
-          let listHeaderEl = doc.getElementById("addon-webext-perm-intro");
-          listHeaderEl.value = listHeader;
-          listHeaderEl.hidden = (msgs.length == 0);
-
           for (let msg of msgs) {
             let item = doc.createElementNS(HTML_NS, "li");
             item.textContent = msg;
             list.appendChild(item);
           }
-          rendered = true;
-        } else if (topic == "dismissed") {
-          rendered = false;
         } else if (topic == "swapping") {
-          rendered = false;
           return true;
         }
         return false;
       },
     };
 
     return new Promise(resolve => {
       win.PopupNotifications.show(target, "addon-webext-permissions", "",
@@ -280,11 +300,52 @@ this.ExtensionsUI = {
                                     {
                                       label: cancelText,
                                       accessKey: cancelKey,
                                       callback: () => resolve(false),
                                     },
                                   ], popupOptions);
     });
   },
+
+  showInstallNotification(target, addon) {
+    let win = target.ownerGlobal;
+    let popups = win.PopupNotifications;
+
+    let addonLabel = `<label class="addon-webext-name">${addon.name}</label>`;
+    let addonIcon = '<image class="addon-addon-icon"/>';
+    let toolbarIcon = '<image class="addon-toolbar-icon"/>';
+
+    let brandBundle = win.document.getElementById("bundle_brand");
+    let appName = brandBundle.getString("brandShortName");
+
+    let bundle = win.gNavigatorBundle;
+    let msg1 = bundle.getFormattedString("addonPostInstall.message1",
+                                         [addonLabel, appName]);
+    let msg2 = bundle.getFormattedString("addonPostInstall.message2",
+                                         [addonLabel, addonIcon, toolbarIcon]);
+
+    let action = {
+      label: bundle.getString("addonPostInstall.okay.label"),
+      accessKey: bundle.getString("addonPostInstall.okay.key"),
+      callback: () => {},
+    };
+
+    let options = {
+      hideClose: true,
+      popupIconURL: addon.iconURL || DEFAULT_EXTENSION_ICON,
+      eventCallback(topic) {
+        if (topic == "showing") {
+          let doc = this.browser.ownerDocument;
+          doc.getElementById("addon-installed-notification-header")
+             .innerHTML = msg1;
+          doc.getElementById("addon-installed-notification-message")
+             .innerHTML = msg2;
+        }
+      }
+    };
+
+    popups.show(target, "addon-installed", "", "addons-notification-icon",
+                action, null, options);
+  },
 };
 
 EventEmitter.decorate(ExtensionsUI);
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -11,17 +11,16 @@ XPCSHELL_TESTS_MANIFESTS += [
 ]
 
 EXTRA_JS_MODULES += [
     'AboutHome.jsm',
     'AboutNewTab.jsm',
     'AttributionCode.jsm',
     'BrowserUITelemetry.jsm',
     'BrowserUsageTelemetry.jsm',
-    'CaptivePortalWatcher.jsm',
     'CastingApps.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
     'ContentObservers.jsm',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
     'DirectoryLinksProvider.jsm',
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -1,18 +1,16 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_BrowserUITelemetry_buckets.js]
 [browser_BrowserUITelemetry_defaults.js]
 [browser_BrowserUITelemetry_sidebar.js]
 [browser_BrowserUITelemetry_syncedtabs.js]
-[browser_CaptivePortalWatcher.js]
-skip-if = os == "win" # Bug 1313894
 [browser_ContentSearch.js]
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
   !/browser/components/search/test/head.js
   !/browser/components/search/test/testEngine.xml
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -549,16 +549,21 @@ function prompt(aBrowser, aRequest) {
           this.remove();
           return true;
         }
       }
 
       function listDevices(menupopup, devices) {
         while (menupopup.lastChild)
           menupopup.removeChild(menupopup.lastChild);
+        // Removing the child nodes of the menupopup doesn't clear the value
+        // attribute of the menulist. This can have unfortunate side effects
+        // when the list is rebuilt with a different content, so we remove
+        // the value attribute explicitly.
+        menupopup.parentNode.removeAttribute("value");
 
         for (let device of devices)
           addDeviceToList(menupopup, device.name, device.deviceIndex);
       }
 
       function listScreenShareDevices(menupopup, devices) {
         while (menupopup.lastChild)
           menupopup.removeChild(menupopup.lastChild);
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -829,18 +829,36 @@ menuitem.bookmark-item {
   max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
 .addon-webext-perm-header {
+  font-size: 1.3em;
+}
+
+.addon-webext-name {
   font-weight: bold;
-  font-size: 1.3em;
+  margin: 0;
+}
+
+.addon-addon-icon {
+  width: 14px;
+  height: 14px;
+  list-style-image: url("chrome://browser/skin/menuPanel.svg");
+  -moz-image-region: rect(0px, 288px, 32px, 256px);
+}
+
+.addon-toolbar-icon {
+  width: 14px;
+  height: 14px;
+  list-style-image: url("chrome://browser/skin/Toolbar.png");
+  -moz-image-region: rect(0, 486px, 18px, 468px);
 }
 
 /* Notification icon box */
 
 .notification-anchor-icon:-moz-focusring {
   outline: 1px dotted -moz-DialogText;
 }
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3087,18 +3087,36 @@ menulist.translate-infobar-element > .me
   max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
 .addon-webext-perm-header {
+  font-size: 1.3em;
+}
+
+.addon-webext-name {
   font-weight: bold;
-  font-size: 1.3em;
+  margin: 0;
+}
+
+.addon-addon-icon {
+  width: 14px;
+  height: 14px;
+  list-style-image: url("chrome://browser/skin/menuPanel.svg");
+  -moz-image-region: rect(0px, 288px, 32px, 256px);
+}
+
+.addon-toolbar-icon {
+  width: 14px;
+  height: 14px;
+  list-style-image: url("chrome://browser/skin/Toolbar.png");
+  -moz-image-region: rect(0, 486px, 18px, 468px);
 }
 
 /* Status panel */
 
 .statuspanel-label {
   margin: 0;
   padding: 2px 4px;
   background: linear-gradient(#fff, #ddd);
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -38,20 +38,20 @@ treecol {
 #blocklistsTree treechildren::-moz-tree-row {
   min-height: 36px;
 }
 
 #selectionCol {
   min-width: 26px;
 }
 
-/* For the "learn more" links, line up after text */
 .learnMore {
   margin-inline-start: 1.5em;
   font-weight: normal;
+  white-space: nowrap;
 }
 
 /* Category List */
 
 #categories {
   max-height: 100vh;
 }
 
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2132,18 +2132,36 @@ toolbarbutton.bookmark-item[dragover="tr
   max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
 .addon-webext-perm-header {
+  font-size: 1.3em;
+}
+
+.addon-webext-name {
   font-weight: bold;
-  font-size: 1.3em;
+  margin: 0;
+}
+
+.addon-addon-icon {
+  width: 14px;
+  height: 14px;
+  list-style-image: url("chrome://browser/skin/menuPanel.svg");
+  -moz-image-region: rect(0px, 288px, 32px, 256px);
+}
+
+.addon-toolbar-icon {
+  width: 14px;
+  height: 14px;
+  list-style-image: url("chrome://browser/skin/Toolbar.png");
+  -moz-image-region: rect(0, 486px, 18px, 468px);
 }
 
 /* Notification icon box */
 
 .notification-anchor-icon:-moz-focusring {
   outline: 1px dotted -moz-DialogText;
 }
 
--- a/browser/tools/mozscreenshots/head.js
+++ b/browser/tools/mozscreenshots/head.js
@@ -29,27 +29,36 @@ function* setup() {
       isnot(aAddon, null, "The mozscreenshots extension should be installed");
       AddonWatcher.ignoreAddonPermanently(aAddon.id);
       TestRunner = Cu.import("chrome://mozscreenshots/content/TestRunner.jsm", {}).TestRunner;
       resolve();
     });
   });
 }
 
+/**
+ * Used by pre-defined sets of configurations to decide whether to run for a build.
+ * @note This is not used by browser_screenshots.js which handles when MOZSCREENSHOTS_SETS is set.
+ * @return {bool} whether to capture screenshots.
+ */
 function shouldCapture() {
-  // Try pushes only capture in browser_screenshots.js with MOZSCREENSHOTS_SETS.
   if (env.get("MOZSCREENSHOTS_SETS")) {
     ok(true, "MOZSCREENSHOTS_SETS was specified so only capture what was " +
        "requested (in browser_screenshots.js)");
     return false;
   }
 
-  // Automation isn't able to schedule test jobs to only run on nightlies so we handle it here
-  // (see also: bug 1116275).
-  let capture = AppConstants.MOZ_UPDATE_CHANNEL == "nightly" ||
-                AppConstants.SOURCE_REVISION_URL == "";
+  if (AppConstants.MOZ_UPDATE_CHANNEL == "nightly") {
+    ok(true, "Screenshots aren't captured on Nightlies");
+    return false;
+  }
+
+  // Don't run pre-defined sets (e.g. primaryUI) on try, require MOZSCREENSHOTS_SETS.
+  // The job is only scheduled on specific repos:
+  // https://dxr.mozilla.org/build-central/search?q=MOCHITEST_BC_SCREENSHOTS
+  let capture = !AppConstants.SOURCE_REVISION_URL.includes("/try/rev/");
   if (!capture) {
-    ok(true, "Capturing is disabled for this MOZ_UPDATE_CHANNEL or REPO");
+    ok(true, "Capturing is disabled for this REPO. You may need to use MOZSCREENSHOTS_SETS");
   }
   return capture;
 }
 
 add_task(setup);
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.jsm
@@ -66,22 +66,10 @@ this.LightweightThemes = {
         // Wait for LWT listener
         return new Promise(resolve => {
           setTimeout(() => {
             resolve("lightLWT");
           }, 500);
         });
       },
     },
-
-    compactLight: {
-      applyConfig: Task.async(() => {
-        LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme("firefox-compact-light@mozilla.org");
-      }),
-    },
-
-    compactDark: {
-      applyConfig: Task.async(() => {
-        LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme("firefox-compact-dark@mozilla.org");
-      }),
-    },
   },
 };
--- a/build/build-clang/build-clang.py
+++ b/build/build-clang/build-clang.py
@@ -465,42 +465,31 @@ if __name__ == "__main__":
     cc = get_tool(config, "cc")
     cxx = get_tool(config, "cxx")
     ld = get_tool(config, "link" if is_windows() else "ld")
     ar = get_tool(config, "lib" if is_windows() else "ar")
     ranlib = None if is_windows() else get_tool(config, "ranlib")
 
     if not os.path.exists(source_dir):
         os.makedirs(source_dir)
-    if os.path.exists(llvm_source_dir):
-        svn_update(llvm_source_dir, llvm_revision)
-    else:
-        svn_co(source_dir, llvm_repo, llvm_source_dir, llvm_revision)
-    if os.path.exists(clang_source_dir):
-        svn_update(clang_source_dir, llvm_revision)
-    else:
-        svn_co(source_dir, clang_repo, clang_source_dir, llvm_revision)
-    if os.path.exists(compiler_rt_source_dir):
-        svn_update(compiler_rt_source_dir, llvm_revision)
-    else:
-        svn_co(source_dir, compiler_repo, compiler_rt_source_dir, llvm_revision)
-    if os.path.exists(libcxx_source_dir):
-        svn_update(libcxx_source_dir, llvm_revision)
-    else:
-        svn_co(source_dir, libcxx_repo, libcxx_source_dir, llvm_revision)
+
+    def checkout_or_update(repo, checkout_dir):
+        if os.path.exists(checkout_dir):
+            svn_update(checkout_dir, llvm_revision)
+        else:
+            svn_co(source_dir, repo, checkout_dir, llvm_revision)
+
+    checkout_or_update(llvm_repo, llvm_source_dir)
+    checkout_or_update(clang_repo, clang_source_dir)
+    checkout_or_update(compiler_repo, compiler_rt_source_dir)
+    checkout_or_update(libcxx_repo, libcxx_source_dir)
     if libcxxabi_repo:
-        if os.path.exists(libcxxabi_source_dir):
-            svn_update(libcxxabi_source_dir, llvm_revision)
-        else:
-            svn_co(source_dir, libcxxabi_repo, libcxxabi_source_dir, llvm_revision)
+        checkout_or_update(libcxxabi_repo, libcxxabi_source_dir)
     if extra_repo:
-        if os.path.exists(extra_source_dir):
-            svn_update(extra_source_dir, llvm_revision)
-        else:
-            svn_co(source_dir, extra_repo, extra_source_dir, llvm_revision)
+        checkout_or_update(extra_repo, extra_source_dir)
     for p in config.get("patches", {}).get(get_platform(), []):
         patch(p, source_dir)
 
     symlinks = [(source_dir + "/clang",
                  llvm_source_dir + "/tools/clang"),
                 (source_dir + "/extra",
                  llvm_source_dir + "/tools/clang/tools/extra"),
                 (source_dir + "/compiler-rt",
--- a/build/build-clang/clang-static-analysis-linux64.json
+++ b/build/build-clang/clang-static-analysis-linux64.json
@@ -1,30 +1,27 @@
 {
-    "llvm_revision": "262557",
+    "llvm_revision": "290136",
     "stages": "3",
     "build_libcxx": true,
     "build_type": "Release",
     "assertions": false,
-    "llvm_repo": "https://llvm.org/svn/llvm-project/llvm/tags/RELEASE_380/final",
-    "clang_repo": "https://llvm.org/svn/llvm-project/cfe/tags/RELEASE_380/final",
-    "compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/tags/RELEASE_380/final",
-    "libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/tags/RELEASE_380/final",
-    "libcxxabi_repo": "https://llvm.org/svn/llvm-project/libcxxabi/tags/RELEASE_380/final",
+    "llvm_repo": "https://llvm.org/svn/llvm-project/llvm/tags/RELEASE_390/final",
+    "clang_repo": "https://llvm.org/svn/llvm-project/cfe/tags/RELEASE_390/final",
+    "compiler_repo": "https://llvm.org/svn/llvm-project/compiler-rt/tags/RELEASE_390/final",
+    "libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/tags/RELEASE_390/final",
+    "libcxxabi_repo": "https://llvm.org/svn/llvm-project/libcxxabi/tags/RELEASE_390/final",
     "python_path": "/usr/bin/python2.7",
     "gcc_dir": "/home/worker/workspace/build/src/gcc",
     "cc": "/home/worker/workspace/build/src/gcc/bin/gcc",
     "cxx": "/home/worker/workspace/build/src/gcc/bin/g++",
     "patches": {
         "macosx64": [
-          "llvm-debug-frame.patch",
-          "return-empty-string-non-mangled.patch"
+          "llvm-debug-frame.patch"
         ],
         "linux64": [
-          "llvm-debug-frame.patch",
-          "return-empty-string-non-mangled.patch"
+          "llvm-debug-frame.patch"
         ],
         "linux32": [
-          "llvm-debug-frame.patch",
-          "return-empty-string-non-mangled.patch"
+          "llvm-debug-frame.patch"
         ]
     }
 }
--- a/build/gyp.mozbuild
+++ b/build/gyp.mozbuild
@@ -104,8 +104,11 @@ if CONFIG['ARM_ARCH']:
 
 # Don't try to compile ssse3/sse4.1 code if toolchain doesn't support
 if CONFIG['INTEL_ARCHITECTURE']:
     if not CONFIG['HAVE_TOOLCHAIN_SUPPORT_MSSSE3'] or not CONFIG['HAVE_TOOLCHAIN_SUPPORT_MSSE4_1']:
         gyp_vars['yuv_disable_asm'] = 1
 
 if CONFIG['MACOS_SDK_DIR']:
     gyp_vars['mac_sdk_path'] = CONFIG['MACOS_SDK_DIR']
+
+if not CONFIG['MOZ_SYSTEM_LIBVPX']:
+    gyp_vars['libvpx_dir'] =  '/media/libvpx/libvpx'
--- a/caps/tests/mochitest/browser_checkloaduri.js
+++ b/caps/tests/mochitest/browser_checkloaduri.js
@@ -16,58 +16,62 @@ const URLs = new Map([
     ["feed:chrome://foo/content/bar.xul", false, false, false],
     ["view-source:http://www.example2.com", false, false, true],
     ["view-source:https://www.example2.com", false, false, true],
     ["view-source:feed:http://www.example2.com", false, false, true],
     ["feed:view-source:http://www.example2.com", false, false, false],
     ["data:text/html,Hi", true, false, true],
     ["view-source:data:text/html,Hi", false, false, true],
     ["javascript:alert('hi')", true, false, true],
+    ["moz://a", false, false, true],
   ]],
   ["feed:http://www.example.com", [
     ["http://www.example2.com", true, true, true],
     ["feed:http://www.example2.com", true, true, true],
     ["https://www.example2.com", true, true, true],
     ["feed:https://www.example2.com", true, true, true],
     ["chrome://foo/content/bar.xul", false, false, true],
     ["feed:chrome://foo/content/bar.xul", false, false, false],
     ["view-source:http://www.example2.com", false, false, true],
     ["view-source:https://www.example2.com", false, false, true],
     ["view-source:feed:http://www.example2.com", false, false, true],
     ["feed:view-source:http://www.example2.com", false, false, false],
     ["data:text/html,Hi", true, false, true],
     ["view-source:data:text/html,Hi", false, false, true],
     ["javascript:alert('hi')", true, false, true],
+    ["moz://a", false, false, true],
   ]],
   ["view-source:http://www.example.com", [
     ["http://www.example2.com", true, true, true],
     ["feed:http://www.example2.com", false, false, true],
     ["https://www.example2.com", true, true, true],
     ["feed:https://www.example2.com", false, false, true],
     ["chrome://foo/content/bar.xul", false, false, true],
     ["feed:chrome://foo/content/bar.xul", false, false, false],
     ["view-source:http://www.example2.com", true, true, true],
     ["view-source:https://www.example2.com", true, true, true],
     ["view-source:feed:http://www.example2.com", false, false, true],
     ["feed:view-source:http://www.example2.com", false, false, false],
     ["data:text/html,Hi", true, false, true],
     ["view-source:data:text/html,Hi", true, false, true],
     ["javascript:alert('hi')", true, false, true],
+    ["moz://a", false, false, true],
   ]],
   ["about:foo", [
     ["about:foo?", true, true, true],
     ["about:foo?bar", true, true, true],
     ["about:foo#", true, true, true],
     ["about:foo#bar", true, true, true],
     ["about:foo?#", true, true, true],
     ["about:foo?bar#baz", true, true, true],
     ["about:bar", false, false, true],
     ["about:bar?foo#baz", false, false, true],
     ["about:bar?foo", false, false, true],
     ["http://www.example.com/", true, true, true],
+    ["moz://a", false, false, true],
   ]],
 ]);
 
 function testURL(source, target, canLoad, canLoadWithoutInherit, canCreate, flags) {
   let threw = false;
   let targetURI;
   try {
     targetURI = makeURI(target);
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -940,17 +940,23 @@ endif
 # choices, and Cargo only supports two, we choose to enable various
 # optimization levels in our Cargo.toml files all the time, and override the
 # optimization level here, if necessary.  (The Cargo.toml files already
 # specify debug-assertions appropriately for --{disable,enable}-debug.)
 ifndef MOZ_OPTIMIZE
 rustflags_override = RUSTFLAGS='-C opt-level=0'
 endif
 
-CARGO_BUILD = env $(rustflags_override) CARGO_TARGET_DIR=. RUSTC=$(RUSTC) MOZ_DIST=$(ABS_DIST) $(CARGO) build $(cargo_build_flags)
+CARGO_BUILD = env $(rustflags_override) \
+	CARGO_TARGET_DIR=. \
+	RUSTC=$(RUSTC) \
+	MOZ_DIST=$(ABS_DIST) \
+	LIBCLANG_PATH=$(MOZ_LIBCLANG_PATH) \
+	CLANG_PATH=$(MOZ_CLANG_PATH) \
+	$(CARGO) build $(cargo_build_flags)
 
 ifdef RUST_LIBRARY_FILE
 
 ifdef RUST_LIBRARY_FEATURES
 rust_features_flag := --features "$(RUST_LIBRARY_FEATURES)"
 endif
 
 # Assume any system libraries rustc links against are already in the target's LIBS.
--- a/devtools/client/aboutdebugging/components/workers/panel.js
+++ b/devtools/client/aboutdebugging/components/workers/panel.js
@@ -71,16 +71,17 @@ module.exports = createClass({
 
     getWorkerForms(this.props.client).then(forms => {
       forms.registrations.forEach(form => {
         workers.service.push({
           icon: WorkerIcon,
           name: form.url,
           url: form.url,
           scope: form.scope,
+          fetch: form.fetch,
           registrationActor: form.actor,
           active: form.active
         });
       });
 
       forms.workers.forEach(form => {
         let worker = {
           icon: WorkerIcon,
@@ -94,16 +95,18 @@ module.exports = createClass({
             if (registration) {
               // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
               // have a scriptSpec, but its associated WorkerDebugger does.
               if (!registration.url) {
                 registration.name = registration.url = form.url;
               }
               registration.workerActor = form.actor;
             } else {
+              worker.fetch = form.fetch;
+
               // If a service worker registration could not be found, this means we are in
               // e10s, and registrations are not forwarded to other processes until they
               // reach the activated state. Augment the worker as a registration worker to
               // display it in aboutdebugging.
               worker.scope = form.scope;
               worker.active = false;
               workers.service.push(worker);
             }
--- a/devtools/client/aboutdebugging/components/workers/service-worker-target.js
+++ b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
@@ -20,16 +20,17 @@ const Strings = Services.strings.createB
 module.exports = createClass({
   displayName: "ServiceWorkerTarget",
 
   propTypes: {
     client: PropTypes.instanceOf(DebuggerClient).isRequired,
     debugDisabled: PropTypes.bool,
     target: PropTypes.shape({
       active: PropTypes.bool,
+      fetch: PropTypes.bool.isRequired,
       icon: PropTypes.string,
       name: PropTypes.string.isRequired,
       url: PropTypes.string,
       scope: PropTypes.string.isRequired,
       // registrationActor can be missing in e10s.
       registrationActor: PropTypes.string,
       workerActor: PropTypes.string
     }).isRequired
@@ -190,16 +191,19 @@ module.exports = createClass({
     }, Strings.GetStringFromName("unregister"));
   },
 
   render() {
     let { target } = this.props;
     let { pushSubscription } = this.state;
     let status = this.getServiceWorkerStatus();
 
+    let fetch = target.fetch ? Strings.GetStringFromName("listeningForFetchEvents") :
+      Strings.GetStringFromName("notListeningForFetchEvents");
+
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
       }),
       dom.span({ className: `target-status target-status-${status}` },
         Strings.GetStringFromName(status)),
@@ -211,16 +215,22 @@ module.exports = createClass({
               dom.strong(null, Strings.GetStringFromName("pushService")),
               dom.span({
                 className: "service-worker-push-url",
                 title: pushSubscription.endpoint
               }, pushSubscription.endpoint)) :
             null
           ),
           dom.li({ className: "target-detail" },
+            dom.strong(null, Strings.GetStringFromName("fetch")),
+            dom.span({
+              className: "service-worker-fetch-flag",
+              title: fetch
+            }, fetch)),
+          dom.li({ className: "target-detail" },
             dom.strong(null, Strings.GetStringFromName("scope")),
             dom.span({
               className: "service-worker-scope",
               title: target.scope
             }, target.scope),
             this.renderUnregisterLink()
           )
         )
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -8,16 +8,18 @@ support-files =
   addons/bad/manifest.json
   addons/bug1273184.xpi
   addons/test-devtools-webextension/*
   addons/test-devtools-webextension-nobg/*
   service-workers/delay-sw.html
   service-workers/delay-sw.js
   service-workers/empty-sw.html
   service-workers/empty-sw.js
+  service-workers/fetch-sw.html
+  service-workers/fetch-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_addons_debug_bootstrapped.js]
 [browser_addons_debug_webextension.js]
 tags = webextensions
 [browser_addons_debug_webextension_inspector.js]
@@ -27,16 +29,17 @@ tags = webextensions
 [browser_addons_debug_webextension_popup.js]
 tags = webextensions
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
 [browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_page_not_found.js]
 [browser_service_workers.js]
+[browser_service_workers_fetch_flag.js]
 [browser_service_workers_not_compatible.js]
 [browser_service_workers_push.js]
 [browser_service_workers_push_service.js]
 [browser_service_workers_start.js]
 [browser_service_workers_status.js]
 [browser_service_workers_timeout.js]
 skip-if = true # Bug 1232931
 [browser_service_workers_unregister.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_fetch_flag.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Service workers can't be loaded from chrome://,
+// but http:// is ok with dom.serviceWorkers.testing.enabled turned on.
+const EMPTY_SW_TAB_URL = URL_ROOT + "service-workers/empty-sw.html";
+const FETCH_SW_TAB_URL = URL_ROOT + "service-workers/fetch-sw.html";
+
+function* testBody(url, expecting) {
+  yield new Promise(done => {
+    let options = {"set": [
+      ["dom.serviceWorkers.enabled", true],
+      ["dom.serviceWorkers.testing.enabled", true],
+    ]};
+    SpecialPowers.pushPrefEnv(options, done);
+  });
+
+  let { tab, document } = yield openAboutDebugging("workers");
+
+  let swTab = yield addTab(url);
+
+  let serviceWorkersElement = getServiceWorkerList(document);
+  yield waitForMutation(serviceWorkersElement, { childList: true });
+
+  let fetchFlags =
+    [...document.querySelectorAll("#service-workers .service-worker-fetch-flag")];
+  fetchFlags = fetchFlags.map(element => element.textContent);
+  ok(fetchFlags.includes(expecting), "Found correct fetch flag.");
+
+  try {
+    yield unregisterServiceWorker(swTab, serviceWorkersElement);
+    ok(true, "Service worker registration unregistered");
+  } catch (e) {
+    ok(false, "SW not unregistered; " + e);
+  }
+
+  // Check that the service worker disappeared from the UI
+  let names = [...document.querySelectorAll("#service-workers .target-name")];
+  names = names.map(element => element.textContent);
+  ok(names.length == 0, "All service workers were removed from the list.");
+
+  yield removeTab(swTab);
+  yield closeAboutDebugging(tab);
+}
+
+add_task(function* () {
+  yield testBody(FETCH_SW_TAB_URL, "Listening for fetch events.");
+  yield testBody(EMPTY_SW_TAB_URL, "Not listening for fetch events.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/fetch-sw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+var sw = navigator.serviceWorker.register("fetch-sw.js", {scope: "fetch-sw/"});
+sw.then(
+  function () {
+    dump("SW registered\n");
+  },
+  function (e) {
+    dump("SW not registered: " + e + "\n");
+  }
+);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/service-workers/fetch-sw.js
@@ -0,0 +1,6 @@
+"use strict";
+
+// Bug 1328293
+self.onfetch = function (event) {
+  // do nothing.
+};
--- a/devtools/client/debugger/new/debugger.css
+++ b/devtools/client/debugger/new/debugger.css
@@ -15,167 +15,201 @@ body {
   width: 100%;
 }
 
 #mount {
   display: flex;
   height: 100%;
 }
 
-
 ::-webkit-scrollbar {
   width: 8px;
   height: 8px;
   background: transparent;
 }
 
 ::-webkit-scrollbar-track {
   border-radius: 8px;
   background: transparent;
 }
 
 ::-webkit-scrollbar-thumb {
   border-radius: 8px;
-  background: rgba(113,113,113,0.5);
+  background: rgba(113, 113, 113, 0.5);
 }
 
 :root.theme-dark .CodeMirror-scrollbar-filler {
   background: transparent;
 }
 .landing-page {
   flex: 1;
   display: flex;
-  width: 100%;
-  height: 100%;
+  width: 100vw;
+  height: 100vh;
   flex-direction: row;
+  align-items: stretch;
+  /* Customs properties */
+  --title-font-size: 24px;
+  --ui-element-font-size: 16px;
+  --primary-line-height: 30px;
+  --secondary-line-height: 25px;
+  --base-spacing: 20px;
+  --base-transition: all 0.25s ease;
 }
 
 .landing-page .sidebar {
   display: flex;
   background-color: var(--theme-tab-toolbar-background);
   width: 200px;
-  height: 100%;
   flex-direction: column;
+  border-right: 1px solid var(--theme-splitter-color);
 }
 
 .landing-page .sidebar h1 {
   color: var(--theme-body-color);
-  font-size: 24px;
+  font-size: var(--title-font-size);
   margin: 0;
-  line-height: 30px;
+  line-height: var(--primary-line-height);
   font-weight: normal;
-  padding: 40px 20px;
+  padding: calc(2 * var(--base-spacing)) var(--base-spacing);
 }
 
 .landing-page .sidebar ul {
   list-style: none;
   padding: 0;
-  line-height: 30px;
-  font-size: 18px;
+  line-height: var(--primary-line-height);
+  font-size: var(--ui-element-font-size);
 }
 
 .landing-page .sidebar li {
-  padding: 5px 20px;
-}
-
-.landing-page .sidebar li.selected {
-  background: var(--theme-search-overlays-semitransparent);
-  transition: all 0.25s ease;
-}
-
-.landing-page .sidebar li:hover {
-  background: var(--theme-selection-background);
-  cursor: pointer;
+  padding: calc(var(--base-spacing) / 4) var(--base-spacing);
 }
 
 .landing-page .sidebar li a {
   color: var(--theme-body-color);
 }
 
-.landing-page .sidebar li:hover a {
+.landing-page .sidebar li.selected {
+  background: var(--theme-highlight-bluegrey);
   color: var(--theme-selection-color);
+  transition: var(--base-transition);
+}
+
+.landing-page .sidebar li.selected a {
+  color: inherit;
+}
+
+.landing-page .sidebar li:hover,
+.landing-page .sidebar li:focus {
+  background: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+  cursor: pointer;
+}
+
+.landing-page .sidebar li:hover a,
+.landing-page .sidebar li:focus a {
+  color: inherit;
 }
 
 .landing-page .panel {
   display: flex;
   flex: 1;
-  height: 100%;
-  overflow: auto;
   flex-direction: column;
+  justify-content: space-between;
 }
 
-.landing-page .panel .title {
-  margin: 20px 40px;
-  width: calc(100% - 80px);
-  font-size: 16px;
-  border-bottom: 1px solid var(--theme-splitter-color);
-  height: 54px;
+.landing-page .panel header {
+  display: flex;
+  align-items: baseline;
+  margin: calc(2 * var(--base-spacing)) 0 0;
+  padding-bottom: var(--base-spacing);
 }
 
-.landing-page .panel h2 {
-  color: var(--theme-body-color);
-  font-weight: normal;
+.landing-page .panel header input {
+  flex: 1;
+  background-color: var(--theme-tab-toolbar-background);
+  color: var(--theme-comment);
+  font-size: var(--ui-element-font-size);
+  border: 1px solid var(--theme-splitter-color);
+  padding: calc(var(--base-spacing) / 2);
+  margin: 0 var(--base-spacing);
+  transition: var(--base-transition);
 }
 
-.landing-page .panel .center {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
+.landing-page .panel header input::placeholder {
+  color: var(--theme-body-color-inactive);
 }
 
-.landing-page .panel .center .center-message {
-  margin: 40px;
-  font-size: 16px;
-  line-height: 25px;
-  padding: 10px;
+.landing-page .panel header input:focus {
+  border: 1px solid var(--theme-selection-background);
+}
+
+.landing-page .panel .center-message {
+  font-size: var(--ui-element-font-size);
+  line-height: var(--secondary-line-height);
+  padding: calc(var(--base-spacing) / 2);
 }
 
 .landing-page .center a {
   color: var(--theme-highlight-bluegrey);
   text-decoration: none;
 }
 
 .landing-page .tab-group {
-  margin: 40px;
+  flex: 1;
+  overflow-y: auto;
 }
 
 .landing-page .tab-list {
   list-style: none;
-  padding: 0px;
-  margin: 0px;
+  padding: 0;
+  margin: 0;
 }
 
 .landing-page .tab {
   border-bottom: 1px solid var(--theme-splitter-color);
-  padding: 10px;
+  padding: calc(var(--base-spacing) / 2) var(--base-spacing);
   font-family: sans-serif;
 }
 
-.landing-page .tab:hover {
-  background-color: var(--theme-toolbar-background);
-  cursor: pointer;
-}
-
 .landing-page .tab-title {
-  line-height: 25px;
-  font-size: 16px;
+  line-height: var(--secondary-line-height);
+  font-size: var(--ui-element-font-size);
   color: var(--theme-highlight-bluegrey);
+  word-break: break-all;
 }
 
 .landing-page .tab-url {
   color: var(--theme-comment);
+  word-break: break-all;
+}
+
+.landing-page .tab:focus,
+.landing-page .tab.active {
+  background: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+  cursor: pointer;
+  transition: var(--base-transition);
 }
 
-.landing-page .panel .center .footer-note {
-  flex: 1;
-  padding: 50px;
+.landing-page .tab:focus .tab-title,
+.landing-page .tab.active .tab-title {
+  color: inherit;
+}
+
+.landing-page .tab:focus .tab-url,
+.landing-page .tab.active .tab-url {
+  color: var(--theme-highlight-gray);
+}
+
+.landing-page .panel .footer-note {
+  padding: var(--base-spacing) 0;
+  text-align: center;
   font-size: 14px;
   color: var(--theme-comment);
-  bottom: 0;
-  position: absolute;
 }
 /* vim:set ts=2 sw=2 sts=2 et: */
 
 /* 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/. */
 
 :root.theme-light,
@@ -207,21 +241,22 @@ body {
 }
 
 .debugger {
   display: flex;
   flex: 1;
   height: 100%;
 }
 
-.center-pane {
+.editor-pane {
   display: flex;
   position: relative;
   flex: 1;
   background-color: var(--theme-tab-toolbar-background);
+  height: calc(100% - 1px);
   overflow: hidden;
 }
 
 .editor-container {
   display: flex;
   flex: 1;
 }
 
@@ -244,33 +279,16 @@ body {
   flex: 1;
 }
 
 .search-container .close-button {
   width: 16px;
   margin-top: 25px;
   margin-right: 20px;
 }
-
-.welcomebox {
-  width: calc(100% - 1px);
-
-  /* Offsetting it by 30px for the sources-header area */
-  height: calc(100% - 30px);
-  position: absolute;
-  top: 30px;
-  left: 0;
-  padding: 50px 0;
-  text-align: center;
-  font-size: 1.25em;
-  color: var(--theme-comment-alt);
-  background-color: var(--theme-tab-toolbar-background);
-  font-weight: lighter;
-  z-index: 100;
-}
 menupopup {
   position: fixed;
   z-index: 10000;
   background: white;
   border: 1px solid #cccccc;
   padding: 5px 0;
   background: #f2f2f2;
   border-radius: 5px;
@@ -280,24 +298,35 @@ menupopup {
 }
 
 menuitem {
   display: block;
   padding: 0 20px;
   line-height: 20px;
   font-weight: 500;
   font-size: 13px;
+  -moz-user-select: none;
+  user-select: none;
 }
 
 menuitem:hover {
   background: #3780fb;
   color: white;
   cursor: pointer;
 }
 
+menuitem[disabled=true] {
+  color: #cccccc;
+}
+
+menuitem[disabled=true]:hover {
+  background-color: transparent;
+  cursor: default;
+}
+
 menuseparator {
   border-bottom: 1px solid #cacdd3;
   width: 100%;
   height: 5px;
   display: block;
   margin-bottom: 5px;
 }