Merge draft head draft
authorGregory Szorc <gregory.szorc@gmail.com>
Thu, 25 Jan 2018 01:07:55 -0800
changeset 746376 93baac10756396829fcabe3294e9232d77802fde
parent 746375 f60e5d2c5744095d8c0333d1b98e99353fcd69fa (diff)
parent 721591 81bce4779be2feca70ace6a1a616b0c7dc042e6b (current diff)
child 746377 46cba603ad24e29fbc3e2de0cf597e2b9b14bc29
push id96834
push usergszorc@mozilla.com
push dateThu, 25 Jan 2018 17:14:24 +0000
milestone60.0a1
Merge draft head
--- a/.eslintignore
+++ b/.eslintignore
@@ -72,16 +72,20 @@ browser/branding/**/firefox-branding.js
 # Gzipped test file.
 browser/base/content/test/general/gZipOfflineChild.html
 browser/base/content/test/urlbar/file_blank_but_not_blank.html
 # New tab is likely to be replaced soon.
 browser/base/content/newtab/**
 # Test files that are really json not js, and don't need to be linted.
 browser/components/sessionstore/test/unit/data/sessionstore_valid.js
 browser/components/sessionstore/test/unit/data/sessionstore_invalid.js
+# This file is split into two in order to keep it as a valid json file
+# for documentation purposes (policies.json) but to be accessed by the
+# code as a .jsm (schema.jsm)
+browser/components/enterprisepolicies/schemas/schema.jsm
 # generated & special files in cld2
 browser/components/translation/cld2/**
 # Screenshots and Follow-on search are imported as a system add-on and have
 # their own lint rules currently.
 browser/extensions/followonsearch/**
 browser/extensions/screenshots/**
 browser/extensions/pdfjs/content/build**
 browser/extensions/pdfjs/content/web**
--- a/.gitignore
+++ b/.gitignore
@@ -99,16 +99,20 @@ testing/web-platform/products/
 # Android Gradle artifacts.
 mobile/android/gradle/.gradle
 
 # XCode project cruft
 /*.xcodeproj/
 embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/project.xcworkspace/xcuserdata
 embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/xcuserdata
 
+# Rust port of mozbase are libraries
+testing/mozbase/rust/*/target
+testing/mozbase/rust/*/Cargo.lock
+
 # Ignore mozharness execution files
 testing/mozharness/.tox/
 testing/mozharness/build/
 testing/mozharness/logs/
 testing/mozharness/.coverage
 testing/mozharness/nosetests.xml
 
 # Ignore ESLint node_modules
--- a/.hgignore
+++ b/.hgignore
@@ -106,16 +106,20 @@ GPATH
 # Android Gradle artifacts.
 ^mobile/android/gradle/.gradle
 
 # XCode project cruft
 ^[^/]*\.xcodeproj/
 ^embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/project.xcworkspace/xcuserdata
 ^embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/xcuserdata
 
+# Rust port of mozbase are Rust libraries
+^testing/mozbase/rust/.*/target
+^testing/mozbase/rust/.*/Cargo.lock
+
 # Ignore mozharness execution files
 ^testing/mozharness/.tox/
 ^testing/mozharness/build/
 ^testing/mozharness/logs/
 ^testing/mozharness/.coverage
 ^testing/mozharness/nosetests.xml
 
 # Ignore tox generated dir
@@ -140,20 +144,20 @@ GPATH
 ^testing/talos/talos/tests/tp5n
 ^testing/talos/talos/tests/devtools/damp.manifest.develop
 ^talos-venv
 ^py3venv
 ^testing/talos/talos/mitmproxy/mitmdump
 ^testing/talos/talos/mitmproxy/mitmproxy
 ^testing/talos/talos/mitmproxy/mitmweb
 
-# Ignore talos speedometer files; source is copied from in-tree /third_party
-# into testing/talos/talos/tests/webkit/PerformanceTests/Speedometer when
-# talos speedometer test is run locally
-^testing/talos/talos/tests/webkit/PerformanceTests/Speedometer
+# Ignore talos webkit benchmark files; source is copied from in-tree /third_party
+# into testing/talos/talos/tests/webkit/PerformanceTests/ when run locally
+# i.e. speedometer, motionmark, stylebench
+^testing/talos/talos/tests/webkit/PerformanceTests
 
 # Ignore toolchains.json created by tooltool.
 ^toolchains\.json
 
 # Ignore files created when running a reftest.
 ^lextab.py$
 
 # tup database
--- a/.hgtags
+++ b/.hgtags
@@ -133,8 +133,9 @@ 1196bf3032e1bce1fb07a01fd9082a767426c5fb
 f80dc9fc34680105b714a49b4704bb843f5f7004 FIREFOX_AURORA_53_BASE
 6583496f169cd8a13c531ed16e98e8bf313eda8e FIREFOX_AURORA_54_BASE
 f9605772a0c9098ed1bcaa98089b2c944ed69e9b FIREFOX_BETA_55_BASE
 320642944e42a889db13c6c55b404e32319d4de6 FIREFOX_BETA_56_BASE
 8e818b5e9b6bef0fc1a5c527ecf30b0d56a02f14 FIREFOX_BETA_57_BASE
 f7e9777221a34f9f23c2e4933307eb38b621b679 FIREFOX_NIGHTLY_57_END
 40a14ca1cf04499f398e4cb8ba359b39eae4e216 FIREFOX_BETA_58_BASE
 1f91961bb79ad06fd4caef9e5dfd546afd5bf42c FIREFOX_NIGHTLY_58_END
+5faab9e619901b1513fd4ca137747231be550def FIREFOX_NIGHTLY_59_END
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1416465 - Old track files may cause problems if they have wildcards.
+Merge day clobber
--- a/accessible/base/ARIAMap.cpp
+++ b/accessible/base/ARIAMap.cpp
@@ -612,16 +612,47 @@ static const nsRoleMapEntry sWAIRoleMaps
     roles::FORM,
     kUseMapRole,
     eNoValue,
     eNoAction,
     eNoLiveAttr,
     eLandmark,
     kNoReqStates
   },
+  { // graphics-document
+    &nsGkAtoms::graphicsDocument,
+    roles::DOCUMENT,
+    kUseMapRole,
+    eNoValue,
+    eNoAction,
+    eNoLiveAttr,
+    kGenericAccType,
+    kNoReqStates,
+    eReadonlyUntilEditable
+  },
+  { // graphics-object
+    &nsGkAtoms::graphicsObject,
+    roles::GROUPING,
+    kUseMapRole,
+    eNoValue,
+    eNoAction,
+    eNoLiveAttr,
+    kGenericAccType,
+    kNoReqStates
+  },
+  { // graphics-symbol
+    &nsGkAtoms::graphicsSymbol,
+    roles::GRAPHIC,
+    kUseMapRole,
+    eNoValue,
+    eNoAction,
+    eNoLiveAttr,
+    kGenericAccType,
+    kNoReqStates
+  },
   { // grid
     &nsGkAtoms::grid,
     roles::TABLE,
     kUseMapRole,
     eNoValue,
     eNoAction,
     eNoLiveAttr,
     eSelect | eTable,
--- a/accessible/base/Logging.cpp
+++ b/accessible/base/Logging.cpp
@@ -829,17 +829,17 @@ logging::Node(const char* aDescr, nsINod
   }
 
   if (aNode->IsNodeOfType(nsINode::eDOCUMENT)) {
     printf("%s: %p, document\n", aDescr, static_cast<void*>(aNode));
     return;
   }
 
   nsINode* parentNode = aNode->GetParentNode();
-  int32_t idxInParent = parentNode ? parentNode->IndexOf(aNode) : - 1;
+  int32_t idxInParent = parentNode ? parentNode->ComputeIndexOf(aNode) : - 1;
 
   if (aNode->IsNodeOfType(nsINode::eTEXT)) {
     printf("%s: %p, text node, idx in parent: %d\n",
            aDescr, static_cast<void*>(aNode), idxInParent);
     return;
   }
 
   if (!aNode->IsElement()) {
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -18,17 +18,16 @@
 #include "RootAccessible.h"
 #include "TreeWalker.h"
 #include "xpcAccessibleDocument.h"
 
 #include "nsIMutableArray.h"
 #include "nsICommandManager.h"
 #include "nsIDocShell.h"
 #include "nsIDocument.h"
-#include "nsIDOMAttr.h"
 #include "nsIDOMCharacterData.h"
 #include "nsIDOMDocument.h"
 #include "nsIDOMXULDocument.h"
 #include "nsIDOMMutationEvent.h"
 #include "nsPIDOMWindow.h"
 #include "nsIEditingSession.h"
 #include "nsIFrame.h"
 #include "nsIInterfaceRequestorUtils.h"
--- a/accessible/generic/HyperTextAccessible.cpp
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -423,17 +423,17 @@ HyperTextAccessible::OffsetToDOMPoint(in
     innerOffset = 1;
   }
 
   // Case of embedded object. The point is either before or after the element.
   NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
   nsINode* node = child->GetNode();
   nsINode* parentNode = node->GetParentNode();
   return parentNode ?
-    DOMPoint(parentNode, parentNode->IndexOf(node) + innerOffset) :
+    DOMPoint(parentNode, parentNode->ComputeIndexOf(node) + innerOffset) :
     DOMPoint();
 }
 
 DOMPoint
 HyperTextAccessible::ClosestNotGeneratedDOMPoint(const DOMPoint& aDOMPoint,
                                                  nsIContent* aElementContent)
 {
   MOZ_ASSERT(aDOMPoint.node, "The node must not be null");
@@ -2076,17 +2076,17 @@ HyperTextAccessible::GetDOMPointByFrameO
     NS_ASSERTION(!aAccessible->IsDoc(),
                  "Shouldn't be called on document accessible!");
 
     nsIContent* content = aAccessible->GetContent();
     NS_ASSERTION(content, "Shouldn't operate on defunct accessible!");
 
     nsIContent* parent = content->GetParent();
 
-    aPoint->idx = parent->IndexOf(content) + 1;
+    aPoint->idx = parent->ComputeIndexOf(content) + 1;
     aPoint->node = parent;
 
   } else if (aFrame->IsTextFrame()) {
     nsIContent* content = aFrame->GetContent();
     NS_ENSURE_STATE(content);
 
     nsIFrame *primaryFrame = content->GetPrimaryFrame();
     nsresult rv = RenderedToContentOffset(primaryFrame, aOffset, &(aPoint->idx));
@@ -2096,17 +2096,17 @@ HyperTextAccessible::GetDOMPointByFrameO
 
   } else {
     nsIContent* content = aFrame->GetContent();
     NS_ENSURE_STATE(content);
 
     nsIContent* parent = content->GetParent();
     NS_ENSURE_STATE(parent);
 
-    aPoint->idx = parent->IndexOf(content);
+    aPoint->idx = parent->ComputeIndexOf(content);
     aPoint->node = parent;
   }
 
   return NS_OK;
 }
 
 // HyperTextAccessible
 void
--- a/accessible/tests/mochitest/attributes/a11y.ini
+++ b/accessible/tests/mochitest/attributes/a11y.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 support-files =
   !/accessible/tests/mochitest/*.js
 
 [test_dpub_aria_xml-roles.html]
+[test_graphics_aria_xml-roles.html]
 [test_obj.html]
 [test_obj_css.html]
 [test_obj_css.xul]
 [test_obj_group.html]
 [test_obj_group.xul]
 [test_obj_group_tree.xul]
 [test_tag.html]
 [test_xml-roles.html]
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>XML roles tests</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>
+  <script type="application/javascript"
+          src="../role.js"></script>
+  <script type="application/javascript"
+          src="../attributes.js"></script>
+
+  <script type="application/javascript">
+
+    function doTest() {
+      // Graphics ARIA roles should be exposed via the xml-roles object attribute.
+      let graphics_attrs = [
+        "graphics-document",
+        "graphics-object",
+        "graphics-symbol"
+      ];
+      for (let attr of graphics_attrs) {
+        testAttrs(attr, {"xml-roles": attr}, true);
+      }
+      SimpleTest.finish();
+    }
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTest);
+  </script>
+</head>
+<body>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432513"
+     title="implement ARIA Graphics roles">
+    Bug 1432513
+  </a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test"></pre>
+  <div id="graphics-document" role="graphics-document">document</div>
+  <div id="graphics-object" role="graphics-object">object</div>
+  <div id="graphics-symbol" role="graphics-symbol">symbol</div>
+</body>
+</html>
--- a/accessible/tests/mochitest/role/a11y.ini
+++ b/accessible/tests/mochitest/role/a11y.ini
@@ -3,9 +3,10 @@ support-files =
   !/accessible/tests/mochitest/*.js
   !/accessible/tests/mochitest/moz.png
 
 [test_aria.html]
 [test_aria.xul]
 [test_dpub_aria.html]
 [test_general.html]
 [test_general.xul]
+[test_graphics_aria.html]
 [test_svg.html]
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/role/test_graphics_aria.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Test Graphics ARIA roles</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>
+  <script type="application/javascript"
+          src="../role.js"></script>
+
+  <script type="application/javascript">
+
+    function doTest() {
+      // Graphics ARIA role map.
+      testRole("graphics-document", ROLE_DOCUMENT);
+      testRole("graphics-object", ROLE_GROUPING);
+      testRole("graphics-symbol", ROLE_GRAPHIC);
+      SimpleTest.finish();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTest);
+  </script>
+</head>
+<body>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432513"
+     title="implement ARIA Graphics roles">
+    Bug 1432513
+  </a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test"></pre>
+  <div id="graphics-document" role="graphics-document">document</div>
+  <div id="graphics-object" role="graphics-object">object</div>
+  <div id="graphics-symbol" role="graphics-symbol">symbol</div>
+</body>
+</html>
--- a/accessible/tests/mochitest/treeview.js
+++ b/accessible/tests/mochitest/treeview.js
@@ -102,17 +102,16 @@ nsTreeView.prototype =
     return info.parentIndex;
   },
   hasNextSibling: function hasNextSibling(aRowIndex, aAfterIndex) { },
   getLevel: function getLevel(aIndex) {
     var info = this.getInfoByIndex(aIndex);
     return info.level;
   },
   getImageSrc: function getImageSrc(aRow, aCol) {},
-  getProgressMode: function getProgressMode(aRow, aCol) {},
   isContainer: function isContainer(aIndex) {
     var data = this.getDataForIndex(aIndex);
     return data.open != undefined;
   },
   isContainerOpen: function isContainerOpen(aIndex) {
     var data = this.getDataForIndex(aIndex);
     return data.open;
   },
--- a/browser/app/Makefile.in
+++ b/browser/app/Makefile.in
@@ -48,16 +48,23 @@ ifdef COMPILE_ENVIRONMENT
 libs::
 	cp -p $(MOZ_APP_NAME)$(BIN_SUFFIX) $(DIST)/bin/$(MOZ_APP_NAME)-bin$(BIN_SUFFIX)
 endif
 
 GARBAGE += $(addprefix $(FINAL_TARGET)/defaults/pref/, firefox.js)
 
 endif
 
+# channel-prefs.js is handled separate from other prefs due to bug 756325
+# DO NOT change the content of channel-prefs.js without taking the appropriate
+# steps. See bug 1431342.
+libs:: $(srcdir)/profile/channel-prefs.js
+	$(NSINSTALL) -D $(DIST)/bin/defaults/pref
+	$(call py_action,preprocessor,-Fsubstitution $(PREF_PPFLAGS) $(ACDEFINES) $^ -o $(DIST)/bin/defaults/pref/channel-prefs.js)
+
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 
 MAC_APP_NAME = $(MOZ_APP_DISPLAYNAME)
 
 ifdef MOZ_DEBUG
 MAC_APP_NAME := $(MAC_APP_NAME)Debug
 endif
 
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -257,16 +257,20 @@
     </emItem>
     <emItem blockID="i862" id="{CA8C84C6-3918-41b1-BE77-049B2BDD887C}">
       <prefs>
         <pref>browser.startup.homepage</pref>
         <pref>browser.search.defaultenginename</pref>
       </prefs>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
+    <emItem blockID="5bf72f70-a611-4845-af3f-d4dabe8862b6" id="/^(\{1490068c-d8b7-4bd2-9621-a648942b312c\})|(\{d47ebc8a-c1ea-4a42-9ca3-f723fff034bd\})|(\{83d6f65c-7fc0-47d0-9864-a488bfcaa376\})|(\{e804fa4c-08e0-4dae-a237-8680074eba07\})|(\{ea618d26-780e-4f0f-91fd-2a6911064204\})|(\{ce93dcc7-f911-4098-8238-7f023dcdfd0d\})|(\{7eaf96aa-d4e7-41b0-9f12-775c2ac7f7c0\})|(\{b019c485-2a48-4f5b-be13-a7af94bc1a3e\})|(\{9b8a3057-8bf4-4a9e-b94b-867e4e71a50c\})|(\{eb3ebb14-6ced-4f60-9800-85c3de3680a4\})|(\{01f409a5-d617-47be-a574-d54325fe05d1\})$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
     <emItem blockID="i882" id="69ffxtbr@PackageTracer_69.com">
       <prefs>
         <pref>browser.startup.homepage</pref>
         <pref>browser.search.defaultenginename</pref>
       </prefs>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i706" id="thefoxonlybetter@quicksaver">
@@ -968,44 +972,24 @@
     <emItem blockID="i44" id="sigma@labs.mozilla">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i96" id="youtubeee@youtuber3.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
-    <emItem blockID="i564" id="/^(firefox@vebergreat\.net|EFGLQA@78ETGYN-0W7FN789T87\.COM)$/">
-      <prefs/>
-      <versionRange minVersion="0" maxVersion="*" severity="1"/>
-    </emItem>
-    <emItem blockID="i500" id="{2aab351c-ad56-444c-b935-38bffe18ad26}">
-      <prefs/>
-      <versionRange minVersion="0" maxVersion="*" severity="3"/>
-    </emItem>
     <emItem blockID="i97" id="support3_en@adobe122.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
-    <emItem blockID="i439" id="{d2cf9842-af95-48cd-b873-bfbb48cd7f5e}">
-      <prefs/>
-      <versionRange minVersion="0" maxVersion="*" severity="1"/>
-    </emItem>
     <emItem blockID="i576" id="newmoz@facebook.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
-    <emItem blockID="i46" id="{841468a1-d7f4-4bd3-84e6-bb0f13a06c64}">
-      <prefs/>
-      <versionRange minVersion="0.1" maxVersion="*" severity="1">
-        <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
-          <versionRange maxVersion="9.0" minVersion="9.0a1"/>
-        </targetApplication>
-      </versionRange>
-    </emItem>
     <emItem blockID="i776" id="g@uzcERQ6ko.net">
       <prefs>
         <pref>browser.startup.homepage</pref>
         <pref>browser.search.defaultenginename</pref>
       </prefs>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i494" id="/^({e9df9360-97f8-4690-afe6-996c80790da4}|{687578b9-7132-4a7a-80e4-30ee31099e03}|{46a3135d-3683-48cf-b94c-82655cbc0e8a}|{49c795c2-604a-4d18-aeb1-b3eba27e5ea2}|{7473b6bd-4691-4744-a82b-7854eb3d70b6}|{96f454ea-9d38-474f-b504-56193e00c1a5})$/">
@@ -1057,16 +1041,20 @@
         </targetApplication>
       </versionRange>
       <versionRange minVersion="1.5.7.5" maxVersion="1.5.7.5" severity="1"/>
     </emItem>
     <emItem blockID="i515" id="/^({bf9194c2-b86d-4ebc-9b53-1c08b6ff779e}|{61a83e16-7198-49c6-8874-3e4e8faeb4f3}|{f0af464e-5167-45cf-9cf0-66b396d1918c}|{5d9968c3-101c-4944-ba71-72d77393322d}|{01e86e69-a2f8-48a0-b068-83869bdba3d0})$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
+    <emItem blockID="4ca8206f-bc2a-4428-9439-7f3142dc08db" id="/^(\{ac06c6b2-3fd6-45ee-9237-6235aa347215\})|(\{d461cc1b-8a36-4ff0-b330-1824c148f326\})|(\{d1ab5ebd-9505-481d-a6cd-6b9db8d65977\})|(\{07953f60-447e-4f53-a5ef-ed060487f616\})|(\{2d3c5a5a-8e6f-4762-8aff-b24953fe1cc9\})|(\{f82b3ad5-e590-4286-891f-05adf5028d2f\})|(\{f96245ad-3bb0-46c5-8ca9-2917d69aa6ca\})|(\{2f53e091-4b16-4b60-9cae-69d0c55b2e78\})|(\{18868c3a-a209-41a6-855d-f99f782d1606\})|(\{47352fbf-80d9-4b70-9398-fb7bffa3da53\})$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
     <emItem blockID="i596" id="{b99c8534-7800-48fa-bd71-519a46cdc7e1}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i461" id="{8E9E3331-D360-4f87-8803-52DE43566502}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
@@ -1253,16 +1241,20 @@
           <versionRange maxVersion="*" minVersion="56.0a1"/>
         </targetApplication>
       </versionRange>
     </emItem>
     <emItem blockID="i1034" id="a88a77ahjjfjakckmmabsy278djasi@jetpack">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="8088b39a-3e6d-4a17-a22f-3f95c0464bd6" id="{5b0f6d3c-10fd-414c-a135-dffd26d7de0f}">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
     <emItem blockID="9abc7502-bd6f-40d7-b035-abe721345360" id="{368eb817-31b4-4be9-a761-b67598faf9fa}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="i562" id="iobitapps@mybrowserbar.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
@@ -1507,16 +1499,20 @@
     <emItem blockID="i852" id="6lIy@T.edu">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i856" id="/^({94d62e35-4b43-494c-bf52-ba5935df36ef}|firefox@advanceelite\.com|{bb7b7a60-f574-47c2-8a0b-4c56f2da9802})$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="9dfeee42-e6a8-49e0-8979-0648f7368239" id="/^({fce89242-66d3-4946-9ed0-e66078f172fc})|({0c4df994-4f4a-4646-ae5d-8936be8a4188})|({6cee30bc-a27c-43ea-ac72-302862db62b2})|({e08ebf0b-431d-4ed1-88bb-02e5db8b9443})$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
     <emItem blockID="i520" id="/^({7316e43a-3ebd-4bb4-95c1-9caf6756c97f}|{0cc09160-108c-4759-bab1-5c12c216e005}|{ef03e721-f564-4333-a331-d4062cee6f2b}|{465fcfbb-47a4-4866-a5d5-d12f9a77da00}|{7557724b-30a9-42a4-98eb-77fcb0fd1be3}|{b7c7d4b0-7a84-4b73-a7ef-48ef59a52c3b})$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="f7569261-f575-4719-8202-552b20d013b0" id="{7e907a15-0a4c-4ff4-b64f-5eeb8f841349}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
@@ -2150,16 +2146,36 @@
     <emItem blockID="i117" id="{ce7e73df-6a44-4028-8079-5927a588c948}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="1.0.8" severity="1"/>
     </emItem>
     <emItem blockID="i258" id="helperbar@helperbar.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="1.0" severity="1"/>
     </emItem>
+    <emItem blockID="i564" id="/^(firefox@vebergreat\.net|EFGLQA@78ETGYN-0W7FN789T87\.COM)$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="1"/>
+    </emItem>
+    <emItem blockID="i500" id="{2aab351c-ad56-444c-b935-38bffe18ad26}">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="i439" id="{d2cf9842-af95-48cd-b873-bfbb48cd7f5e}">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="1"/>
+    </emItem>
+    <emItem blockID="i46" id="{841468a1-d7f4-4bd3-84e6-bb0f13a06c64}">
+      <prefs/>
+      <versionRange minVersion="0.1" maxVersion="*" severity="1">
+        <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+          <versionRange maxVersion="9.0" minVersion="9.0a1"/>
+        </targetApplication>
+      </versionRange>
+    </emItem>
   </emItems>
   <pluginItems>
     <pluginItem blockID="p416">
       <match exp="JavaAppletPlugin\.plugin" name="filename"/>
       <versionRange maxVersion="Java 6 Update 45" minVersion="Java 6 Update 42" severity="0" vulnerabilitystatus="1">
         <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
           <versionRange maxVersion="*" minVersion="17.0"/>
         </targetApplication>
@@ -4425,16 +4441,19 @@
       <serialNumber>JD1wxDd8IgmiqX7MyPPg1g==</serialNumber>
     </certItem>
     <certItem issuerName="MDsxGDAWBgNVBAoTD0N5YmVydHJ1c3QsIEluYzEfMB0GA1UEAxMWQ3liZXJ0cnVzdCBHbG9iYWwgUm9vdA==">
       <serialNumber>BAAAAAABQaHhNLo=</serialNumber>
     </certItem>
     <certItem issuerName="MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMw==">
       <serialNumber>AwBGo0Zmp6KRryAguuMvXATI</serialNumber>
     </certItem>
+    <certItem issuerName="MHIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJUWDEQMA4GA1UEBxMHSG91c3RvbjEVMBMGA1UEChMMY1BhbmVsLCBJbmMuMS0wKwYDVQQDEyRjUGFuZWwsIEluYy4gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHk=">
+      <serialNumber>AJk3QFH13eHUHHVnsvwS0Vo=</serialNumber>
+    </certItem>
     <certItem issuerName="MG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3Q=">
       <serialNumber>U3t2Vk8pfxTcaUPpIq0seQ==</serialNumber>
     </certItem>
     <certItem issuerName="MEQxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMR0wGwYDVQQDExRHZW9UcnVzdCBTU0wgQ0EgLSBHMw==">
       <serialNumber>bx/XHJqcwxDOptxJ2lh5vw==</serialNumber>
     </certItem>
     <certItem issuerName="MHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWU=">
       <serialNumber>LU4d0t7PAsZNgJGZcb+o/w==</serialNumber>
--- a/browser/app/moz.build
+++ b/browser/app/moz.build
@@ -38,19 +38,16 @@ GeckoProgram(CONFIG['MOZ_APP_NAME'])
 
 SOURCES += [
     'nsBrowserApp.cpp',
 ]
 
 # Neither channel-prefs.js nor firefox.exe want to end up in dist/bin/browser.
 DIST_SUBDIR = ""
 
-# channel-prefs.js is handled separate from other prefs due to bug 756325
-JS_PREFERENCE_PP_FILES += ['profile/channel-prefs.js']
-
 LOCAL_INCLUDES += [
     '!/build',
     '/toolkit/xre',
     '/xpcom/base',
     '/xpcom/build',
 ]
 
 if CONFIG['LIBFUZZER']:
--- a/browser/app/profile/channel-prefs.js
+++ b/browser/app/profile/channel-prefs.js
@@ -1,6 +1,5 @@
 /* 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
 pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@");
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -910,19 +910,16 @@ pref("browser.sessionstore.cleanup.forge
 // Maximum number of bytes of DOMSessionStorage data we collect per origin.
 pref("browser.sessionstore.dom_storage_limit", 2048);
 // Amount of failed SessionFile writes until we restart the worker.
 pref("browser.sessionstore.max_write_failures", 5);
 
 // allow META refresh by default
 pref("accessibility.blockautorefresh", false);
 
-// Whether useAsyncTransactions is enabled or not.
-pref("browser.places.useAsyncTransactions", true);
-
 // Whether history is enabled or not.
 pref("places.history.enabled", true);
 
 // the (maximum) number of the recent visits to sample
 // when calculating frecency
 pref("places.frecency.numVisits", 10);
 
 // buckets (in days) for frecency calculation
@@ -1091,28 +1088,29 @@ pref("security.sandbox.content.level", 3
 
 #if defined(XP_LINUX) && defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX)
 // This pref is introduced as part of bug 742434, the naming is inspired from
 // its Windows/Mac counterpart, but on Linux it's an integer which means:
 // 0 -> "no sandbox"
 // 1 -> "content sandbox using seccomp-bpf when available"
 // 2 -> "seccomp-bpf + write file broker"
 // 3 -> "seccomp-bpf + read/write file brokering"
+// 4 -> all of the above + network/socket restrictions
 // Content sandboxing on Linux is currently in the stage of
 // 'just getting it enabled', which includes a very permissive whitelist. We
 // enable seccomp-bpf on nightly to see if everything is running, or if we need
 // to whitelist more system calls.
 //
 // So the purpose of this setting is to allow nightly users to disable the
 // sandbox while we fix their problems. This way, they won't have to wait for
 // another nightly release which disables seccomp-bpf again.
 //
 // This setting may not be required anymore once we decide to permanently
 // enable the content sandbox.
-pref("security.sandbox.content.level", 3);
+pref("security.sandbox.content.level", 4);
 pref("security.sandbox.content.write_path_whitelist", "");
 pref("security.sandbox.content.read_path_whitelist", "");
 pref("security.sandbox.content.syscall_whitelist", "");
 #endif
 
 #if defined(XP_MACOSX) || defined(XP_WIN)
 #if defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX)
 // ID (a UUID when set by gecko) that is used to form the name of a
@@ -1181,16 +1179,17 @@ pref("services.sync.prefs.sync.browser.s
 pref("services.sync.prefs.sync.browser.search.update", true);
 pref("services.sync.prefs.sync.browser.sessionstore.restore_on_demand", true);
 pref("services.sync.prefs.sync.browser.startup.homepage", true);
 pref("services.sync.prefs.sync.browser.startup.page", true);
 pref("services.sync.prefs.sync.browser.tabs.loadInBackground", true);
 pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true);
 pref("services.sync.prefs.sync.browser.tabs.warnOnOpen", true);
 pref("services.sync.prefs.sync.browser.urlbar.autocomplete.enabled", true);
+pref("services.sync.prefs.sync.browser.urlbar.matchBuckets", true);
 pref("services.sync.prefs.sync.browser.urlbar.maxRichResults", true);
 pref("services.sync.prefs.sync.browser.urlbar.suggest.bookmark", true);
 pref("services.sync.prefs.sync.browser.urlbar.suggest.history", true);
 pref("services.sync.prefs.sync.browser.urlbar.suggest.history.onlyTyped", true);
 pref("services.sync.prefs.sync.browser.urlbar.suggest.openpage", true);
 pref("services.sync.prefs.sync.browser.urlbar.suggest.searches", true);
 pref("services.sync.prefs.sync.dom.disable_open_during_load", true);
 pref("services.sync.prefs.sync.dom.disable_window_flip", true);
@@ -1336,16 +1335,20 @@ pref("security.insecure_password.ui.enab
 
 // Show in-content login form warning UI for insecure login fields
 pref("security.insecure_field_warning.contextual.enabled", true);
 
 // Show degraded UI for http pages; disabled for now
 pref("security.insecure_connection_icon.enabled", false);
 pref("security.insecure_connection_icon.pbmode.enabled", false);
 
+// Show "Not Secure" text for http pages; disabled for now
+pref("security.insecure_connection_text.enabled", false);
+pref("security.insecure_connection_text.pbmode.enabled", false);
+
 // 1 = allow MITM for certificate pinning checks.
 pref("security.cert_pinning.enforcement_level", 1);
 
 
 // Override the Gecko-default value of false for Firefox.
 pref("plain_text.wrap_long_lines", true);
 
 // If this turns true, Moz*Gesture events are not called stopPropagation()
@@ -1725,17 +1728,23 @@ pref("extensions.formautofill.creditCard
 // 2: saw the doorhanger
 // 3: submitted an autofill'ed credit card form
 pref("extensions.formautofill.creditCards.used", 0);
 pref("extensions.formautofill.firstTimeUse", true);
 pref("extensions.formautofill.heuristics.enabled", true);
 pref("extensions.formautofill.section.enabled", true);
 pref("extensions.formautofill.loglevel", "Warn");
 // Comma separated list of countries Form Autofill supports
+#ifdef MOZ_UPDATE_CHANNEL == release
+pref("extensions.formautofill.supportedCountries", "US");
+pref("extensions.formautofill.supportRTL", false);
+#else
 pref("extensions.formautofill.supportedCountries", "US,CA,DE");
+pref("extensions.formautofill.supportRTL", true);
+#endif
 
 // Whether or not to restore a session with lazy-browser tabs.
 pref("browser.sessionstore.restore_tabs_lazily", true);
 
 pref("browser.suppress_first_window_animation", true);
 
 // Preferences for Photon onboarding system extension
 pref("browser.onboarding.enabled", true);
--- a/browser/base/content/browser-media.js
+++ b/browser/base/content/browser-media.js
@@ -29,21 +29,29 @@ var gEMEHandler = {
       return false;
     }
     if (keySystem == "com.widevine.alpha" &&
         Services.prefs.getPrefType("media.gmp-widevinecdm.visible")) {
       return Services.prefs.getBoolPref("media.gmp-widevinecdm.visible");
     }
     return true;
   },
-  getLearnMoreLink(msgId) {
-    let text = gNavigatorBundle.getString("emeNotifications." + msgId + ".learnMoreLabel");
+  getEMEDisabledFragment(msgId) {
+    let mainMessage = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.message");
+    let text = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.learnMoreLabel");
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
-    return "<label class='text-link' href='" + baseURL + "drm-content'>" +
-           text + "</label>";
+    let link = document.createElement("label");
+    link.className = "text-link";
+    link.setAttribute("href", baseURL + "drm-content");
+    link.textContent = text;
+    return BrowserUtils.getLocalizedFragment(document, mainMessage, link);
+  },
+  getMessageWithBrandName(notificationId) {
+    let msgId = "emeNotifications." + notificationId + ".message";
+    return gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]);
   },
   receiveMessage({target: browser, data: data}) {
     let parsedData;
     try {
       parsedData = JSON.parse(data);
     } catch (ex) {
       Cu.reportError("Malformed EME video message with data: " + data);
       return;
@@ -51,91 +59,75 @@ var gEMEHandler = {
     let {status: status, keySystem: keySystem} = parsedData;
     // Don't need to show if disabled or keysystem not visible.
     if (!this.uiEnabled || !this.isKeySystemVisible(keySystem)) {
       return;
     }
 
     let notificationId;
     let buttonCallback;
-    let params = [];
+    // Notification message can be either a string or a DOM fragment.
+    let notificationMessage;
     switch (status) {
       case "available":
       case "cdm-created":
         // Only show the chain icon for proprietary CDMs. Clearkey is not one.
         if (keySystem != "org.w3.clearkey") {
           this.showPopupNotificationForSuccess(browser, keySystem);
         }
         // ... and bail!
         return;
 
       case "api-disabled":
       case "cdm-disabled":
         notificationId = "drmContentDisabled";
         buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem);
-        params = [this.getLearnMoreLink(notificationId)];
+        notificationMessage = this.getEMEDisabledFragment();
         break;
 
       case "cdm-insufficient-version":
         notificationId = "drmContentCDMInsufficientVersion";
-        params = [this._brandShortName];
+        notificationMessage = this.getMessageWithBrandName(notificationId);
         break;
 
       case "cdm-not-installed":
         notificationId = "drmContentCDMInstalling";
-        params = [this._brandShortName];
+        notificationMessage = this.getMessageWithBrandName(notificationId);
         break;
 
       case "cdm-not-supported":
         // Not to pop up user-level notification because they cannot do anything
         // about it.
         return;
       default:
         Cu.reportError(new Error("Unknown message ('" + status + "') dealing with EME key request: " + data));
         return;
     }
 
-    this.showNotificationBar(browser, notificationId, keySystem, params, buttonCallback);
-  },
-  showNotificationBar(browser, notificationId, keySystem, labelParams, callback) {
+    // Now actually create the notification
+
     let box = gBrowser.getNotificationBox(browser);
     if (box.getNotificationWithValue(notificationId)) {
       return;
     }
 
-    let msgPrefix = "emeNotifications." + notificationId + ".";
-    let msgId = msgPrefix + "message";
-
-    let message = labelParams.length ?
-                  gNavigatorBundle.getFormattedString(msgId, labelParams) :
-                  gNavigatorBundle.getString(msgId);
-
     let buttons = [];
-    if (callback) {
+    if (buttonCallback) {
+      let msgPrefix = "emeNotifications." + notificationId + ".";
       let btnLabelId = msgPrefix + "button.label";
       let btnAccessKeyId = msgPrefix + "button.accesskey";
       buttons.push({
         label: gNavigatorBundle.getString(btnLabelId),
         accessKey: gNavigatorBundle.getString(btnAccessKeyId),
-        callback
+        callback: buttonCallback,
       });
     }
 
     let iconURL = "chrome://browser/skin/drm-icon.svg";
-
-    // Do a little dance to get rich content into the notification:
-    let fragment = document.createDocumentFragment();
-    let descriptionContainer = document.createElement("description");
-    // eslint-disable-next-line no-unsanitized/property
-    descriptionContainer.innerHTML = message;
-    while (descriptionContainer.childNodes.length) {
-      fragment.appendChild(descriptionContainer.childNodes[0]);
-    }
-
-    box.appendNotification(fragment, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM,
+    box.appendNotification(notificationMessage, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM,
                            buttons);
   },
   showPopupNotificationForSuccess(browser, keySystem) {
     // We're playing EME content! Remove any "we can't play because..." messages.
     var box = gBrowser.getNotificationBox(browser);
     ["drmContentDisabled",
      "drmContentCDMInstalling"
      ].forEach(function(value) {
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -6,18 +6,16 @@
 /* eslint-env mozilla/browser-window */
 
 XPCOMUtils.defineLazyScriptGetter(this, ["PlacesToolbar", "PlacesMenu",
                                          "PlacesPanelview", "PlacesPanelMenuView"],
                                   "chrome://browser/content/places/browserPlacesViews.js");
 
 var StarUI = {
   _itemGuids: null,
-  // TODO (bug 1131491): _itemIdsMap is only used for the old transactions manager.
-  _itemIdsMap: null,
   _batching: false,
   _isNewBookmark: false,
   _isComposing: false,
   _autoCloseTimer: 0,
   // The autoclose timer is diasbled if the user interacts with the
   // popup, such as making a change through typing or clicking on
   // the popup.
   _autoCloseTimerEnabled: true,
@@ -91,46 +89,30 @@ var StarUI = {
 
           if (this._anchorToolbarButton) {
             this._anchorToolbarButton.removeAttribute("open");
             this._anchorToolbarButton = null;
           }
           this._restoreCommandsState();
           let removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
           this._removeBookmarksOnPopupHidden = false;
-          let idsForRemoval = this._itemIdsMap;
           let guidsForRemoval = this._itemGuids;
           this._itemGuids = null;
-          this._itemIdsMap = null;
 
           if (this._batching) {
             this.endBatch();
           }
 
           if (removeBookmarksOnPopupHidden && guidsForRemoval) {
             if (this._isNewBookmark) {
-              if (!PlacesUIUtils.useAsyncTransactions) {
-                PlacesUtils.transactionManager.undoTransaction();
-                break;
-              }
               PlacesTransactions.undo().catch(Cu.reportError);
               break;
             }
             // Remove all bookmarks for the bookmark's url, this also removes
             // the tags for the url.
-            if (!PlacesUIUtils.useAsyncTransactions) {
-              if (idsForRemoval) {
-                for (let itemId of idsForRemoval.values()) {
-                  let txn = new PlacesRemoveItemTransaction(itemId);
-                  PlacesUtils.transactionManager.doTransaction(txn);
-                }
-              }
-              break;
-            }
-
             PlacesTransactions.Remove(guidsForRemoval)
                               .transact().catch(Cu.reportError);
           } else if (this._isNewBookmark) {
             LibraryUI.triggerLibraryAnimation("bookmark");
           }
         }
         break;
       }
@@ -225,17 +207,16 @@ var StarUI = {
     // Slow double-clicks (not true double-clicks) shouldn't
     // cause the panel to flicker.
     if (this.panel.state == "showing" ||
         this.panel.state == "open") {
       return;
     }
 
     this._isNewBookmark = aIsNewBookmark;
-    this._itemIdsMap = null;
     this._itemGuids = null;
     // TODO (bug 1131491): Deprecate this once async transactions are enabled
     // and the legacy transactions code is gone.
     if (typeof(aNode) == "number") {
       let itemId = aNode;
       let guid = await PlacesUtils.promiseItemGuid(itemId);
       aNode = await PlacesUIUtils.fetchNodeLike(guid);
     }
@@ -286,19 +267,16 @@ var StarUI = {
 
     // The label of the remove button differs if the URI is bookmarked
     // multiple times.
     this._itemGuids = [];
 
     await PlacesUtils.bookmarks.fetch({url: aUrl},
       bookmark => this._itemGuids.push(bookmark.guid));
 
-    if (!PlacesUIUtils.useAsyncTransactions) {
-      this._itemIdsMap = await PlacesUtils.promiseManyItemIds(this._itemGuids);
-    }
     let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
     let bookmarksCount = this._itemGuids.length;
     let label = PluralForm.get(bookmarksCount, forms)
                           .replace("#1", bookmarksCount);
     this._element("editBookmarkPanelRemoveButton").label = label;
 
     this.beginBatch();
 
@@ -360,37 +338,29 @@ var StarUI = {
   // editBookmarkOverlay so that all of the actions done in the panel
   // are treated by PlacesTransactions as a single batch.  To do so,
   // we start a PlacesTransactions batch when the star UI panel is shown, and
   // we keep the batch ongoing until the panel is hidden.
   _batchBlockingDeferred: null,
   beginBatch() {
     if (this._batching)
       return;
-    if (PlacesUIUtils.useAsyncTransactions) {
-      this._batchBlockingDeferred = PromiseUtils.defer();
-      PlacesTransactions.batch(async () => {
-        await this._batchBlockingDeferred.promise;
-      });
-    } else {
-      PlacesUtils.transactionManager.beginBatch(null);
-    }
+    this._batchBlockingDeferred = PromiseUtils.defer();
+    PlacesTransactions.batch(async () => {
+      await this._batchBlockingDeferred.promise;
+    });
     this._batching = true;
   },
 
   endBatch() {
     if (!this._batching)
       return;
 
-    if (PlacesUIUtils.useAsyncTransactions) {
-      this._batchBlockingDeferred.resolve();
-      this._batchBlockingDeferred = null;
-    } else {
-      PlacesUtils.transactionManager.endBatch(false);
-    }
+    this._batchBlockingDeferred.resolve();
+    this._batchBlockingDeferred = null;
     this._batching = false;
   }
 };
 
 // Checks if an element is visible without flushing layout changes.
 function isVisible(element) {
   let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIDOMWindowUtils);
@@ -408,85 +378,16 @@ var PlacesCommandHook = {
    *        whether or not to show the edit-bookmark UI for the bookmark item
    * @param [optional] aUrl
    *        Option to provide a URL to bookmark rather than the current page
    * @param [optional] aTitle
    *        Option to provide a title for a bookmark to use rather than the
    *        getting the current page's title
    */
   async bookmarkPage(aBrowser, aShowEditUI, aUrl = null, aTitle = null) {
-    if (PlacesUIUtils.useAsyncTransactions) {
-      await this._bookmarkPagePT(aBrowser, aShowEditUI, aUrl, aTitle);
-      return;
-    }
-
-    // If aUrl is provided, we want to bookmark that url rather than the
-    // the current page
-    var uri = aUrl ? Services.io.newURI(aUrl) : aBrowser.currentURI;
-    var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
-    let isNewBookmark = itemId == -1;
-    if (isNewBookmark) {
-      // Bug 1148838 - Make this code work for full page plugins.
-      var title;
-      var description;
-      var charset;
-
-      let docInfo = aUrl ? {} : await this._getPageDetails(aBrowser);
-
-      try {
-        title = aTitle ||
-                (docInfo.isErrorPage ? PlacesUtils.history.getPageTitle(uri)
-                                     : aBrowser.contentTitle) ||
-                uri.displaySpec;
-        description = docInfo.description;
-        charset = aUrl ? null : aBrowser.characterSet;
-      } catch (e) { }
-
-      if (aShowEditUI) {
-        // If we bookmark the page here but open right into a cancelable
-        // state (i.e. new bookmark in Library), start batching here so
-        // all of the actions can be undone in a single undo step.
-        StarUI.beginBatch();
-      }
-
-      var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
-      var txn = new PlacesCreateBookmarkTransaction(uri,
-                                                    PlacesUtils.unfiledBookmarksFolderId,
-                                                    PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                                    title, null, [descAnno]);
-      PlacesUtils.transactionManager.doTransaction(txn);
-      itemId = txn.item.id;
-      // Set the character-set.
-      if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
-        PlacesUtils.setCharsetForURI(uri, charset);
-    }
-
-    // Revert the contents of the location bar
-    gURLBar.handleRevert();
-
-    // If it was not requested to open directly in "edit" mode, we are done.
-    if (!aShowEditUI)
-      return;
-
-    let anchor = BookmarkingUI.anchor;
-    if (anchor) {
-      await StarUI.showEditBookmarkPopup(itemId, anchor,
-                                         "bottomcenter topright", isNewBookmark,
-                                         uri);
-      return;
-    }
-
-    // Fall back to showing the panel over the content area.
-    await StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap",
-                                       isNewBookmark, uri);
-  },
-
-  // TODO: Replace bookmarkPage code with this function once legacy
-  // transactions are removed.
-  async _bookmarkPagePT(aBrowser, aShowEditUI, aUrl, aTitle) {
     // If aUrl is provided, we want to bookmark that url rather than the
     // the current page
     let url = aUrl ? new URL(aUrl) : new URL(aBrowser.currentURI.spec);
     let info = await PlacesUtils.bookmarks.fetch({ url });
     let isNewBookmark = !info;
     if (!info) {
       let parentGuid = PlacesUtils.bookmarks.unfiledGuid;
       info = { url, parentGuid };
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -160,17 +160,17 @@
 
     <!-- Sync broadcasters -->
     <!-- A broadcaster of a number of attributes suitable for "sync now" UI -
         A 'syncstatus' attribute is set while actively syncing, and the label
         attribute which changes from "sync now" to "syncing" etc. -->
     <broadcaster id="sync-status"/>
     <!-- broadcasters of the "hidden" attribute to reflect setup state for
          menus -->
-    <broadcaster id="sync-setup-state"/>
+    <broadcaster id="sync-setup-state" hidden="true"/>
     <broadcaster id="sync-unverified-state" hidden="true"/>
     <broadcaster id="sync-syncnow-state" hidden="true"/>
     <broadcaster id="sync-reauth-state" hidden="true"/>
     <broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;"
                  type="checkbox" group="sidebar"
                  sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml"
                  oncommand="SidebarUI.toggle('viewTabsSidebar');"/>
     <broadcaster id="workOfflineMenuitemState"/>
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -117,18 +117,22 @@ var gSync = {
     for (let topic of this._obs) {
       Services.obs.addObserver(this, topic, true);
     }
 
     this._generateNodeGetters();
     this._definePrefGetters();
 
     // initial label for the sync buttons.
-    let broadcaster = document.getElementById("sync-status");
-    broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
+    let statusBroadcaster = document.getElementById("sync-status");
+    statusBroadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
+    // We start with every broadcasters hidden, so that we don't need to init
+    // the sync UI on windows like pageInfo.xul (see bug 1384856).
+    let setupBroadcaster = document.getElementById("sync-setup-state");
+    setupBroadcaster.hidden = false;
 
     this._maybeUpdateUIState();
 
     EnsureFxAccountsWebChannel();
 
     this._initialized = true;
   },
 
@@ -171,16 +175,21 @@ var gSync = {
   updateAllUI(state) {
     this.updatePanelPopup(state);
     this.updateStateBroadcasters(state);
     this.updateSyncButtonsTooltip(state);
     this.updateSyncStatus(state);
   },
 
   updatePanelPopup(state) {
+    // Some windows (e.g. places.xul) won't contain the panel UI, so we can
+    // abort immediately for those (bug 1384856).
+    if (!this.appMenuContainer) {
+      return;
+    }
     let defaultLabel = this.appMenuStatus.getAttribute("defaultlabel");
     // The localization string is for the signed in text, but it's the default text as well
     let defaultTooltiptext = this.appMenuStatus.getAttribute("signedinTooltiptext");
 
     const status = state.status;
     // Reset the status bar to its original state.
     this.appMenuLabel.setAttribute("label", defaultLabel);
     this.appMenuStatus.setAttribute("tooltiptext", defaultTooltiptext);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2877,18 +2877,16 @@ function UpdatePopupNotificationsVisibil
 function PageProxyClickHandler(aEvent) {
   if (aEvent.button == 1 && Services.prefs.getBoolPref("middlemouse.paste"))
     middleMousePaste(aEvent);
 }
 
 // Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED   = 2;
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
-const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND    = 4;
-const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND      = 5;
 
 const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
 
 /**
  * Handle command events bubbling up from error page content
  * or from about:newtab or from remote error pages that invoke
  * us via async messaging.
  */
@@ -6588,17 +6586,23 @@ var CanvasPermissionPromptHelper = {
     }
 
     let uri = Services.io.newURI(aData);
     if (gBrowser.selectedBrowser !== browser) {
       // Must belong to some other window.
       return;
     }
 
-    let message = gNavigatorBundle.getFormattedString("canvas.siteprompt", [ uri.asciiHost ]);
+    let message = {};
+    let header = gNavigatorBundle.getFormattedString("canvas.siteprompt", ["<>"], 1);
+
+    header = header.split("<>");
+    message.start = header[0];
+    message.host = uri.asciiHost;
+    message.end = header[1];
 
     function setCanvasPermission(aURI, aPerm, aPersistent) {
       Services.perms.add(aURI, "canvas", aPerm,
                           aPersistent ? Ci.nsIPermissionManager.EXPIRE_NEVER
                                       : Ci.nsIPermissionManager.EXPIRE_SESSION);
     }
 
     let mainAction = {
@@ -7508,18 +7512,25 @@ var gIdentityHandler = {
         } else {
           this._identityBox.classList.add("weakCipher");
         }
       } else {
         let warnOnInsecure = Services.prefs.getBoolPref("security.insecure_connection_icon.enabled") ||
                              (Services.prefs.getBoolPref("security.insecure_connection_icon.pbmode.enabled") &&
                              PrivateBrowsingUtils.isWindowPrivate(window));
         let className = warnOnInsecure ? "notSecure" : "unknownIdentity";
-
         this._identityBox.className = className;
+
+        let warnTextOnInsecure = Services.prefs.getBoolPref("security.insecure_connection_text.enabled") ||
+                                 (Services.prefs.getBoolPref("security.insecure_connection_text.pbmode.enabled") &&
+                                 PrivateBrowsingUtils.isWindowPrivate(window));
+        if (warnTextOnInsecure) {
+          icon_label = gNavigatorBundle.getString("identity.notSecure.label");
+          this._identityBox.classList.add("notSecureText");
+        }
       }
       if (this._hasInsecureLoginForms) {
         // Insecure login forms can only be present on "unknown identity"
         // pages, either already insecure or with mixed active content loaded.
         this._identityBox.classList.add("insecureLoginForms");
       }
     }
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -591,19 +591,16 @@
       <hbox class="private-browsing-indicator"/>
       <hbox id="titlebar-fullscreen-button"/>
     </hbox>
 #endif
   </hbox>
 </vbox>
 #endif
 
-<deck flex="1" id="tab-view-deck">
-<vbox flex="1" id="browser-panel">
-
   <toolbox id="navigator-toolbox">
     <!-- Menu -->
     <toolbar type="menubar" id="toolbar-menubar" class="chromeclass-menubar" customizable="true"
              mode="icons"
 #ifdef MENUBAR_CAN_AUTOHIDE
              toolbarname="&menubarCmd.label;"
              accesskey="&menubarCmd.accesskey;"
              autohide="true"
@@ -1233,15 +1230,9 @@
       &pointerlockWarning.generic.label;
     </html:div>
   </html:div>
 
   <vbox id="browser-bottombox" layer="true">
     <notificationbox id="global-notificationbox" notificationside="bottom"/>
   </vbox>
 
-</vbox>
-# <iframe id="tab-view"> is dynamically appended as the 2nd child of #tab-view-deck.
-#     Introducing the iframe dynamically, as needed, was found to be better than
-#     starting with an empty iframe here in browser.xul from a Ts standpoint.
-</deck>
-
 </window>
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -62,22 +62,16 @@ addEventListener("pageshow", function(ev
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 addEventListener("blur", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 
-// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
-const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0;
-const TLS_ERROR_REPORT_TELEMETRY_EXPANDED = 1;
-const TLS_ERROR_REPORT_TELEMETRY_SUCCESS  = 6;
-const TLS_ERROR_REPORT_TELEMETRY_FAILURE  = 7;
-
 const SEC_ERROR_BASE          = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
 const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
 
 const SEC_ERROR_EXPIRED_CERTIFICATE                = SEC_ERROR_BASE + 11;
 const SEC_ERROR_UNKNOWN_ISSUER                     = SEC_ERROR_BASE + 13;
 const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE         = SEC_ERROR_BASE + 30;
 const SEC_ERROR_OCSP_FUTURE_RESPONSE               = SEC_ERROR_BASE + 131;
 const SEC_ERROR_OCSP_OLD_RESPONSE                  = SEC_ERROR_BASE + 132;
@@ -404,16 +398,19 @@ var AboutNetAndCertErrorListener = {
         return true;
       }
     }
 
     return false;
   },
 
   onPageLoad(evt) {
+    // Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
+    const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0;
+
     if (this.isAboutCertError) {
       let originalTarget = evt.originalTarget;
       let ownerDoc = originalTarget.ownerDocument;
       ClickEventHandler.onCertError(originalTarget, ownerDoc);
     }
 
     let automatic = Services.prefs.getBoolPref("security.ssl.errorReporting.automatic");
     content.dispatchEvent(new content.CustomEvent("AboutNetErrorOptions", {
--- a/browser/base/content/pageinfo/pageInfo.js
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -123,17 +123,16 @@ pageInfoTreeView.prototype = {
   isSeparator(index) { return false; },
   isSorted() { return this.sortcol > -1; },
   canDrop(index, orientation) { return false; },
   drop(row, orientation) { return false; },
   getParentIndex(index) { return 0; },
   hasNextSibling(index, after) { return false; },
   getLevel(index) { return 0; },
   getImageSrc(row, column) { },
-  getProgressMode(row, column) { },
   getCellValue(row, column) { },
   toggleOpenState(index) { },
   cycleHeader(col) { },
   selectionChanged() { },
   cycleCell(row, column) { },
   isEditable(row, column) { return false; },
   isSelectable(row, column) { return false; },
   performAction(action) { },
@@ -257,19 +256,16 @@ function getClipboardHelper() {
         return Components.classes["@mozilla.org/widget/clipboardhelper;1"].getService(Components.interfaces.nsIClipboardHelper);
     } catch (e) {
         // do nothing, later code will handle the error
         return null;
     }
 }
 const gClipboardHelper = getClipboardHelper();
 
-// Interface for image loading content
-const nsIImageLoadingContent = Components.interfaces.nsIImageLoadingContent;
-
 // namespaces, don't need all of these yet...
 const XLinkNS  = "http://www.w3.org/1999/xlink";
 const XULNS    = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const XMLNS    = "http://www.w3.org/XML/1998/namespace";
 const XHTMLNS  = "http://www.w3.org/1999/xhtml";
 const XHTML2NS = "http://www.w3.org/2002/06/xhtml2";
 
 const XHTMLNSre  = "^http\:\/\/www\.w3\.org\/1999\/xhtml$";
--- a/browser/base/content/pageinfo/security.js
+++ b/browser/base/content/pageinfo/security.js
@@ -4,16 +4,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
 
 /* import-globals-from pageInfo.js */
 
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+                                  "resource://gre/modules/PluralForm.jsm");
 
 var security = {
   init(uri, windowInfo) {
     this.uri = uri;
     this.windowInfo = windowInfo;
   },
 
   // Display the server certificate (static)
@@ -224,25 +226,22 @@ function securityOnLoad(uri, windowInfo)
   var noStr = pageInfoBundle.getString("no");
 
   setText("security-privacy-cookies-value",
           hostHasCookies(uri) ? yesStr : noStr);
   setText("security-privacy-passwords-value",
           realmHasPasswords(uri) ? yesStr : noStr);
 
   var visitCount = previousVisitCount(info.hostName);
-  if (visitCount > 1) {
-    setText("security-privacy-history-value",
-            pageInfoBundle.getFormattedString("securityNVisits", [visitCount.toLocaleString()]));
-  } else if (visitCount == 1) {
-    setText("security-privacy-history-value",
-            pageInfoBundle.getString("securityOneVisit"));
-  } else {
-    setText("security-privacy-history-value", noStr);
-  }
+
+  let visitCountStr = visitCount > 0
+    ? PluralForm.get(visitCount, pageInfoBundle.getString("securityVisitsNumber"))
+        .replace("#1", visitCount.toLocaleString())
+    : pageInfoBundle.getString("securityNoVisits");
+  setText("security-privacy-history-value", visitCountStr);
 
   /* Set the Technical Detail section messages */
   const pkiBundle = document.getElementById("pkiBundle");
   var hdr;
   var msg1;
   var msg2;
 
   if (info.isBroken) {
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1569,16 +1569,27 @@
           }
           this._tabAttrModified(tab, ["sharing"]);
 
           if (aBrowser == this.mCurrentBrowser)
             gIdentityHandler.updateSharingIndicator();
         ]]></body>
       </method>
 
+      <method name="getTabSharingState">
+        <parameter name="aTab"/>
+        <body><![CDATA[
+          // Normalize the state object for consumers (ie.extensions).
+          let state = Object.assign({}, aTab._sharingState);
+          // ensure bool if undefined
+          state.camera = !!state.camera;
+          state.microphone = !!state.microphone;
+          return state;
+        ]]></body>
+      </method>
 
       <!-- TODO: remove after 57, once we know add-ons can no longer use it. -->
       <method name="setTabTitleLoading">
         <parameter name="aTab"/>
         <body/>
       </method>
 
       <method name="setInitialTabTitle">
@@ -3878,17 +3889,17 @@
         </body>
       </method>
 
       <method name="hideTab">
         <parameter name="aTab"/>
         <body>
         <![CDATA[
           if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
-              !aTab.closing) {
+              !aTab.closing && !aTab._sharingState) {
             aTab.setAttribute("hidden", "true");
             this._visibleTabs = null; // invalidate cache
 
             this.tabContainer._updateCloseButtons();
 
             this.tabContainer._setPositionalAttributes();
 
             let event = document.createEvent("Events");
@@ -7825,18 +7836,16 @@
         <setter>
           <![CDATA[
           if (val)
             this.setAttribute("visuallyselected", "true");
           else
             this.removeAttribute("visuallyselected");
           this.parentNode.tabbrowser._tabAttrModified(this, ["visuallyselected"]);
 
-          this._setPositionAttributes(val);
-
           return val;
           ]]>
         </setter>
       </property>
 
       <property name="_selected">
         <setter>
           <![CDATA[
--- a/browser/base/content/test/general/browser_bug553455.js
+++ b/browser/base/content/test/general/browser_bug553455.js
@@ -239,17 +239,17 @@ async function test_disabledInstall() {
   let triggers = encodeURIComponent(JSON.stringify({
     "XPI": "amosigned.xpi"
   }));
   BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
   is(notification.button.label, "Enable", "Should have seen the right button");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "Software installation is currently disabled. Click Enable and try again.");
 
   let closePromise = waitForNotificationClose();
   // Click on Enable
   EventUtils.synthesizeMouseAtCenter(notification.button, {});
   await closePromise;
 
   try {
@@ -270,17 +270,17 @@ async function test_blockedInstall() {
   }));
   BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
   is(notification.button.label, "Allow", "Should have seen the right button");
   is(notification.getAttribute("origin"), "example.com",
      "Should have seen the right origin host");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      gApp + " prevented this site from asking you to install software on your computer.",
      "Should have seen the right message");
 
   let dialogPromise = waitForInstallDialog();
   // Click on Allow
   EventUtils.synthesizeMouse(notification.button, 20, 10, {});
   // Notification should have changed to progress notification
   ok(PopupNotifications.isPanelOpen, "Notification should still be open");
@@ -289,17 +289,17 @@ async function test_blockedInstall() {
   let installDialog = await dialogPromise;
 
   notificationPromise = waitForNotification("addon-install-restart");
   acceptInstallDialog(installDialog);
   panel = await notificationPromise;
 
   notification = panel.childNodes[0];
   is(notification.button.label, "Restart Now", "Should have seen the right button");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "XPI Test will be installed after you restart " + gApp + ".",
      "Should have seen the right message");
 
   let installs = await getInstalls();
   is(installs.length, 1, "Should be one pending install");
   installs[0].cancel();
   await removeTab();
 },
@@ -326,17 +326,17 @@ async function test_whitelistedInstall()
      "tab selected in response to the addon-install-confirmation notification");
 
   let notificationPromise = waitForNotification("addon-install-restart");
   acceptInstallDialog(installDialog);
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
   is(notification.button.label, "Restart Now", "Should have seen the right button");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "XPI Test will be installed after you restart " + gApp + ".",
      "Should have seen the right message");
 
   let installs = await getInstalls();
   is(installs.length, 1, "Should be one pending install");
   installs[0].cancel();
 
   Services.perms.remove(makeURI("http://example.com/"), "install");
@@ -352,17 +352,17 @@ async function test_failedDownload() {
   let triggers = encodeURIComponent(JSON.stringify({
     "XPI": "missing.xpi"
   }));
   BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
   await progressPromise;
   let panel = await failPromise;
 
   let notification = panel.childNodes[0];
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "The add-on could not be downloaded because of a connection failure.",
      "Should have seen the right message");
 
   Services.perms.remove(makeURI("http://example.com/"), "install");
   await removeTab();
 },
 
 async function test_corruptFile() {
@@ -374,17 +374,17 @@ async function test_corruptFile() {
   let triggers = encodeURIComponent(JSON.stringify({
     "XPI": "corrupt.xpi"
   }));
   BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
   await progressPromise;
   let panel = await failPromise;
 
   let notification = panel.childNodes[0];
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "The add-on downloaded from this site could not be installed " +
      "because it appears to be corrupt.",
      "Should have seen the right message");
 
   Services.perms.remove(makeURI("http://example.com/"), "install");
   await removeTab();
 },
 
@@ -397,17 +397,17 @@ async function test_incompatible() {
   let triggers = encodeURIComponent(JSON.stringify({
     "XPI": "incompatible.xpi"
   }));
   BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
   await progressPromise;
   let panel = await failPromise;
 
   let notification = panel.childNodes[0];
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "XPI Test could not be installed because it is not compatible with " +
      gApp + " " + gVersion + ".",
      "Should have seen the right message");
 
   Services.perms.remove(makeURI("http://example.com/"), "install");
   await removeTab();
 },
 
@@ -536,17 +536,17 @@ async function test_allUnverified() {
   let triggers = encodeURIComponent(JSON.stringify({
     "Extension XPI": "restartless-unsigned.xpi"
   }));
   BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "installtrigger.html?" + triggers);
   await progressPromise;
   let installDialog = await dialogPromise;
 
   let notification = document.getElementById("addon-install-confirmation-notification");
-  let message = notification.getAttribute("startlabel");
+  let message = notification.getAttribute("label");
   is(message, "Caution: This site would like to install an unverified add-on in " + gApp + ". Proceed at your own risk.");
 
   let container = document.getElementById("addon-install-confirmation-content");
   is(container.childNodes.length, 1, "Should be one item listed");
   is(container.childNodes[0].firstChild.getAttribute("value"), "XPI Test", "Should have the right add-on");
   is(container.childNodes[0].childNodes.length, 1, "Shouldn't have the unverified marker");
 
   let notificationPromise = waitForNotification("addon-installed");
@@ -574,17 +574,17 @@ async function test_url() {
   let installDialog = await dialogPromise;
 
   let notificationPromise = waitForNotification("addon-install-restart");
   acceptInstallDialog(installDialog);
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
   is(notification.button.label, "Restart Now", "Should have seen the right button");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "XPI Test will be installed after you restart " + gApp + ".",
      "Should have seen the right message");
 
   let installs = await getInstalls();
   is(installs.length, 1, "Should be one pending install");
   installs[0].cancel();
 
   await removeTab();
@@ -611,17 +611,17 @@ async function test_localFile() {
   gBrowser.loadURI(path);
   await failPromise;
 
   // Wait for the browser code to add the failure notification
   await waitForSingleNotification();
 
   let notification = PopupNotifications.panel.childNodes[0];
   is(notification.id, "addon-install-failed-notification", "Should have seen the install fail");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "This add-on could not be installed because it appears to be corrupt.",
      "Should have seen the right message");
 
   await removeTab();
 },
 
 async function test_tabClose() {
   if (!Services.prefs.getBoolPref("xpinstall.customConfirmationUI", false)) {
@@ -700,17 +700,17 @@ async function test_urlBar() {
   let installDialog = await dialogPromise;
 
   let notificationPromise = waitForNotification("addon-install-restart");
   acceptInstallDialog(installDialog);
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
   is(notification.button.label, "Restart Now", "Should have seen the right button");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "XPI Test will be installed after you restart " + gApp + ".",
      "Should have seen the right message");
 
   let installs = await getInstalls();
   is(installs.length, 1, "Should be one pending install");
   installs[0].cancel();
 
   await removeTab();
@@ -726,17 +726,17 @@ async function test_wrongHost() {
 
   let progressPromise = waitForProgressNotification();
   let notificationPromise = waitForNotification("addon-install-failed");
   gBrowser.loadURI(TESTROOT + "corrupt.xpi");
   await progressPromise;
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "The add-on downloaded from this site could not be installed " +
      "because it appears to be corrupt.",
      "Should have seen the right message");
 
   await removeTab();
 },
 
 async function test_reload() {
@@ -753,17 +753,17 @@ async function test_reload() {
   let installDialog = await dialogPromise;
 
   let notificationPromise = waitForNotification("addon-install-restart");
   acceptInstallDialog(installDialog);
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
   is(notification.button.label, "Restart Now", "Should have seen the right button");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "XPI Test will be installed after you restart " + gApp + ".",
      "Should have seen the right message");
 
   function testFail() {
     ok(false, "Reloading should not have hidden the notification");
   }
   PopupNotifications.panel.addEventListener("popuphiding", testFail);
   let requestedUrl = TESTROOT2 + "enabled.html";
@@ -794,17 +794,17 @@ async function test_theme() {
   let installDialog = await dialogPromise;
 
   let notificationPromise = waitForNotification("addon-install-restart");
   acceptInstallDialog(installDialog);
   let panel = await notificationPromise;
 
   let notification = panel.childNodes[0];
   is(notification.button.label, "Restart Now", "Should have seen the right button");
-  is(notification.getAttribute("startlabel"),
+  is(notification.getAttribute("label"),
      "Theme Test will be installed after you restart " + gApp + ".",
      "Should have seen the right message");
 
   let addon = await new Promise(resolve => {
     AddonManager.getAddonByID("{972ce4c6-7e08-4474-a285-3208198ce6fd}", function(result) {
       resolve(result);
     });
   });
--- a/browser/base/content/test/popupNotifications/head.js
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -200,19 +200,19 @@ function checkPopup(popup, notifyObj) {
                                                      "popup-notification-icon");
   if (notifyObj.id == "geolocation") {
     isnot(icon.boxObject.width, 0, "icon for geo displayed");
     ok(popup.anchorNode.classList.contains("notification-anchor-icon"),
        "notification anchored to icon");
   }
 
   if (typeof notifyObj.message == "string") {
-    is(notification.getAttribute("startlabel"), notifyObj.message, "message matches");
+    is(notification.getAttribute("label"), notifyObj.message, "message matches");
   } else {
-    is(notification.getAttribute("startlabel"), notifyObj.message.start, "message matches");
+    is(notification.getAttribute("label"), notifyObj.message.start, "message matches");
     is(notification.getAttribute("hostname"), notifyObj.message.host, "message matches");
     is(notification.getAttribute("endlabel"), notifyObj.message.end, "message matches");
   }
 
   is(notification.id, notifyObj.id + "-notification", "id matches");
   if (notifyObj.mainAction) {
     is(notification.getAttribute("buttonlabel"), notifyObj.mainAction.label,
        "main action label matches");
--- a/browser/base/content/test/siteIdentity/browser_check_identity_state.js
+++ b/browser/base/content/test/siteIdentity/browser_check_identity_state.js
@@ -1,16 +1,17 @@
 /*
  * Test the identity mode UI for a variety of page types
  */
 
 "use strict";
 
 const DUMMY = "browser/browser/base/content/test/siteIdentity/dummy_page.html";
 const INSECURE_ICON_PREF = "security.insecure_connection_icon.enabled";
+const INSECURE_TEXT_PREF = "security.insecure_connection_text.enabled";
 const INSECURE_PBMODE_ICON_PREF = "security.insecure_connection_icon.pbmode.enabled";
 
 function loadNewTab(url) {
   return BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
 }
 
 function getIdentityMode(aWindow = window) {
   return aWindow.document.getElementById("identity-box").className;
@@ -51,16 +52,79 @@ async function webpageTest(secureCheck) 
   await SpecialPowers.popPrefEnv();
 }
 
 add_task(async function test_webpage() {
   await webpageTest(false);
   await webpageTest(true);
 });
 
+async function webpageTestTextWarning(secureCheck) {
+  await SpecialPowers.pushPrefEnv({set: [[INSECURE_TEXT_PREF, secureCheck]]});
+  let oldTab = gBrowser.selectedTab;
+
+  let newTab = await loadNewTab("http://example.com/" + DUMMY);
+  if (secureCheck) {
+    is(getIdentityMode(), "unknownIdentity notSecureText", "Identity should have not secure text");
+  } else {
+    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  }
+
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.selectedTab = newTab;
+  if (secureCheck) {
+    is(getIdentityMode(), "unknownIdentity notSecureText", "Identity should have not secure text");
+  } else {
+    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  }
+
+  gBrowser.removeTab(newTab);
+  await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage_text_warning() {
+  await webpageTestTextWarning(false);
+  await webpageTestTextWarning(true);
+});
+
+async function webpageTestTextWarningCombined(secureCheck) {
+  await SpecialPowers.pushPrefEnv({set: [
+    [INSECURE_TEXT_PREF, secureCheck],
+    [INSECURE_ICON_PREF, secureCheck]
+  ]});
+  let oldTab = gBrowser.selectedTab;
+
+  let newTab = await loadNewTab("http://example.com/" + DUMMY);
+  if (secureCheck) {
+    is(getIdentityMode(), "notSecure notSecureText", "Identity should be not secure");
+  } else {
+    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  }
+
+  gBrowser.selectedTab = oldTab;
+  is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+
+  gBrowser.selectedTab = newTab;
+  if (secureCheck) {
+    is(getIdentityMode(), "notSecure notSecureText", "Identity should be not secure");
+  } else {
+    is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
+  }
+
+  gBrowser.removeTab(newTab);
+  await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_webpage_text_warning_combined() {
+  await webpageTestTextWarning(false);
+  await webpageTestTextWarning(true);
+});
+
 async function blankPageTest(secureCheck) {
   let oldTab = gBrowser.selectedTab;
   await SpecialPowers.pushPrefEnv({set: [[INSECURE_ICON_PREF, secureCheck]]});
 
   let newTab = await loadNewTab("about:blank");
   is(getIdentityMode(), "unknownIdentity", "Identity should be unknown");
 
   gBrowser.selectedTab = oldTab;
--- a/browser/base/content/test/static/browser_misused_characters_in_strings.js
+++ b/browser/base/content/test/static/browser_misused_characters_in_strings.js
@@ -140,20 +140,16 @@ let gWhitelist = [{
     file: "pocket.properties",
     key: "tos",
     type: "double-quote"
   }, {
     file: "aboutNetworking.dtd",
     key: "aboutNetworking.logTutorial",
     type: "single-quote"
   }, {
-    file: "preferences.properties",
-    key: "searchResults.needHelp2",
-    type: "double-quote"
-  }, {
     file: "aboutdevtools.dtd",
     key: "aboutDevtools.newsletter.privacy.label",
     type: "single-quote"
   }
 ];
 
 /**
  * Check if an error should be ignored due to matching one of the whitelist
--- a/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
@@ -70,16 +70,31 @@ add_task(async function plainEnterOnSugg
 
 add_task(async function ctrlEnterOnSuggestion() {
   await testPressEnterOnSuggestion("http://www.foofoo.com/",
                                    AppConstants.platform === "macosx" ?
                                      { metaKey: true } :
                                      { ctrlKey: true });
 });
 
+add_task(async function copySuggestionText() {
+  gURLBar.focus();
+  await promiseAutocompleteResultPopup("foo");
+  let [idx, suggestion] = await promiseFirstSuggestion();
+  for (let i = 0; i < idx; ++i) {
+    EventUtils.synthesizeKey("VK_DOWN", {});
+  }
+  gURLBar.select();
+  await new Promise((resolve, reject) => waitForClipboard(suggestion, function() {
+    goDoCommand("cmd_copy");
+  }, resolve, reject));
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  await promisePopupHidden(gURLBar.popup);
+});
+
 function getFirstSuggestion() {
   let controller = gURLBar.popup.input.controller;
   let matchCount = controller.matchCount;
   for (let i = 0; i < matchCount; i++) {
     let url = controller.getValueAt(i);
     let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
     if (mozActionMatch) {
       let [, type, paramStr] = mozActionMatch;
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -974,41 +974,48 @@ file, You can obtain one at http://mozil
             }
             return aURI;
           ]]>
         </body>
       </method>
 
       <method name="_getSelectedValueForClipboard">
         <body><![CDATA[
-          // Grab the actual input field's value, not our value, which could include moz-action:
+          // Grab the actual input field's value, not our value, which could
+          // include "moz-action:".
           var inputVal = this.inputField.value;
           let selection = this.editor.selection;
           const flags = Ci.nsIDocumentEncoder.OutputPreformatted |
                         Ci.nsIDocumentEncoder.OutputRaw;
           let selectedVal = selection.QueryInterface(Ci.nsISelectionPrivate)
                                      .toStringWithFormat("text/plain", flags, 0);
 
           // Handle multiple-range selection as a string for simplicity.
           if (selection.rangeCount > 1) {
              return selectedVal;
           }
 
-          // If the selection doesn't start at the beginning or doesn't span the full domain or
-          // the URL bar is modified or there is no text at all, nothing else to do here.
+          // If the selection doesn't start at the beginning or doesn't span the
+          // full domain or the URL bar is modified or there is no text at all,
+          // nothing else to do here.
           if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "")
             return selectedVal;
           // The selection doesn't span the full domain if it doesn't contain a slash and is
           // followed by some character other than a slash.
           if (!selectedVal.includes("/")) {
             let remainder = inputVal.replace(selectedVal, "");
             if (remainder != "" && remainder[0] != "/")
               return selectedVal;
           }
 
+          // If the value was filled by a search suggestion, just return it.
+          let action = this._parseActionUrl(this.value);
+          if (action && action.type == "searchengine")
+            return selectedVal;
+
           let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
 
           let uri;
           if (this.getAttribute("pageproxystate") == "valid") {
             uri = gBrowser.currentURI;
           } else {
             // We're dealing with an autocompleted value, create a new URI from that.
             try {
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -4,16 +4,19 @@
 # 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/.
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "General")
 
 SPHINX_TREES['sslerrorreport'] = 'content/docs/sslerrorreport'
 
+with Files('content/docs/sslerrorreport/**'):
+    SCHEDULES.exclusive = ['docs']
+
 MOCHITEST_MANIFESTS += [
     'content/test/general/mochitest.ini',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'content/test/chrome/chrome.ini',
 ]
 
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/EnterprisePolicies.js
@@ -0,0 +1,358 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  NetUtil: "resource://gre/modules/NetUtil.jsm",
+  Policies: "resource:///modules/policies/Policies.jsm",
+  PoliciesValidator: "resource:///modules/policies/PoliciesValidator.jsm",
+});
+
+// This is the file that will be searched for in the
+// ${InstallDir}/distribution folder.
+const POLICIES_FILENAME = "policies.json";
+
+// For easy testing, modify the helpers/sample.json file,
+// and set PREF_ALTERNATE_PATH in firefox.js as:
+// /your/repo/browser/components/enterprisepolicies/helpers/sample.json
+const PREF_ALTERNATE_PATH     = "browser.policies.alternatePath";
+
+// This pref is meant to be temporary: it will only be used while we're
+// testing this feature without rolling it out officially. When the
+// policy engine is released, this pref should be removed.
+const PREF_ENABLED            = "browser.policies.enabled";
+const PREF_LOGLEVEL           = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
+  return new ConsoleAPI({
+    prefix: "Enterprise Policies",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+// ==== Start XPCOM Boilerplate ==== \\
+
+// Factory object
+const EnterprisePoliciesFactory = {
+  _instance: null,
+  createInstance: function BGSF_createInstance(outer, iid) {
+    if (outer != null)
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    return this._instance == null ?
+      this._instance = new EnterprisePoliciesManager() : this._instance;
+  }
+};
+
+// ==== End XPCOM Boilerplate ==== //
+
+// Constructor
+function EnterprisePoliciesManager() {
+  Services.obs.addObserver(this, "profile-after-change", true);
+  Services.obs.addObserver(this, "final-ui-startup", true);
+  Services.obs.addObserver(this, "sessionstore-windows-restored", true);
+  Services.obs.addObserver(this, "EnterprisePolicies:Restart", true);
+}
+
+EnterprisePoliciesManager.prototype = {
+  // for XPCOM
+  classID:          Components.ID("{ea4e1414-779b-458b-9d1f-d18e8efbc145}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsIEnterprisePolicies]),
+
+  // redefine the default factory for XPCOMUtils
+  _xpcom_factory: EnterprisePoliciesFactory,
+
+  _initialize() {
+    if (!Services.prefs.getBoolPref(PREF_ENABLED, false)) {
+      this.status = Ci.nsIEnterprisePolicies.INACTIVE;
+      return;
+    }
+
+    this._file = new JSONFileReader(getConfigurationFile());
+    this._file.readData();
+
+    if (!this._file.exists) {
+      this.status = Ci.nsIEnterprisePolicies.INACTIVE;
+      return;
+    }
+
+    if (this._file.failed) {
+      this.status = Ci.nsIEnterprisePolicies.FAILED;
+      return;
+    }
+
+    this.status = Ci.nsIEnterprisePolicies.ACTIVE;
+    this._activatePolicies();
+  },
+
+  _activatePolicies() {
+    let { schema } = Cu.import("resource:///modules/policies/schema.jsm", {});
+    let json = this._file.json;
+
+    for (let policyName of Object.keys(json.policies)) {
+      let policySchema = schema.properties[policyName];
+      let policyParameters = json.policies[policyName];
+
+      if (!policySchema) {
+        log.error(`Unknown policy: ${policyName}`);
+        continue;
+      }
+
+      let [parametersAreValid, parsedParameters] =
+        PoliciesValidator.validateAndParseParameters(policyParameters,
+                                                     policySchema);
+
+      if (!parametersAreValid) {
+        log.error(`Invalid parameters specified for ${policyName}.`);
+        continue;
+      }
+
+      let policyImpl = Policies[policyName];
+
+      for (let timing of Object.keys(this._callbacks)) {
+        let policyCallback = policyImpl["on" + timing];
+        if (policyCallback) {
+          this._schedulePolicyCallback(
+            timing,
+            policyCallback.bind(null,
+                                this, /* the EnterprisePoliciesManager */
+                                parsedParameters));
+        }
+      }
+    }
+  },
+
+  _callbacks: {
+    BeforeAddons: [],
+    ProfileAfterChange: [],
+    BeforeUIStartup: [],
+    AllWindowsRestored: [],
+  },
+
+  _schedulePolicyCallback(timing, callback) {
+    this._callbacks[timing].push(callback);
+  },
+
+  _runPoliciesCallbacks(timing) {
+    let callbacks = this._callbacks[timing];
+    while (callbacks.length > 0) {
+      let callback = callbacks.shift();
+      try {
+        callback();
+      } catch (ex) {
+        log.error("Error running ", callback, `for ${timing}:`, ex);
+      }
+    }
+  },
+
+  async _restart() {
+    if (!Cu.isInAutomation) {
+      return;
+    }
+
+    DisallowedFeatures = {};
+
+    this._status = Ci.nsIEnterprisePolicies.UNINITIALIZED;
+    for (let timing of Object.keys(this._callbacks)) {
+      this._callbacks[timing] = [];
+    }
+    delete Services.ppmm.initialProcessData.policies;
+    Services.ppmm.broadcastAsyncMessage("EnterprisePolicies:Restart", null);
+
+    let { PromiseUtils } = Cu.import("resource://gre/modules/PromiseUtils.jsm",
+                                     {});
+
+    // Simulate the startup process. This step-by-step is a bit ugly but it
+    // tries to emulate the same behavior as of a normal startup.
+
+    await PromiseUtils.idleDispatch(() => {
+      this.observe(null, "policies-startup", null);
+    });
+
+    await PromiseUtils.idleDispatch(() => {
+      this.observe(null, "profile-after-change", null);
+    });
+
+    await PromiseUtils.idleDispatch(() => {
+      this.observe(null, "final-ui-startup", null);
+    });
+
+    await PromiseUtils.idleDispatch(() => {
+      this.observe(null, "sessionstore-windows-restored", null);
+    });
+  },
+
+  // nsIObserver implementation
+  observe: function BG_observe(subject, topic, data) {
+    switch (topic) {
+      case "policies-startup":
+        this._initialize();
+        this._runPoliciesCallbacks("BeforeAddons");
+        break;
+
+      case "profile-after-change":
+        // Before the first set of policy callbacks runs, we must
+        // initialize the service.
+        this._runPoliciesCallbacks("ProfileAfterChange");
+        break;
+
+      case "final-ui-startup":
+        this._runPoliciesCallbacks("BeforeUIStartup");
+        break;
+
+      case "sessionstore-windows-restored":
+        this._runPoliciesCallbacks("AllWindowsRestored");
+
+        // After the last set of policy callbacks ran, notify the test observer.
+        Services.obs.notifyObservers(null,
+                                     "EnterprisePolicies:AllPoliciesApplied");
+        break;
+
+      case "EnterprisePolicies:Restart":
+        this._restart().then(null, Cu.reportError);
+        break;
+    }
+  },
+
+  disallowFeature(feature, neededOnContentProcess = false) {
+    DisallowedFeatures[feature] = true;
+
+    // NOTE: For optimization purposes, only features marked as needed
+    // on content process will be passed onto the child processes.
+    if (neededOnContentProcess) {
+      Services.ppmm.initialProcessData.policies
+                                      .disallowedFeatures.push(feature);
+
+      if (Services.ppmm.childCount > 1) {
+        // If there has been a content process already initialized, let's
+        // broadcast the newly disallowed feature.
+        Services.ppmm.broadcastAsyncMessage(
+          "EnterprisePolicies:DisallowFeature", {feature}
+        );
+      }
+    }
+  },
+
+  // ------------------------------
+  // public nsIEnterprisePolicies members
+  // ------------------------------
+
+  _status: Ci.nsIEnterprisePolicies.UNINITIALIZED,
+
+  set status(val) {
+    this._status = val;
+    if (val != Ci.nsIEnterprisePolicies.INACTIVE) {
+      Services.ppmm.initialProcessData.policies = {
+        status: val,
+        disallowedFeatures: [],
+      };
+    }
+    return val;
+  },
+
+  get status() {
+    return this._status;
+  },
+
+  isAllowed: function BG_sanitize(feature) {
+    return !(feature in DisallowedFeatures);
+  },
+};
+
+let DisallowedFeatures = {};
+
+function JSONFileReader(file) {
+  this._file = file;
+  this._data = {
+    exists: null,
+    failed: false,
+    json: null,
+  };
+}
+
+JSONFileReader.prototype = {
+  get exists() {
+    if (this._data.exists === null) {
+      this.readData();
+    }
+
+    return this._data.exists;
+  },
+
+  get failed() {
+    return this._data.failed;
+  },
+
+  get json() {
+    if (this._data.failed) {
+      return null;
+    }
+
+    if (this._data.json === null) {
+      this.readData();
+    }
+
+    return this._data.json;
+  },
+
+  readData() {
+    try {
+      let data = Cu.readUTF8File(this._file);
+      if (data) {
+        this._data.exists = true;
+        this._data.json = JSON.parse(data);
+      } else {
+        this._data.exists = false;
+      }
+    } catch (ex) {
+      if (ex instanceof Components.Exception &&
+          ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
+        this._data.exists = false;
+      } else if (ex instanceof SyntaxError) {
+        log.error("Error parsing JSON file");
+        this._data.failed = true;
+      } else {
+        log.error("Error reading file");
+        this._data.failed = true;
+      }
+    }
+  }
+};
+
+function getConfigurationFile() {
+  let configFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile);
+  configFile.append(POLICIES_FILENAME);
+
+  let prefType = Services.prefs.getPrefType(PREF_ALTERNATE_PATH);
+
+  if ((prefType == Services.prefs.PREF_STRING) && !configFile.exists()) {
+    // We only want to use the alternate file path if the file on the install
+    // folder doesn't exist. Otherwise it'd be possible for a user to override
+    // the admin-provided policies by changing the user-controlled prefs.
+    // This pref is only meant for tests, so it's fine to use this extra
+    // synchronous configFile.exists() above.
+    configFile = Cc["@mozilla.org/file/local;1"]
+                   .createInstance(Ci.nsIFile);
+    let alternatePath = Services.prefs.getStringPref(PREF_ALTERNATE_PATH);
+    configFile.initWithPath(alternatePath);
+  }
+
+  return configFile;
+}
+
+var components = [EnterprisePoliciesManager];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/EnterprisePolicies.manifest
@@ -0,0 +1,5 @@
+component {ea4e1414-779b-458b-9d1f-d18e8efbc145} EnterprisePolicies.js process=main
+contract @mozilla.org/browser/enterprisepolicies;1 {ea4e1414-779b-458b-9d1f-d18e8efbc145} process=main
+
+component {dc6358f8-d167-4566-bf5b-4350b5e6a7a2} EnterprisePoliciesContent.js process=content
+contract @mozilla.org/browser/enterprisepolicies;1 {dc6358f8-d167-4566-bf5b-4350b5e6a7a2} process=content
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/EnterprisePoliciesContent.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_LOGLEVEL           = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
+  return new ConsoleAPI({
+    prefix: "Enterprise Policies Child",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+
+// ==== Start XPCOM Boilerplate ==== \\
+
+// Factory object
+const EnterprisePoliciesFactory = {
+  _instance: null,
+  createInstance: function BGSF_createInstance(outer, iid) {
+    if (outer != null)
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    return this._instance == null ?
+      this._instance = new EnterprisePoliciesManagerContent() : this._instance;
+  }
+};
+
+// ==== End XPCOM Boilerplate ==== //
+
+
+function EnterprisePoliciesManagerContent() {
+  let policies = Services.cpmm.initialProcessData.policies;
+  if (policies) {
+    this._status = policies.status;
+    // make a copy of the array so that we can keep adding to it
+    // in a way that is not confusing.
+    this._disallowedFeatures = policies.disallowedFeatures.slice();
+  }
+
+  Services.cpmm.addMessageListener("EnterprisePolicies:DisallowFeature", this);
+  Services.cpmm.addMessageListener("EnterprisePolicies:Restart", this);
+}
+
+EnterprisePoliciesManagerContent.prototype = {
+  // for XPCOM
+  classID:          Components.ID("{dc6358f8-d167-4566-bf5b-4350b5e6a7a2}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener,
+                                         Ci.nsIEnterprisePolicies]),
+
+  // redefine the default factory for XPCOMUtils
+  _xpcom_factory: EnterprisePoliciesFactory,
+
+  _status: Ci.nsIEnterprisePolicies.INACTIVE,
+
+  _disallowedFeatures: [],
+
+  receiveMessage({name, data}) {
+    switch (name) {
+      case "EnterprisePolicies:DisallowFeature":
+        this._disallowedFeatures.push(data.feature);
+        break;
+
+      case "EnterprisePolicies:Restart":
+        this._disallowedFeatures = [];
+        break;
+    }
+  },
+
+  get status() {
+    return this._status;
+  },
+
+  isAllowed(feature) {
+    return !this._disallowedFeatures.includes(feature);
+  }
+};
+
+var components = [EnterprisePoliciesManagerContent];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -0,0 +1,123 @@
+/* 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 Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_LOGLEVEL           = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
+  return new ConsoleAPI({
+    prefix: "Policies.jsm",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+this.EXPORTED_SYMBOLS = ["Policies"];
+
+this.Policies = {
+  "block_about_config": {
+    onBeforeUIStartup(manager, param) {
+      if (param == true) {
+        manager.disallowFeature("about:config", true);
+      }
+    }
+  },
+
+  "dont_check_default_browser": {
+    onBeforeUIStartup(manager, param) {
+      setAndLockPref("browser.shell.checkDefaultBrowser", false);
+    }
+  },
+
+  "flash_plugin": {
+    onBeforeUIStartup(manager, param) {
+      addAllowDenyPermissions("plugin:flash", param.allow, param.block);
+    }
+  },
+
+  "popups": {
+    onBeforeUIStartup(manager, param) {
+      addAllowDenyPermissions("popup", param.allow, param.block);
+    }
+  },
+
+  "install_addons": {
+    onBeforeUIStartup(manager, param) {
+      addAllowDenyPermissions("install", param.allow, param.block);
+    }
+  },
+
+  "cookies": {
+    onBeforeUIStartup(manager, param) {
+      addAllowDenyPermissions("cookie", param.allow, param.block);
+    }
+  },
+};
+
+/*
+ * ====================
+ * = HELPER FUNCTIONS =
+ * ====================
+ *
+ * The functions below are helpers to be used by several policies.
+ */
+
+function setAndLockPref(prefName, prefValue) {
+  if (Services.prefs.prefIsLocked(prefName)) {
+    Services.prefs.unlockPref(prefName);
+  }
+
+  let defaults = Services.prefs.getDefaultBranch("");
+
+  switch (typeof(prefValue)) {
+    case "boolean":
+      defaults.setBoolPref(prefName, prefValue);
+      break;
+
+    case "number":
+      if (!Number.isInteger(prefValue)) {
+        throw new Error(`Non-integer value for ${prefName}`);
+      }
+
+      defaults.setIntPref(prefName, prefValue);
+      break;
+
+    case "string":
+      defaults.setStringPref(prefName, prefValue);
+      break;
+  }
+
+  Services.prefs.lockPref(prefName);
+}
+
+function addAllowDenyPermissions(permissionName, allowList, blockList) {
+  allowList = allowList || [];
+  blockList = blockList || [];
+
+  for (let origin of allowList) {
+    Services.perms.add(origin,
+                       permissionName,
+                       Ci.nsIPermissionManager.ALLOW_ACTION,
+                       Ci.nsIPermissionManager.EXPIRE_POLICY);
+  }
+
+  for (let origin of blockList) {
+    Services.perms.add(origin,
+                       permissionName,
+                       Ci.nsIPermissionManager.DENY_ACTION,
+                       Ci.nsIPermissionManager.EXPIRE_POLICY);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/PoliciesValidator.jsm
@@ -0,0 +1,148 @@
+/* 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 Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_LOGLEVEL           = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {});
+  return new ConsoleAPI({
+    prefix: "PoliciesValidator.jsm",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+this.EXPORTED_SYMBOLS = ["PoliciesValidator"];
+
+this.PoliciesValidator = {
+  validateAndParseParameters(param, properties) {
+    return validateAndParseParamRecursive(param, properties);
+  }
+};
+
+function validateAndParseParamRecursive(param, properties) {
+  if (properties.enum) {
+    if (properties.enum.includes(param)) {
+      return [true, param];
+    }
+    return [false, null];
+  }
+
+  log.debug(`checking @${param}@ for type ${properties.type}`);
+  switch (properties.type) {
+    case "boolean":
+    case "number":
+    case "integer":
+    case "string":
+    case "URL":
+    case "origin":
+      return validateAndParseSimpleParam(param, properties.type);
+
+    case "array":
+      if (!Array.isArray(param)) {
+        log.error("Array expected but not received");
+        return [false, null];
+      }
+
+      let parsedArray = [];
+      for (let item of param) {
+        log.debug(`in array, checking @${item}@ for type ${properties.items.type}`);
+        let [valid, parsedValue] = validateAndParseParamRecursive(item, properties.items);
+        if (!valid) {
+          return [false, null];
+        }
+
+        parsedArray.push(parsedValue);
+      }
+
+      return [true, parsedArray];
+
+    case "object": {
+      if (typeof(param) != "object") {
+        log.error("Object expected but not received");
+        return [false, null];
+      }
+
+      let parsedObj = {};
+      for (let property of Object.keys(properties.properties)) {
+        log.debug(`in object, for property ${property} checking @${param[property]}@ for type ${properties.properties[property].type}`);
+        let [valid, parsedValue] = validateAndParseParamRecursive(param[property], properties.properties[property]);
+        if (!valid) {
+          return [false, null];
+        }
+
+        parsedObj[property] = parsedValue;
+      }
+
+      return [true, parsedObj];
+    }
+  }
+
+  return [false, null];
+}
+
+function validateAndParseSimpleParam(param, type) {
+  let valid = false;
+  let parsedParam = param;
+
+  switch (type) {
+    case "boolean":
+    case "number":
+    case "string":
+      valid = (typeof(param) == type);
+      break;
+
+    // integer is an alias to "number" that some JSON schema tools use
+    case "integer":
+      valid = (typeof(param) == "number");
+      break;
+
+    case "origin":
+      if (typeof(param) != "string") {
+        break;
+      }
+
+      try {
+        parsedParam = Services.io.newURI(param);
+
+        let pathQueryRef = parsedParam.pathQueryRef;
+        // Make sure that "origin" types won't accept full URLs.
+        if (pathQueryRef != "/" && pathQueryRef != "") {
+          valid = false;
+        } else {
+          valid = true;
+        }
+      } catch (ex) {
+        valid = false;
+      }
+      break;
+
+    case "URL":
+      if (typeof(param) != "string") {
+        break;
+      }
+
+      try {
+        parsedParam = Services.io.newURI(param);
+        valid = true;
+      } catch (ex) {
+        valid = false;
+      }
+      break;
+  }
+
+  return [valid, parsedParam];
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/moz.build
@@ -0,0 +1,8 @@
+# -*- 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/.
+
+with Files("**"):
+    BUG_COMPONENT = ("Firefox", "Enterprise Policies")
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/sample.json
@@ -0,0 +1,18 @@
+{
+  "policies": {
+    "block_about_config": true,
+    "dont_check_default_browser": true,
+
+    "flash_plugin": {
+      "allow": [
+        "https://www.example.com"
+      ],
+
+      "block": [
+        "https://www.example.org"
+      ]
+    },
+
+    "block_about_profiles": true
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/moz.build
@@ -0,0 +1,30 @@
+# -*- 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/.
+
+with Files("**"):
+    BUG_COMPONENT = ("Firefox", "Enterprise Policies")
+
+DIRS += [
+    'helpers',
+    'schemas',
+]
+
+TEST_DIRS += [
+	'tests'
+]
+
+EXTRA_COMPONENTS += [
+    'EnterprisePolicies.js',
+    'EnterprisePolicies.manifest',
+    'EnterprisePoliciesContent.js',
+]
+
+EXTRA_JS_MODULES.policies += [
+    'Policies.jsm',
+    'PoliciesValidator.jsm',
+]
+
+FINAL_LIBRARY = 'browsercomps'
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/schemas/configuration.json
@@ -0,0 +1,10 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+  "properties": {
+    "policies": {
+      "$ref": "policies.json"
+    }
+  },
+  "required": ["policies"]
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/schemas/moz.build
@@ -0,0 +1,12 @@
+# -*- 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/.
+
+with Files("**"):
+    BUG_COMPONENT = ("Firefox", "Enterprise Policies")
+
+EXTRA_PP_JS_MODULES.policies += [
+    'schema.jsm',
+]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -0,0 +1,109 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+  "properties": {
+    "block_about_config": {
+      "description": "Blocks access to the about:config page.",
+      "first_available": "60.0",
+
+      "type": "boolean",
+      "enum": [true]
+    },
+
+    "dont_check_default_browser": {
+      "description": "Don't check for the default browser on startup.",
+      "first_available": "60.0",
+
+      "type": "boolean",
+      "enum": [true]
+    },
+
+    "flash_plugin": {
+      "description": "Allow or deny flash plugin usage.",
+      "first_available": "60.0",
+
+      "type": "object",
+      "properties": {
+        "allow": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        },
+
+        "block": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        }
+      }
+    },
+
+    "popups": {
+      "description": "Allow or deny popup usage.",
+      "first_available": "60.0",
+
+      "type": "object",
+      "properties": {
+        "allow": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        },
+
+        "block": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        }
+      }
+    },
+
+    "install_addons": {
+      "description": "Allow or deny popup websites to install webextensions.",
+      "first_available": "60.0",
+
+      "type": "object",
+      "properties": {
+        "allow": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        },
+
+        "block": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        }
+      }
+    },
+
+    "cookies": {
+      "description": "Allow or deny websites to set cookies.",
+      "first_available": "60.0",
+
+      "type": "object",
+      "properties": {
+        "allow": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        },
+
+        "block": {
+          "type": "array",
+          "items": {
+            "type": "origin"
+          }
+        }
+      }
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/schemas/schema.jsm
@@ -0,0 +1,10 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["schema"];
+
+this.schema =
+#include policies-schema.json
copy from browser/components/newtab/tests/browser/.eslintrc.js
copy to browser/components/enterprisepolicies/tests/browser/.eslintrc.js
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+prefs =
+  browser.policies.enabled=true
+support-files =
+  head.js
+  config_dont_check_default_browser.json
+  config_popups_cookies_addons_flash.json
+  config_setAndLockPref.json
+  config_simple_policies.json
+  config_broken_json.json
+
+[browser_policies_broken_json.js]
+[browser_policies_popups_cookies_addons_flash.js]
+[browser_policies_setAndLockPref_API.js]
+[browser_policies_simple_policies.js]
+[browser_policies_validate_and_parse_API.js]
+[browser_policy_default_browser_check.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js
@@ -0,0 +1,15 @@
+/* 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";
+
+add_task(async function test_clean_slate() {
+  await startWithCleanSlate();
+});
+
+add_task(async function test_broken_json() {
+  await setupPolicyEngineWithJson("config_broken_json.json");
+
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.FAILED, "Engine was correctly set to the error state");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_popups_cookies_addons_flash.js
@@ -0,0 +1,121 @@
+/* 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";
+
+function URI(str) {
+  return Services.io.newURI(str);
+}
+
+add_task(async function test_start_with_disabled_engine() {
+  await startWithCleanSlate();
+});
+
+add_task(async function test_setup_preexisting_permissions() {
+  // Pre-existing ALLOW permissions that should be overriden
+  // with DENY.
+  Services.perms.add(URI("https://www.pre-existing-allow.com"),
+                     "popup",
+                     Ci.nsIPermissionManager.ALLOW_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  Services.perms.add(URI("https://www.pre-existing-allow.com"),
+                     "install",
+                     Ci.nsIPermissionManager.ALLOW_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  Services.perms.add(URI("https://www.pre-existing-allow.com"),
+                     "cookie",
+                     Ci.nsIPermissionManager.ALLOW_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  Services.perms.add(URI("https://www.pre-existing-allow.com"),
+                     "plugin:flash",
+                     Ci.nsIPermissionManager.ALLOW_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  // Pre-existing DENY permissions that should be overriden
+  // with ALLOW.
+  Services.perms.add(URI("https://www.pre-existing-deny.com"),
+                     "popup",
+                     Ci.nsIPermissionManager.DENY_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  Services.perms.add(URI("https://www.pre-existing-deny.com"),
+                     "install",
+                     Ci.nsIPermissionManager.DENY_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  Services.perms.add(URI("https://www.pre-existing-deny.com"),
+                     "cookie",
+                     Ci.nsIPermissionManager.DENY_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  Services.perms.add(URI("https://www.pre-existing-deny.com"),
+                     "plugin:flash",
+                     Ci.nsIPermissionManager.DENY_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+});
+
+add_task(async function test_setup_activate_policies() {
+  await setupPolicyEngineWithJson("config_popups_cookies_addons_flash.json");
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
+});
+
+function checkPermission(url, expected, permissionName) {
+  let expectedValue = Ci.nsIPermissionManager[`${expected}_ACTION`];
+  let uri = Services.io.newURI(`https://www.${url}`);
+
+  is(Services.perms.testPermission(uri, permissionName),
+    expectedValue,
+    `Correct (${permissionName}=${expected}) for URL ${url}`);
+
+  if (expected != "UNKNOWN") {
+    let permission = Services.perms.getPermissionObjectForURI(
+      uri, permissionName, true);
+    ok(permission, "Permission object exists");
+    is(permission.expireType, Ci.nsIPermissionManager.EXPIRE_POLICY,
+       "Permission expireType is correct");
+  }
+}
+
+function checkAllPermissionsForType(type) {
+  checkPermission("allow.com", "ALLOW", type);
+  checkPermission("deny.com", "DENY", type);
+  checkPermission("unknown.com", "UNKNOWN", type);
+  checkPermission("pre-existing-allow.com", "DENY", type);
+  checkPermission("pre-existing-deny.com", "ALLOW", type);
+}
+
+add_task(async function test_popups_policy() {
+  checkAllPermissionsForType("popup");
+});
+
+add_task(async function test_webextensions_policy() {
+  checkAllPermissionsForType("install");
+});
+
+add_task(async function test_cookies_policy() {
+  checkAllPermissionsForType("cookie");
+});
+
+add_task(async function test_flash_policy() {
+  checkAllPermissionsForType("plugin:flash");
+});
+
+add_task(async function test_change_permission() {
+  // Checks that changing a permission will still retain the
+  // value set through the engine.
+  Services.perms.add(URI("https://www.allow.com"), "popup",
+                     Ci.nsIPermissionManager.DENY_ACTION,
+                     Ci.nsIPermissionManager.EXPIRE_SESSION);
+
+  checkPermission("allow.com", "ALLOW", "popup");
+
+  // Also change one un-managed permission to make sure it doesn't
+  // cause any problems to the policy engine or the permission manager.
+  Services.perms.add(URI("https://www.unmanaged.com"), "popup",
+                   Ci.nsIPermissionManager.DENY_ACTION,
+                   Ci.nsIPermissionManager.EXPIRE_SESSION);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_setAndLockPref_API.js
@@ -0,0 +1,127 @@
+/* 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";
+
+add_task(async function test_clean_slate() {
+  await startWithCleanSlate();
+});
+
+let { Policies, setAndLockPref } = Cu.import("resource:///modules/policies/Policies.jsm", {});
+
+function checkPref(prefName, expectedValue) {
+  let prefType, prefValue;
+  switch (typeof(expectedValue)) {
+    case "boolean":
+      prefType = Services.prefs.PREF_BOOL;
+      prefValue = Services.prefs.getBoolPref(prefName);
+      break;
+
+    case "number":
+      prefType = Services.prefs.PREF_INT;
+      prefValue = Services.prefs.getIntPref(prefName);
+      break;
+
+    case "string":
+      prefType = Services.prefs.PREF_STRING;
+      prefValue = Services.prefs.getStringPref(prefName);
+      break;
+  }
+
+  ok(Services.prefs.prefIsLocked(prefName), `Pref ${prefName} is correctly locked`);
+  is(Services.prefs.getPrefType(prefName), prefType, `Pref ${prefName} has the correct type`);
+  is(prefValue, expectedValue, `Pref ${prefName} has the correct value`);
+}
+
+add_task(async function test_API_directly() {
+  setAndLockPref("policies.test.boolPref", true);
+  checkPref("policies.test.boolPref", true);
+
+  // Check that a previously-locked pref can be changed
+  // (it will be unlocked first).
+  setAndLockPref("policies.test.boolPref", false);
+  checkPref("policies.test.boolPref", false);
+
+  setAndLockPref("policies.test.intPref", 0);
+  checkPref("policies.test.intPref", 0);
+
+  setAndLockPref("policies.test.stringPref", "policies test");
+  checkPref("policies.test.stringPref", "policies test");
+
+  // Test that user values do not override the prefs, and the get*Pref call
+  // still return the value set through setAndLockPref
+  Services.prefs.setBoolPref("policies.test.boolPref", true);
+  checkPref("policies.test.boolPref", false);
+
+  Services.prefs.setIntPref("policies.test.intPref", 10);
+  checkPref("policies.test.intPref", 0);
+
+  Services.prefs.setStringPref("policies.test.stringPref", "policies test");
+  checkPref("policies.test.stringPref", "policies test");
+
+  try {
+    // Test that a non-integer value is correctly rejected, even though
+    // typeof(val) == "number"
+    setAndLockPref("policies.test.intPref", 1.5);
+    ok(false, "Integer value should be rejected");
+  } catch (ex) {
+    ok(true, "Integer value was rejected");
+  }
+});
+
+add_task(async function test_API_through_policies() {
+  // Ensure that the values received by the policies have the correct
+  // type to make sure things are properly working.
+
+  // Implement functions to handle the three simple policies
+  // that will be added to the schema.
+  Policies.bool_policy = {
+    onBeforeUIStartup(manager, param) {
+      setAndLockPref("policies.test2.boolPref", param);
+    }
+  };
+
+  Policies.int_policy = {
+    onBeforeUIStartup(manager, param) {
+      setAndLockPref("policies.test2.intPref", param);
+    }
+  };
+
+  Policies.string_policy = {
+    onBeforeUIStartup(manager, param) {
+      setAndLockPref("policies.test2.stringPref", param);
+    }
+  };
+
+  await setupPolicyEngineWithJson(
+    "config_setAndLockPref.json",
+    /* custom schema */
+    {
+      properties: {
+        "bool_policy": {
+          "type": "boolean"
+        },
+
+        "int_policy": {
+          "type": "integer"
+        },
+
+        "string_policy": {
+          "type": "string"
+        }
+      }
+    }
+  );
+
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
+
+  // The expected values come from config_setAndLockPref.json
+  checkPref("policies.test2.boolPref", true);
+  checkPref("policies.test2.intPref", 42);
+  checkPref("policies.test2.stringPref", "policies test 2");
+
+  delete Policies.bool_policy;
+  delete Policies.int_policy;
+  delete Policies.string_policy;
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_simple_policies.js
@@ -0,0 +1,101 @@
+/* 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";
+
+add_task(async function test_clean_slate() {
+  await startWithCleanSlate();
+});
+
+add_task(async function test_simple_policies() {
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+    // Initialize the service in the content process, in case it hasn't
+    // already started.
+    Services.policies;
+  });
+
+  let { Policies } = Cu.import("resource:///modules/policies/Policies.jsm", {});
+
+  let policy0Ran = false, policy1Ran = false, policy2Ran = false, policy3Ran = false;
+
+  // Implement functions to handle the four simple policies that will be added
+  // to the schema.
+  Policies.simple_policy0 = {
+    onProfileAfterChange(manager, param) {
+      is(param, true, "Param matches what was passed in config file");
+      policy0Ran = true;
+    }
+  };
+
+  Policies.simple_policy1 = {
+    onProfileAfterChange(manager, param) {
+      is(param, true, "Param matches what was passed in config file");
+      manager.disallowFeature("feature1", /* needed in content process */ true);
+      policy1Ran = true;
+    }
+  };
+
+  Policies.simple_policy2 = {
+    onBeforeUIStartup(manager, param) {
+      is(param, true, "Param matches what was passed in config file");
+      manager.disallowFeature("feature2", /* needed in content process */ false);
+      policy2Ran = true;
+    }
+  };
+
+  Policies.simple_policy3 = {
+    onAllWindowsRestored(manager, param) {
+      is(param, false, "Param matches what was passed in config file");
+      policy3Ran = true;
+    }
+  };
+
+  await setupPolicyEngineWithJson(
+    "config_simple_policies.json",
+    /* custom schema */
+    {
+      properties: {
+        "simple_policy0": {
+          "type": "boolean"
+        },
+
+        "simple_policy1": {
+          "type": "boolean"
+        },
+
+        "simple_policy2": {
+          "type": "boolean"
+        },
+
+        "simple_policy3": {
+          "type": "boolean"
+        }
+
+      }
+    }
+  );
+
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
+  is(Services.policies.isAllowed("feature1"), false, "Dummy feature was disallowed");
+  is(Services.policies.isAllowed("feature2"), false, "Dummy feature was disallowed");
+
+  ok(policy0Ran, "Policy 0 ran correctly through BeforeAddons");
+  ok(policy1Ran, "Policy 1 ran correctly through onProfileAfterChange");
+  ok(policy2Ran, "Policy 2 ran correctly through onBeforeUIStartup");
+  ok(policy3Ran, "Policy 3 ran correctly through onAllWindowsRestored");
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      is(Services.policies.isAllowed("feature1"), false, "Correctly disallowed in the content process");
+      // Feature 2 wasn't explictly marked as needed in the content process, so it is not marked
+      // as disallowed there.
+      is(Services.policies.isAllowed("feature2"), true, "Correctly missing in the content process");
+    }
+  });
+
+  delete Policies.simple_policy0;
+  delete Policies.simple_policy1;
+  delete Policies.simple_policy2;
+  delete Policies.simple_policy3;
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_validate_and_parse_API.js
@@ -0,0 +1,231 @@
+/* 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";
+
+/* This file will test the parameters parsing and validation directly through
+   the PoliciesValidator API.
+ */
+
+const { PoliciesValidator } = Cu.import("resource:///modules/policies/PoliciesValidator.jsm", {});
+
+add_task(async function test_boolean_values() {
+  let schema = {
+    type: "boolean"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(true, schema);
+  ok(valid && parsed === true, "Parsed boolean value correctly");
+
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(false, schema);
+  ok(valid && parsed === false, "Parsed boolean value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("0", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters("true", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(undefined, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_number_values() {
+  let schema = {
+    type: "number"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(1, schema);
+  ok(valid && parsed === 1, "Parsed number value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("1", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_integer_values() {
+  // Integer is an alias for number
+  let schema = {
+    type: "integer"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(1, schema);
+  ok(valid && parsed == 1, "Parsed integer value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("1", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_string_values() {
+  let schema = {
+    type: "string"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters("foobar", schema);
+  ok(valid && parsed == "foobar", "Parsed string value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters(1, schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(undefined, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_URL_values() {
+  let schema = {
+    type: "URL"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters("https://www.example.com/foo#bar", schema);
+  ok(valid, "URL is valid");
+  ok(parsed instanceof Ci.nsIURI, "parsed is a nsIURI");
+  is(parsed.prePath, "https://www.example.com", "prePath is correct");
+  is(parsed.pathQueryRef, "/foo#bar", "pathQueryRef is correct");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("www.example.com", schema)[0], "Scheme is required for URL");
+  ok(!PoliciesValidator.validateAndParseParameters("https://:!$%", schema)[0], "Invalid URL");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+});
+
+add_task(async function test_origin_values() {
+  // Origin is a URL that doesn't contain a path/query string (i.e., it's only scheme + host + port)
+  let schema = {
+    type: "origin"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters("https://www.example.com", schema);
+  ok(valid, "Origin is valid");
+  ok(parsed instanceof Ci.nsIURI, "parsed is a nsIURI");
+  is(parsed.prePath, "https://www.example.com", "prePath is correct");
+  is(parsed.pathQueryRef, "/", "pathQueryRef is corect");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("https://www.example.com/foobar", schema)[0], "Origin cannot contain a path part");
+  ok(!PoliciesValidator.validateAndParseParameters("https://:!$%", schema)[0], "Invalid origin");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+});
+
+add_task(async function test_array_values() {
+  // The types inside an array object must all be the same
+  let schema = {
+    type: "array",
+    items: {
+      type: "number"
+    }
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters([1, 2, 3], schema);
+  ok(valid, "Array is valid");
+  ok(Array.isArray(parsed), "parsed is an array");
+  is(parsed.length, 3, "array is correct");
+
+  // An empty array is also valid
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters([], schema);
+  ok(valid, "Array is valid");
+  ok(Array.isArray(parsed), "parsed is an array");
+  is(parsed.length, 0, "array is correct");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters([1, true, 3], schema)[0], "Mixed types");
+  ok(!PoliciesValidator.validateAndParseParameters(2, schema)[0], "Type is correct but not in an array");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Object is not an array");
+});
+
+add_task(async function test_object_values() {
+  let schema = {
+    type: "object",
+    properties: {
+      url: {
+        type: "URL"
+      },
+      title: {
+        type: "string"
+      }
+    }
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(
+    {
+      url: "https://www.example.com/foo#bar",
+      title: "Foo",
+      alias: "Bar"
+    },
+    schema);
+
+  ok(valid, "Object is valid");
+  ok(typeof(parsed) == "object", "parsed in an object");
+  ok(parsed.url instanceof Ci.nsIURI, "types inside the object are also parsed");
+  is(parsed.url.spec, "https://www.example.com/foo#bar", "URL was correctly parsed");
+  is(parsed.title, "Foo", "title was correctly parsed");
+  is(parsed.alias, undefined, "property not described in the schema is not present in the parsed object");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters(
+    {
+      url: "https://www.example.com/foo#bar",
+      title: 3,
+    },
+    schema)[0], "Mismatched type for title");
+
+  ok(!PoliciesValidator.validateAndParseParameters(
+    {
+      url: "www.example.com",
+      title: 3,
+    },
+    schema)[0], "Invalid URL inside the object");
+});
+
+add_task(async function test_array_of_objects() {
+  // This schema is used, for example, for bookmarks
+  let schema = {
+    type: "array",
+    items: {
+      type: "object",
+      properties: {
+        url: {
+          type: "URL",
+        },
+        title: {
+          type: "string"
+        }
+      }
+    }
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(
+    [{
+      url: "https://www.example.com/bookmark1",
+      title: "Foo",
+    },
+    {
+      url: "https://www.example.com/bookmark2",
+      title: "Bar",
+    }],
+    schema);
+
+  ok(valid, "Array is valid");
+  is(parsed.length, 2, "Correct number of items");
+
+  ok(typeof(parsed[0]) == "object" && typeof(parsed[1]) == "object", "Correct objects inside array");
+
+  is(parsed[0].url.spec, "https://www.example.com/bookmark1", "Correct URL for bookmark 1");
+  is(parsed[1].url.spec, "https://www.example.com/bookmark2", "Correct URL for bookmark 2");
+
+  is(parsed[0].title, "Foo", "Correct title for bookmark 1");
+  is(parsed[1].title, "Bar", "Correct title for bookmark 2");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_default_browser_check.js
@@ -0,0 +1,28 @@
+/* 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 { ShellService } = Cu.import("resource:///modules/ShellService.jsm", {});
+
+add_task(async function test_default_browser_check() {
+  ShellService._checkedThisSession = false;
+  // On a normal profile, the default is true. However, this gets set to false on the
+  // testing profile. Let's start with true for a sanity check.
+
+  ShellService.shouldCheckDefaultBrowser = true;
+  is(ShellService.shouldCheckDefaultBrowser, true, "Sanity check");
+
+  await setupPolicyEngineWithJson("config_dont_check_default_browser.json");
+
+  is(ShellService.shouldCheckDefaultBrowser, false, "Policy changed it to not check");
+
+  // Try to change it to true and check that it doesn't take effect
+  ShellService.shouldCheckDefaultBrowser = true;
+
+  is(ShellService.shouldCheckDefaultBrowser, false, "Policy is enforced");
+
+  // Unlock the pref because if it stays locked, and this test runs twice in a row,
+  // the first sanity check will fail.
+  Services.prefs.unlockPref("browser.shell.checkDefaultBrowser");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/config_broken_json.json
@@ -0,0 +1,3 @@
+{
+  "policies
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/config_dont_check_default_browser.json
@@ -0,0 +1,5 @@
+{
+  "policies": {
+    "dont_check_default_browser": true
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/config_popups_cookies_addons_flash.json
@@ -0,0 +1,51 @@
+{
+  "policies": {
+    "popups": {
+      "allow": [
+        "https://www.allow.com",
+        "https://www.pre-existing-deny.com"
+      ],
+
+      "block": [
+        "https://www.deny.com",
+        "https://www.pre-existing-allow.com"
+      ]
+    },
+
+    "cookies": {
+      "allow": [
+        "https://www.allow.com",
+        "https://www.pre-existing-deny.com"
+      ],
+
+      "block": [
+        "https://www.deny.com",
+        "https://www.pre-existing-allow.com"
+      ]
+    },
+
+    "install_addons": {
+      "allow": [
+        "https://www.allow.com",
+        "https://www.pre-existing-deny.com"
+      ],
+
+      "block": [
+        "https://www.deny.com",
+        "https://www.pre-existing-allow.com"
+      ]
+    },
+
+    "flash_plugin": {
+      "allow": [
+        "https://www.allow.com",
+        "https://www.pre-existing-deny.com"
+      ],
+
+      "block": [
+        "https://www.deny.com",
+        "https://www.pre-existing-allow.com"
+      ]
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/config_setAndLockPref.json
@@ -0,0 +1,7 @@
+{
+  "policies": {
+    "bool_policy": true,
+    "int_policy": 42,
+    "string_policy": "policies test 2"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/config_simple_policies.json
@@ -0,0 +1,8 @@
+{
+  "policies": {
+    "simple_policy0": true,
+    "simple_policy1": true,
+    "simple_policy2": true,
+    "simple_policy3": false
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/head.js
@@ -0,0 +1,34 @@
+/* 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";
+
+async function setupPolicyEngineWithJson(jsonName, customSchema) {
+  let filePath = getTestFilePath(jsonName ? jsonName : "non-existing-file.json");
+  Services.prefs.setStringPref("browser.policies.alternatePath", filePath);
+
+  let resolve = null;
+  let promise = new Promise((r) => resolve = r);
+
+  Services.obs.addObserver(function observer() {
+    Services.obs.removeObserver(observer, "EnterprisePolicies:AllPoliciesApplied");
+    resolve();
+  }, "EnterprisePolicies:AllPoliciesApplied");
+
+  // Clear any previously used custom schema
+  Cu.unload("resource:///modules/policies/schema.jsm");
+
+  if (customSchema) {
+    let schemaModule = Cu.import("resource:///modules/policies/schema.jsm", {});
+    schemaModule.schema = customSchema;
+  }
+
+  Services.obs.notifyObservers(null, "EnterprisePolicies:Restart");
+  return promise;
+}
+
+async function startWithCleanSlate() {
+  await setupPolicyEngineWithJson("");
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.INACTIVE, "Engine is inactive");
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/moz.build
@@ -0,0 +1,12 @@
+# -*- 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/.
+
+with Files("**"):
+    BUG_COMPONENT = ("Firefox", "General")
+
+BROWSER_CHROME_MANIFESTS += [
+    'browser/browser.ini'
+]
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -611,16 +611,24 @@ class Tab extends TabBase {
   }
 
   get frameLoader() {
     // If we don't have a frameLoader yet, just return a dummy with no width and
     // height.
     return super.frameLoader || {lazyWidth: 0, lazyHeight: 0};
   }
 
+  get hidden() {
+    return this.nativeTab.hidden;
+  }
+
+  get sharingState() {
+    return this.window.gBrowser.getTabSharingState(this.nativeTab);
+  }
+
   get cookieStoreId() {
     return getCookieStoreIdForTab(this, this.nativeTab);
   }
 
   get openerTabId() {
     let opener = this.nativeTab.openerTab;
     if (opener && opener.parentNode && opener.ownerDocument == this.nativeTab.ownerDocument) {
       return tabTracker.getId(opener);
@@ -712,16 +720,17 @@ class Tab extends TabBase {
   static convertFromSessionStoreClosedData(extension, tabData, window = null) {
     let result = {
       sessionId: String(tabData.closedId),
       index: tabData.pos ? tabData.pos : 0,
       windowId: window && windowTracker.getId(window),
       highlighted: false,
       active: false,
       pinned: false,
+      hidden: tabData.state ? tabData.state.hidden : tabData.hidden,
       incognito: Boolean(tabData.state && tabData.state.isPrivate),
       lastAccessed: tabData.state ? tabData.state.lastAccessed : tabData.lastAccessed,
     };
 
     if (extension.tabManager.hasTabPermission(tabData)) {
       let entries = tabData.state ? tabData.state.entries : tabData.entries;
       let lastTabIndex = tabData.state ? tabData.state.index : tabData.index;
       // We need to take lastTabIndex - 1 because the index in the tab data is
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -25,16 +25,20 @@ var {
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   IconDetails,
   StartupCache,
 } = ExtensionParent;
 
+var {
+  ExtensionError,
+} = ExtensionUtils;
+
 Cu.importGlobalProperties(["InspectorUtils"]);
 
 const POPUP_PRELOAD_TIMEOUT_MS = 200;
 const POPUP_OPEN_MS_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_OPEN_MS";
 const POPUP_RESULT_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT";
 
 var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
@@ -670,18 +674,21 @@ this.browserAction = class extends Exten
 
           let popup = browserAction.getProperty(tab, "popup");
           return Promise.resolve(popup);
         },
 
         setBadgeBackgroundColor: function(details) {
           let tab = getTab(details.tabId);
           let color = details.color;
-          if (!Array.isArray(color)) {
+          if (typeof color == "string") {
             let col = InspectorUtils.colorToRGBA(color);
+            if (!col) {
+              throw new ExtensionError(`Invalid badge background color: "${color}"`);
+            }
             color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
           }
           browserAction.setProperty(tab, "badgeBackgroundColor", color);
         },
 
         getBadgeBackgroundColor: function(details, callback) {
           let tab = getTab(details.tabId);
 
--- a/browser/components/extensions/ext-devtools-network.js
+++ b/browser/components/extensions/ext-devtools-network.js
@@ -20,13 +20,17 @@ this.devtools_network = class extends Ex
               target.on("navigate", listener);
             });
             return () => {
               targetPromise.then(target => {
                 target.off("navigate", listener);
               });
             };
           }).api(),
+
+          getHAR: function() {
+            return context.devToolsToolbox.getHARFromNetMonitor();
+          },
         },
       },
     };
   }
 };
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -274,32 +274,33 @@ this.pageAction = class extends Extensio
 
         isShown(details) {
           let tab = tabTracker.getTab(details.tabId);
           return pageAction.getProperty(tab, "show");
         },
 
         setTitle(details) {
           let tab = tabTracker.getTab(details.tabId);
-
-          // Clear the tab-specific title when given a null string.
-          pageAction.setProperty(tab, "title", details.title || null);
+          pageAction.setProperty(tab, "title", details.title);
         },
 
         getTitle(details) {
           let tab = tabTracker.getTab(details.tabId);
 
           let title = pageAction.getProperty(tab, "title");
           return Promise.resolve(title);
         },
 
         setIcon(details) {
           let tab = tabTracker.getTab(details.tabId);
 
           let icon = IconDetails.normalize(details, extension, context);
+          if (!Object.keys(icon).length) {
+            icon = null;
+          }
           pageAction.setProperty(tab, "icon", icon);
         },
 
         setPopup(details) {
           let tab = tabTracker.getTab(details.tabId);
 
           // Note: Chrome resolves arguments to setIcon relative to the calling
           // context, but resolves arguments to setPopup relative to the extension
--- a/browser/components/extensions/ext-sidebarAction.js
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -4,20 +4,16 @@
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-browser.js */
 /* globals WINDOW_ID_CURRENT */
 
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
-  ExtensionError,
-} = ExtensionUtils;
-
-var {
   IconDetails,
 } = ExtensionParent;
 
 var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> SidebarAction]
 let sidebarActionMap = new WeakMap();
 
@@ -51,23 +47,24 @@ this.sidebarAction = class extends Exten
     this.browserStyle = options.browser_style || options.browser_style === null;
 
     this.defaults = {
       enabled: true,
       title: options.default_title || extension.name,
       icon: IconDetails.normalize({path: options.default_icon}, extension),
       panel: options.default_panel || "",
     };
+    this.globals = Object.create(this.defaults);
 
-    this.tabContext = new TabContext(tab => Object.create(this.defaults),
+    this.tabContext = new TabContext(tab => Object.create(this.globals),
                                      extension);
 
     // We need to ensure our elements are available before session restore.
     this.windowOpenListener = (window) => {
-      this.createMenuItem(window, this.defaults);
+      this.createMenuItem(window, this.globals);
     };
     windowTracker.addOpenListener(this.windowOpenListener);
 
     this.updateHeader = (event) => {
       let window = event.target.ownerGlobal;
       let details = this.tabContext.get(window.gBrowser.selectedTab);
       let header = window.document.getElementById("sidebar-switcher-target");
       if (window.SidebarUI.currentID === this.id) {
@@ -286,40 +283,44 @@ this.sidebarAction = class extends Exten
    * @param {XULElement|null} nativeTab
    *        Webextension tab object, may be null.
    * @param {string} prop
    *        String property to retrieve ["icon", "title", or "panel"].
    * @param {string} value
    *        Value for property.
    */
   setProperty(nativeTab, prop, value) {
+    let values;
     if (nativeTab === null) {
-      this.defaults[prop] = value;
-    } else if (value !== null) {
-      this.tabContext.get(nativeTab)[prop] = value;
+      values = this.globals;
     } else {
-      delete this.tabContext.get(nativeTab)[prop];
+      values = this.tabContext.get(nativeTab);
+    }
+    if (value === null) {
+      delete values[prop];
+    } else {
+      values[prop] = value;
     }
 
     this.updateOnChange(nativeTab);
   }
 
   /**
-   * Retrieve a property from the tab or defaults if tab is null.
+   * Retrieve a property from the tab or globals if tab is null.
    *
    * @param {XULElement|null} nativeTab
    *        Browser tab object, may be null.
    * @param {string} prop
    *        String property to retrieve ["icon", "title", or "panel"]
    * @returns {string} value
    *          Value for prop.
    */
   getProperty(nativeTab, prop) {
     if (nativeTab === null) {
-      return this.defaults[prop];
+      return this.globals[prop];
     }
     return this.tabContext.get(nativeTab)[prop];
   }
 
   /**
    * Triggers this sidebar action for the given window, with the same effects as
    * if it were toggled via menu or toolbarbutton by a user.
    *
@@ -376,53 +377,48 @@ this.sidebarAction = class extends Exten
       }
       return null;
     }
 
     return {
       sidebarAction: {
         async setTitle(details) {
           let nativeTab = getTab(details.tabId);
-
-          let title = details.title;
-          // Clear the tab-specific title when given a null string.
-          if (nativeTab && title === "") {
-            title = null;
-          }
-          sidebarAction.setProperty(nativeTab, "title", title);
+          sidebarAction.setProperty(nativeTab, "title", details.title);
         },
 
         getTitle(details) {
           let nativeTab = getTab(details.tabId);
 
           let title = sidebarAction.getProperty(nativeTab, "title");
           return Promise.resolve(title);
         },
 
         async setIcon(details) {
           let nativeTab = getTab(details.tabId);
 
           let icon = IconDetails.normalize(details, extension, context);
+          if (!Object.keys(icon).length) {
+            icon = null;
+          }
           sidebarAction.setProperty(nativeTab, "icon", icon);
         },
 
         async setPanel(details) {
           let nativeTab = getTab(details.tabId);
 
           let url;
-          // Clear the tab-specific url when given a null string.
-          if (nativeTab && details.panel === "") {
+          // Clear the url when given null or empty string.
+          if (!details.panel) {
             url = null;
-          } else if (details.panel !== "") {
+          } else {
             url = context.uri.resolve(details.panel);
             if (!context.checkLoadURL(url)) {
               return Promise.reject({message: `Access denied for URL ${url}`});
             }
-          } else {
-            throw new ExtensionError("Invalid url for sidebar panel.");
           }
 
           sidebarAction.setProperty(nativeTab, "panel", url);
         },
 
         getPanel(details) {
           let nativeTab = getTab(details.tabId);
 
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -15,16 +15,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
   return Services.strings.createBundle("chrome://global/locale/extensions.properties");
 });
 
 var {
   ExtensionError,
 } = ExtensionUtils;
 
+const TABHIDE_PREFNAME = "extensions.webextensions.tabhide.enabled";
+
+// WeakMap[Tab -> ExtensionID]
+let hiddenTabs = new WeakMap();
+
 let tabListener = {
   tabReadyInitialized: false,
   tabReadyPromises: new WeakMap(),
   initializingTabs: new WeakSet(),
 
   initTabReady() {
     if (!this.tabReadyInitialized) {
       windowTracker.addListener("progress", this);
@@ -72,16 +77,37 @@ let tabListener = {
         this.tabReadyPromises.set(nativeTab, deferred);
       }
     }
     return deferred.promise;
   },
 };
 
 this.tabs = class extends ExtensionAPI {
+  onShutdown(reason) {
+    if (!this.extension.hasPermission("tabHide")) {
+      return;
+    }
+    if (reason == "ADDON_DISABLE" ||
+        reason == "ADDON_UNINSTALL") {
+      // Show all hidden tabs if a tab managing extension is uninstalled or
+      // disabled.  If a user has more than one, the extensions will need to
+      // self-manage re-hiding tabs.
+      for (let tab of this.extension.tabManager.query()) {
+        let nativeTab = tabTracker.getTab(tab.id);
+        if (hiddenTabs.get(nativeTab) === this.extension.id) {
+          hiddenTabs.delete(nativeTab);
+          if (nativeTab.ownerGlobal) {
+            nativeTab.ownerGlobal.gBrowser.showTab(nativeTab);
+          }
+        }
+      }
+    }
+  }
+
   getAPI(context) {
     let {extension} = context;
 
     let {tabManager} = extension;
 
     function getTabOrActive(tabId) {
       if (tabId !== null) {
         return tabTracker.getTab(tabId);
@@ -256,25 +282,34 @@ this.tabs = class extends ExtensionAPI {
                 needed.push("mutedInfo");
               }
               if (changed.includes("soundplaying")) {
                 needed.push("audible");
               }
               if (changed.includes("label")) {
                 needed.push("title");
               }
+              if (changed.includes("sharing")) {
+                needed.push("sharingState");
+              }
             } else if (event.type == "TabPinned") {
               needed.push("pinned");
             } else if (event.type == "TabUnpinned") {
               needed.push("pinned");
             } else if (event.type == "TabBrowserInserted" &&
                        !event.detail.insertedOnTabCreation) {
               needed.push("discarded");
             } else if (event.type == "TabBrowserDiscarded") {
               needed.push("discarded");
+            } else if (event.type == "TabShow") {
+              needed.push("hidden");
+              // Always remove the tab from the hiddenTabs map.
+              hiddenTabs.delete(event.originalTarget);
+            } else if (event.type == "TabHide") {
+              needed.push("hidden");
             }
 
             let tab = tabManager.getWrapper(event.originalTarget);
             let changeInfo = {};
             for (let prop of needed) {
               changeInfo[prop] = tab[prop];
             }
 
@@ -305,26 +340,30 @@ this.tabs = class extends ExtensionAPI {
           };
 
           windowTracker.addListener("status", statusListener);
           windowTracker.addListener("TabAttrModified", listener);
           windowTracker.addListener("TabPinned", listener);
           windowTracker.addListener("TabUnpinned", listener);
           windowTracker.addListener("TabBrowserInserted", listener);
           windowTracker.addListener("TabBrowserDiscarded", listener);
+          windowTracker.addListener("TabShow", listener);
+          windowTracker.addListener("TabHide", listener);
 
           tabTracker.on("tab-isarticle", isArticleChangeListener);
 
           return () => {
             windowTracker.removeListener("status", statusListener);
             windowTracker.removeListener("TabAttrModified", listener);
             windowTracker.removeListener("TabPinned", listener);
             windowTracker.removeListener("TabUnpinned", listener);
             windowTracker.removeListener("TabBrowserInserted", listener);
             windowTracker.removeListener("TabBrowserDiscarded", listener);
+            windowTracker.removeListener("TabShow", listener);
+            windowTracker.removeListener("TabHide", listener);
             tabTracker.off("tab-isarticle", isArticleChangeListener);
           };
         }).api(),
 
         create(createProperties) {
           return new Promise((resolve, reject) => {
             let window = createProperties.windowId !== null ?
               windowTracker.getWindow(createProperties.windowId, context) :
@@ -877,60 +916,91 @@ this.tabs = class extends ExtensionAPI {
           picker.appendFilter("PDF", "*.pdf");
           picker.defaultExtension = "pdf";
           picker.defaultString = activeTab.linkedBrowser.contentTitle + ".pdf";
 
           return new Promise(resolve => {
             picker.open(function(retval) {
               if (retval == 0 || retval == 2) {
                 // OK clicked (retval == 0) or replace confirmed (retval == 2)
+
+                // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
+                // the print progress listener is never called. This workaround ensures that a correct status is always returned.
                 try {
                   let fstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
-                  fstream.init(picker.file, 0x2A, 0x1B6, 0); // write|create|truncate, file permissions rw-rw-rw- = 0666 = 0x1B6
-                  fstream.close(); // unlock file
+                  fstream.init(picker.file, 0x2A, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
+                  fstream.close();
                 } catch (e) {
                   resolve(retval == 0 ? "not_saved" : "not_replaced");
                   return;
                 }
 
                 let psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService);
                 let printSettings = psService.newPrintSettings;
 
+                printSettings.printerName = "";
+                printSettings.isInitializedFromPrinter = true;
+                printSettings.isInitializedFromPrefs = true;
+
                 printSettings.printToFile = true;
                 printSettings.toFileName = picker.file.path;
 
                 printSettings.printSilent = true;
                 printSettings.showPrintProgress = false;
 
                 printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
                 printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
 
+                if (pageSettings.paperSizeUnit !== null) {
+                  printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
+                }
+                if (pageSettings.paperWidth !== null) {
+                  printSettings.paperWidth = pageSettings.paperWidth;
+                }
+                if (pageSettings.paperHeight !== null) {
+                  printSettings.paperHeight = pageSettings.paperHeight;
+                }
                 if (pageSettings.orientation !== null) {
                   printSettings.orientation = pageSettings.orientation;
                 }
                 if (pageSettings.scaling !== null) {
                   printSettings.scaling = pageSettings.scaling;
                 }
                 if (pageSettings.shrinkToFit !== null) {
                   printSettings.shrinkToFit = pageSettings.shrinkToFit;
                 }
                 if (pageSettings.showBackgroundColors !== null) {
                   printSettings.printBGColors = pageSettings.showBackgroundColors;
                 }
                 if (pageSettings.showBackgroundImages !== null) {
                   printSettings.printBGImages = pageSettings.showBackgroundImages;
                 }
-                if (pageSettings.paperSizeUnit !== null) {
-                  printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
+                if (pageSettings.edgeLeft !== null) {
+                  printSettings.edgeLeft = pageSettings.edgeLeft;
+                }
+                if (pageSettings.edgeRight !== null) {
+                  printSettings.edgeRight = pageSettings.edgeRight;
+                }
+                if (pageSettings.edgeTop !== null) {
+                  printSettings.edgeTop = pageSettings.edgeTop;
+                }
+                if (pageSettings.edgeBottom !== null) {
+                  printSettings.edgeBottom = pageSettings.edgeBottom;
                 }
-                if (pageSettings.paperWidth !== null) {
-                  printSettings.paperWidth = pageSettings.paperWidth;
+                if (pageSettings.marginLeft !== null) {
+                  printSettings.marginLeft = pageSettings.marginLeft;
+                }
+                if (pageSettings.marginRight !== null) {
+                  printSettings.marginRight = pageSettings.marginRight;
                 }
-                if (pageSettings.paperHeight !== null) {
-                  printSettings.paperHeight = pageSettings.paperHeight;
+                if (pageSettings.marginTop !== null) {
+                  printSettings.marginTop = pageSettings.marginTop;
+                }
+                if (pageSettings.marginBottom !== null) {
+                  printSettings.marginBottom = pageSettings.marginBottom;
                 }
                 if (pageSettings.headerLeft !== null) {
                   printSettings.headerStrLeft = pageSettings.headerLeft;
                 }
                 if (pageSettings.headerCenter !== null) {
                   printSettings.headerStrCenter = pageSettings.headerCenter;
                 }
                 if (pageSettings.headerRight !== null) {
@@ -940,32 +1010,35 @@ this.tabs = class extends ExtensionAPI {
                   printSettings.footerStrLeft = pageSettings.footerLeft;
                 }
                 if (pageSettings.footerCenter !== null) {
                   printSettings.footerStrCenter = pageSettings.footerCenter;
                 }
                 if (pageSettings.footerRight !== null) {
                   printSettings.footerStrRight = pageSettings.footerRight;
                 }
-                if (pageSettings.marginLeft !== null) {
-                  printSettings.marginLeft = pageSettings.marginLeft;
-                }
-                if (pageSettings.marginRight !== null) {
-                  printSettings.marginRight = pageSettings.marginRight;
-                }
-                if (pageSettings.marginTop !== null) {
-                  printSettings.marginTop = pageSettings.marginTop;
-                }
-                if (pageSettings.marginBottom !== null) {
-                  printSettings.marginBottom = pageSettings.marginBottom;
-                }
 
-                activeTab.linkedBrowser.print(activeTab.linkedBrowser.outerWindowID, printSettings, null);
+                let printProgressListener = {
+                  onLocationChange(webProgress, request, location, flags) { },
+                  onProgressChange(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { },
+                  onSecurityChange(webProgress, request, state) { },
+                  onStateChange(webProgress, request, flags, status) {
+                    if ((flags & Ci.nsIWebProgressListener.STATE_STOP) && (flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT)) {
+                      resolve(retval == 0 ? "saved" : "replaced");
+                    }
+                  },
+                  onStatusChange: function(webProgress, request, status, message) {
+                    if (status != 0) {
+                      resolve(retval == 0 ? "not_saved" : "not_replaced");
+                    }
+                  },
+                  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener]),
+                };
 
-                resolve(retval == 0 ? "saved" : "replaced");
+                activeTab.linkedBrowser.print(activeTab.linkedBrowser.outerWindowID, printSettings, printProgressListener);
               } else {
                 // Cancel clicked (retval == 1)
                 resolve("canceled");
               }
             });
           });
         },
 
@@ -973,13 +1046,54 @@ this.tabs = class extends ExtensionAPI {
           let tab = await promiseTabWhenReady(tabId);
           if (!tab.isInReaderMode && !tab.isArticle) {
             throw new ExtensionError("The specified tab cannot be placed into reader mode.");
           }
           tab = getTabOrActive(tabId);
 
           tab.linkedBrowser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
         },
+
+        show(tabIds) {
+          if (!Services.prefs.getBoolPref(TABHIDE_PREFNAME, false)) {
+            throw new ExtensionError(`tabs.show is currently experimental and must be enabled with the ${TABHIDE_PREFNAME} preference.`);
+          }
+
+          if (!Array.isArray(tabIds)) {
+            tabIds = [tabIds];
+          }
+
+          for (let tabId of tabIds) {
+            let tab = tabTracker.getTab(tabId);
+            if (tab.ownerGlobal) {
+              hiddenTabs.delete(tab);
+              tab.ownerGlobal.gBrowser.showTab(tab);
+            }
+          }
+        },
+
+        hide(tabIds) {
+          if (!Services.prefs.getBoolPref(TABHIDE_PREFNAME, false)) {
+            throw new ExtensionError(`tabs.hide is currently experimental and must be enabled with the ${TABHIDE_PREFNAME} preference.`);
+          }
+
+          if (!Array.isArray(tabIds)) {
+            tabIds = [tabIds];
+          }
+
+          let hidden = [];
+          let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
+          for (let tab of tabs) {
+            if (tab.ownerGlobal && !tab.hidden) {
+              tab.ownerGlobal.gBrowser.hideTab(tab);
+              if (tab.hidden) {
+                hiddenTabs.set(tab, extension.id);
+                hidden.push(tabTracker.getId(tab));
+              }
+            }
+          }
+          return hidden;
+        },
       },
     };
     return self;
   }
 };
--- a/browser/components/extensions/schemas/devtools_network.json
+++ b/browser/components/extensions/schemas/devtools_network.json
@@ -40,17 +40,16 @@
             ]
           }
         ]
       }
     ],
     "functions": [
       {
         "name": "getHAR",
-        "unsupported": true,
         "type": "function",
         "description": "Returns HAR log that contains all known network requests.",
         "async": "callback",
         "parameters": [
           {
             "name": "callback",
             "type": "function",
             "description": "A function that receives the HAR log when the request completes.",
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -119,17 +119,23 @@
         "type": "function",
         "description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
-              "title": {"type": "string", "description": "The tooltip string."}
+              "title": {
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
+                "description": "The tooltip string."
+              }
             }
           }
         ]
       },
       {
         "name": "getTitle",
         "type": "function",
         "description": "Gets the title of the page action.",
@@ -211,17 +217,20 @@
         "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
               "popup": {
-                "type": "string",
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
                 "description": "The html file to show in a popup.  If set to the empty string (''), no popup is shown."
               }
             }
           }
         ]
       },
       {
         "name": "getPopup",
--- a/browser/components/extensions/schemas/sidebar_action.json
+++ b/browser/components/extensions/schemas/sidebar_action.json
@@ -59,17 +59,20 @@
         "description": "Sets the title of the sidebar action. This shows up in the tooltip.",
         "async": true,
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "title": {
-                "type": "string",
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
                 "description": "The string the sidebar action should display when moused over."
               },
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "description": "Sets the sidebar title for the tab specified by tabId. Automatically resets when the tab is closed."
               }
             }
@@ -151,17 +154,20 @@
             "properties": {
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "minimum": 0,
                 "description": "Sets the sidebar url for the tab specified by tabId. Automatically resets when the tab is closed."
               },
               "panel": {
-                "type": "string",
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
                 "description": "The url to the html file to show in a sidebar.  If set to the empty string (''), no sidebar is shown."
               }
             }
           }
         ]
       },
       {
         "name": "getPanel",
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -7,17 +7,18 @@
     "namespace": "manifest",
     "types": [
       {
         "$extend": "OptionalPermission",
         "choices": [{
           "type": "string",
           "enum": [
             "activeTab",
-            "tabs"
+            "tabs",
+            "tabHide"
           ]
         }]
       }
     ]
   },
   {
     "namespace": "tabs",
     "description": "Use the <code>browser.tabs</code> API to interact with the browser's tab system. You can use this API to create, modify, and rearrange tabs in the browser.",
@@ -48,16 +49,36 @@
           "extensionId": {
             "type": "string",
             "optional": true,
             "description": "The ID of the extension that changed the muted state. Not set if an extension was not the reason the muted state last changed."
           }
         }
       },
       {
+        "id": "SharingState",
+        "type": "object",
+        "description": "Tab sharing state for screen, microphone and camera.",
+        "properties": {
+          "screen": {
+            "type": "string",
+            "optional": true,
+            "description": "If the tab is sharing the screen the value will be one of \"Screen\", \"Window\", or \"Application\", or undefined if not screen sharing."
+          },
+          "camera": {
+            "type": "boolean",
+            "description": "True if the tab is using the camera."
+          },
+          "microphone": {
+            "type": "boolean",
+            "description": "True if the tab is using the microphone."
+          }
+        }
+      },
+      {
         "id": "Tab",
         "type": "object",
         "properties": {
           "id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
           "index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
           "windowId": {"type": "integer", "optional": true, "minimum": 0, "description": "The ID of the window the tab is contained within."},
           "openerTabId": {"type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
           "selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
@@ -70,20 +91,22 @@
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
           "discarded": {"type": "boolean", "optional": true, "description": "True while the tab is not loaded with content."},
           "incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
           "width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
           "height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
+          "hidden": {"type": "boolean", "optional": true, "description": "True if the tab is hidden."},
           "sessionId": {"type": "string", "optional": true, "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API."},
           "cookieStoreId": {"type": "string", "optional": true, "description": "The CookieStoreId used for the tab."},
           "isArticle": {"type": "boolean", "optional": true, "description": "Whether the document in the tab can be rendered in reader mode."},
-          "isInReaderMode": {"type": "boolean", "optional": true, "description": "Whether the document in the tab is being rendered in reader mode."}
+          "isInReaderMode": {"type": "boolean", "optional": true, "description": "Whether the document in the tab is being rendered in reader mode."},
+          "sharingState": {"$ref": "SharingState", "optional": true, "description": "Current tab sharing state for screen, microphone and camera."}
         }
       },
       {
         "id": "ZoomSettingsMode",
         "type": "string",
         "description": "Defines how zoom changes are handled, i.e. which entity is responsible for the actual scaling of the page; defaults to <code>automatic</code>.",
         "enum": [
           {
@@ -137,16 +160,31 @@
           }
         }
       },
       {
         "id": "PageSettings",
         "type": "object",
         "description": "The page settings including: orientation, scale, background, margins, headers, footers.",
         "properties": {
+          "paperSizeUnit": {
+            "type": "integer",
+            "optional": true,
+            "description": "The page size unit: 0 = inches, 1 = millimeters. Default: 0."
+          },
+          "paperWidth": {
+            "type": "number",
+            "optional": true,
+            "description": "The paper width in paper size units. Default: 8.5."
+          },
+          "paperHeight": {
+            "type": "number",
+            "optional": true,
+            "description": "The paper height in paper size units. Default: 11.0."
+          },
           "orientation": {
             "type": "integer",
             "optional": true,
             "description": "The page content orientation: 0 = portrait, 1 = landscape. Default: 0."
           },
           "scaling": {
             "type": "number",
             "optional": true,
@@ -162,30 +200,55 @@
             "optional": true,
             "description": "Whether the page background colors should be shown. Default: false."
           },
           "showBackgroundImages": {
             "type": "boolean",
             "optional": true,
             "description": "Whether the page background images should be shown. Default: false."
           },
-          "paperSizeUnit": {
-            "type": "integer",
+          "edgeLeft": {
+            "type": "number",
+            "optional": true,
+            "description": "The spacing between the left header/footer and the left edge of the paper (inches). Default: 0."
+          },
+          "edgeRight": {
+            "type": "number",
             "optional": true,
-            "description": "The page size unit: 0 = inches, 1 = millimeters. Default: 0."
+            "description": "The spacing between the right header/footer and the right edge of the paper (inches). Default: 0."
           },
-          "paperWidth": {
+          "edgeTop": {
+            "type": "number",
+            "optional": true,
+            "description": "The spacing between the top of the headers and the top edge of the paper (inches). Default: 0"
+          },
+          "edgeBottom": {
             "type": "number",
             "optional": true,
-            "description": "The paper width in paper size units. Default: 8.5."
+            "description": "The spacing between the bottom of the footers and the bottom edge of the paper (inches). Default: 0."
           },
-          "paperHeight": {
+          "marginLeft": {
+            "type": "number",
+            "optional": true,
+            "description": "The margin between the page content and the left edge of the paper (inches). Default: 0.5."
+          },
+          "marginRight": {
             "type": "number",
             "optional": true,
-            "description": "The paper height in paper size units. Default: 11.0."
+            "description": "The margin between the page content and the right edge of the paper (inches). Default: 0.5."
+          },
+          "marginTop": {
+            "type": "number",
+            "optional": true,
+            "description": "The margin between the page content and the top edge of the paper (inches). Default: 0.5."
+          },
+          "marginBottom": {
+            "type": "number",
+            "optional": true,
+            "description": "The margin between the page content and the bottom edge of the paper (inches). Default: 0.5."
           },
           "headerLeft": {
             "type": "string",
             "optional": true,
             "description": "The text for the page's left header. Default: '&T'."
           },
           "headerCenter": {
             "type": "string",
@@ -206,36 +269,16 @@
             "type": "string",
             "optional": true,
             "description": "The text for the page's center footer. Default: ''."
           },
           "footerRight": {
             "type": "string",
             "optional": true,
             "description": "The text for the page's right footer. Default: '&D'."
-          },
-          "marginLeft": {
-            "type": "number",
-            "optional": true,
-            "description": "The margin between the page content and the left edge of the paper (inches). Default: 0.5."
-          },
-          "marginRight": {
-            "type": "number",
-            "optional": true,
-            "description": "The margin between the page content and the right edge of the paper (inches). Default: 0.5."
-          },
-          "marginTop": {
-            "type": "number",
-            "optional": true,
-            "description": "The margin between the page content and the top edge of the paper (inches). Default: 0.5."
-          },
-          "marginBottom": {
-            "type": "number",
-            "optional": true,
-            "description": "The margin between the page content and the bottom edge of the paper (inches). Default: 0.5."
           }
         }
       },
       {
         "id": "TabStatus",
         "type": "string",
         "enum": ["loading", "complete"],
         "description": "Whether the tabs have completed loading."
@@ -595,16 +638,21 @@
                 "optional": true,
                 "description": "Whether the tabs have completed loading."
               },
               "discarded": {
                 "type": "boolean",
                 "optional": true,
                 "description": "True while the tabs are not loaded with content."
               },
+              "hidden": {
+                "type": "boolean",
+                "optional": true,
+                "description": "True while the tabs are hidden."
+              },
               "title": {
                 "type": "string",
                 "optional": true,
                 "description": "Match page titles against a pattern."
               },
               "url": {
                 "choices": [
                   {"type": "string"},
@@ -635,16 +683,34 @@
                 "optional": true,
                 "description": "The CookieStoreId used for the tab."
               },
               "openerTabId": {
                 "type": "integer",
                 "minimum": 0,
                 "optional": true,
                 "description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab."
+              },
+              "screen": {
+                "choices": [
+                  {"type": "string", "enum": ["Screen", "Window", "Application"]},
+                  {"type": "boolean"}
+                ],
+                "optional": true,
+                "description": "True for any screen sharing, or a string to specify type of screen sharing."
+              },
+              "camera": {
+                "type": "boolean",
+                "optional": true,
+                "description": "True if the tab is using the camera."
+              },
+              "microphone": {
+                "type": "boolean",
+                "optional": true,
+                "description": "True if the tab is using the microphone."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "parameters": [
               {
@@ -1227,16 +1293,50 @@
               {
                 "type": "string",
                 "name": "status",
                 "description": "Save status: saved, replaced, canceled, not_saved, not_replaced."
               }
             ]
           }
         ]
+      },
+      {
+        "name": "show",
+        "type": "function",
+        "description": "Shows one or more tabs.",
+        "permissions": ["tabHide"],
+        "async": true,
+        "parameters": [
+          {
+            "name": "tabIds",
+            "description": "The TAB ID or list of TAB IDs to show.",
+            "choices": [
+              {"type": "integer", "minimum": 0},
+              {"type": "array", "items": {"type": "integer", "minimum": 0}}
+            ]
+          }
+        ]
+      },
+      {
+        "name": "hide",
+        "type": "function",
+        "description": "Hides one or more tabs. The <code>\"tabHide\"</code> permission is required to hide tabs.  Not all tabs are hidable.  Returns an array of hidden tabs.",
+        "permissions": ["tabHide"],
+        "async": true,
+        "parameters": [
+          {
+            "name": "tabIds",
+            "description": "The TAB ID or list of TAB IDs to hide.",
+            "choices": [
+              {"type": "integer", "minimum": 0},
+              {"type": "array", "items": {"type": "integer", "minimum": 0}}
+            ]
+          }
+        ]
       }
     ],
     "events": [
       {
         "name": "onCreated",
         "type": "function",
         "description": "Fired when a tab is created. Note that the tab's URL may not be set at the time this event fired, but you can listen to onUpdated events to be notified when a URL is set.",
         "parameters": [
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -148,33 +148,37 @@ skip-if = !e10s
 [browser_ext_tabs_events.js]
 [browser_ext_tabs_executeScript.js]
 [browser_ext_tabs_executeScript_good.js]
 [browser_ext_tabs_executeScript_bad.js]
 [browser_ext_tabs_executeScript_multiple.js]
 [browser_ext_tabs_executeScript_no_create.js]
 [browser_ext_tabs_executeScript_runAt.js]
 [browser_ext_tabs_getCurrent.js]
+[browser_ext_tabs_hide.js]
 [browser_ext_tabs_insertCSS.js]
 [browser_ext_tabs_lastAccessed.js]
 [browser_ext_tabs_lazy.js]
 [browser_ext_tabs_removeCSS.js]
 [browser_ext_tabs_move_array.js]
 [browser_ext_tabs_move_window.js]
 [browser_ext_tabs_move_window_multiple.js]
 [browser_ext_tabs_move_window_pinned.js]
 [browser_ext_tabs_onHighlighted.js]
 [browser_ext_tabs_onUpdated.js]
 [browser_ext_tabs_opener.js]
 [browser_ext_tabs_printPreview.js]
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_readerMode.js]
 [browser_ext_tabs_reload.js]
 [browser_ext_tabs_reload_bypass_cache.js]
+[browser_ext_tabs_saveAsPDF.js]
+skip-if = os == 'mac' # Save as PDF not supported on Mac OS X
 [browser_ext_tabs_sendMessage.js]
+[browser_ext_tabs_sharingState.js]
 [browser_ext_tabs_cookieStoreId.js]
 [browser_ext_tabs_update.js]
 [browser_ext_tabs_zoom.js]
 [browser_ext_tabs_update_url.js]
 [browser_ext_themes_icons.js]
 [browser_ext_themes_validation.js]
 [browser_ext_url_overrides_newtab.js]
 [browser_ext_user_events.js]
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -468,39 +468,34 @@ add_task(async function testPropertyRemo
     "files": {
       "default.png": imageBuffer,
       "i1.png": imageBuffer,
       "i2.png": imageBuffer,
       "i3.png": imageBuffer,
     },
 
     getTests: function(tabs, expectGlobals) {
-      let contextUri = browser.runtime.getURL("_generated_background_page.html");
+      let defaultIcon = "chrome://browser/content/extension.svg";
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title",
          "badge": "",
          "badgeBackgroundColor": [0xd9, 0x00, 0x00, 0xFF]},
         {"icon": browser.runtime.getURL("i1.png"),
          "popup": browser.runtime.getURL("p1.html"),
          "title": "t1",
          "badge": "b1",
          "badgeBackgroundColor": [0x11, 0x11, 0x11, 0xFF]},
         {"icon": browser.runtime.getURL("i2.png"),
          "popup": browser.runtime.getURL("p2.html"),
          "title": "t2",
          "badge": "b2",
          "badgeBackgroundColor": [0x22, 0x22, 0x22, 0xFF]},
-        {"icon": contextUri,
-         "popup": "",
-         "title": "",
-         "badge": "",
-         "badgeBackgroundColor": [0x11, 0x11, 0x11, 0xFF]},
-        {"icon": contextUri,
+        {"icon": defaultIcon,
          "popup": "",
          "title": "",
          "badge": "",
          "badgeBackgroundColor": [0x22, 0x22, 0x22, 0xFF]},
         {"icon": browser.runtime.getURL("i3.png"),
          "popup": browser.runtime.getURL("p3.html"),
          "title": "t3",
          "badge": "b3",
@@ -536,28 +531,25 @@ add_task(async function testPropertyRemo
         },
         async expect => {
           browser.test.log("Set empty tab values, expect empty values except for bgcolor.");
           let tabId = tabs[0];
           browser.browserAction.setIcon({tabId, path: ""});
           browser.browserAction.setPopup({tabId, popup: ""});
           browser.browserAction.setTitle({tabId, title: ""});
           browser.browserAction.setBadgeText({tabId, text: ""});
-          browser.browserAction.setBadgeBackgroundColor({tabId, color: ""});
+          await browser.test.assertRejects(
+            browser.browserAction.setBadgeBackgroundColor({tabId, color: ""}),
+            /^Invalid badge background color: ""$/,
+            "Expected invalid badge background color error"
+          );
           await expectGlobals(details[1]);
           expect(details[3]);
         },
         async expect => {
-          browser.test.log("The invalid color removed tab bgcolor, restore previous tab bgcolor.");
-          let tabId = tabs[0];
-          browser.browserAction.setBadgeBackgroundColor({tabId, color: "#222"});
-          await expectGlobals(details[1]);
-          expect(details[4]);
-        },
-        async expect => {
           browser.test.log("Remove tab values, expect global values.");
           let tabId = tabs[0];
           browser.browserAction.setIcon({tabId, path: null});
           browser.browserAction.setPopup({tabId, popup: null});
           browser.browserAction.setTitle({tabId, title: null});
           browser.browserAction.setBadgeText({tabId, text: null});
           browser.browserAction.setBadgeBackgroundColor({tabId, color: null});
           await expectGlobals(details[1]);
@@ -565,18 +557,18 @@ add_task(async function testPropertyRemo
         },
         async expect => {
           browser.test.log("Change global values, expect the new values.");
           browser.browserAction.setIcon({path: "i3.png"});
           browser.browserAction.setPopup({popup: "p3.html"});
           browser.browserAction.setTitle({title: "t3"});
           browser.browserAction.setBadgeText({text: "b3"});
           browser.browserAction.setBadgeBackgroundColor({color: "#333"});
-          await expectGlobals(details[5]);
-          expect(details[5]);
+          await expectGlobals(details[4]);
+          expect(details[4]);
         },
         async expect => {
           browser.test.log("Remove global values, expect defaults.");
           browser.browserAction.setIcon({path: null});
           browser.browserAction.setPopup({popup: null});
           browser.browserAction.setBadgeText({text: null});
           browser.browserAction.setTitle({title: null});
           browser.browserAction.setBadgeBackgroundColor({color: null});
--- a/browser/components/extensions/test/browser/browser_ext_devtools_network.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_network.js
@@ -1,69 +1,102 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const {gDevTools} = require("devtools/client/framework/devtools");
 
+function background() {
+  browser.test.onMessage.addListener(msg => {
+    let code;
+    if (msg === "navigate") {
+      code = "window.wrappedJSObject.location.href = 'http://example.com/';";
+      browser.tabs.executeScript({code});
+    } else if (msg === "reload") {
+      code = "window.wrappedJSObject.location.reload(true);";
+      browser.tabs.executeScript({code});
+    }
+  });
+  browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+    if (changeInfo.status === "complete" && tab.url === "http://example.com/") {
+      browser.test.sendMessage("tabUpdated");
+    }
+  });
+  browser.test.sendMessage("ready");
+}
+
+function devtools_page() {
+  let eventCount = 0;
+  let listener = url => {
+    eventCount++;
+    browser.test.assertEq("http://example.com/", url, "onNavigated received the expected url.");
+    browser.test.sendMessage("onNavigatedFired", eventCount);
+
+    if (eventCount === 2) {
+      eventCount = 0;
+      browser.devtools.network.onNavigated.removeListener(listener);
+    }
+  };
+  browser.devtools.network.onNavigated.addListener(listener);
+
+  let harLogCount = 0;
+  let harListener = async msg => {
+    if (msg !== "getHAR") {
+      return;
+    }
+
+    harLogCount++;
+
+    const harLog = await browser.devtools.network.getHAR();
+    browser.test.sendMessage("getHAR-result", harLog);
+
+    if (harLogCount === 2) {
+      harLogCount = 0;
+      browser.test.onMessage.removeListener(harListener);
+    }
+  };
+  browser.test.onMessage.addListener(harListener);
+}
+
+function waitForRequestAdded(toolbox) {
+  return new Promise(resolve => {
+    let netPanel = toolbox.getPanel("netmonitor");
+    netPanel.panelWin.once("NetMonitor:RequestAdded", () => {
+      resolve();
+    });
+  });
+}
+
+let extData = {
+  background,
+  manifest: {
+    permissions: ["tabs", "http://mochi.test/", "http://example.com/"],
+    devtools_page: "devtools_page.html",
+  },
+  files: {
+    "devtools_page.html": `<!DOCTYPE html>
+      <html>
+        <head>
+          <meta charset="utf-8">
+          <script src="devtools_page.js"></script>
+        </head>
+        <body>
+        </body>
+      </html>`,
+    "devtools_page.js": devtools_page,
+  },
+};
+
+/**
+ * Test for `chrome.devtools.network.onNavigate()` API
+ */
 add_task(async function test_devtools_network_on_navigated() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
-
-  function background() {
-    browser.test.onMessage.addListener(msg => {
-      let code;
-      if (msg === "navigate") {
-        code = "window.wrappedJSObject.location.href = 'http://example.com/';";
-        browser.tabs.executeScript({code});
-      } else if (msg === "reload") {
-        code = "window.wrappedJSObject.location.reload(true);";
-        browser.tabs.executeScript({code});
-      }
-    });
-    browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
-      if (changeInfo.status === "complete" && tab.url === "http://example.com/") {
-        browser.test.sendMessage("tabUpdated");
-      }
-    });
-    browser.test.sendMessage("ready");
-  }
-
-  function devtools_page() {
-    let eventCount = 0;
-    let listener = url => {
-      eventCount++;
-      browser.test.assertEq("http://example.com/", url, "onNavigated received the expected url.");
-      if (eventCount === 2) {
-        browser.devtools.network.onNavigated.removeListener(listener);
-      }
-      browser.test.sendMessage("onNavigatedFired", eventCount);
-    };
-    browser.devtools.network.onNavigated.addListener(listener);
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      permissions: ["tabs", "http://mochi.test/", "http://example.com/"],
-      devtools_page: "devtools_page.html",
-    },
-    files: {
-      "devtools_page.html": `<!DOCTYPE html>
-        <html>
-          <head>
-            <meta charset="utf-8">
-            <script src="devtools_page.js"></script>
-          </head>
-          <body>
-          </body>
-        </html>`,
-      "devtools_page.js": devtools_page,
-    },
-  });
+  let extension = ExtensionTestUtils.loadExtension(extData);
 
   await extension.startup();
   await extension.awaitMessage("ready");
 
   let target = gDevTools.getTargetForTab(tab);
 
   await gDevTools.showToolbox(target, "webconsole");
   info("Developer toolbox opened.");
@@ -85,8 +118,60 @@ add_task(async function test_devtools_ne
   await gDevTools.closeToolbox(target);
 
   await target.destroy();
 
   await extension.unload();
 
   await BrowserTestUtils.removeTab(tab);
 });
+
+/**
+ * Test for `chrome.devtools.network.getHAR()` API
+ */
+add_task(async function test_devtools_network_get_har() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+  let extension = ExtensionTestUtils.loadExtension(extData);
+
+  await extension.startup();
+  await extension.awaitMessage("ready");
+
+  let target = gDevTools.getTargetForTab(tab);
+
+  // Open the Toolbox
+  let toolbox = await gDevTools.showToolbox(target, "webconsole");
+  info("Developer toolbox opened.");
+
+  // Get HAR, it should be empty since the Net panel wasn't selected.
+  const getHAREmptyPromise = extension.awaitMessage("getHAR-result");
+  extension.sendMessage("getHAR");
+  const getHAREmptyResult = await getHAREmptyPromise;
+  is(getHAREmptyResult.log.entries.length, 0, "HAR log should be empty");
+
+  // Select the Net panel.
+  await toolbox.selectTool("netmonitor");
+
+  // Reload the page to collect some HTTP requests.
+  extension.sendMessage("navigate");
+
+  // Wait till the navigation is complete and request
+  // added into the net panel.
+  await Promise.all([
+    extension.awaitMessage("tabUpdated"),
+    extension.awaitMessage("onNavigatedFired"),
+    waitForRequestAdded(toolbox),
+  ]);
+
+  // Get HAR, it should not be empty now.
+  const getHARPromise = extension.awaitMessage("getHAR-result");
+  extension.sendMessage("getHAR");
+  const getHARResult = await getHARPromise;
+  is(getHARResult.log.entries.length, 1, "HAR log should not be empty");
+
+  // Shutdown
+  await gDevTools.closeToolbox(target);
+
+  await target.destroy();
+
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -47,29 +47,30 @@ add_task(async function testTabSwitchCon
       },
 
       "default.png": imageBuffer,
       "1.png": imageBuffer,
       "2.png": imageBuffer,
     },
 
     getTests: function(tabs) {
+      let defaultIcon = "chrome://browser/content/extension.svg";
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default T\u00edtulo \u263a"},
         {"icon": browser.runtime.getURL("1.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default T\u00edtulo \u263a"},
         {"icon": browser.runtime.getURL("2.png"),
          "popup": browser.runtime.getURL("2.html"),
          "title": "Title 2"},
-        {"icon": browser.runtime.getURL("2.png"),
-         "popup": browser.runtime.getURL("2.html"),
-         "title": "Default T\u00edtulo \u263a"},
+        {"icon": defaultIcon,
+         "popup": "",
+         "title": ""},
       ];
 
       let promiseTabLoad = details => {
         return new Promise(resolve => {
           browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
             if (tabId == details.id && changed.url == details.url) {
               browser.tabs.onUpdated.removeListener(listener);
               resolve();
@@ -119,21 +120,31 @@ add_task(async function testTabSwitchCon
 
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"});
           browser.tabs.update(tabs[1], {url: "about:blank?0#ref"});
           await promise;
 
           expect(details[2]);
         },
         expect => {
-          browser.test.log("Clear the title. Expect default title.");
+          browser.test.log("Set empty string values. Expect empty strings but default icon.");
+          browser.pageAction.setIcon({tabId: tabs[1], path: ""});
+          browser.pageAction.setPopup({tabId: tabs[1], popup: ""});
           browser.pageAction.setTitle({tabId: tabs[1], title: ""});
 
           expect(details[3]);
         },
+        expect => {
+          browser.test.log("Clear the values. Expect default ones.");
+          browser.pageAction.setIcon({tabId: tabs[1], path: null});
+          browser.pageAction.setPopup({tabId: tabs[1], popup: null});
+          browser.pageAction.setTitle({tabId: tabs[1], title: null});
+
+          expect(details[0]);
+        },
         async expect => {
           browser.test.log("Navigate to a new page. Expect icon hidden.");
 
           // TODO: This listener should not be necessary, but the |tabs.update|
           // callback currently fires too early in e10s windows.
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?1"});
 
           browser.tabs.update(tabs[1], {url: "about:blank?1"});
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_title.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_title.js
@@ -59,16 +59,19 @@ add_task(async function testTabSwitchCon
         {"icon": browser.runtime.getURL("1.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default T\u00edtulo \u263a"},
         {"icon": browser.runtime.getURL("2.png"),
          "popup": browser.runtime.getURL("2.html"),
          "title": "Title 2"},
         {"icon": browser.runtime.getURL("2.png"),
          "popup": browser.runtime.getURL("2.html"),
+         "title": ""},
+        {"icon": browser.runtime.getURL("2.png"),
+         "popup": browser.runtime.getURL("2.html"),
          "title": "Default T\u00edtulo \u263a"},
       ];
 
       let promiseTabLoad = details => {
         return new Promise(resolve => {
           browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
             if (tabId == details.id && changed.url == details.url) {
               browser.tabs.onUpdated.removeListener(listener);
@@ -119,21 +122,27 @@ add_task(async function testTabSwitchCon
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"});
 
           browser.tabs.update(tabs[1], {url: "about:blank?0#ref"});
 
           await promise;
           expect(details[2]);
         },
         expect => {
-          browser.test.log("Clear the title. Expect default title.");
+          browser.test.log("Set empty title. Expect empty title.");
           browser.pageAction.setTitle({tabId: tabs[1], title: ""});
 
           expect(details[3]);
         },
+        expect => {
+          browser.test.log("Clear the title. Expect default title.");
+          browser.pageAction.setTitle({tabId: tabs[1], title: null});
+
+          expect(details[4]);
+        },
         async expect => {
           browser.test.log("Navigate to a new page. Expect icon hidden.");
 
           // TODO: This listener should not be necessary, but the |tabs.update|
           // callback currently fires too early in e10s windows.
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?1"});
 
           browser.tabs.update(tabs[1], {url: "about:blank?1"});
@@ -191,16 +200,19 @@ add_task(async function testDefaultTitle
     getTests: function(tabs) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
+        {"title": "",
+         "popup": "",
+         "icon": browser.runtime.getURL("icon.png")},
       ];
 
       return [
         expect => {
           browser.test.log("Initial state. No icon visible.");
           expect(null);
         },
         async expect => {
@@ -209,16 +221,21 @@ add_task(async function testDefaultTitle
           expect(details[0]);
         },
         expect => {
           browser.test.log("Change the title. Expect new title.");
           browser.pageAction.setTitle({tabId: tabs[0], title: "Foo Title"});
           expect(details[1]);
         },
         expect => {
+          browser.test.log("Set empty title. Expect empty title.");
+          browser.pageAction.setTitle({tabId: tabs[0], title: ""});
+          expect(details[2]);
+        },
+        expect => {
           browser.test.log("Clear the title. Expect extension title.");
-          browser.pageAction.setTitle({tabId: tabs[0], title: ""});
+          browser.pageAction.setTitle({tabId: tabs[0], title: null});
           expect(details[0]);
         },
       ];
     },
   });
 });
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
@@ -29,21 +29,22 @@ let extData = {
         browser.test.sendMessage("sidebar");
       };
     },
   },
 
   background: function() {
     browser.test.onMessage.addListener(async ({msg, data}) => {
       if (msg === "set-panel") {
-        await browser.sidebarAction.setPanel({panel: ""}).then(() => {
-          browser.test.notifyFail("empty panel settable");
-        }).catch(() => {
-          browser.test.notifyPass("unable to set empty panel");
-        });
+        await browser.sidebarAction.setPanel({panel: null});
+        browser.test.assertEq(
+          await browser.sidebarAction.getPanel({}),
+          browser.runtime.getURL("sidebar.html"),
+          "Global panel can be reverted to the default."
+        );
       } else if (msg === "isOpen") {
         let {arg = {}, result} = data;
         let isOpen = await browser.sidebarAction.isOpen(arg);
         browser.test.assertEq(result, isOpen, "expected value from isOpen");
       }
       browser.test.sendMessage("done");
     });
   },
@@ -91,17 +92,16 @@ add_task(async function sidebar_two_side
 
 add_task(async function sidebar_empty_panel() {
   let extension = ExtensionTestUtils.loadExtension(extData);
   await extension.startup();
   // Test sidebar is opened on install
   await extension.awaitMessage("sidebar");
   ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible in first window");
   await sendMessage(extension, "set-panel");
-  await extension.awaitFinish();
   await extension.unload();
 });
 
 add_task(async function sidebar_isOpen() {
   info("Load extension1");
   let extension1 = ExtensionTestUtils.loadExtension(extData);
   await extension1.startup();
 
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -293,17 +293,17 @@ add_task(async function testTabSwitchCon
           browser.test.log("Change tab panel.");
           let tabId = tabs[0];
           await browser.sidebarAction.setPanel({tabId, panel: "2.html"});
           expect(details[6]);
         },
         async expect => {
           browser.test.log("Revert tab panel.");
           let tabId = tabs[0];
-          await browser.sidebarAction.setPanel({tabId, panel: ""});
+          await browser.sidebarAction.setPanel({tabId, panel: null});
           expect(details[4]);
         },
       ];
     },
   });
 });
 
 add_task(async function testDefaultTitle() {
@@ -319,72 +319,176 @@ add_task(async function testDefaultTitle
       "permissions": ["tabs"],
     },
 
     files: {
       "sidebar.html": sidebar,
       "icon.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectDefaults) {
+    getTests: function(tabs, expectGlobals) {
       let details = [
         {"title": "Foo Extension",
          "panel": browser.runtime.getURL("sidebar.html"),
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
          "panel": browser.runtime.getURL("sidebar.html"),
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Bar Title",
          "panel": browser.runtime.getURL("sidebar.html"),
          "icon": browser.runtime.getURL("icon.png")},
-        {"title": "",
-         "panel": browser.runtime.getURL("sidebar.html"),
-         "icon": browser.runtime.getURL("icon.png")},
       ];
 
       return [
         async expect => {
-          browser.test.log("Initial state. Expect extension title as default title.");
+          browser.test.log("Initial state. Expect default extension title.");
 
-          await expectDefaults(details[0]);
+          await expectGlobals(details[0]);
           expect(details[0]);
         },
         async expect => {
-          browser.test.log("Change the title. Expect new title.");
+          browser.test.log("Change the tab title. Expect new title.");
           browser.sidebarAction.setTitle({tabId: tabs[0], title: "Foo Title"});
 
-          await expectDefaults(details[0]);
+          await expectGlobals(details[0]);
           expect(details[1]);
         },
         async expect => {
-          browser.test.log("Change the default. Expect same properties.");
+          browser.test.log("Change the global title. Expect same properties.");
           browser.sidebarAction.setTitle({title: "Bar Title"});
 
-          await expectDefaults(details[2]);
+          await expectGlobals(details[2]);
           expect(details[1]);
         },
         async expect => {
-          browser.test.log("Clear the title. Expect new default title.");
-          browser.sidebarAction.setTitle({tabId: tabs[0], title: ""});
+          browser.test.log("Clear the tab title. Expect new global title.");
+          browser.sidebarAction.setTitle({tabId: tabs[0], title: null});
 
-          await expectDefaults(details[2]);
+          await expectGlobals(details[2]);
           expect(details[2]);
         },
         async expect => {
-          browser.test.log("Set default title to null string. Expect null string from API, extension title in UI.");
-          browser.sidebarAction.setTitle({title: ""});
+          browser.test.log("Clear the global title. Expect default title.");
+          browser.sidebarAction.setTitle({title: null});
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          await expectGlobals(details[0]);
+          expect(details[0]);
         },
         async expect => {
           browser.test.assertRejects(
             browser.sidebarAction.setPanel({panel: "about:addons"}),
             /Access denied for URL about:addons/,
             "unable to set panel to about:addons");
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          await expectGlobals(details[0]);
+          expect(details[0]);
         },
       ];
     },
   });
 });
+
+add_task(async function testPropertyRemoval() {
+  await runTests({
+    manifest: {
+      "name": "Foo Extension",
+
+      "sidebar_action": {
+        "default_icon": "default.png",
+        "default_panel": "default.html",
+        "default_title": "Default Title",
+      },
+
+      "permissions": ["tabs"],
+    },
+
+    files: {
+      "default.html": sidebar,
+      "p1.html": sidebar,
+      "p2.html": sidebar,
+      "p3.html": sidebar,
+      "default.png": imageBuffer,
+      "i1.png": imageBuffer,
+      "i2.png": imageBuffer,
+      "i3.png": imageBuffer,
+    },
+
+    getTests: function(tabs, expectGlobals) {
+      let defaultIcon = "chrome://browser/content/extension.svg";
+      let details = [
+        {"icon": browser.runtime.getURL("default.png"),
+         "panel": browser.runtime.getURL("default.html"),
+         "title": "Default Title"},
+        {"icon": browser.runtime.getURL("i1.png"),
+         "panel": browser.runtime.getURL("p1.html"),
+         "title": "t1"},
+        {"icon": browser.runtime.getURL("i2.png"),
+         "panel": browser.runtime.getURL("p2.html"),
+         "title": "t2"},
+        {"icon": defaultIcon,
+         "panel": browser.runtime.getURL("p1.html"),
+         "title": ""},
+        {"icon": browser.runtime.getURL("i3.png"),
+         "panel": browser.runtime.getURL("p3.html"),
+         "title": "t3"},
+      ];
+
+      return [
+        async expect => {
+          browser.test.log("Initial state, expect default properties.");
+          await expectGlobals(details[0]);
+          expect(details[0]);
+        },
+        async expect => {
+          browser.test.log("Set global values, expect the new values.");
+          browser.sidebarAction.setIcon({path: "i1.png"});
+          browser.sidebarAction.setPanel({panel: "p1.html"});
+          browser.sidebarAction.setTitle({title: "t1"});
+          await expectGlobals(details[1]);
+          expect(details[1]);
+        },
+        async expect => {
+          browser.test.log("Set tab values, expect the new values.");
+          let tabId = tabs[0];
+          browser.sidebarAction.setIcon({tabId, path: "i2.png"});
+          browser.sidebarAction.setPanel({tabId, panel: "p2.html"});
+          browser.sidebarAction.setTitle({tabId, title: "t2"});
+          await expectGlobals(details[1]);
+          expect(details[2]);
+        },
+        async expect => {
+          browser.test.log("Set empty tab values.");
+          let tabId = tabs[0];
+          browser.sidebarAction.setIcon({tabId, path: ""});
+          browser.sidebarAction.setPanel({tabId, panel: ""});
+          browser.sidebarAction.setTitle({tabId, title: ""});
+          await expectGlobals(details[1]);
+          expect(details[3]);
+        },
+        async expect => {
+          browser.test.log("Remove tab values, expect global values.");
+          let tabId = tabs[0];
+          browser.sidebarAction.setIcon({tabId, path: null});
+          browser.sidebarAction.setPanel({tabId, panel: null});
+          browser.sidebarAction.setTitle({tabId, title: null});
+          await expectGlobals(details[1]);
+          expect(details[1]);
+        },
+        async expect => {
+          browser.test.log("Change global values, expect the new values.");
+          browser.sidebarAction.setIcon({path: "i3.png"});
+          browser.sidebarAction.setPanel({panel: "p3.html"});
+          browser.sidebarAction.setTitle({title: "t3"});
+          await expectGlobals(details[4]);
+          expect(details[4]);
+        },
+        async expect => {
+          browser.test.log("Remove global values, expect defaults.");
+          browser.sidebarAction.setIcon({path: null});
+          browser.sidebarAction.setPanel({panel: null});
+          browser.sidebarAction.setTitle({title: null});
+          await expectGlobals(details[0]);
+          expect(details[0]);
+        },
+      ];
+    },
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_hide.js
@@ -0,0 +1,203 @@
+"use strict";
+
+const {Utils} = Cu.import("resource://gre/modules/sessionstore/Utils.jsm", {});
+const triggeringPrincipal_base64 = Utils.SERIALIZED_SYSTEMPRINCIPAL;
+
+// Ensure the pref prevents API use when the extension has the tabHide permission.
+add_task(async function test_pref_disabled() {
+  async function background() {
+    let tabs = await browser.tabs.query({hidden: false});
+    let ids = tabs.map(tab => tab.id);
+
+    await browser.test.assertRejects(
+      browser.tabs.hide(ids),
+      /tabs.hide is currently experimental/,
+      "Got the expected error when pref not enabled"
+    ).catch(err => {
+      browser.test.notifyFail("pref-test");
+      throw err;
+    });
+
+    browser.test.notifyPass("pref-test");
+  }
+
+  let extdata = {
+    manifest: {permissions: ["tabs", "tabHide"]},
+    background,
+  };
+  let extension = ExtensionTestUtils.loadExtension(extdata);
+  await extension.startup();
+  await extension.awaitFinish("pref-test");
+  await extension.unload();
+});
+
+add_task(async function test_tabs_showhide() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.webextensions.tabhide.enabled", true]],
+  });
+
+  async function background() {
+    browser.test.onMessage.addListener(async (msg, data) => {
+      switch (msg) {
+        case "hideall": {
+          let tabs = await browser.tabs.query({hidden: false});
+          browser.test.assertEq(tabs.length, 5, "got 5 tabs");
+          let ids = tabs.map(tab => tab.id);
+          browser.test.log(`working with ids ${JSON.stringify(ids)}`);
+
+          let hidden = await browser.tabs.hide(ids);
+          browser.test.assertEq(hidden.length, 3, "hid 3 tabs");
+          tabs = await browser.tabs.query({hidden: true});
+          ids = tabs.map(tab => tab.id);
+          browser.test.assertEq(JSON.stringify(hidden.sort()),
+                                JSON.stringify(ids.sort()), "hidden tabIds match");
+
+          browser.test.sendMessage("hidden", {hidden});
+          break;
+        }
+        case "showall": {
+          let tabs = await browser.tabs.query({hidden: true});
+          for (let tab of tabs) {
+            browser.test.assertTrue(tab.hidden, "tab is hidden");
+          }
+          let ids = tabs.map(tab => tab.id);
+          browser.tabs.show(ids);
+          browser.test.sendMessage("shown");
+          break;
+        }
+      }
+    });
+  }
+
+  let extdata = {
+    manifest: {permissions: ["tabs", "tabHide"]},
+    background,
+  };
+  let extension = ExtensionTestUtils.loadExtension(extdata);
+  await extension.startup();
+
+  let sessData = {
+    windows: [{
+      tabs: [
+        {entries: [{url: "about:blank", triggeringPrincipal_base64}]},
+        {entries: [{url: "https://example.com/", triggeringPrincipal_base64}]},
+        {entries: [{url: "https://mochi.test:8888/", triggeringPrincipal_base64}]},
+      ],
+    }, {
+      tabs: [
+        {entries: [{url: "about:blank", triggeringPrincipal_base64}]},
+        {entries: [{url: "http://test1.example.com/", triggeringPrincipal_base64}]},
+      ],
+    }],
+  };
+
+  // Set up a test session with 2 windows and 5 tabs.
+  let oldState = SessionStore.getBrowserState();
+  let restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
+  SessionStore.setBrowserState(JSON.stringify(sessData));
+  await restored;
+
+  // Attempt to hide all the tabs, however the active tab in each window cannot
+  // be hidden, so the result will be 3 hidden tabs.
+  extension.sendMessage("hideall");
+  await extension.awaitMessage("hidden");
+
+  // We have 2 windows in this session.  Otherwin is the non-current window.
+  // In each window, the first tab will be the selected tab and should not be
+  // hidden.  The rest of the tabs should be hidden at this point.  Hidden
+  // status was already validated inside the extension, this double checks
+  // from chrome code.
+  let otherwin;
+  for (let win of BrowserWindowIterator()) {
+    if (win != window) {
+      otherwin = win;
+    }
+    let tabs = Array.from(win.gBrowser.tabs.values());
+    ok(!tabs[0].hidden, "first tab not hidden");
+    for (let i = 1; i < tabs.length; i++) {
+      ok(tabs[i].hidden, "tab hidden value is correct");
+    }
+  }
+
+  // Test closing the last visible tab, the next tab which is hidden should become
+  // the selectedTab and will be visible.
+  ok(!otherwin.gBrowser.selectedTab.hidden, "selected tab is not hidden");
+  await BrowserTestUtils.removeTab(otherwin.gBrowser.selectedTab);
+  ok(!otherwin.gBrowser.selectedTab.hidden, "tab was unhidden");
+
+  // Showall will unhide any remaining hidden tabs.
+  extension.sendMessage("showall");
+  await extension.awaitMessage("shown");
+
+  // Check from chrome code that all tabs are visible again.
+  for (let win of BrowserWindowIterator()) {
+    let tabs = Array.from(win.gBrowser.tabs.values());
+    for (let i = 0; i < tabs.length; i++) {
+      ok(!tabs[i].hidden, "tab hidden value is correct");
+    }
+  }
+
+  // Close second window.
+  await BrowserTestUtils.closeWindow(otherwin);
+
+  await extension.unload();
+
+  // Restore pre-test state.
+  restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
+  SessionStore.setBrowserState(oldState);
+  await restored;
+});
+
+// Test our shutdown handling.  Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown.  We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_shutdown() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.webextensions.tabhide.enabled", true]],
+  });
+
+  let tabs = [
+    await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/", true, true),
+    await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true, true),
+  ];
+
+  async function background() {
+    let tabs = await browser.tabs.query({url: "http://example.com/"});
+    let testTab = tabs[0];
+
+    browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+      if ("hidden" in changeInfo) {
+        browser.test.assertEq(tabId, testTab.id, "correct tab was hidden");
+        browser.test.assertTrue(changeInfo.hidden, "tab is hidden");
+        browser.test.sendMessage("changeInfo");
+      }
+    });
+
+    let hidden = await browser.tabs.hide(testTab.id);
+    browser.test.assertEq(hidden[0], testTab.id, "tab was hidden");
+    tabs = await browser.tabs.query({hidden: true});
+    browser.test.assertEq(tabs[0].id, testTab.id, "tab was hidden");
+    browser.test.sendMessage("ready");
+  }
+
+  let extdata = {
+    manifest: {permissions: ["tabs", "tabHide"]},
+    useAddonManager: "temporary", // For testing onShutdown.
+    background,
+  };
+  let extension = ExtensionTestUtils.loadExtension(extdata);
+  await extension.startup();
+
+  // test onUpdated
+  await Promise.all([
+    extension.awaitMessage("ready"),
+    extension.awaitMessage("changeInfo"),
+  ]);
+  Assert.ok(tabs[0].hidden, "Tab is hidden by extension");
+
+  await extension.unload();
+
+  Assert.ok(!tabs[0].hidden, "Tab is not hidden after unloading extension");
+  await BrowserTestUtils.removeTab(tabs[0]);
+  await BrowserTestUtils.removeTab(tabs[1]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
@@ -0,0 +1,103 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testReturnStatus(expectedStatus) {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+  let saveDir = FileUtils.getDir("TmpD", [`testSaveDir-${Math.random()}`], true);
+
+  let saveFile = saveDir.clone();
+  saveFile.append("testSaveFile.pdf");
+  if (saveFile.exists()) {
+    saveFile.remove(false);
+  }
+
+  if (expectedStatus == "replaced") {
+    // Create file that can be replaced
+    saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+  } else if (expectedStatus == "not_saved") {
+    // Create directory with same name as file - so that file cannot be saved
+    saveFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o666);
+  } else if (expectedStatus == "not_replaced") {
+    // Create file that cannot be replaced
+    saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o444);
+  }
+
+  let MockFilePicker = SpecialPowers.MockFilePicker;
+  MockFilePicker.init(window);
+
+  if (expectedStatus == "replaced" || expectedStatus == "not_replaced") {
+    MockFilePicker.returnValue = MockFilePicker.returnReplace;
+  } else if (expectedStatus == "canceled") {
+    MockFilePicker.returnValue = MockFilePicker.returnCancel;
+  } else {
+    MockFilePicker.returnValue = MockFilePicker.returnOK;
+  }
+
+  MockFilePicker.displayDirectory = saveDir;
+  MockFilePicker.showCallback = function(fp) {
+    MockFilePicker.setFiles([saveFile]);
+    MockFilePicker.filterIndex = 0; // *.* - all file extensions
+  };
+
+  let manifest = {
+    "description": expectedStatus,
+  };
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: manifest,
+
+    background: async function() {
+      let pageSettings = {};
+
+      let status = await browser.tabs.saveAsPDF(pageSettings);
+
+      let expected = chrome.runtime.getManifest().description;
+
+      browser.test.assertEq(expected, status, "saveAsPDF " + expected);
+
+      browser.test.notifyPass("tabs.saveAsPDF");
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("tabs.saveAsPDF");
+  await extension.unload();
+
+  if (expectedStatus == "saved" || expectedStatus == "replaced") {
+    // Check that first four bytes of saved PDF file are "%PDF"
+    let text = await OS.File.read(saveFile.path, {encoding: "utf-8", bytes: 4});
+    is(text, "%PDF", "Got correct magic number");
+  }
+
+  MockFilePicker.cleanup();
+
+  if (expectedStatus == "not_saved" || expectedStatus == "not_replaced") {
+    saveFile.permissions = 0o666;
+  }
+
+  saveDir.remove(true);
+
+  await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testSaveAsPDF_saved() {
+  await testReturnStatus("saved");
+});
+
+add_task(async function testSaveAsPDF_replaced() {
+  await testReturnStatus("replaced");
+});
+
+add_task(async function testSaveAsPDF_canceled() {
+  await testReturnStatus("canceled");
+});
+
+add_task(async function testSaveAsPDF_not_saved() {
+  await testReturnStatus("not_saved");
+});
+
+add_task(async function testSaveAsPDF_not_replaced() {
+  await testReturnStatus("not_replaced");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
@@ -0,0 +1,68 @@
+"use strict";
+
+add_task(async function test_tabs_mediaIndicators() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.webextensions.tabhide.enabled", true]],
+  });
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+  // setBrowserSharing is called when a request for media icons occurs.  We're
+  // just testing that extension tabs get the info and are updated when it is
+  // called.
+  gBrowser.setBrowserSharing(tab.linkedBrowser, {screen: "Window", microphone: true, camera: true});
+
+  async function background() {
+    let tabs = await browser.tabs.query({microphone: true});
+    let testTab = tabs[0];
+
+    let state = testTab.sharingState;
+    browser.test.assertTrue(state.camera, "sharing camera was turned on");
+    browser.test.assertTrue(state.microphone, "sharing mic was turned on");
+    browser.test.assertEq(state.screen, "Window", "sharing screen is window");
+
+    tabs = await browser.tabs.query({screen: true});
+    browser.test.assertEq(tabs.length, 1, "screen sharing tab was found");
+
+    tabs = await browser.tabs.query({screen: "Window"});
+    browser.test.assertEq(tabs.length, 1, "screen sharing (window) tab was found");
+
+    tabs = await browser.tabs.query({screen: "Screen"});
+    browser.test.assertEq(tabs.length, 0, "screen sharing tab was not found");
+
+    // Verify we cannot hide a sharing tab.
+    let hidden = await browser.tabs.hide(testTab.id);
+    browser.test.assertEq(hidden.length, 0, "unable to hide sharing tab");
+    tabs = await browser.tabs.query({hidden: true});
+    browser.test.assertEq(tabs.length, 0, "unable to hide sharing tab");
+
+    browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+      if (testTab.id !== tabId) {
+        return;
+      }
+      let state = tab.sharingState;
+      browser.test.assertFalse(state.camera, "sharing camera was turned off");
+      browser.test.assertFalse(state.microphone, "sharing mic was turned off");
+      browser.test.assertFalse(state.screen, "sharing screen was turned off");
+      browser.test.notifyPass("done");
+    });
+    browser.test.sendMessage("ready");
+  }
+
+  let extdata = {
+    manifest: {permissions: ["tabs", "tabHide"]},
+    useAddonManager: "temporary",
+    background,
+  };
+  let extension = ExtensionTestUtils.loadExtension(extdata);
+  await extension.startup();
+
+  // Test that onUpdated is called after the sharing state is changed from
+  // chrome code.
+  await extension.awaitMessage("ready");
+  gBrowser.setBrowserSharing(tab.linkedBrowser, {});
+
+  await extension.awaitFinish("done");
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/browser/components/extensions/test/browser/browser_ext_user_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_user_events.js
@@ -13,27 +13,28 @@ add_task(async function testSources() {
           browser.test.sendMessage("request", {success: true, result});
         } catch (err) {
           browser.test.sendMessage("request", {success: false, errmsg: err.message});
         }
       }
 
       let tabs = await browser.tabs.query({active: true, currentWindow: true});
       await browser.pageAction.show(tabs[0].id);
-      browser.test.sendMessage("page-action-shown");
 
       browser.pageAction.onClicked.addListener(request);
       browser.browserAction.onClicked.addListener(request);
 
       browser.contextMenus.create({
         id: "menu",
         title: "test user events",
         contexts: ["page"],
       });
       browser.contextMenus.onClicked.addListener(request);
+
+      browser.test.sendMessage("actions-ready");
     },
 
     manifest: {
       browser_action: {default_title: "test"},
       page_action: {default_title: "test"},
       permissions: ["contextMenus"],
       optional_permissions: ["cookies"],
     },
@@ -44,18 +45,18 @@ add_task(async function testSources() {
     ok(result.success, `request() did not throw when called from ${what}`);
     is(result.result, true, `request() succeeded when called from ${what}`);
   }
 
   // Remove Sidebar button to prevent pushing extension button to overflow menu
   CustomizableUI.removeWidgetFromArea("sidebar-button");
 
   await extension.startup();
+  await extension.awaitMessage("actions-ready");
 
-  await extension.awaitMessage("page-action-shown");
   clickPageAction(extension);
   await check("page action click");
 
   clickBrowserAction(extension);
   await check("browser action click");
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
   gBrowser.selectedTab = tab;
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -15,17 +15,17 @@
  *          openTabContextMenu closeTabContextMenu
  *          openToolsMenu closeToolsMenu
  *          imageBuffer imageBufferFromDataURI
  *          getListStyleImage getPanelForNode
  *          awaitExtensionPanel awaitPopupResize
  *          promiseContentDimensions alterContent
  *          promisePrefChangeObserved openContextMenuInFrame
  *          promiseAnimationFrame getCustomizableUIPanelID
- *          awaitEvent
+ *          awaitEvent BrowserWindowIterator
  */
 
 // There are shutdown issues for which multiple rejections are left uncaught.
 // This bug should be fixed, but for the moment this directory is whitelisted.
 //
 // NOTE: Entire directory whitelisting should be kept to a minimum. Normally you
 //       should use "expectUncaughtRejection" to flag individual failures.
 const {PromiseTestUtils} = Cu.import("resource://testing-common/PromiseTestUtils.jsm", {});
@@ -479,8 +479,18 @@ function awaitEvent(eventName, id) {
         Management.off(eventName, listener);
         resolve();
       }
     };
 
     Management.on(eventName, listener);
   });
 }
+
+function* BrowserWindowIterator() {
+  let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+  while (windowsEnum.hasMoreElements()) {
+    let currentWindow = windowsEnum.getNext();
+    if (!currentWindow.closed) {
+      yield currentWindow;
+    }
+  }
+}
--- a/browser/components/migration/.eslintrc.js
+++ b/browser/components/migration/.eslintrc.js
@@ -1,14 +1,14 @@
 "use strict";
 
 module.exports = {
   "rules": {
     "block-scoped-var": "error",
-    "comma-dangle": "off",
+    "comma-dangle": ["error", "always-multiline"],
     "complexity": ["error", {"max": 21}],
     "indent-legacy": ["error", 2, {"SwitchCase": 1, "ArrayExpression": "first", "ObjectExpression": "first"}],
     "max-nested-callbacks": ["error", 3],
     "new-parens": "error",
     "no-extend-native": "error",
     "no-fallthrough": ["error", { "commentPattern": ".*[Ii]ntentional(?:ly)?\\s+fall(?:ing)?[\\s-]*through.*" }],
     "no-multi-str": "error",
     "no-return-assign": "error",
--- a/browser/components/migration/360seProfileMigrator.js
+++ b/browser/components/migration/360seProfileMigrator.js
@@ -111,17 +111,17 @@ Bookmarks.prototype = {
   },
 
   migrate(aCallback) {
     return (async () => {
       let folderMap = new Map();
       let toolbarBMs = [];
 
       let connection = await Sqlite.openConnection({
-        path: this._file.path
+        path: this._file.path,
       });
 
       try {
         let rows = await connection.execute(
           `WITH RECURSIVE
            bookmark(id, parent_id, is_folder, title, url, pos) AS (
              VALUES(0, -1, 1, '', '', 0)
              UNION
@@ -140,30 +140,30 @@ Bookmarks.prototype = {
           let url = row.getResultByName("url");
 
           let bmToInsert;
 
           if (is_folder) {
             bmToInsert = {
               children: [],
               title,
-              type: PlacesUtils.bookmarks.TYPE_FOLDER
+              type: PlacesUtils.bookmarks.TYPE_FOLDER,
             };
             folderMap.set(id, bmToInsert);
           } else {
             try {
               new URL(url);
             } catch (ex) {
               Cu.reportError(`Ignoring ${url} when importing from 360se because of exception: ${ex}`);
               continue;
             }
 
             bmToInsert = {
               title,
-              url
+              url,
             };
           }
 
           if (folderMap.has(parent_id)) {
             folderMap.get(parent_id).children.push(bmToInsert);
           } else if (parent_id === 0) {
             toolbarBMs.push(bmToInsert);
           }
@@ -177,148 +177,147 @@ Bookmarks.prototype = {
         if (!MigrationUtils.isStartupMigration) {
           parentGuid =
             await MigrationUtils.createImportedBookmarksFolder("360se", parentGuid);
         }
         await MigrationUtils.insertManyBookmarksWrapper(toolbarBMs, parentGuid);
       }
     })().then(() => aCallback(true),
                         e => { Cu.reportError(e); aCallback(false); });
-  }
+  },
 };
 
 function Qihoo360seProfileMigrator() {
   let paths = [
     // for v6 and above
     {
       users: ["360se6", "apps", "data", "users"],
-      defaultUser: "default"
+      defaultUser: "default",
     },
     // for earlier versions
     {
       users: ["360se"],
-      defaultUser: "data"
-    }
+      defaultUser: "data",
+    },
   ];
   this._usersDir = null;
   this._defaultUserPath = null;
   for (let path of paths) {
     let usersDir = FileUtils.getDir("AppData", path.users, false);
     if (usersDir.exists()) {
       this._usersDir = usersDir;
       this._defaultUserPath = path.defaultUser;
       break;
     }
   }
 }
 
 Qihoo360seProfileMigrator.prototype = Object.create(MigratorPrototype);
 
-Object.defineProperty(Qihoo360seProfileMigrator.prototype, "sourceProfiles", {
-  get() {
-    if ("__sourceProfiles" in this)
-      return this.__sourceProfiles;
+Qihoo360seProfileMigrator.prototype.getSourceProfiles = function() {
+  if ("__sourceProfiles" in this)
+    return this.__sourceProfiles;
+
+  if (!this._usersDir) {
+    this.__sourceProfiles = [];
+    return this.__sourceProfiles;
+  }
 
-    if (!this._usersDir) {
-      this.__sourceProfiles = [];
-      return this.__sourceProfiles;
+  let profiles = [];
+  let noLoggedInUser = true;
+  try {
+    let loginIni = this._usersDir.clone();
+    loginIni.append("login.ini");
+    if (!loginIni.exists()) {
+      throw new Error("360 Secure Browser's 'login.ini' does not exist.");
+    }
+    if (!loginIni.isReadable()) {
+      throw new Error("360 Secure Browser's 'login.ini' file could not be read.");
     }
 
-    let profiles = [];
-    let noLoggedInUser = true;
+    let loginIniInUtf8 = copyToTempUTF8File(loginIni, "GBK");
+    let loginIniObj = parseINIStrings(loginIniInUtf8);
     try {
-      let loginIni = this._usersDir.clone();
-      loginIni.append("login.ini");
-      if (!loginIni.exists()) {
-        throw new Error("360 Secure Browser's 'login.ini' does not exist.");
-      }
-      if (!loginIni.isReadable()) {
-        throw new Error("360 Secure Browser's 'login.ini' file could not be read.");
+      loginIniInUtf8.remove(false);
+    } catch (ex) {}
+
+    let nowLoginEmail = loginIniObj.NowLogin && loginIniObj.NowLogin.email;
+
+    /*
+     * NowLogin section may:
+     * 1. be missing or without email, before any user logs in.
+     * 2. represents the current logged in user
+     * 3. represents the most recent logged in user
+     *
+     * In the second case, user represented by NowLogin should be the first
+     * profile; otherwise the default user should be selected by default.
+     */
+    if (nowLoginEmail) {
+      if (loginIniObj.NowLogin.IsLogined === "1") {
+        noLoggedInUser = false;
       }
 
-      let loginIniInUtf8 = copyToTempUTF8File(loginIni, "GBK");
-      let loginIniObj = parseINIStrings(loginIniInUtf8);
-      try {
-        loginIniInUtf8.remove(false);
-      } catch (ex) {}
-
-      let nowLoginEmail = loginIniObj.NowLogin && loginIniObj.NowLogin.email;
-
-      /*
-       * NowLogin section may:
-       * 1. be missing or without email, before any user logs in.
-       * 2. represents the current logged in user
-       * 3. represents the most recent logged in user
-       *
-       * In the second case, user represented by NowLogin should be the first
-       * profile; otherwise the default user should be selected by default.
-       */
-      if (nowLoginEmail) {
-        if (loginIniObj.NowLogin.IsLogined === "1") {
-          noLoggedInUser = false;
-        }
-
-        profiles.push({
-          id: this._getIdFromConfig(loginIniObj.NowLogin),
-          name: nowLoginEmail,
-        });
-      }
-
-      for (let section in loginIniObj) {
-        if (!loginIniObj[section].email ||
-            (nowLoginEmail && loginIniObj[section].email == nowLoginEmail)) {
-          continue;
-        }
-
-        profiles.push({
-          id: this._getIdFromConfig(loginIniObj[section]),
-          name: loginIniObj[section].email,
-        });
-      }
-    } catch (e) {
-      Cu.reportError("Error detecting 360 Secure Browser profiles: " + e);
-    } finally {
-      profiles[noLoggedInUser ? "unshift" : "push"]({
-        id: this._defaultUserPath,
-        name: "Default",
+      profiles.push({
+        id: this._getIdFromConfig(loginIniObj.NowLogin),
+        name: nowLoginEmail,
       });
     }
 
-    this.__sourceProfiles = profiles.filter(profile => {
-      let resources = this.getResources(profile);
-      return resources && resources.length > 0;
+    for (let section in loginIniObj) {
+      if (!loginIniObj[section].email ||
+          (nowLoginEmail && loginIniObj[section].email == nowLoginEmail)) {
+        continue;
+      }
+
+      profiles.push({
+        id: this._getIdFromConfig(loginIniObj[section]),
+        name: loginIniObj[section].email,
+      });
+    }
+  } catch (e) {
+    Cu.reportError("Error detecting 360 Secure Browser profiles: " + e);
+  } finally {
+    profiles[noLoggedInUser ? "unshift" : "push"]({
+      id: this._defaultUserPath,
+      name: "Default",
     });
-    return this.__sourceProfiles;
   }
-});
+
+  this.__sourceProfiles = profiles.filter(profile => {
+    let resources = this.getResources(profile);
+    return resources && resources.length > 0;
+  });
+  return this.__sourceProfiles;
+};
 
 Qihoo360seProfileMigrator.prototype._getIdFromConfig = function(aConfig) {
   return aConfig.UserMd5 || getHash(aConfig.email);
 };
 
 Qihoo360seProfileMigrator.prototype.getResources = function(aProfile) {
   let profileFolder = this._usersDir.clone();
   profileFolder.append(aProfile.id);
 
   if (!profileFolder.exists()) {
     return [];
   }
 
   let resources = [
-    new Bookmarks(profileFolder)
+    new Bookmarks(profileFolder),
   ];
   return resources.filter(r => r.exists);
 };
 
-Qihoo360seProfileMigrator.prototype.getLastUsedDate = function() {
-  let bookmarksPaths = this.sourceProfiles.map(({id}) => {
+Qihoo360seProfileMigrator.prototype.getLastUsedDate = async function() {
+  let sourceProfiles = await this.getSourceProfiles();
+  let bookmarksPaths = sourceProfiles.map(({id}) => {
     return OS.Path.join(this._usersDir.path, id, kBookmarksFileName);
   });
   if (!bookmarksPaths.length) {
-    return Promise.resolve(new Date(0));
+    return new Date(0);
   }
   let datePromises = bookmarksPaths.map(path => {
     return OS.File.stat(path).catch(() => null).then(info => {
       return info ? info.lastModificationDate : 0;
     });
   });
   return Promise.all(datePromises).then(dates => {
     return new Date(Math.max.apply(Math, dates));
--- a/browser/components/migration/AutoMigrate.jsm
+++ b/browser/components/migration/AutoMigrate.jsm
@@ -85,27 +85,27 @@ const AutoMigrate = {
 
   /**
    * Automatically pick a migrator and resources to migrate,
    * then migrate those and start up.
    *
    * @throws if automatically deciding on migrators/data
    *         failed for some reason.
    */
-  migrate(profileStartup, migratorKey, profileToMigrate) {
+  async migrate(profileStartup, migratorKey, profileToMigrate) {
     let histogram = Services.telemetry.getHistogramById(
       "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_PROCESS_SUCCESS");
     histogram.add(0);
-    let {migrator, pickedKey} = this.pickMigrator(migratorKey);
+    let {migrator, pickedKey} = await this.pickMigrator(migratorKey);
     histogram.add(5);
 
-    profileToMigrate = this.pickProfile(migrator, profileToMigrate);
+    profileToMigrate = await this.pickProfile(migrator, profileToMigrate);
     histogram.add(10);
 
-    let resourceTypes = migrator.getMigrateData(profileToMigrate, profileStartup);
+    let resourceTypes = await migrator.getMigrateData(profileToMigrate, profileStartup);
     if (!(resourceTypes & this.resourceTypesToUse)) {
       throw new Error("No usable resources were found for the selected browser!");
     }
     histogram.add(15);
 
     let sawErrors = false;
     let migrationObserver = (subject, topic) => {
       if (topic == "Migration:ItemError") {
@@ -124,56 +124,56 @@ const AutoMigrate = {
             return {state: this._saveUndoStateTrackerForShutdown};
           });
       }
     };
 
     MigrationUtils.initializeUndoData();
     Services.obs.addObserver(migrationObserver, "Migration:Ended");
     Services.obs.addObserver(migrationObserver, "Migration:ItemError");
-    migrator.migrate(this.resourceTypesToUse, profileStartup, profileToMigrate);
+    await migrator.migrate(this.resourceTypesToUse, profileStartup, profileToMigrate);
     histogram.add(20);
   },
 
   /**
    * Pick and return a migrator to use for automatically migrating.
    *
    * @param {String} migratorKey   optional, a migrator key to prefer/pick.
    * @returns {Object}             an object with the migrator to use for migrating, as
    *                               well as the key we eventually ended up using to obtain it.
    */
-  pickMigrator(migratorKey) {
+  async pickMigrator(migratorKey) {
     if (!migratorKey) {
       let defaultKey = MigrationUtils.getMigratorKeyForDefaultBrowser();
       if (!defaultKey) {
         throw new Error("Could not determine default browser key to migrate from");
       }
       migratorKey = defaultKey;
     }
     if (migratorKey == "firefox") {
       throw new Error("Can't automatically migrate from Firefox.");
     }
 
-    let migrator = MigrationUtils.getMigrator(migratorKey);
+    let migrator = await MigrationUtils.getMigrator(migratorKey);
     if (!migrator) {
       throw new Error("Migrator specified or a default was found, but the migrator object is not available (or has no data).");
     }
     return {migrator, pickedKey: migratorKey};
   },
 
   /**
    * Pick a source profile (from the original browser) to use.
    *
    * @param {Migrator} migrator     the migrator object to use
    * @param {String}   suggestedId  the id of the profile to migrate, if pre-specified, or null
    * @returns                       the profile to migrate, or null if migrating
    *                                from the default profile.
    */
-  pickProfile(migrator, suggestedId) {
-    let profiles = migrator.sourceProfiles;
+  async pickProfile(migrator, suggestedId) {
+    let profiles = await migrator.getSourceProfiles();
     if (profiles && !profiles.length) {
       throw new Error("No profile data found to migrate.");
     }
     if (suggestedId) {
       if (!profiles) {
         throw new Error("Profile specified but only a default profile found.");
       }
       let suggestedProfile = profiles.find(profile => profile.id == suggestedId);
--- a/browser/components/migration/ChromeMigrationUtils.jsm
+++ b/browser/components/migration/ChromeMigrationUtils.jsm
@@ -1,20 +1,18 @@
 /* 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";
 
 this.EXPORTED_SYMBOLS = ["ChromeMigrationUtils"];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
-const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 this.ChromeMigrationUtils = {
   _chromeUserDataPath: null,
 
@@ -32,17 +30,20 @@ this.ChromeMigrationUtils = {
   // }
   _extensionLocaleStrings: {},
 
   /**
    * Get all extensions installed in a specific profile.
    * @param {String} profileId - A Chrome user profile ID. For example, "Profile 1".
    * @returns {Array} All installed Chrome extensions information.
    */
-  async getExtensionList(profileId = this.getLastUsedProfileId()) {
+  async getExtensionList(profileId) {
+    if (profileId === undefined) {
+      profileId = await this.getLastUsedProfileId();
+    }
     let path = this.getExtensionPath(profileId);
     let iterator = new OS.File.DirectoryIterator(path);
     let extensionList = [];
     await iterator.forEach(async entry => {
       if (entry.isDir) {
         let extensionInformation = await this.getExtensionInformation(entry.name, profileId);
         if (extensionInformation) {
           extensionList.push(extensionInformation);
@@ -53,17 +54,20 @@ this.ChromeMigrationUtils = {
   },
 
   /**
    * Get information of a specific Chrome extension.
    * @param {String} extensionId - The extension ID.
    * @param {String} profileId - The user profile's ID.
    * @retruns {Object} The Chrome extension information.
    */
-  async getExtensionInformation(extensionId, profileId = this.getLastUsedProfileId()) {
+  async getExtensionInformation(extensionId, profileId) {
+    if (profileId === undefined) {
+      profileId = await this.getLastUsedProfileId();
+    }
     let extensionInformation = null;
     try {
       let manifestPath = this.getExtensionPath(profileId);
       manifestPath = OS.Path.join(manifestPath, extensionId);
       // If there are multiple sub-directories in the extension directory,
       // read the files in the latest directory.
       let directories = await this._getSortedByVersionSubDirectoryNames(manifestPath);
       if (!directories[0]) {
@@ -146,50 +150,44 @@ this.ChromeMigrationUtils = {
   },
 
   /**
    * Check that a specific extension is installed or not.
    * @param {String} extensionId - The extension ID.
    * @param {String} profileId - The user profile's ID.
    * @returns {Boolean} Return true if the extension is installed otherwise return false.
    */
-  async isExtensionInstalled(extensionId, profileId = this.getLastUsedProfileId()) {
+  async isExtensionInstalled(extensionId, profileId) {
+    if (profileId === undefined) {
+      profileId = await this.getLastUsedProfileId();
+    }
     let extensionPath = this.getExtensionPath(profileId);
     let isInstalled = await OS.File.exists(OS.Path.join(extensionPath, extensionId));
     return isInstalled;
   },
 
   /**
    * Get the last used user profile's ID.
    * @returns {String} The last used user profile's ID.
    */
-  getLastUsedProfileId() {
-    let localState = this.getLocalState();
+  async getLastUsedProfileId() {
+    let localState = await this.getLocalState();
     return localState ? localState.profile.last_used : "Default";
   },
 
   /**
    * Get the local state file content.
    * @returns {Object} The JSON-based content.
    */
-  getLocalState() {
-    let localStateFile = new FileUtils.File(this.getChromeUserDataPath());
-    localStateFile.append("Local State");
-    if (!localStateFile.exists())
-      throw new Error("Chrome's 'Local State' file does not exist.");
-    if (!localStateFile.isReadable())
-      throw new Error("Chrome's 'Local State' file could not be read.");
-
+  async getLocalState() {
     let localState = null;
     try {
-      let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
-      fstream.init(localStateFile, -1, 0, 0);
-      let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
-                                                        { charset: "UTF-8" });
-      localState = JSON.parse(inputStream);
+      let localStatePath = OS.Path.join(this.getChromeUserDataPath(), "Local State");
+      let localStateJson = await OS.File.read(localStatePath, { encoding: "utf-8" });
+      localState = JSON.parse(localStateJson);
     } catch (ex) {
       Cu.reportError(ex);
       throw ex;
     }
     return localState;
   },
 
   /**
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -3,29 +3,26 @@
 /* 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, results: Cr, utils: Cu } = Components;
 
-const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
-
 const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
 const S100NS_PER_MS = 10;
 
 const AUTH_TYPE = {
   SCHEME_HTML: 0,
   SCHEME_BASIC: 1,
-  SCHEME_DIGEST: 2
+  SCHEME_DIGEST: 2,
 };
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/ChromeMigrationUtils.jsm");
 Cu.import("resource:///modules/MigrationUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
@@ -87,149 +84,159 @@ function convertBookmarks(items, errorAc
       Cu.reportError(ex);
       errorAccumulator(ex);
     }
   }
   return itemsToInsert;
 }
 
 function ChromeProfileMigrator() {
-  let path = ChromeMigrationUtils.getDataPath("Chrome");
-  let chromeUserDataFolder = new FileUtils.File(path);
-  this._chromeUserDataFolder = chromeUserDataFolder.exists() ?
-    chromeUserDataFolder : null;
+  this._chromeUserDataPathSuffix = "Chrome";
 }
 
 ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
 
+ChromeProfileMigrator.prototype._getChromeUserDataPathIfExists = async function() {
+  if (this._chromeUserDataPath) {
+    return this._chromeUserDataPath;
+  }
+  let path = ChromeMigrationUtils.getDataPath(this._chromeUserDataPathSuffix);
+  let exists = await OS.File.exists(path);
+  if (exists) {
+    this._chromeUserDataPath = path;
+  } else {
+    this._chromeUserDataPath = null;
+  }
+  return this._chromeUserDataPath;
+};
+
 ChromeProfileMigrator.prototype.getResources =
-  function Chrome_getResources(aProfile) {
-    if (this._chromeUserDataFolder) {
-      let profileFolder = this._chromeUserDataFolder.clone();
-      profileFolder.append(aProfile.id);
-      if (profileFolder.exists()) {
-        let possibleResources = [
+  async function Chrome_getResources(aProfile) {
+    let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
+    if (chromeUserDataPath) {
+      let profileFolder = OS.Path.join(chromeUserDataPath, aProfile.id);
+      if (await OS.File.exists(profileFolder)) {
+        let possibleResourcePromises = [
           GetBookmarksResource(profileFolder),
           GetHistoryResource(profileFolder),
           GetCookiesResource(profileFolder),
         ];
         if (AppConstants.platform == "win") {
-          possibleResources.push(GetWindowsPasswordsResource(profileFolder));
+          possibleResourcePromises.push(GetWindowsPasswordsResource(profileFolder));
         }
+        let possibleResources = await Promise.all(possibleResourcePromises);
         return possibleResources.filter(r => r != null);
       }
     }
     return [];
   };
 
 ChromeProfileMigrator.prototype.getLastUsedDate =
-  function Chrome_getLastUsedDate() {
-    let datePromises = this.sourceProfiles.map(profile => {
-      let basePath = OS.Path.join(this._chromeUserDataFolder.path, profile.id);
-      let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(leafName => {
+  async function Chrome_getLastUsedDate() {
+    let sourceProfiles = await this.getSourceProfiles();
+    let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
+    if (!chromeUserDataPath) {
+      return new Date(0);
+    }
+    let datePromises = sourceProfiles.map(async profile => {
+      let basePath = OS.Path.join(chromeUserDataPath, profile.id);
+      let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(async leafName => {
         let path = OS.Path.join(basePath, leafName);
-        return OS.File.stat(path).catch(() => null).then(info => {
-          return info ? info.lastModificationDate : 0;
-        });
+        let info = await OS.File.stat(path).catch(() => null);
+        return info ? info.lastModificationDate : 0;
       });
-      return Promise.all(fileDatePromises).then(dates => {
-        return Math.max.apply(Math, dates);
-      });
+      let dates = await Promise.all(fileDatePromises);
+      return Math.max(...dates);
     });
-    return Promise.all(datePromises).then(dates => {
-      dates.push(0);
-      return new Date(Math.max.apply(Math, dates));
-    });
+    let datesOuter = await Promise.all(datePromises);
+    datesOuter.push(0);
+    return new Date(Math.max(...datesOuter));
   };
 
-Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", {
-  get: function Chrome_sourceProfiles() {
+ChromeProfileMigrator.prototype.getSourceProfiles =
+  async function Chrome_getSourceProfiles() {
     if ("__sourceProfiles" in this)
       return this.__sourceProfiles;
 
-    if (!this._chromeUserDataFolder)
+    let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
+    if (!chromeUserDataPath)
       return [];
 
     let profiles = [];
     try {
-      let info_cache = ChromeMigrationUtils.getLocalState().profile.info_cache;
+      let localState = await ChromeMigrationUtils.getLocalState();
+      let info_cache = localState.profile.info_cache;
       for (let profileFolderName in info_cache) {
-        let profileFolder = this._chromeUserDataFolder.clone();
-        profileFolder.append(profileFolderName);
         profiles.push({
           id: profileFolderName,
           name: info_cache[profileFolderName].name || profileFolderName,
         });
       }
     } catch (e) {
       Cu.reportError("Error detecting Chrome profiles: " + e);
       // If we weren't able to detect any profiles above, fallback to the Default profile.
-      let defaultProfileFolder = this._chromeUserDataFolder.clone();
-      defaultProfileFolder.append("Default");
-      if (defaultProfileFolder.exists()) {
+      let defaultProfilePath = OS.Path.join(chromeUserDataPath, "Default");
+      if (await OS.File.exists(defaultProfilePath)) {
         profiles = [{
           id: "Default",
           name: "Default",
         }];
       }
     }
 
-    // Only list profiles from which any data can be imported
-    this.__sourceProfiles = profiles.filter(function(profile) {
-      let resources = this.getResources(profile);
-      return resources && resources.length > 0;
-    }, this);
-    return this.__sourceProfiles;
-  }
-});
+    let profileResources = await Promise.all(profiles.map(async profile => ({
+      profile,
+      resources: await this.getResources(profile),
+    })));
 
-Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", {
-  get: function Chrome_sourceHomePageURL() {
-    let prefsFile = this._chromeUserDataFolder.clone();
-    prefsFile.append("Preferences");
-    if (prefsFile.exists()) {
-      // XXX reading and parsing JSON is synchronous.
-      let fstream = Cc[FILE_INPUT_STREAM_CID].
-                    createInstance(Ci.nsIFileInputStream);
-      fstream.init(prefsFile, -1, 0, 0);
+    // Only list profiles from which any data can be imported
+    this.__sourceProfiles = profileResources.filter(({resources}) => {
+      return resources && resources.length > 0;
+    }, this).map(({profile}) => profile);
+    return this.__sourceProfiles;
+  };
+
+ChromeProfileMigrator.prototype.getSourceHomePageURL =
+  async function Chrome_getSourceHomePageURL() {
+    let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
+    if (!chromeUserDataPath)
+      return "";
+    let prefsPath = OS.Path.join(chromeUserDataPath, "Preferences");
+    if (await OS.File.exists(prefsPath)) {
       try {
-        return JSON.parse(
-          NetUtil.readInputStreamToString(fstream, fstream.available(),
-                                          { charset: "UTF-8" })
-            ).homepage;
+        let json = await OS.File.read(prefsPath, {encoding: "UTF-8"});
+        return JSON.parse(json).homepage;
       } catch (e) {
         Cu.reportError("Error parsing Chrome's preferences file: " + e);
       }
     }
     return "";
-  }
-});
+  };
 
 Object.defineProperty(ChromeProfileMigrator.prototype, "sourceLocked", {
   get: function Chrome_sourceLocked() {
     // There is an exclusive lock on some SQLite databases. Assume they are locked for now.
     return true;
   },
 });
 
-function GetBookmarksResource(aProfileFolder) {
-  let bookmarksFile = aProfileFolder.clone();
-  bookmarksFile.append("Bookmarks");
-  if (!bookmarksFile.exists())
+async function GetBookmarksResource(aProfileFolder) {
+  let bookmarksPath = OS.Path.join(aProfileFolder, "Bookmarks");
+  if (!(await OS.File.exists(bookmarksPath)))
     return null;
 
   return {
     type: MigrationUtils.resourceTypes.BOOKMARKS,
 
     migrate(aCallback) {
       return (async function() {
         let gotErrors = false;
         let errorGatherer = function() { gotErrors = true; };
         // Parse Chrome bookmark file that is JSON format
-        let bookmarkJSON = await OS.File.read(bookmarksFile.path, {encoding: "UTF-8"});
+        let bookmarkJSON = await OS.File.read(bookmarksPath, {encoding: "UTF-8"});
         let roots = JSON.parse(bookmarkJSON).roots;
 
         // Importing bookmark bar items
         if (roots.bookmark_bar.children &&
             roots.bookmark_bar.children.length > 0) {
           // Toolbar
           let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
           let bookmarks = convertBookmarks(roots.bookmark_bar.children, errorGatherer);
@@ -252,24 +259,23 @@ function GetBookmarksResource(aProfileFo
           }
           await MigrationUtils.insertManyBookmarksWrapper(bookmarks, parentGuid);
         }
         if (gotErrors) {
           throw new Error("The migration included errors.");
         }
       })().then(() => aCallback(true),
               () => aCallback(false));
-    }
+    },
   };
 }
 
-function GetHistoryResource(aProfileFolder) {
-  let historyFile = aProfileFolder.clone();
-  historyFile.append("History");
-  if (!historyFile.exists())
+async function GetHistoryResource(aProfileFolder) {
+  let historyPath = OS.Path.join(aProfileFolder, "History");
+  if (!(await OS.File.exists(historyPath)))
     return null;
 
   return {
     type: MigrationUtils.resourceTypes.HISTORY,
 
     migrate(aCallback) {
       (async function() {
         const MAX_AGE_IN_DAYS = Services.prefs.getIntPref("browser.migrate.chrome.history.maxAgeInDays");
@@ -280,17 +286,17 @@ function GetHistoryResource(aProfileFold
           let maxAge = dateToChromeTime(Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
           query += " AND last_visit_time > " + maxAge;
         }
         if (LIMIT) {
           query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
         }
 
         let rows =
-          await MigrationUtils.getRowsFromDBWithoutLocks(historyFile.path, "Chrome history", query);
+          await MigrationUtils.getRowsFromDBWithoutLocks(historyPath, "Chrome history", query);
         let places = [];
         for (let row of rows) {
           try {
             // if having typed_count, we changes transition type to typed.
             let transType = PlacesUtils.history.TRANSITION_LINK;
             if (row.getResultByName("typed_count") > 0)
               transType = PlacesUtils.history.TRANSITION_TYPED;
 
@@ -315,41 +321,40 @@ function GetHistoryResource(aProfileFold
               ignoreErrors: true,
               ignoreResults: true,
               handleCompletion(updatedCount) {
                 if (updatedCount > 0) {
                   resolve();
                 } else {
                   reject(new Error("Couldn't add visits"));
                 }
-              }
+              },
             });
           });
         }
       })().then(() => { aCallback(true); },
               ex => {
                 Cu.reportError(ex);
                 aCallback(false);
               });
-    }
+    },
   };
 }
 
-function GetCookiesResource(aProfileFolder) {
-  let cookiesFile = aProfileFolder.clone();
-  cookiesFile.append("Cookies");
-  if (!cookiesFile.exists())
+async function GetCookiesResource(aProfileFolder) {
+  let cookiesPath = OS.Path.join(aProfileFolder, "Cookies");
+  if (!(await OS.File.exists(cookiesPath)))
     return null;
 
   return {
     type: MigrationUtils.resourceTypes.COOKIES,
 
     async migrate(aCallback) {
       // We don't support decrypting cookies yet so only import plaintext ones.
-      let rows = await MigrationUtils.getRowsFromDBWithoutLocks(cookiesFile.path, "Chrome cookies",
+      let rows = await MigrationUtils.getRowsFromDBWithoutLocks(cookiesPath, "Chrome cookies",
        `SELECT host_key, name, value, path, expires_utc, secure, httponly, encrypted_value
         FROM cookies
         WHERE length(encrypted_value) = 0`).catch(ex => {
           Cu.reportError(ex);
           aCallback(false);
         });
       // If the promise was rejected we will have already called aCallback,
       // so we can just return here.
@@ -380,27 +385,26 @@ function GetCookiesResource(aProfileFold
           Cu.reportError(e);
         }
       }
       aCallback(true);
     },
   };
 }
 
-function GetWindowsPasswordsResource(aProfileFolder) {
-  let loginFile = aProfileFolder.clone();
-  loginFile.append("Login Data");
-  if (!loginFile.exists())
+async function GetWindowsPasswordsResource(aProfileFolder) {
+  let loginPath = OS.Path.join(aProfileFolder, "Login Data");
+  if (!(await OS.File.exists(loginPath)))
     return null;
 
   return {
     type: MigrationUtils.resourceTypes.PASSWORDS,
 
     async migrate(aCallback) {
-      let rows = await MigrationUtils.getRowsFromDBWithoutLocks(loginFile.path, "Chrome passwords",
+      let rows = await MigrationUtils.getRowsFromDBWithoutLocks(loginPath, "Chrome passwords",
        `SELECT origin_url, action_url, username_element, username_value,
         password_element, password_value, signon_realm, scheme, date_created,
         times_used FROM logins WHERE blacklisted_by_user = 0`).catch(ex => {
           Cu.reportError(ex);
           aCallback(false);
         });
       // If the promise was rejected we will have already called aCallback,
       // so we can just return here.
@@ -465,36 +469,32 @@ ChromeProfileMigrator.prototype.classDes
 ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome";
 ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
 
 
 /**
  *  Chromium migration
  **/
 function ChromiumProfileMigrator() {
-  let path = ChromeMigrationUtils.getDataPath("Chromium");
-  let chromiumUserDataFolder = new FileUtils.File(path);
-  this._chromeUserDataFolder = chromiumUserDataFolder.exists() ? chromiumUserDataFolder : null;
+  this._chromeUserDataPathSuffix = "Chromium";
 }
 
 ChromiumProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 ChromiumProfileMigrator.prototype.classDescription = "Chromium Profile Migrator";
 ChromiumProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium";
 ChromiumProfileMigrator.prototype.classID = Components.ID("{8cece922-9720-42de-b7db-7cef88cb07ca}");
 
 var componentsArray = [ChromeProfileMigrator, ChromiumProfileMigrator];
 
 /**
  * Chrome Canary
  * Not available on Linux
  **/
 function CanaryProfileMigrator() {
-  let path = ChromeMigrationUtils.getDataPath("Canary");
-  let chromeUserDataFolder = new FileUtils.File(path);
-  this._chromeUserDataFolder = chromeUserDataFolder.exists() ? chromeUserDataFolder : null;
+  this._chromeUserDataPathSuffix = "Canary";
 }
 CanaryProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 CanaryProfileMigrator.prototype.classDescription = "Chrome Canary Profile Migrator";
 CanaryProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=canary";
 CanaryProfileMigrator.prototype.classID = Components.ID("{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}");
 
 if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
   componentsArray.push(CanaryProfileMigrator);
--- a/browser/components/migration/ESEDBReader.jsm
+++ b/browser/components/migration/ESEDBReader.jsm
@@ -28,27 +28,27 @@ let gESEInstanceCounter = 0;
 
 // We limit the length of strings that we read from databases.
 const MAX_STR_LENGTH = 64 * 1024;
 
 // Kernel-related types:
 const KERNEL = {};
 KERNEL.FILETIME = new ctypes.StructType("FILETIME", [
   {dwLowDateTime: ctypes.uint32_t},
-  {dwHighDateTime: ctypes.uint32_t}
+  {dwHighDateTime: ctypes.uint32_t},
 ]);
 KERNEL.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
   {wYear: ctypes.uint16_t},
   {wMonth: ctypes.uint16_t},
   {wDayOfWeek: ctypes.uint16_t},
   {wDay: ctypes.uint16_t},
   {wHour: ctypes.uint16_t},
   {wMinute: ctypes.uint16_t},
   {wSecond: ctypes.uint16_t},
-  {wMilliseconds: ctypes.uint16_t}
+  {wMilliseconds: ctypes.uint16_t},
 ]);
 
 // DB column types, cribbed from the ESE header
 var COLUMN_TYPES = {
   JET_coltypBit:           1, /* True, False, or NULL */
   JET_coltypUnsignedByte:  2, /* 1-byte integer, unsigned */
   JET_coltypShort:         3, /* 2-byte integer, signed */
   JET_coltypLong:          4, /* 4-byte integer, signed */
@@ -92,17 +92,17 @@ ESE.JET_COLUMNDEF = new ctypes.StructTyp
   {"cbStruct": ctypes.unsigned_long },
   {"columnid": ESE.JET_COLUMNID },
   {"coltyp": ESE.JET_COLTYP },
   {"wCountry": ctypes.unsigned_short }, // sepcifies the country/region for the column definition
   {"langid": ctypes.unsigned_short },
   {"cp": ctypes.unsigned_short },
   {"wCollate": ctypes.unsigned_short }, /* Must be 0 */
   {"cbMax": ctypes.unsigned_long },
-  {"grbit": ESE.JET_GRBIT }
+  {"grbit": ESE.JET_GRBIT },
 ]);
 
 // Track open databases
 let gOpenDBs = new Map();
 
 // Track open libraries
 let gLibs = {};
 this.ESE = ESE; // Required for tests.
--- a/browser/components/migration/EdgeProfileMigrator.js
+++ b/browser/components/migration/EdgeProfileMigrator.js
@@ -126,31 +126,31 @@ EdgeTypedURLMigrator.prototype = {
 
       // Note that the time will be in microseconds (PRTime),
       // and Date.now() returns milliseconds. Places expects PRTime,
       // so we multiply the Date.now return value to make up the difference.
       let visitDate = time || (Date.now() * 1000);
       places.push({
         uri,
         visits: [{ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
-                   visitDate}]
+                   visitDate}],
       });
     }
 
     if (places.length == 0) {
       aCallback(typedURLs.size == 0);
       return;
     }
 
     MigrationUtils.insertVisitsWrapper(places, {
       ignoreErrors: true,
       ignoreResults: true,
       handleCompletion(updatedCount) {
         aCallback(updatedCount > 0);
-      }
+      },
     });
   },
 };
 
 function EdgeReadingListMigrator(dbOverride) {
   this.dbOverride = dbOverride;
 }
 
@@ -176,17 +176,17 @@ EdgeReadingListMigrator.prototype = {
   async _migrateReadingList(parentGuid) {
     if (await ESEDBReader.dbLocked(this.db)) {
       throw new Error("Edge seems to be running - its database is locked.");
     }
     let columnFn = db => {
       let columns = [
         {name: "URL", type: "string"},
         {name: "Title", type: "string"},
-        {name: "AddedDate", type: "date"}
+        {name: "AddedDate", type: "date"},
       ];
 
       // Later versions have an IsDeleted column:
       let isDeletedColumn = db.checkForColumn("ReadingList", "IsDeleted");
       if (isDeletedColumn && isDeletedColumn.dbType == ESEDBReader.COLUMN_TYPES.JET_coltypBit) {
         columns.push({name: "IsDeleted", type: "boolean"});
       }
       return columns;
@@ -279,17 +279,17 @@ EdgeBookmarksMigrator.prototype = {
     let folderMap = new Map();
     let columns = [
       {name: "URL", type: "string"},
       {name: "Title", type: "string"},
       {name: "DateUpdated", type: "date"},
       {name: "IsFolder", type: "boolean"},
       {name: "IsDeleted", type: "boolean"},
       {name: "ParentId", type: "guid"},
-      {name: "ItemId", type: "guid"}
+      {name: "ItemId", type: "guid"},
     ];
     let filterFn = row => {
       if (row.IsDeleted) {
         return false;
       }
       if (row.IsFolder) {
         folderMap.set(row.ItemId, row);
       }
@@ -369,20 +369,21 @@ EdgeProfileMigrator.prototype.getResourc
   ];
   let windowsVaultFormPasswordsMigrator =
     MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
   windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords";
   resources.push(windowsVaultFormPasswordsMigrator);
   return resources.filter(r => r.exists);
 };
 
-EdgeProfileMigrator.prototype.getLastUsedDate = function() {
+EdgeProfileMigrator.prototype.getLastUsedDate = async function() {
   // Don't do this if we don't have a single profile (see the comment for
   // sourceProfiles) or if we can't find the database file:
-  if (this.sourceProfiles !== null || !gEdgeDatabase) {
+  let sourceProfiles = await this.getSourceProfiles();
+  if (sourceProfiles !== null || !gEdgeDatabase) {
     return Promise.resolve(new Date(0));
   }
   let logFilePath = OS.Path.join(gEdgeDatabase.parent.path, "LogFiles", "edb.log");
   let dbPath = gEdgeDatabase.path;
   let cookieMigrator = MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE);
   let cookiePaths = cookieMigrator._cookiesFolders.map(f => f.path);
   let datePromises = [logFilePath, dbPath, ...cookiePaths].map(path => {
     return OS.File.stat(path).catch(() => null).then(info => {
@@ -390,32 +391,33 @@ EdgeProfileMigrator.prototype.getLastUse
     });
   });
   datePromises.push(new Promise(resolve => {
     let typedURLs = new Map();
     try {
       typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot);
     } catch (ex) {}
     let times = [0, ...typedURLs.values()];
-    resolve(Math.max.apply(Math, times));
+    // dates is an array of PRTimes, which are in microseconds - convert to milliseconds
+    resolve(Math.max.apply(Math, times) / 1000);
   }));
   return Promise.all(datePromises).then(dates => {
     return new Date(Math.max.apply(Math, dates));
   });
 };
 
 /* Somewhat counterintuitively, this returns:
  * - |null| to indicate "There is only 1 (default) profile" (on win10+)
  * - |[]| to indicate "There are no profiles" (on <=win8.1) which will avoid using this migrator.
  * See MigrationUtils.jsm for slightly more info on how sourceProfiles is used.
  */
-EdgeProfileMigrator.prototype.__defineGetter__("sourceProfiles", function() {
+EdgeProfileMigrator.prototype.getSourceProfiles = function() {
   let isWin10OrHigher = AppConstants.isPlatformAndVersionAtLeast("win", "10");
   return isWin10OrHigher ? null : [];
-});
+};
 
 EdgeProfileMigrator.prototype.__defineGetter__("sourceLocked", function() {
   // There is an exclusive lock on some databases. Assume they are locked for now.
   return true;
 });
 
 
 EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator";
--- a/browser/components/migration/FirefoxProfileMigrator.js
+++ b/browser/components/migration/FirefoxProfileMigrator.js
@@ -56,21 +56,19 @@ FirefoxProfileMigrator.prototype._getAll
   }
   return allProfiles;
 };
 
 function sorter(a, b) {
   return a.id.toLocaleLowerCase().localeCompare(b.id.toLocaleLowerCase());
 }
 
-Object.defineProperty(FirefoxProfileMigrator.prototype, "sourceProfiles", {
-  get() {
-    return [...this._getAllProfiles().keys()].map(x => ({id: x, name: x})).sort(sorter);
-  }
-});
+FirefoxProfileMigrator.prototype.getSourceProfiles = function() {
+  return [...this._getAllProfiles().keys()].map(x => ({id: x, name: x})).sort(sorter);
+};
 
 FirefoxProfileMigrator.prototype._getFileObject = function(dir, fileName) {
   let file = dir.clone();
   file.append(fileName);
 
   // File resources are monolithic.  We don't make partial copies since
   // they are not expected to work alone. Return null to avoid trying to
   // copy non-existing files.
@@ -117,17 +115,17 @@ FirefoxProfileMigrator.prototype._getRes
     }
     return {
       type: aMigrationType,
       migrate(aCallback) {
         for (let file of files) {
           file.copyTo(currentProfileDir, "");
         }
         aCallback(true);
-      }
+      },
     };
   };
 
   function savePrefs() {
     // If we've used the pref service to write prefs for the new profile, it's too
     // early in startup for the service to have a profile directory, so we have to
     // manually tell it where to save the prefs file.
     let newPrefsFile = currentProfileDir.clone();
@@ -176,17 +174,17 @@ FirefoxProfileMigrator.prototype._getRes
             // session with the "what's new" page:
             Services.prefs.setCharPref("browser.startup.homepage_override.mstone", mstone);
             Services.prefs.setCharPref("browser.startup.homepage_override.buildID", buildID);
             savePrefs();
             aCallback(true);
           }, function() {
             aCallback(false);
           });
-        }
+        },
       };
     }
   }
 
   // Sync/FxA related data
   let sync = {
     name: "sync", // name is used only by tests.
     type: types.OTHERDATA,
@@ -209,17 +207,17 @@ FirefoxProfileMigrator.prototype._getRes
             await OS.File.copy(oldPath, OS.Path.join(currentProfileDir.path, "signedInUser.json"));
           }
         }
       } catch (ex) {
         aCallback(false);
         return;
       }
       aCallback(true);
-    }
+    },
   };
 
   // Telemetry related migrations.
   let times = {
     name: "times", // name is used only by tests.
     type: types.OTHERDATA,
     migrate: aCallback => {
       let file = this._getFileObject(sourceProfileDir, "times.json");
@@ -227,17 +225,17 @@ FirefoxProfileMigrator.prototype._getRes
         file.copyTo(currentProfileDir, "");
       }
       // And record the fact a migration (ie, a reset) happened.
       let timesAccessor = new ProfileAge(currentProfileDir.path);
       timesAccessor.recordProfileReset().then(
         () => aCallback(true),
         () => aCallback(false)
       );
-    }
+    },
   };
   let telemetry = {
     name: "telemetry", // name is used only by tests...
     type: types.OTHERDATA,
     migrate: aCallback => {
       let createSubDir = (name) => {
         let dir = currentProfileDir.clone();
         dir.append(name);
@@ -278,25 +276,25 @@ FirefoxProfileMigrator.prototype._getRes
           if (stateFile) {
             let dest = createSubDir("healthreport");
             stateFile.copyTo(dest, "");
           }
         }
       }
 
       aCallback(true);
-    }
+    },
   };
 
   return [places, cookies, passwords, formData, dictionary, bookmarksBackups,
           session, sync, times, telemetry, favicons].filter(r => r);
 };
 
 Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", {
-  get: () => true
+  get: () => true,
 });
 
 
 FirefoxProfileMigrator.prototype.classDescription = "Firefox Profile Migrator";
 FirefoxProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=firefox";
 FirefoxProfileMigrator.prototype.classID = Components.ID("{91185366-ba97-4438-acba-48deaca63386}");
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FirefoxProfileMigrator]);
--- a/browser/components/migration/IEProfileMigrator.js
+++ b/browser/components/migration/IEProfileMigrator.js
@@ -73,35 +73,35 @@ History.prototype = {
       // and Date.now() returns milliseconds. Places expects PRTime,
       // so we multiply the Date.now return value to make up the difference.
       let lastVisitTime = entry.get("time") || (Date.now() * 1000);
 
       places.push(
         { uri,
           title,
           visits: [{ transitionType,
-                     visitDate: lastVisitTime }]
+                     visitDate: lastVisitTime }],
         }
       );
     }
 
     // Check whether there is any history to import.
     if (places.length == 0) {
       aCallback(true);
       return;
     }
 
     MigrationUtils.insertVisitsWrapper(places, {
       ignoreErrors: true,
       ignoreResults: true,
       handleCompletion(updatedCount) {
         aCallback(updatedCount > 0);
-      }
+      },
     });
-  }
+  },
 };
 
 // IE form password migrator supporting windows from XP until 7 and IE from 7 until 11
 function IE7FormPasswords() {
   // used to distinguish between this migrator and other passwords migrators in tests.
   this.name = "IE7FormPasswords";
 }
 
@@ -267,34 +267,34 @@ IE7FormPasswords.prototype = {
       // Bytes 12-19 are not needed and not documented
       {"unknown2": ctypes.uint32_t},
       {"unknown3": ctypes.uint32_t},
       // Bytes 20-23 are the data count: each username and password is considered as a data
       {"dataMax": ctypes.uint32_t},
       // Bytes 24-35 are not needed and not documented
       {"unknown4": ctypes.uint32_t},
       {"unknown5": ctypes.uint32_t},
-      {"unknown6": ctypes.uint32_t}
+      {"unknown6": ctypes.uint32_t},
     ]);
 
     // the structure of a IE7 decrypted login item
     let loginItem = new ctypes.StructType("loginItem", [
       // Bytes 0-3 are the offset of the username
       {"usernameOffset": ctypes.uint32_t},
       // Bytes 4-11 are the date
       {"loDateTime": ctypes.uint32_t},
       {"hiDateTime": ctypes.uint32_t},
       // Bytes 12-15 are not needed and not documented
       {"foo": ctypes.uint32_t},
       // Bytes 16-19 are the offset of the password
       {"passwordOffset": ctypes.uint32_t},
       // Bytes 20-31 are not needed and not documented
       {"unknown1": ctypes.uint32_t},
       {"unknown2": ctypes.uint32_t},
-      {"unknown3": ctypes.uint32_t}
+      {"unknown3": ctypes.uint32_t},
     ]);
 
     let url = uri.prePath;
     let results = [];
     let arr = this._crypto.stringToArray(data);
     // convert data to ctypes.unsigned_char.array(arr.length)
     let cdata = ctypes.unsigned_char.array(arr.length)(arr);
     // Bytes 0-35 contain the loginData data structure for all the logins sharing the same URL
@@ -365,47 +365,46 @@ IEProfileMigrator.prototype.getLastUsedD
     });
   });
   datePromises.push(new Promise(resolve => {
     let typedURLs = new Map();
     try {
       typedURLs = MSMigrationUtils.getTypedURLs("Software\\Microsoft\\Internet Explorer");
     } catch (ex) {}
     let dates = [0, ...typedURLs.values()];
-    resolve(Math.max.apply(Math, dates));
+    // dates is an array of PRTimes, which are in microseconds - convert to milliseconds
+    resolve(Math.max.apply(Math, dates) / 1000);
   }));
   return Promise.all(datePromises).then(dates => {
     return new Date(Math.max.apply(Math, dates));
   });
 };
 
-Object.defineProperty(IEProfileMigrator.prototype, "sourceHomePageURL", {
-  get: function IE_get_sourceHomePageURL() {
-    let defaultStartPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
-                                                      kMainKey, "Default_Page_URL");
-    let startPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
-                                               kMainKey, "Start Page");
-    // If the user didn't customize the Start Page, he is still on the default
-    // page, that may be considered the equivalent of our about:home.  There's
-    // no reason to retain it, since it is heavily targeted to IE.
-    let homepage = startPage != defaultStartPage ? startPage : "";
+IEProfileMigrator.prototype.getSourceHomePageURL = function IE_getSourceHomePageURL() {
+  let defaultStartPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+                                                    kMainKey, "Default_Page_URL");
+  let startPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                                             kMainKey, "Start Page");
+  // If the user didn't customize the Start Page, he is still on the default
+  // page, that may be considered the equivalent of our about:home.  There's
+  // no reason to retain it, since it is heavily targeted to IE.
+  let homepage = startPage != defaultStartPage ? startPage : "";
 
-    // IE7+ supports secondary home pages located in a REG_MULTI_SZ key.  These
-    // are in addition to the Start Page, and no empty entries are possible,
-    // thus a Start Page is always defined if any of these exists, though it
-    // may be the default one.
-    let secondaryPages = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
-                                                    kMainKey, "Secondary Start Pages");
-    if (secondaryPages) {
-      if (homepage)
-        secondaryPages.unshift(homepage);
-      homepage = secondaryPages.join("|");
-    }
+  // IE7+ supports secondary home pages located in a REG_MULTI_SZ key.  These
+  // are in addition to the Start Page, and no empty entries are possible,
+  // thus a Start Page is always defined if any of these exists, though it
+  // may be the default one.
+  let secondaryPages = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                                                  kMainKey, "Secondary Start Pages");
+  if (secondaryPages) {
+    if (homepage)
+      secondaryPages.unshift(homepage);
+    homepage = secondaryPages.join("|");
+  }
 
-    return homepage;
-  }
-});
+  return homepage;
+};
 
 IEProfileMigrator.prototype.classDescription = "IE Profile Migrator";
 IEProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=ie";
 IEProfileMigrator.prototype.classID = Components.ID("{3d2532e3-4932-4774-b7ba-968f5899d3a4}");
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([IEProfileMigrator]);
--- a/browser/components/migration/MSMigrationUtils.jsm
+++ b/browser/components/migration/MSMigrationUtils.jsm
@@ -60,22 +60,22 @@ function CtypesKernelHelpers() {
   this._structs.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
     {wYear: wintypes.WORD},
     {wMonth: wintypes.WORD},
     {wDayOfWeek: wintypes.WORD},
     {wDay: wintypes.WORD},
     {wHour: wintypes.WORD},
     {wMinute: wintypes.WORD},
     {wSecond: wintypes.WORD},
-    {wMilliseconds: wintypes.WORD}
+    {wMilliseconds: wintypes.WORD},
   ]);
 
   this._structs.FILETIME = new ctypes.StructType("FILETIME", [
     {dwLowDateTime: wintypes.DWORD},
-    {dwHighDateTime: wintypes.DWORD}
+    {dwHighDateTime: wintypes.DWORD},
   ]);
 
   try {
     this._libs.kernel32 = ctypes.open("Kernel32");
 
     this._functions.FileTimeToSystemTime =
       this._libs.kernel32.declare("FileTimeToSystemTime",
                                   ctypes.winapi_abi,
@@ -128,17 +128,17 @@ CtypesKernelHelpers.prototype = {
     // then divide by 1000 to get seconds, and round down:
     return Math.floor(Date.UTC(systemTime.wYear,
                                systemTime.wMonth - 1,
                                systemTime.wDay,
                                systemTime.wHour,
                                systemTime.wMinute,
                                systemTime.wSecond,
                                systemTime.wMilliseconds) / 1000);
-  }
+  },
 };
 
 function CtypesVaultHelpers() {
   this._structs = {};
   this._functions = {};
 
   this._structs.GUID = new ctypes.StructType("GUID", [
     {id: wintypes.DWORD.array(4)},
@@ -251,17 +251,17 @@ CtypesVaultHelpers.prototype = {
    */
   finalize() {
     this._structs = {};
     this._functions = {};
     try {
       this._vaultcliLib.close();
     } catch (ex) {}
     this._vaultcliLib = null;
-  }
+  },
 };
 
 /**
  * Checks whether an host is an IP (v4 or v6) address.
  *
  * @param aHost
  *        The host to check.
  * @return whether aHost is an IP address.
@@ -639,17 +639,17 @@ Cookies.prototype = {
                            name,
                            value,
                            Number(flags) & 0x1, // secure
                            false, // httpOnly
                            false, // session
                            expireTime,
                            {});
     }
-  }
+  },
 };
 
 function getTypedURLs(registryKeyPath) {
   // The list of typed URLs is a sort of annotation stored in the registry.
   // The number of entries stored is not UI-configurable, but has changed
   // between different Windows versions. We just keep reading up to the first
   // non-existing entry to support different limits / states of the registry.
   let typedURLs = new Map();
@@ -667,17 +667,18 @@ function getTypedURLs(registryKeyPath) {
                            registryKeyPath + "\\TypedURLsTime",
                            Ci.nsIWindowsRegKey.ACCESS_READ);
     } catch (ex) {
       typedURLTimeKey = null;
     }
     let entryName;
     for (let entry = 1; typedURLKey.hasValue((entryName = "url" + entry)); entry++) {
       let url = typedURLKey.readStringValue(entryName);
-      let timeTyped = 0;
+      // If we can't get a date for whatever reason, default to 6 months ago
+      let timeTyped = Date.now() - 31536000 / 2;
       if (typedURLTimeKey && typedURLTimeKey.hasValue(entryName)) {
         let urlTime = "";
         try {
           urlTime = typedURLTimeKey.readBinaryValue(entryName);
         } catch (ex) {
           Cu.reportError("Couldn't read url time for " + entryName);
         }
         if (urlTime.length == 8) {
@@ -687,26 +688,30 @@ function getTypedURLs(registryKeyPath) {
             if (c.length == 1)
               c = "0" + c;
             urlTimeHex.unshift(c);
           }
           try {
             let hi = parseInt(urlTimeHex.slice(0, 4).join(""), 16);
             let lo = parseInt(urlTimeHex.slice(4, 8).join(""), 16);
             // Convert to seconds since epoch:
-            timeTyped = cTypes.fileTimeToSecondsSinceEpoch(hi, lo);
-            // Callers expect PRTime, which is microseconds since epoch:
-            timeTyped *= 1000 * 1000;
+            let secondsSinceEpoch = cTypes.fileTimeToSecondsSinceEpoch(hi, lo);
+
+            // If the date is very far in the past, just use the default
+            if (secondsSinceEpoch > Date.now() / 1000000) {
+              // Callers expect PRTime, which is microseconds since epoch:
+              timeTyped = secondsSinceEpoch * 1000;
+            }
           } catch (ex) {
             // Ignore conversion exceptions. Callers will have to deal
-            // with the fallback value (0).
+            // with the fallback value.
           }
         }
       }
-      typedURLs.set(url, timeTyped);
+      typedURLs.set(url, timeTyped * 1000);
     }
   } catch (ex) {
     Cu.reportError("Error reading typed URL history: " + ex);
   } finally {
     if (typedURLKey) {
       typedURLKey.close();
     }
     if (typedURLTimeKey) {
@@ -861,17 +866,17 @@ WindowsVaultFormPasswords.prototype = {
       ctypesKernelHelpers.finalize();
       ctypesVaultHelpers.finalize();
       aCallback(migrationSucceeded);
     }
     if (aOnlyCheckExists) {
       return false;
     }
     return undefined;
-  }
+  },
 };
 
 var MSMigrationUtils = {
   MIGRATION_TYPE_IE: 1,
   MIGRATION_TYPE_EDGE: 2,
   CtypesKernelHelpers,
   getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) {
     return new Bookmarks(migrationType);
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -45,17 +45,17 @@ var gPreviousDefaultBrowserKey = "";
 
 let gKeepUndoData = false;
 let gUndoData = null;
 
 XPCOMUtils.defineLazyGetter(this, "gAvailableMigratorKeys", function() {
   if (AppConstants.platform == "win") {
     return [
       "firefox", "edge", "ie", "chrome", "chromium", "360se",
-      "canary"
+      "canary",
     ];
   }
   if (AppConstants.platform == "macosx") {
     return ["firefox", "safari", "chrome", "chromium", "canary"];
   }
   if (AppConstants.XP_UNIX) {
     return ["firefox", "chrome", "chromium"];
   }
@@ -78,17 +78,17 @@ function getMigrationBundle() {
  * 2. Create the prototype for the migrator, extending MigratorPrototype.
  *    Namely: MosaicMigrator.prototype = Object.create(MigratorPrototype);
  * 3. Set classDescription, contractID and classID for your migrator, and set
  *    NSGetFactory appropriately.
  * 4. If the migrator supports multiple profiles, override the sourceProfiles
  *    Here we default for single-profile migrator.
  * 5. Implement getResources(aProfile) (see below).
  * 6. If the migrator supports reading the home page of the source browser,
- *    override |sourceHomePageURL| getter.
+ *    override |getSourceHomePageURL| getter.
  * 7. For startup-only migrators, override |startupOnlyMigrator|.
  */
 this.MigratorPrototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserProfileMigrator]),
 
   /**
    * OVERRIDE IF AND ONLY IF the source supports multiple profiles.
    *
@@ -98,17 +98,17 @@ this.MigratorPrototype = {
    *   name - a pretty name to display to the user in the UI
    *
    * Only profiles from which data can be imported should be listed.  Otherwise
    * the behavior of the migration wizard isn't well-defined.
    *
    * For a single-profile source (e.g. safari, ie), this returns null,
    * and not an empty array.  That is the default implementation.
    */
-  get sourceProfiles() {
+  getSourceProfiles() {
     return null;
   },
 
   /**
    * MUST BE OVERRIDDEN.
    *
    * Returns an array of "migration resources" objects for the given profile,
    * or for the "default" profile, if the migrator does not support multiple
@@ -201,18 +201,18 @@ this.MigratorPrototype = {
   },
 
   /**
    * DO NOT OVERRIDE - After deCOMing migration, the UI will just call
    * getResources.
    *
    * @see nsIBrowserProfileMigrator
    */
-  getMigrateData: function MP_getMigrateData(aProfile) {
-    let resources = this._getMaybeCachedResources(aProfile);
+  getMigrateData: async function MP_getMigrateData(aProfile) {
+    let resources = await this._getMaybeCachedResources(aProfile);
     if (!resources) {
       return [];
     }
     let types = resources.map(r => r.type);
     return types.reduce((a, b) => { a |= b; return a; }, 0);
   },
 
   getBrowserKey: function MP_getBrowserKey() {
@@ -220,18 +220,18 @@ this.MigratorPrototype = {
   },
 
   /**
    * DO NOT OVERRIDE - After deCOMing migration, the UI will just call
    * migrate for each resource.
    *
    * @see nsIBrowserProfileMigrator
    */
-  migrate: function MP_migrate(aItems, aStartup, aProfile) {
-    let resources = this._getMaybeCachedResources(aProfile);
+  migrate: async function MP_migrate(aItems, aStartup, aProfile) {
+    let resources = await this._getMaybeCachedResources(aProfile);
     if (resources.length == 0)
       throw new Error("migrate called for a non-existent source");
 
     if (aItems != Ci.nsIBrowserProfileMigrator.ALL)
       resources = resources.filter(r => aItems & r.type);
 
     // Used to periodically give back control to the main-thread loop.
     let unblockMainThread = function() {
@@ -406,51 +406,51 @@ this.MigratorPrototype = {
   },
 
   /**
    * DO NOT OVERRIDE - After deCOMing migration, this code
    * won't be part of the migrator itself.
    *
    * @see nsIBrowserProfileMigrator
    */
-  get sourceExists() {
+  async isSourceAvailable() {
     if (this.startupOnlyMigrator && !MigrationUtils.isStartupMigration)
       return false;
 
     // For a single-profile source, check if any data is available.
     // For multiple-profiles source, make sure that at least one
     // profile is available.
     let exists = false;
     try {
-      let profiles = this.sourceProfiles;
+      let profiles = await this.getSourceProfiles();
       if (!profiles) {
-        let resources = this._getMaybeCachedResources("");
+        let resources = await this._getMaybeCachedResources("");
         if (resources && resources.length > 0)
           exists = true;
       } else {
         exists = profiles.length > 0;
       }
     } catch (ex) {
       Cu.reportError(ex);
     }
     return exists;
   },
 
   /** * PRIVATE STUFF - DO NOT OVERRIDE ***/
-  _getMaybeCachedResources: function PMB__getMaybeCachedResources(aProfile) {
+  _getMaybeCachedResources: async function PMB__getMaybeCachedResources(aProfile) {
     let profileKey = aProfile ? aProfile.id : "";
     if (this._resourcesByProfile) {
       if (profileKey in this._resourcesByProfile)
         return this._resourcesByProfile[profileKey];
     } else {
       this._resourcesByProfile = { };
     }
-    this._resourcesByProfile[profileKey] = this.getResources(aProfile);
+    this._resourcesByProfile[profileKey] = await this.getResources(aProfile);
     return this._resourcesByProfile[profileKey];
-  }
+  },
 };
 
 this.MigrationUtils = Object.freeze({
   resourceTypes: {
     SETTINGS:   Ci.nsIBrowserProfileMigrator.SETTINGS,
     COOKIES:    Ci.nsIBrowserProfileMigrator.COOKIES,
     HISTORY:    Ci.nsIBrowserProfileMigrator.HISTORY,
     FORMDATA:   Ci.nsIBrowserProfileMigrator.FORMDATA,
@@ -527,17 +527,17 @@ this.MigrationUtils = Object.freeze({
    *
    * @see nsIStringBundle
    */
   getLocalizedString: function MU_getLocalizedString(aKey, aReplacements) {
     aKey = aKey.replace(/_(canary|chromium)$/, "_chrome");
 
     const OVERRIDES = {
       "4_firefox": "4_firefox_history_and_bookmarks",
-      "64_firefox": "64_firefox_other"
+      "64_firefox": "64_firefox_other",
     };
     aKey = OVERRIDES[aKey] || aKey;
 
     if (aReplacements === undefined)
       return getMigrationBundle().GetStringFromName(aKey);
     return getMigrationBundle().formatStringFromName(
       aKey, aReplacements, aReplacements.length);
   },
@@ -584,17 +584,17 @@ this.MigrationUtils = Object.freeze({
    * @param parentGuid
    *        the GUID of the folder in which the new folder should be created.
    * @return the GUID of the new folder.
    */
   async createImportedBookmarksFolder(sourceNameStr, parentGuid) {
     let source = this.getLocalizedString("sourceName" + sourceNameStr);
     let title = this.getLocalizedString("importedBookmarksFolder", [source]);
     return (await PlacesUtils.bookmarks.insert({
-      type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title
+      type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title,
     })).guid;
   },
 
   /**
    * Get all the rows corresponding to a select query from a database, without
    * requiring a lock on the database. If fetching data fails (because someone
    * else tried to write to the DB at the same time, for example), we will
    * retry the fetch after a 100ms timeout, up to 10 times.
@@ -658,16 +658,38 @@ this.MigrationUtils = Object.freeze({
 
   get _migrators() {
     if (!gMigrators) {
       gMigrators = new Map();
     }
     return gMigrators;
   },
 
+  spinResolve: function MU_spinResolve(promise) {
+    if (!(promise instanceof Promise)) {
+      return promise;
+    }
+    let done = false;
+    let result = null;
+    let error = null;
+    promise.catch(e => {
+      error = e;
+    }).then(r => {
+      result = r;
+      done = true;
+    });
+
+    Services.tm.spinEventLoopUntil(() => done);
+    if (error) {
+      throw error;
+    } else {
+      return result;
+    }
+  },
+
   /*
    * Returns the migrator for the given source, if any data is available
    * for this source, or null otherwise.
    *
    * @param aKey internal name of the migration source.
    *             Supported values: ie (windows),
    *                               edge (windows),
    *                               safari (mac),
@@ -680,30 +702,30 @@ this.MigrationUtils = Object.freeze({
    * If null is returned,  either no data can be imported
    * for the given migrator, or aMigratorKey is invalid  (e.g. ie on mac,
    * or mosaic everywhere).  This method should be used rather than direct
    * getService for future compatibility (see bug 718280).
    *
    * @return profile migrator implementing nsIBrowserProfileMigrator, if it can
    *         import any data, null otherwise.
    */
-  getMigrator: function MU_getMigrator(aKey) {
+  getMigrator: async function MU_getMigrator(aKey) {
     let migrator = null;
     if (this._migrators.has(aKey)) {
       migrator = this._migrators.get(aKey);
     } else {
       try {
         migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=" +
                       aKey].createInstance(Ci.nsIBrowserProfileMigrator);
       } catch (ex) { Cu.reportError(ex); }
       this._migrators.set(aKey, migrator);
     }
 
     try {
-      return migrator && migrator.sourceExists ? migrator : null;
+      return migrator && (await migrator.isSourceAvailable()) ? migrator : null;
     } catch (ex) { Cu.reportError(ex); return null; }
   },
 
   /**
    * Figure out what is the default browser, and if there is a migrator
    * for it, return that migrator's internal name.
    * For the time being, the "internal name" of a migrator is its contract-id
    * trailer (e.g. ie for @mozilla.org/profile/migrator;1?app=browser&type=ie),
@@ -871,17 +893,18 @@ this.MigrationUtils = Object.freeze({
                            "_blank",
                            features,
                            params);
   },
 
   /**
    * Show the migration wizard for startup-migration.  This should only be
    * called by ProfileMigrator (see ProfileMigrator.js), which implements
-   * nsIProfileMigrator.
+   * nsIProfileMigrator. This runs asynchronously if we are running an
+   * automigration.
    *
    * @param aProfileStartup
    *        the nsIProfileStartup instance provided to ProfileMigrator.migrate.
    * @param [optional] aMigratorKey
    *        If set, the migration wizard will import from the corresponding
    *        migrator, bypassing the source-selection page.  Otherwise, the
    *        source-selection page will be displayed, either with the default
    *        browser selected, if it could be detected and if there is a
@@ -890,60 +913,74 @@ this.MigrationUtils = Object.freeze({
    *         the OS we run on.  See migration.xul).
    * @param [optional] aProfileToMigrate
    *        If set, the migration wizard will import from the profile indicated.
    * @throws if aMigratorKey is invalid or if it points to a non-existent
    *         source.
    */
   startupMigration:
   function MU_startupMigrator(aProfileStartup, aMigratorKey, aProfileToMigrate) {
+    if (Services.prefs.getBoolPref("browser.migrate.automigrate.enabled", false)) {
+      this.asyncStartupMigration(aProfileStartup,
+                                 aMigratorKey,
+                                 aProfileToMigrate);
+    } else {
+      this.spinResolve(this.asyncStartupMigration(aProfileStartup,
+                                                  aMigratorKey,
+                                                  aProfileToMigrate));
+    }
+  },
+
+  asyncStartupMigration:
+  async function MU_asyncStartupMigrator(aProfileStartup, aMigratorKey, aProfileToMigrate) {
     if (!aProfileStartup) {
       throw new Error("an profile-startup instance is required for startup-migration");
     }
     gProfileStartup = aProfileStartup;
 
     let skipSourcePage = false, migrator = null, migratorKey = "";
     if (aMigratorKey) {
-      migrator = this.getMigrator(aMigratorKey);
+      migrator = await this.getMigrator(aMigratorKey);
       if (!migrator) {
         // aMigratorKey must point to a valid source, so, if it doesn't
         // cleanup and throw.
         this.finishMigration();
         throw new Error("startMigration was asked to open auto-migrate from " +
                         "a non-existent source: " + aMigratorKey);
       }
       migratorKey = aMigratorKey;
       skipSourcePage = true;
     } else {
       let defaultBrowserKey = this.getMigratorKeyForDefaultBrowser();
       if (defaultBrowserKey) {
-        migrator = this.getMigrator(defaultBrowserKey);
+        migrator = await this.getMigrator(defaultBrowserKey);
         if (migrator)
           migratorKey = defaultBrowserKey;
       }
     }
 
     if (!migrator) {
+      let migrators = await Promise.all(gAvailableMigratorKeys.map(key => this.getMigrator(key)));
       // If there's no migrator set so far, ensure that there is at least one
       // migrator available before opening the wizard.
       // Note that we don't need to check the default browser first, because
       // if that one existed we would have used it in the block above this one.
-      if (!gAvailableMigratorKeys.some(key => !!this.getMigrator(key))) {
+      if (!migrators.some(m => m)) {
         // None of the keys produced a usable migrator, so finish up here:
         this.finishMigration();
         return;
       }
     }
 
     let isRefresh = migrator && skipSourcePage &&
                     migratorKey == AppConstants.MOZ_APP_NAME;
 
     if (!isRefresh && AutoMigrate.enabled) {
       try {
-        AutoMigrate.migrate(aProfileStartup, migratorKey, aProfileToMigrate);
+        await AutoMigrate.migrate(aProfileStartup, migratorKey, aProfileToMigrate);
         return;
       } catch (ex) {
         // If automigration failed, continue and show the dialog.
         Cu.reportError(ex);
       }
     }
 
     let migrationEntryPoint = this.MIGRATION_ENTRYPOINT_FIRSTRUN;
@@ -975,17 +1012,17 @@ this.MigrationUtils = Object.freeze({
       return insertionPromise;
     }
     // If we keep undo data, add a promise handler that stores the undo data once
     // the bookmark has been inserted in the DB, and then returns the bookmark.
     let {parentGuid} = bookmark;
     return insertionPromise.then(bm => {
       let {guid, lastModified, type} = bm;
       gUndoData.get("bookmarks").push({
-        parentGuid, guid, lastModified, type
+        parentGuid, guid, lastModified, type,
       });
       return bm;
     });
   },
 
   insertManyBookmarksWrapper(bookmarks, parent) {
     let insertionPromise = PlacesUtils.bookmarks.insertTree({guid: parent, children: bookmarks});
     return insertionPromise.then(insertedItems => {
--- a/browser/components/migration/ProfileMigrator.js
+++ b/browser/components/migration/ProfileMigrator.js
@@ -10,12 +10,12 @@ Components.utils.import("resource:///mod
 function ProfileMigrator() {
 }
 
 ProfileMigrator.prototype = {
   migrate: MigrationUtils.startupMigration.bind(MigrationUtils),
   QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProfileMigrator]),
   classDescription: "Profile Migrator",
   contractID: "@mozilla.org/toolkit/profile-migrator;1",
-  classID: Components.ID("6F8BB968-C14F-4D6F-9733-6C6737B35DCE")
+  classID: Components.ID("6F8BB968-C14F-4D6F-9733-6C6737B35DCE"),
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ProfileMigrator]);
--- a/browser/components/migration/SafariProfileMigrator.js
+++ b/browser/components/migration/SafariProfileMigrator.js
@@ -232,27 +232,27 @@ History.prototype = {
           }
         }
         if (places.length > 0) {
           MigrationUtils.insertVisitsWrapper(places, {
             ignoreErrors: true,
             ignoreResults: true,
             handleCompletion(updatedCount) {
               aCallback(updatedCount > 0);
-            }
+            },
           });
         } else {
           aCallback(false);
         }
       } catch (ex) {
         Cu.reportError(ex);
         aCallback(false);
       }
     });
-  }
+  },
 };
 
 /**
  * Safari's preferences property list is independently used for three purposes:
  * (a) importation of preferences
  * (b) importation of search strings
  * (c) retrieving the home page.
  *
@@ -283,34 +283,16 @@ MainPreferencesPropertyList.prototype = 
           } catch (ex) {
             Cu.reportError(ex);
           }
         }
         this._callbacks.splice(0);
       });
     }
   },
-
-  // Workaround for nsIBrowserProfileMigrator.sourceHomePageURL until
-  // it's replaced with an async method.
-  _readSync: function MPPL__readSync() {
-    if ("_dict" in this)
-      return this._dict;
-
-    let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
-                      createInstance(Ci.nsIFileInputStream);
-    inputStream.init(this._file, -1, -1, 0);
-    let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
-                       createInstance(Ci.nsIBinaryInputStream);
-    binaryStream.setInputStream(inputStream);
-    let bytes = binaryStream.readByteArray(inputStream.available());
-    this._dict = PropertyListUtils._readFromArrayBufferSync(
-      new Uint8Array(bytes).buffer);
-    return this._dict;
-  }
 };
 
 function SearchStrings(aMainPreferencesPropertyListInstance) {
   this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance;
 }
 SearchStrings.prototype = {
   type: MigrationUtils.resourceTypes.OTHERDATA,
 
@@ -326,17 +308,17 @@ SearchStrings.prototype = {
             let changes = recentSearchStrings.map((searchString) => (
               {op: "add",
                fieldname: "searchbar-history",
                value: searchString}));
             FormHistory.update(changes);
           }
         }
       }, aCallback));
-  }
+  },
 };
 
 function SafariProfileMigrator() {
 }
 
 SafariProfileMigrator.prototype = Object.create(MigratorPrototype);
 
 SafariProfileMigrator.prototype.getResources = function SM_getResources() {
@@ -394,27 +376,25 @@ Object.defineProperty(SafariProfileMigra
             new MainPreferencesPropertyList(file);
           return this._mainPreferencesPropertyList;
         }
       }
       this._mainPreferencesPropertyList = null;
       return this._mainPreferencesPropertyList;
     }
     return this._mainPreferencesPropertyList;
-  }
+  },
 });
 
-Object.defineProperty(SafariProfileMigrator.prototype, "sourceHomePageURL", {
-  get: function get_sourceHomePageURL() {
-    if (this.mainPreferencesPropertyList) {
-      let dict = this.mainPreferencesPropertyList._readSync();
-      if (dict.has("HomePage"))
-        return dict.get("HomePage");
-    }
-    return "";
+SafariProfileMigrator.prototype.getSourceHomePageURL = async function SM_getSourceHomePageURL() {
+  if (this.mainPreferencesPropertyList) {
+    let dict = await new Promise(resolve => this.mainPreferencesPropertyList.read(resolve));
+    if (dict.has("HomePage"))
+      return dict.get("HomePage");
   }
-});
+  return "";
+};
 
 SafariProfileMigrator.prototype.classDescription = "Safari Profile Migrator";
 SafariProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=safari";
 SafariProfileMigrator.prototype.classID = Components.ID("{4b609ecf-60b2-4655-9df4-dc149e474da1}");
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SafariProfileMigrator]);
--- a/browser/components/migration/content/migration.js
+++ b/browser/components/migration/content/migration.js
@@ -38,17 +38,17 @@ var MigrationWizard = { /* exported Migr
     this.isInitialMigration = entryPointId == MigrationUtils.MIGRATION_ENTRYPOINT_FIRSTRUN;
 
     if (args.length > 1) {
       this._source = args[1];
       this._migrator = args[2] instanceof kIMig ? args[2] : null;
       this._autoMigrate = args[3].QueryInterface(kIPStartup);
       this._skipImportSourcePage = args[4];
       if (this._migrator && args[5]) {
-        let sourceProfiles = this._migrator.sourceProfiles;
+        let sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
         this._selectedProfile = sourceProfiles.find(profile => profile.id == args[5]);
       }
 
       if (this._autoMigrate) {
         // Show the "nothing" option in the automigrate case to provide an
         // easily identifiable way to avoid migration and create a new profile.
         document.getElementById("nothing").hidden = false;
       }
@@ -62,38 +62,56 @@ var MigrationWizard = { /* exported Migr
     os.removeObserver(this, "Migration:Started");
     os.removeObserver(this, "Migration:ItemBeforeMigrate");
     os.removeObserver(this, "Migration:ItemAfterMigrate");
     os.removeObserver(this, "Migration:ItemError");
     os.removeObserver(this, "Migration:Ended");
     MigrationUtils.finishMigration();
   },
 
+  spinResolve(promise) {
+    let canAdvance = this._wiz.canAdvance;
+    let canRewind = this._wiz.canRewind;
+    let canCancel = this._canCancel;
+    this._wiz.canAdvance = false;
+    this._wiz.canRewind = false;
+    this._canCancel = false;
+    let result = MigrationUtils.spinResolve(promise);
+    this._wiz.canAdvance = canAdvance;
+    this._wiz.canRewind = canRewind;
+    this._canCancel = canCancel;
+    return result;
+  },
+
+  onWizardCancel() {
+    return this._canCancel;
+  },
+
   // 1 - Import Source
   onImportSourcePageShow() {
     // Show warning message to close the selected browser when needed
-    function toggleCloseBrowserWarning() {
+    let toggleCloseBrowserWarning = () => {
       let visibility = "hidden";
       if (group.selectedItem.id != "nothing") {
-        let migrator = MigrationUtils.getMigrator(group.selectedItem.id);
+        let migrator = this.spinResolve(MigrationUtils.getMigrator(group.selectedItem.id));
         visibility = migrator.sourceLocked ? "visible" : "hidden";
       }
       document.getElementById("closeSourceBrowser").style.visibility = visibility;
-    }
+    };
     this._wiz.canRewind = false;
 
     var selectedMigrator = null;
     this._availableMigrators = [];
 
     // Figure out what source apps are are available to import from:
     var group = document.getElementById("importSourceGroup");
     for (var i = 0; i < group.childNodes.length; ++i) {
       var migratorKey = group.childNodes[i].id;
       if (migratorKey != "nothing") {
-        var migrator = MigrationUtils.getMigrator(migratorKey);
+        var migrator = this.spinResolve(MigrationUtils.getMigrator(migratorKey));
         if (migrator) {
           // Save this as the first selectable item, if we don't already have
           // one, or if it is the migrator that was passed to us.
           if (!selectedMigrator || this._source == migratorKey)
             selectedMigrator = group.childNodes[i];
           this._availableMigrators.push([migratorKey, migrator]);
         } else {
           // Hide this option
@@ -143,25 +161,25 @@ var MigrationWizard = { /* exported Migr
       Services.telemetry.getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
                         .add(MigrationUtils.getSourceIdForTelemetry("nothing"));
       document.documentElement.cancel();
       return false;
     }
 
     if (!this._migrator || (newSource != this._source)) {
       // Create the migrator for the selected source.
-      this._migrator = MigrationUtils.getMigrator(newSource);
+      this._migrator = this.spinResolve(MigrationUtils.getMigrator(newSource));
 
       this._itemsFlags = kIMig.ALL;
       this._selectedProfile = null;
     }
     this._source = newSource;
 
     // check for more than one source profile
-    var sourceProfiles = this._migrator.sourceProfiles;
+    var sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
     if (this._skipImportSourcePage) {
       this._wiz.currentPage.next = "homePageImport";
     } else if (sourceProfiles && sourceProfiles.length > 1) {
       this._wiz.currentPage.next = "selectProfile";
     } else {
       if (this._autoMigrate)
         this._wiz.currentPage.next = "homePageImport";
       else
@@ -184,54 +202,57 @@ var MigrationWizard = { /* exported Migr
 
     var profiles = document.getElementById("profiles");
     while (profiles.hasChildNodes())
       profiles.firstChild.remove();
 
     // Note that this block is still reached even if the user chose 'From File'
     // and we canceled the dialog.  When that happens, _migrator will be null.
     if (this._migrator) {
-      var sourceProfiles = this._migrator.sourceProfiles;
+      var sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
 
       for (let profile of sourceProfiles) {
         var item = document.createElement("radio");
         item.id = profile.id;
         item.setAttribute("label", profile.name);
         profiles.appendChild(item);
       }
     }
 
     profiles.selectedItem = this._selectedProfile ? document.getElementById(this._selectedProfile.id) : profiles.firstChild;
   },
 
   onSelectProfilePageRewound() {
     var profiles = document.getElementById("profiles");
-    this._selectedProfile = this._migrator.sourceProfiles.find(
+    let sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
+    this._selectedProfile = sourceProfiles.find(
       profile => profile.id == profiles.selectedItem.id
     ) || null;
   },
 
   onSelectProfilePageAdvanced() {
     var profiles = document.getElementById("profiles");
-    this._selectedProfile = this._migrator.sourceProfiles.find(
+    let sourceProfiles = this.spinResolve(this._migrator.getSourceProfiles());
+    this._selectedProfile = sourceProfiles.find(
       profile => profile.id == profiles.selectedItem.id
     ) || null;
 
     // If we're automigrating or just doing bookmarks don't show the item selection page
     if (this._autoMigrate)
       this._wiz.currentPage.next = "homePageImport";
   },
 
   // 3 - ImportItems
   onImportItemsPageShow() {
     var dataSources = document.getElementById("dataSources");
     while (dataSources.hasChildNodes())
       dataSources.firstChild.remove();
 
-    var items = this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate);
+    var items = this.spinResolve(this._migrator.getMigrateData(this._selectedProfile,
+                                                               this._autoMigrate));
     for (var i = 0; i < 16; ++i) {
       var itemID = (items >> i) & 0x1 ? Math.pow(2, i) : 0;
       if (itemID > 0) {
         var checkbox = document.createElement("checkbox");
         checkbox.id = itemID;
         checkbox.setAttribute("label",
           MigrationUtils.getLocalizedString(itemID + "_" + this._source));
         dataSources.appendChild(checkbox);
@@ -299,19 +320,19 @@ var MigrationWizard = { /* exported Migr
 
     var singleStart = document.getElementById("homePageSingleStart");
     singleStart.setAttribute("label", mainStr);
     singleStart.setAttribute("value", "DEFAULT");
 
     var appName = MigrationUtils.getBrowserName(this._source);
 
     // semi-wallpaper for crash when multiple profiles exist, since we haven't initialized mSourceProfile in places
-    this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate);
+    this.spinResolve(this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate));
 
-    var oldHomePageURL = this._migrator.sourceHomePageURL;
+    var oldHomePageURL = this.spinResolve(this._migrator.getSourceHomePageURL());
 
     if (oldHomePageURL && appName) {
       var oldHomePageLabel =
         brandBundle.getFormattedString("homePageImport", [appName]);
       var oldHomePage = document.getElementById("oldHomePage");
       oldHomePage.setAttribute("label", oldHomePageLabel);
       oldHomePage.setAttribute("value", oldHomePageURL);
       oldHomePage.removeAttribute("hidden");
@@ -332,25 +353,28 @@ var MigrationWizard = { /* exported Migr
 
   // 5 - Migrating
   onMigratingPageShow() {
     this._wiz.getButton("cancel").disabled = true;
     this._wiz.canRewind = false;
     this._wiz.canAdvance = false;
 
     // When automigrating, show all of the data that can be received from this source.
-    if (this._autoMigrate)
-      this._itemsFlags = this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate);
+    if (this._autoMigrate) {
+      this._itemsFlags =
+        this.spinResolve(this._migrator.getMigrateData(this._selectedProfile,
+                                                       this._autoMigrate));
+    }
 
     this._listItems("migratingItems");
     setTimeout(() => this.onMigratingMigrate(), 0);
   },
 
-  onMigratingMigrate() {
-    this._migrator.migrate(this._itemsFlags, this._autoMigrate, this._selectedProfile);
+  async onMigratingMigrate() {
+    await this._migrator.migrate(this._itemsFlags, this._autoMigrate, this._selectedProfile);
 
     Services.telemetry.getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
                       .add(MigrationUtils.getSourceIdForTelemetry(this._source));
     if (!this._autoMigrate) {
       let hist = Services.telemetry.getKeyedHistogramById("FX_MIGRATION_USAGE");
       let exp = 0;
       let items = this._itemsFlags;
       while (items) {
--- a/browser/components/migration/nsIBrowserProfileMigrator.idl
+++ b/browser/components/migration/nsIBrowserProfileMigrator.idl
@@ -33,45 +33,48 @@ interface nsIBrowserProfileMigrator : ns
   void migrate(in unsigned short aItems, in nsIProfileStartup aStartup, in jsval aProfile);
 
   /**
    * A bit field containing profile items that this migrator
    * offers for import. 
    * @param   aProfile the profile that we are looking for available data
    *          to import
    * @param   aDoingStartup "true" if the profile is not currently being used.
-   * @return  bit field containing profile items (see above)
+   * @return  Promise containing a bit field containing profile items (see above)
    * @note    a return value of 0 represents no items rather than ALL.
    */
-  unsigned short getMigrateData(in jsval aProfile, in boolean aDoingStartup);
+  jsval getMigrateData(in jsval aProfile, in boolean aDoingStartup);
 
   /**
    * Get the last time data from this browser was modified
    * @return a promise that resolves to a JS Date object
    */
   jsval getLastUsedDate();
 
   /**
-   * Whether or not there is any data that can be imported from this
+   * Get whether or not there is any data that can be imported from this
    * browser (i.e. whether or not it is installed, and there exists
    * a user profile)
+   * @return a promise that resolves with a boolean.
    */
-  readonly attribute boolean          sourceExists;
+  jsval isSourceAvailable();
 
 
   /**
    * An enumeration of available profiles. If the import source does
    * not support profiles, this attribute is null.
+   * @return a promise that resolves with an array of profiles or null.
    */
-  readonly attribute jsval            sourceProfiles;
+  jsval getSourceProfiles();
 
   /**
    * The import source homepage.  Returns null if not present/available
+   * @return a promise that resolves with a string or null.
    */
-  readonly attribute AUTF8String      sourceHomePageURL;
+  jsval getSourceHomePageURL();
 
 
   /**
    * Whether the source browser data is locked/in-use meaning migration likely
    * won't succeed and the user should be warned.
    */
   readonly attribute boolean          sourceLocked;
 };
--- a/browser/components/migration/tests/unit/head_migration.js
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -25,19 +25,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 var gProfD = do_get_profile();
 
 Cu.import("resource://testing-common/AppInfo.jsm");
 updateAppInfo();
 
 /**
  * Migrates the requested resource and waits for the migration to be complete.
  */
-function promiseMigration(migrator, resourceType, aProfile = null) {
+async function promiseMigration(migrator, resourceType, aProfile = null) {
   // Ensure resource migration is available.
-  let availableSources = migrator.getMigrateData(aProfile, false);
+  let availableSources = await migrator.getMigrateData(aProfile, false);
   Assert.ok((availableSources & resourceType) > 0, "Resource supported by migrator");
 
   return new Promise(resolve => {
     Services.obs.addObserver(function onMigrationEnded() {
       Services.obs.removeObserver(onMigrationEnded, "Migration:Ended");
       resolve();
     }, "Migration:Ended");
 
--- a/browser/components/migration/tests/unit/test_360se_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_360se_bookmarks.js
@@ -1,18 +1,18 @@
 "use strict";
 
 add_task(async function() {
   registerFakePath("AppData", do_get_file("AppData/Roaming/"));
 
-  let migrator = MigrationUtils.getMigrator("360se");
+  let migrator = await MigrationUtils.getMigrator("360se");
   // Sanity check for the source.
-  Assert.ok(migrator.sourceExists);
+  Assert.ok(await migrator.isSourceAvailable());
 
-  let profiles = migrator.sourceProfiles;
+  let profiles = await migrator.getSourceProfiles();
   Assert.equal(profiles.length, 2, "Should present two profiles");
   Assert.equal(profiles[0].name, "test@firefox.com.cn", "Current logged in user should be the first");
   Assert.equal(profiles[profiles.length - 1].name, "Default", "Default user should be the last");
 
   // Wait for the imported bookmarks.  Check that "From 360 Secure Browser"
   // folders are created on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceName360se");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
@@ -40,17 +40,17 @@ add_task(async function() {
     onItemRemoved() {},
     onItemChanged() {},
     onItemVisited() {},
     onItemMoved() {},
   };
   PlacesUtils.bookmarks.addObserver(bmObserver);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, {
-    id: "default"
+    id: "default",
   });
   PlacesUtils.bookmarks.removeObserver(bmObserver);
 
   // Check the bookmarks have been imported to all the expected parents.
   Assert.ok(!expectedParents.length, "No more expected parents");
   Assert.ok(gotFolder, "Should have seen the folder get imported");
   Assert.equal(itemCount, 10, "Should import all 10 items.");
   // Check that the telemetry matches:
--- a/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
@@ -39,11 +39,11 @@ add_task(async function test_getLocaleSt
 });
 
 add_task(async function test_isExtensionInstalled_function() {
   let isInstalled = await ChromeMigrationUtils.isExtensionInstalled("fake-extension-1", "Default");
   Assert.ok(isInstalled, "The fake-extension-1 extension should be installed.");
 });
 
 add_task(async function test_getLastUsedProfileId_function() {
-  let profileId = ChromeMigrationUtils.getLastUsedProfileId();
+  let profileId = await ChromeMigrationUtils.getLastUsedProfileId();
   Assert.equal(profileId, "Default", "The last used profile ID should be Default.");
 });
--- a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
@@ -67,19 +67,19 @@ add_task(async function() {
       };
       currentMenuKids.push(nextFolder);
       currentMenuKids = nextFolder.children;
     }
   }
 
   await OS.File.writeAtomic(target.path, JSON.stringify(bookmarksData), {encoding: "utf-8"});
 
-  let migrator = MigrationUtils.getMigrator("chrome");
+  let migrator = await MigrationUtils.getMigrator("chrome");
   // Sanity check for the source.
-  Assert.ok(migrator.sourceExists);
+  Assert.ok(await migrator.isSourceAvailable());
 
   let itemsSeen = {bookmarks: 0, folders: 0};
   let bmObserver = {
     onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
       if (!aTitle.includes("Chrome")) {
         itemsSeen[aItemType == PlacesUtils.bookmarks.TYPE_FOLDER ? "folders" : "bookmarks"]++;
       }
     },
--- a/browser/components/migration/tests/unit/test_Chrome_cookies.js
+++ b/browser/components/migration/tests/unit/test_Chrome_cookies.js
@@ -1,17 +1,17 @@
 "use strict";
 
 Cu.import("resource://gre/modules/ForgetAboutSite.jsm");
 
 add_task(async function() {
   registerFakePath("ULibDir", do_get_file("Library/"));
-  let migrator = MigrationUtils.getMigrator("chrome");
+  let migrator = await MigrationUtils.getMigrator("chrome");
 
-  Assert.ok(migrator.sourceExists, "Sanity check the source exists");
+  Assert.ok(await migrator.isSourceAvailable(), "Sanity check the source exists");
 
   const COOKIE = {
     expiry: 2145934800,
     host: "unencryptedcookie.invalid",
     isHttpOnly: false,
     isSession: false,
     name: "testcookie",
     path: "/",
--- a/browser/components/migration/tests/unit/test_Chrome_passwords.js
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js
@@ -138,18 +138,18 @@ add_task(async function setup() {
   });
 });
 
 add_task(async function test_importIntoEmptyDB() {
   for (let login of TEST_LOGINS) {
     await promiseSetPassword(login);
   }
 
-  let migrator = MigrationUtils.getMigrator("chrome");
-  Assert.ok(migrator.sourceExists, "Sanity check the source exists");
+  let migrator = await MigrationUtils.getMigrator("chrome");
+  Assert.ok(await migrator.isSourceAvailable(), "Sanity check the source exists");
 
   let logins = Services.logins.getAllLogins({});
   Assert.equal(logins.length, 0, "There are no logins initially");
 
   // Migrate the logins.
   await promiseMigration(migrator, MigrationUtils.resourceTypes.PASSWORDS, PROFILE);
 
   logins = Services.logins.getAllLogins({});
@@ -159,18 +159,18 @@ add_task(async function test_importIntoE
 
   for (let i = 0; i < TEST_LOGINS.length; i++) {
     checkLoginsAreEqual(logins[i], TEST_LOGINS[i], i + 1);
   }
 });
 
 // Test that existing logins for the same primary key don't get overwritten
 add_task(async function test_importExistingLogins() {
-  let migrator = MigrationUtils.getMigrator("chrome");
-  Assert.ok(migrator.sourceExists, "Sanity check the source exists");
+  let migrator = await MigrationUtils.getMigrator("chrome");
+  Assert.ok(await migrator.isSourceAvailable(), "Sanity check the source exists");
 
   Services.logins.removeAllLogins();
   let logins = Services.logins.getAllLogins({});
   Assert.equal(logins.length, 0, "There are no logins after removing all of them");
 
   let newLogins = [];
 
   // Create 3 new logins that are different but where the key properties are still the same.
--- a/browser/components/migration/tests/unit/test_IE_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -1,14 +1,14 @@
 "use strict";
 
 add_task(async function() {
-  let migrator = MigrationUtils.getMigrator("ie");
+  let migrator = await MigrationUtils.getMigrator("ie");
   // Sanity check for the source.
-  Assert.ok(migrator.sourceExists);
+  Assert.ok(await migrator.isSourceAvailable());
 
   // Wait for the imported bookmarks.  Check that "From Internet Explorer"
   // folders are created in the menu and on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameIE");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.bookmarksMenuFolderId,
                           PlacesUtils.toolbarFolderId ];
--- a/browser/components/migration/tests/unit/test_IE_cookies.js
+++ b/browser/components/migration/tests/unit/test_IE_cookies.js
@@ -1,17 +1,17 @@
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                   "resource://gre/modules/ctypes.jsm");
 
 add_task(async function() {
-  let migrator = MigrationUtils.getMigrator("ie");
+  let migrator = await MigrationUtils.getMigrator("ie");
   // Sanity check for the source.
-  Assert.ok(migrator.sourceExists);
+  Assert.ok(await migrator.isSourceAvailable());
 
   const BOOL = ctypes.bool;
   const LPCTSTR = ctypes.char16_t.ptr;
   const DWORD = ctypes.uint32_t;
   const LPDWORD = DWORD.ptr;
 
   let wininet = ctypes.open("Wininet");
 
@@ -56,17 +56,17 @@ add_task(async function() {
   let date = (new Date()).getDate();
   const COOKIE = {
     get host() {
       return new URL(this.href).host;
     },
     href: `http://mycookietest.${Math.random()}.com`,
     name: "testcookie",
     value: "testvalue",
-    expiry: new Date(new Date().setDate(date + 2))
+    expiry: new Date(new Date().setDate(date + 2)),
   };
   let data = ctypes.char16_t.array()(256);
   let sizeRef = DWORD(256).address();
 
   registerCleanupFunction(() => {
     // Remove the cookie.
     try {
       let expired = new Date(new Date().setDate(date - 2));
--- a/browser/components/migration/tests/unit/test_Safari_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
@@ -1,16 +1,16 @@
 "use strict";
 
 add_task(async function() {
   registerFakePath("ULibDir", do_get_file("Library/"));
 
-  let migrator = MigrationUtils.getMigrator("safari");
+  let migrator = await MigrationUtils.getMigrator("safari");
   // Sanity check for the source.
-  Assert.ok(migrator.sourceExists);
+  Assert.ok(await migrator.isSourceAvailable());
 
   // Wait for the imported bookmarks.  Check that "From Safari"
   // folders are created on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameSafari");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.toolbarFolderId ];
   let itemCount = 0;
--- a/browser/components/migration/tests/unit/test_automigration.js
+++ b/browser/components/migration/tests/unit/test_automigration.js
@@ -38,120 +38,134 @@ async function visitsForURL(url) {
     `SELECT count(*) FROM moz_historyvisits v
      JOIN moz_places h ON h.id = v.place_id
      WHERE url_hash = hash(:url) AND url = :url`,
      {url});
   visitCount = visitCount[0].getInt64(0);
   return visitCount;
 }
 
+async function promiseThrows(fn) {
+  let failed = false;
+  try {
+    await fn();
+  } catch (e) {
+    failed = true;
+  }
+  Assert.ok(failed);
+}
 
 /**
  * Test automatically picking a browser to migrate from
  */
 add_task(async function checkMigratorPicking() {
-  Assert.throws(() => AutoMigrate.pickMigrator("firefox"),
-                /Can't automatically migrate from Firefox/,
-                "Should throw when explicitly picking Firefox.");
+  await promiseThrows(() => AutoMigrate.pickMigrator("firefox"),
+                      /Can't automatically migrate from Firefox/,
+                      "Should throw when explicitly picking Firefox.");
 
-  Assert.throws(() => AutoMigrate.pickMigrator("gobbledygook"),
-                /migrator object is not available/,
-                "Should throw when passing unknown migrator key");
+  await promiseThrows(() => AutoMigrate.pickMigrator("gobbledygook"),
+                      /migrator object is not available/,
+                      "Should throw when passing unknown migrator key");
   gShimmedMigratorKeyPicker = function() {
     return "firefox";
   };
-  Assert.throws(() => AutoMigrate.pickMigrator(),
-                /Can't automatically migrate from Firefox/,
-                "Should throw when implicitly picking Firefox.");
+  await promiseThrows(() => AutoMigrate.pickMigrator(),
+                      /Can't automatically migrate from Firefox/,
+                      "Should throw when implicitly picking Firefox.");
   gShimmedMigratorKeyPicker = function() {
     return "gobbledygook";
   };
-  Assert.throws(() => AutoMigrate.pickMigrator(),
-                /migrator object is not available/,
-                "Should throw when an unknown migrator is the default");
+  await promiseThrows(() => AutoMigrate.pickMigrator(),
+                      /migrator object is not available/,
+                      "Should throw when an unknown migrator is the default");
   gShimmedMigratorKeyPicker = function() {
     return "";
   };
-  Assert.throws(() => AutoMigrate.pickMigrator(),
-                /Could not determine default browser key/,
-                "Should throw when an unknown migrator is the default");
+  await promiseThrows(() => AutoMigrate.pickMigrator(),
+                      /Could not determine default browser key/,
+                      "Should throw when an unknown migrator is the default");
 });
 
 
 /**
  * Test automatically picking a profile to migrate from
  */
 add_task(async function checkProfilePicking() {
-  let fakeMigrator = {sourceProfiles: [{id: "a"}, {id: "b"}]};
-  let profB = fakeMigrator.sourceProfiles[1];
-  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
-                /Don't know how to pick a profile when more/,
-                "Should throw when there are multiple profiles.");
-  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
-                /Profile specified was not found/,
-                "Should throw when the profile supplied doesn't exist.");
-  let profileToMigrate = AutoMigrate.pickProfile(fakeMigrator, "b");
+  let fakeMigrator = {
+    _sourceProfiles: [{id: "a"}, {id: "b"}],
+    getSourceProfiles() {
+      return this._sourceProfiles;
+    },
+  };
+  let profB = fakeMigrator._sourceProfiles[1];
+  await promiseThrows(() => AutoMigrate.pickProfile(fakeMigrator),
+                      /Don't know how to pick a profile when more/,
+                      "Should throw when there are multiple profiles.");
+  await promiseThrows(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
+                      /Profile specified was not found/,
+                      "Should throw when the profile supplied doesn't exist.");
+  let profileToMigrate = await AutoMigrate.pickProfile(fakeMigrator, "b");
   Assert.equal(profileToMigrate, profB, "Should return profile supplied");
 
-  fakeMigrator.sourceProfiles = null;
-  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
-                /Profile specified but only a default profile found./,
-                "Should throw when the profile supplied doesn't exist.");
-  profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
+  fakeMigrator._sourceProfiles = null;
+  await promiseThrows(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
+                      /Profile specified but only a default profile found./,
+                      "Should throw when the profile supplied doesn't exist.");
+  profileToMigrate = await AutoMigrate.pickProfile(fakeMigrator);
   Assert.equal(profileToMigrate, null, "Should return default profile when that's the only one.");
 
-  fakeMigrator.sourceProfiles = [];
-  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
-                /No profile data found/,
-                "Should throw when no profile data is present.");
+  fakeMigrator._sourceProfiles = [];
+  await promiseThrows(() => AutoMigrate.pickProfile(fakeMigrator),
+                      /No profile data found/,
+                      "Should throw when no profile data is present.");
 
-  fakeMigrator.sourceProfiles = [{id: "a"}];
-  let profA = fakeMigrator.sourceProfiles[0];
-  profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
+  fakeMigrator._sourceProfiles = [{id: "a"}];
+  let profA = fakeMigrator._sourceProfiles[0];
+  profileToMigrate = await AutoMigrate.pickProfile(fakeMigrator);
   Assert.equal(profileToMigrate, profA, "Should return the only profile if only one is present.");
 });
 
 /**
  * Test the complete automatic process including browser and profile selection,
  * and actual migration (which implies startup)
  */
 add_task(async function checkIntegration() {
   gShimmedMigrator = {
-    get sourceProfiles() {
+    getSourceProfiles() {
       info("Read sourceProfiles");
       return null;
     },
     getMigrateData(profileToMigrate) {
       this._getMigrateDataArgs = profileToMigrate;
       return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
     },
     migrate(types, startup, profileToMigrate) {
       this._migrateArgs = [types, startup, profileToMigrate];
     },
   };
   gShimmedMigratorKeyPicker = function() {
     return "gobbledygook";
   };
-  AutoMigrate.migrate("startup");
+  await AutoMigrate.migrate("startup");
   Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
                      "getMigrateData called with 'null' as a profile");
 
   let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
   let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
   Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
                    "migrate called with 'null' as a profile");
 });
 
 /**
  * Test the undo preconditions and a no-op undo in the automigrator.
  */
 add_task(async function checkUndoPreconditions() {
   let shouldAddData = false;
   gShimmedMigrator = {
-    get sourceProfiles() {
+    getSourceProfiles() {
       info("Read sourceProfiles");
       return null;
     },
     getMigrateData(profileToMigrate) {
       this._getMigrateDataArgs = profileToMigrate;
       return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
     },
     migrate(types, startup, profileToMigrate) {
@@ -169,17 +183,17 @@ add_task(async function checkUndoPrecond
         Services.obs.notifyObservers(null, "Migration:Ended", undefined);
       });
     },
   };
 
   gShimmedMigratorKeyPicker = function() {
     return "gobbledygook";
   };
-  AutoMigrate.migrate("startup");
+  await AutoMigrate.migrate("startup");
   let migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
   Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
                      "getMigrateData called with 'null' as a profile");
 
   let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
   let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
   Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
                    "migrate called with 'null' as a profile");
@@ -188,17 +202,17 @@ add_task(async function checkUndoPrecond
   Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
             "Should have set browser pref");
   Assert.ok(!(await AutoMigrate.canUndo()), "Should not be able to undo migration, as there's no data");
   gShimmedMigrator._migrateArgs = null;
   gShimmedMigrator._getMigrateDataArgs = null;
   Preferences.reset("browser.migrate.automigrate.browser");
   shouldAddData = true;
 
-  AutoMigrate.migrate("startup");
+  await AutoMigrate.migrate("startup");
   migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
   Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
                      "getMigrateData called with 'null' as a profile");
   Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
                    "migrate called with 'null' as a profile");
 
   await migrationFinishedPromise;
   let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
@@ -264,17 +278,17 @@ add_task(async function checkUndoRemoval
       {
         transitionType: PlacesUtils.history.TRANSITION_LINK,
         visitDate: now_uSec,
       },
       {
         transitionType: PlacesUtils.history.TRANSITION_LINK,
         visitDate: now_uSec - 100 * kUsecPerMin,
       },
-    ]
+    ],
   }]);
   await frecencyUpdatePromise;
 
   // Verify that both visits get reported.
   let opts = PlacesUtils.history.getNewQueryOptions();
   opts.resultType = opts.RESULTS_AS_VISIT;
   let query = PlacesUtils.history.getNewQuery();
   query.uri = visitedURI;
@@ -330,33 +344,33 @@ add_task(async function checkUndoRemoval
 
 add_task(async function checkUndoBookmarksState() {
   MigrationUtils.initializeUndoData();
   const {TYPE_FOLDER, TYPE_BOOKMARK} = PlacesUtils.bookmarks;
   let title = "Some example bookmark";
   let url = "http://www.example.com";
   let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
   let {guid, lastModified} = await MigrationUtils.insertBookmarkWrapper({
-    title, url, parentGuid
+    title, url, parentGuid,
   });
   Assert.deepEqual((await MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"),
       [{lastModified, parentGuid, guid, type: TYPE_BOOKMARK}]);
 
   MigrationUtils.initializeUndoData();
   ({guid, lastModified} = await MigrationUtils.insertBookmarkWrapper({
-    title, parentGuid, type: TYPE_FOLDER
+    title, parentGuid, type: TYPE_FOLDER,
   }));
   let folder = {guid, lastModified, parentGuid, type: TYPE_FOLDER};
   let folderGuid = folder.guid;
   ({guid, lastModified} = await MigrationUtils.insertBookmarkWrapper({
-    title, url, parentGuid: folderGuid
+    title, url, parentGuid: folderGuid,
   }));
   let kid1 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
   ({guid, lastModified} = await MigrationUtils.insertBookmarkWrapper({
-    title, url, parentGuid: folderGuid
+    title, url, parentGuid: folderGuid,
   }));
   let kid2 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
 
   let bookmarksUndo = (await MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
   Assert.equal(bookmarksUndo.length, 3);
   // We expect that the last modified time from first kid #1 and then kid #2
   // has been propagated to the folder:
   folder.lastModified = kid2.lastModified;
@@ -370,38 +384,38 @@ add_task(async function checkUndoBookmar
 
 add_task(async function testBookmarkRemovalByUndo() {
   const {TYPE_FOLDER} = PlacesUtils.bookmarks;
   MigrationUtils.initializeUndoData();
   let title = "Some example bookmark";
   let url = "http://www.mymagicaluniqueurl.com";
   let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
   let {guid} = await MigrationUtils.insertBookmarkWrapper({
-    title: "Some folder", parentGuid, type: TYPE_FOLDER
+    title: "Some folder", parentGuid, type: TYPE_FOLDER,
   });
   let folderGuid = guid;
   let itemsToRemove = [];
   ({guid} = await MigrationUtils.insertBookmarkWrapper({
-    title: "Inner folder", parentGuid: folderGuid, type: TYPE_FOLDER
+    title: "Inner folder", parentGuid: folderGuid, type: TYPE_FOLDER,
   }));
   let innerFolderGuid = guid;
   itemsToRemove.push(innerFolderGuid);
 
   ({guid} = await MigrationUtils.insertBookmarkWrapper({
-    title: "Inner inner folder", parentGuid: innerFolderGuid, type: TYPE_FOLDER
+    title: "Inner inner folder", parentGuid: innerFolderGuid, type: TYPE_FOLDER,
   }));
   itemsToRemove.push(guid);
 
   ({guid} = await MigrationUtils.insertBookmarkWrapper({
-    title: "Inner nested item", url: "http://inner-nested-example.com", parentGuid: guid
+    title: "Inner nested item", url: "http://inner-nested-example.com", parentGuid: guid,
   }));
   itemsToRemove.push(guid);
 
   ({guid} = await MigrationUtils.insertBookmarkWrapper({
-    title, url, parentGuid: folderGuid
+    title, url, parentGuid: folderGuid,
   }));
   itemsToRemove.push(guid);
 
   for (let toBeRemovedGuid of itemsToRemove) {
     let dbResultForGuid = await PlacesUtils.bookmarks.fetch(toBeRemovedGuid);
     Assert.ok(dbResultForGuid, "Should be able to find items that will be removed.");
   }
   let bookmarkUndoState = (await MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
@@ -574,17 +588,17 @@ add_task(async function checkUndoVisitsS
 
   // We have to wait until frecency updates have been handled in order
   // to accurately determine whether we're doing the right thing.
   let frecencyUpdatesHandled = new Promise(resolve => {
     PlacesUtils.history.addObserver({
       onManyFrecenciesChanged() {
         PlacesUtils.history.removeObserver(this);
         resolve();
-      }
+      },
     });
   });
   await PlacesUtils.history.insertMany([{
     url: "http://www.example.com/",
     title: "Example",
     visits: [{
       date: new Date("2015-08-16"),
     }],
@@ -603,17 +617,17 @@ add_task(async function checkUndoVisitsS
       date: new Date("2015-09-01"),
     }],
   }]);
   await frecencyUpdatesHandled;
   let undoVisitData = (await MigrationUtils.stopAndRetrieveUndoData()).get("visits");
 
   let frecencyChangesExpected = new Map([
     ["http://www.example.com/", PromiseUtils.defer()],
-    ["http://www.example.org/", PromiseUtils.defer()]
+    ["http://www.example.org/", PromiseUtils.defer()],
   ]);
   let uriDeletedExpected = new Map([
     ["http://www.mozilla.org/", PromiseUtils.defer()],
   ]);
   let wrongMethodDeferred = PromiseUtils.defer();
   let observer = {
     onBeginUpdateBatch() {},
     onEndUpdateBatch() {},
--- a/browser/components/migration/tests/unit/test_fx_telemetry.js
+++ b/browser/components/migration/tests/unit/test_fx_telemetry.js
@@ -214,17 +214,17 @@ add_task(async function test_datareporti
 
   let ok = await promiseTelemetryMigrator(srcDir, targetDir);
   Assert.ok(ok, "callback should have been true");
 
   checkDirectoryContains(targetDir, {
     "datareporting": {
       "state.json": shouldBeCopied,
       "session-state.json": shouldBeCopied,
-    }
+    },
   });
 });
 
 add_task(async function test_no_session_state() {
   let [srcDir, targetDir] = getTestDirs();
 
   // Check that migration still works properly if we only have state.json.
   let subDir = createSubDir(srcDir, "datareporting");
@@ -232,17 +232,17 @@ add_task(async function test_no_session_
   writeToFile(subDir, "state.json", stateContent);
 
   let ok = await promiseTelemetryMigrator(srcDir, targetDir);
   Assert.ok(ok, "callback should have been true");
 
   checkDirectoryContains(targetDir, {
     "datareporting": {
       "state.json": stateContent,
-    }
+    },
   });
 });
 
 add_task(async function test_no_state() {
   let [srcDir, targetDir] = getTestDirs();
 
   // Check that migration still works properly if we only have session-state.json.
   let subDir = createSubDir(srcDir, "datareporting");
@@ -250,17 +250,17 @@ add_task(async function test_no_state() 
   writeToFile(subDir, "session-state.json", sessionStateContent);
 
   let ok = await promiseTelemetryMigrator(srcDir, targetDir);
   Assert.ok(ok, "callback should have been true");
 
   checkDirectoryContains(targetDir, {
     "datareporting": {
       "session-state.json": sessionStateContent,
-    }
+    },
   });
 });
 
 add_task(async function test_times_migration() {
   let [srcDir, targetDir] = getTestDirs();
 
   // create a times.json in the source directory.
   let contents = JSON.stringify({created: 1234});
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -33,16 +33,17 @@ with Files('controlcenter/**'):
 
 
 DIRS += [
     'about',
     'contextualidentity',
     'customizableui',
     'dirprovider',
     'downloads',
+    'enterprisepolicies',
     'extensions',
     'feeds',
     'migration',
     'newtab',
     'originattributes',
     'places',
     'preferences',
     'privatebrowsing',
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -641,17 +641,17 @@ nsBrowserContentHandler.prototype = {
   validate: function bch_validate(cmdLine) {
     // Other handlers may use osint so only handle the osint flag if the url
     // flag is also present and the command line is valid.
     var osintFlagIdx = cmdLine.findFlag("osint", false);
     var urlFlagIdx = cmdLine.findFlag("url", false);
     if (urlFlagIdx > -1 && (osintFlagIdx > -1 ||
         cmdLine.state == nsICommandLine.STATE_REMOTE_EXPLICIT)) {
       var urlParam = cmdLine.getArgument(urlFlagIdx + 1);
-      if (cmdLine.length != urlFlagIdx + 2 || /firefoxurl:/.test(urlParam))
+      if (cmdLine.length != urlFlagIdx + 2 || /firefoxurl:/i.test(urlParam))
         throw NS_ERROR_ABORT;
       var isDefault = false;
       try {
         var url = Services.urlFormatter.formatURLPref("app.support.baseURL") +
                   "win10-default-browser";
         if (urlParam == url) {
           isDefault = ShellService.isDefaultBrowser(false, false);
         }
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -435,17 +435,17 @@ BrowserGlue.prototype = {
           Object.defineProperty(this, "AlertsService", {
             value: subject.wrappedJSObject
           });
         } else if (data == "places-browser-init-complete") {
           if (this._placesBrowserInitComplete) {
             Services.obs.notifyObservers(null, "places-browser-init-complete");
           }
         } else if (data == "migrateMatchBucketsPrefForUIVersion60") {
-          this._migrateMatchBucketsPrefForUIVersion60();
+          this._migrateMatchBucketsPrefForUIVersion60(true);
         }
         break;
       case "initial-migration-will-import-default-bookmarks":
         this._migrationImportsDefaultBookmarks = true;
         break;
       case "initial-migration-did-import-default-bookmarks":
         this._initPlaces(true);
         break;
@@ -2346,28 +2346,48 @@ BrowserGlue.prototype = {
                         .add(promptCount);
     } catch (ex) { /* Don't break the default prompt if telemetry is broken. */ }
 
     if (willPrompt) {
       DefaultBrowserCheck.prompt(RecentWindow.getMostRecentBrowserWindow());
     }
   },
 
-  _migrateMatchBucketsPrefForUIVersion60() {
+  _migrateMatchBucketsPrefForUIVersion60(forceCheck = false) {
+    function check() {
+      if (CustomizableUI.getPlacementOfWidget("search-container")) {
+        Services.prefs.setCharPref(prefName,
+                                   "general:5,suggestion:Infinity");
+      }
+    }
     let prefName = "browser.urlbar.matchBuckets";
     let pref = Services.prefs.getCharPref(prefName, "");
     if (!pref) {
       // Set the pref based on the search bar's current placement.  If it's
       // placed (the urlbar and search bar are not unified), then set the pref
       // (so that history results will come before search suggestions).  If it's
       // not placed (the urlbar and search bar are unified), then leave the pref
       // cleared so that UnifiedComplete.js uses the default value (so that
       // search suggestions will come before history results).
-      if (CustomizableUI.getPlacementOfWidget("search-container")) {
-        Services.prefs.setCharPref(prefName, "general:5,suggestion:Infinity");
+      if (forceCheck) {
+        // This is the case when this is called by the test.
+        check();
+      } else {
+        // This is the normal, non-test case.  At this point the first window
+        // has not been set up yet, so use a CUI listener to get the placement
+        // when the nav-bar is first registered.
+        let listener = {
+          onAreaNodeRegistered(area, container) {
+            if (CustomizableUI.AREA_NAVBAR == area) {
+              check();
+              CustomizableUI.removeListener(listener);
+            }
+          },
+        };
+        CustomizableUI.addListener(listener);
       }
     }
     // Else, the pref has already been set.  Normally this pref does not exist.
     // Either the user customized it, or they were enrolled in the Shield study
     // in Firefox 57 that effectively already migrated the pref.  Either way,
     // leave it at its current value.
   },
 
@@ -2565,22 +2585,27 @@ BrowserGlue.prototype = {
         // Due to bug 1305895, tabs from iOS may not have device information, so
         // we have separate strings to handle those cases. (See Also
         // unnamedTabsArrivingNotificationNoDevice.body below)
         if (deviceName) {
           title = bundle.formatStringFromName("tabArrivingNotificationWithDevice.title", [deviceName], 1);
         } else {
           title = bundle.GetStringFromName("tabArrivingNotification.title");
         }
-        // Use the page URL as the body. We strip the fragment and query to
-        // reduce size, and also format it the same way that the url bar would.
-        body = URIs[0].uri.replace(/[?#].*$/, "");
+        // Use the page URL as the body. We strip the fragment and query (after
+        // the `?` and `#` respectively) to reduce size, and also format it the
+        // same way that the url bar would.
+        body = URIs[0].uri.replace(/([?#]).*$/, "$1");
+        let wasTruncated = body.length < URIs[0].uri.length;
         if (win.gURLBar) {
           body = win.gURLBar.trimValue(body);
         }
+        if (wasTruncated) {
+          body = bundle.formatStringFromName("singleTabArrivingWithTruncatedURL.body", [body], 1);
+        }
       } else {
         title = bundle.GetStringFromName("multipleTabsArrivingNotification.title");
         const allSameDevice = URIs.every(URI => URI.clientId == URIs[0].clientId);
         const unknownDevice = allSameDevice && !deviceName;
         let tabArrivingBody;
         if (unknownDevice) {
           tabArrivingBody = "unnamedTabsArrivingNotificationNoDevice.body";
         } else if (allSameDevice) {
@@ -2831,16 +2856,42 @@ ContentPermissionPrompt.prototype = {
         combinedIntegration.createPermissionPrompt(type, request);
       if (!permissionPrompt) {
         throw Components.Exception(
           `Failed to handle permission of type ${type}`,
           Cr.NS_ERROR_FAILURE);
       }
 
       permissionPrompt.prompt();
+
+      let schemeHistogram = Services.telemetry.getKeyedHistogramById("PERMISSION_REQUEST_ORIGIN_SCHEME");
+      let scheme = 0;
+      // URI is null for system principals.
+      if (request.principal.URI) {
+        switch (request.principal.URI.scheme) {
+          case "http":
+            scheme = 1;
+            break;
+          case "https":
+            scheme = 2;
+            break;
+        }
+      }
+      schemeHistogram.add(type, scheme);
+
+      // request.element should be the browser element in e10s.
+      if (request.element && request.element.contentPrincipal) {
+        let thirdPartyHistogram = Services.telemetry.getKeyedHistogramById("PERMISSION_REQUEST_THIRD_PARTY_ORIGIN");
+        let isThirdParty = request.principal.origin != request.element.contentPrincipal.origin;
+        thirdPartyHistogram.add(type, isThirdParty);
+      }
+
+      let userInputHistogram = Services.telemetry.getKeyedHistogramById("PERMISSION_REQUEST_HANDLING_USER_INPUT");
+      userInputHistogram.add(type, request.isHandlingUserInput);
+
     } catch (ex) {
       Cu.reportError(ex);
       request.cancel();
       throw ex;
     }
   },
 };
 
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -6,21 +6,18 @@
 this.EXPORTED_SYMBOLS = ["PlacesUIUtils"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 
-// PlacesUtils exposes multiple symbols, so we can't use defineLazyModuleGetter
-// until we remove legacy transactions (Bug 1131491).
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
-
 XPCOMUtils.defineLazyModuleGetters(this, {
+  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   RecentWindow: "resource:///modules/RecentWindow.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
   Weave: "resource://services-sync/main.js",
 });
 
@@ -264,261 +261,16 @@ this.PlacesUIUtils = {
     return [
       this.DESCRIPTION_ANNO,
       this.LOAD_IN_SIDEBAR_ANNO,
       PlacesUtils.READ_ONLY_ANNO,
     ];
   },
 
   /**
-   * Get a transaction for copying a uri item (either a bookmark or a history
-   * entry) from one container to another.
-   *
-   * @param   aData
-   *          JSON object of dropped or pasted item properties
-   * @param   aContainer
-   *          The container being copied into
-   * @param   aIndex
-   *          The index within the container the item is copied to
-   * @return A nsITransaction object that performs the copy.
-   *
-   * @note Since a copy creates a completely new item, only some internal
-   *       annotations are synced from the old one.
-   * @see this._copyableAnnotations for the list of copyable annotations.
-   */
-  _getURIItemCopyTransaction:
-  function PUIU__getURIItemCopyTransaction(aData, aContainer, aIndex) {
-    let transactions = [];
-    if (aData.dateAdded) {
-      transactions.push(
-        new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
-      );
-    }
-    if (aData.lastModified) {
-      transactions.push(
-        new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
-      );
-    }
-
-    let annos = [];
-    if (aData.annos) {
-      annos = aData.annos.filter(function(aAnno) {
-        return this._copyableAnnotations.includes(aAnno.name);
-      }, this);
-    }
-
-    // There's no need to copy the keyword since it's bound to the bookmark url.
-    return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(aData.uri),
-                                               aContainer, aIndex, aData.title,
-                                               null, annos, transactions);
-  },
-
-  /**
-   * Gets a transaction for copying (recursively nesting to include children)
-   * a folder (or container) and its contents from one folder to another.
-   *
-   * @param   aData
-   *          Unwrapped dropped folder data - Obj containing folder and children
-   * @param   aContainer
-   *          The container we are copying into
-   * @param   aIndex
-   *          The index in the destination container to insert the new items
-   * @return A nsITransaction object that will perform the copy.
-   *
-   * @note Since a copy creates a completely new item, only some internal
-   *       annotations are synced from the old one.
-   * @see this._copyableAnnotations for the list of copyable annotations.
-   */
-  _getFolderCopyTransaction(aData, aContainer, aIndex) {
-    function getChildItemsTransactions(aRoot) {
-      let transactions = [];
-      let index = aIndex;
-      for (let i = 0; i < aRoot.childCount; ++i) {
-        let child = aRoot.getChild(i);
-        // Temporary hacks until we switch to PlacesTransactions.jsm.
-        let isLivemark =
-          PlacesUtils.annotations.itemHasAnnotation(child.itemId,
-                                                    PlacesUtils.LMANNO_FEEDURI);
-        let [node] = PlacesUtils.unwrapNodes(
-          PlacesUtils.wrapNode(child, PlacesUtils.TYPE_X_MOZ_PLACE, isLivemark),
-          PlacesUtils.TYPE_X_MOZ_PLACE
-        );
-
-        // Make sure that items are given the correct index, this will be
-        // passed by the transaction manager to the backend for the insertion.
-        // Insertion behaves differently for DEFAULT_INDEX (append).
-        if (aIndex != PlacesUtils.bookmarks.DEFAULT_INDEX) {
-          index = i;
-        }
-
-        if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
-          if (node.livemark && node.annos) {
-            transactions.push(
-              PlacesUIUtils._getLivemarkCopyTransaction(node, aContainer, index)
-            );
-          } else {
-            transactions.push(
-              PlacesUIUtils._getFolderCopyTransaction(node, aContainer, index)
-            );
-          }
-        } else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
-          transactions.push(new PlacesCreateSeparatorTransaction(-1, index));
-        } else if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
-          transactions.push(
-            PlacesUIUtils._getURIItemCopyTransaction(node, -1, index)
-          );
-        } else {
-          throw new Error("Unexpected item under a bookmarks folder");
-        }
-      }
-      return transactions;
-    }
-
-    if (aContainer == PlacesUtils.tagsFolderId) { // Copying into a tag folder.
-      let transactions = [];
-      if (!aData.livemark && aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
-        let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
-        let urls = PlacesUtils.getURLsForContainerNode(root);
-        root.containerOpen = false;
-        for (let { uri } of urls) {
-          transactions.push(
-            new PlacesTagURITransaction(Services.io.newURI(uri), [aData.title])
-          );
-        }
-      }
-      return new PlacesAggregatedTransaction("addTags", transactions);
-    }
-
-    if (aData.livemark && aData.annos) { // Copying a livemark.
-      return this._getLivemarkCopyTransaction(aData, aContainer, aIndex);
-    }
-
-    let {root} = PlacesUtils.getFolderContents(aData.id, false, false);
-    let transactions = getChildItemsTransactions(root);
-    root.containerOpen = false;
-
-    if (aData.dateAdded) {
-      transactions.push(
-        new PlacesEditItemDateAddedTransaction(null, aData.dateAdded)
-      );
-    }
-    if (aData.lastModified) {
-      transactions.push(
-        new PlacesEditItemLastModifiedTransaction(null, aData.lastModified)
-      );
-    }
-
-    let annos = [];
-    if (aData.annos) {
-      annos = aData.annos.filter(function(aAnno) {
-        return this._copyableAnnotations.includes(aAnno.name);
-      }, this);
-    }
-
-    return new PlacesCreateFolderTransaction(aData.title, aContainer, aIndex,
-                                             annos, transactions);
-  },
-
-  /**
-   * Gets a transaction for copying a live bookmark item from one container to
-   * another.
-   *
-   * @param   aData
-   *          Unwrapped live bookmarkmark data
-   * @param   aContainer
-   *          The container we are copying into
-   * @param   aIndex
-   *          The index in the destination container to insert the new items
-   * @return A nsITransaction object that will perform the copy.
-   *
-   * @note Since a copy creates a completely new item, only some internal
-   *       annotations are synced from the old one.
-   * @see this._copyableAnnotations for the list of copyable annotations.
-   */
-  _getLivemarkCopyTransaction:
-  function PUIU__getLivemarkCopyTransaction(aData, aContainer, aIndex) {
-    if (!aData.livemark || !aData.annos) {
-      throw new Error("node is not a livemark");
-    }
-
-    let feedURI, siteURI;
-    let annos = [];
-    if (aData.annos) {
-      annos = aData.annos.filter(function(aAnno) {
-        if (aAnno.name == PlacesUtils.LMANNO_FEEDURI) {
-          feedURI = PlacesUtils._uri(aAnno.value);
-        } else if (aAnno.name == PlacesUtils.LMANNO_SITEURI) {
-          siteURI = PlacesUtils._uri(aAnno.value);
-        }
-        return this._copyableAnnotations.includes(aAnno.name);
-      }, this);
-    }
-
-    return new PlacesCreateLivemarkTransaction(feedURI, siteURI, aData.title,
-                                               aContainer, aIndex, annos);
-  },
-
-  /**
-   * Constructs a Transaction for the drop or paste of a blob of data into
-   * a container.
-   * @param   data
-   *          The unwrapped data blob of dropped or pasted data.
-   * @param   type
-   *          The content type of the data
-   * @param   container
-   *          The container the data was dropped or pasted into
-   * @param   index
-   *          The index within the container the item was dropped or pasted at
-   * @param   copy
-   *          The drag action was copy, so don't move folders or links.
-   * @return An object implementing nsITransaction that can perform
-   *         the move/insert.
-   */
-  makeTransaction:
-  function PUIU_makeTransaction(data, type, container, index, copy) {
-    switch (data.type) {
-      case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
-        if (copy) {
-          return this._getFolderCopyTransaction(data, container, index);
-        }
-
-        // Otherwise move the item.
-        return new PlacesMoveItemTransaction(data.id, container, index);
-      case PlacesUtils.TYPE_X_MOZ_PLACE:
-        if (copy || data.id == -1) { // Id is -1 if the place is not bookmarked.
-          return this._getURIItemCopyTransaction(data, container, index);
-        }
-
-        // Otherwise move the item.
-        return new PlacesMoveItemTransaction(data.id, container, index);
-      case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
-        if (copy) {
-          // There is no data in a separator, so copying it just amounts to
-          // inserting a new separator.
-          return new PlacesCreateSeparatorTransaction(container, index);
-        }
-
-        // Otherwise move the item.
-        return new PlacesMoveItemTransaction(data.id, container, index);
-      default:
-        if (type == PlacesUtils.TYPE_X_MOZ_URL ||
-            type == PlacesUtils.TYPE_UNICODE ||
-            type == TAB_DROP_TYPE) {
-          let title = type != PlacesUtils.TYPE_UNICODE ? data.title
-                                                       : data.uri;
-          return new PlacesCreateBookmarkTransaction(PlacesUtils._uri(data.uri),
-                                                     container, index, title);
-        }
-    }
-    return null;
-  },
-
-  /**
-   * ********* PlacesTransactions version of the function defined above ********
-   *
    * Constructs a Places Transaction for the drop or paste of a blob of data
    * into a container.
    *
    * @param   aData
    *          The unwrapped data blob of dropped or pasted data.
    * @param   aNewParentGuid
    *          GUID of the container the data was dropped or pasted into.
    * @param   aIndex
@@ -591,36 +343,32 @@ this.PlacesUIUtils = {
                     "chrome://browser/content/places/bookmarkProperties2.xul" :
                     "chrome://browser/content/places/bookmarkProperties.xul";
 
     let features = "centerscreen,chrome,modal,resizable=yes";
 
     let topUndoEntry;
     let batchBlockingDeferred;
 
-    if (this.useAsyncTransactions) {
-      // Set the transaction manager into batching mode.
-      topUndoEntry = PlacesTransactions.topUndoEntry;
-      batchBlockingDeferred = PromiseUtils.defer();
-      PlacesTransactions.batch(async () => {
-        await batchBlockingDeferred.promise;
-      });
-    }
+    // Set the transaction manager into batching mode.
+    topUndoEntry = PlacesTransactions.topUndoEntry;
+    batchBlockingDeferred = PromiseUtils.defer();
+    PlacesTransactions.batch(async () => {
+      await batchBlockingDeferred.promise;
+    });
 
     aParentWindow.openDialog(dialogURL, "", features, aInfo);
 
     let performed = ("performed" in aInfo && aInfo.performed);
 
-    if (this.useAsyncTransactions) {
-      batchBlockingDeferred.resolve();
+    batchBlockingDeferred.resolve();
 
-      if (!performed &&
-          topUndoEntry != PlacesTransactions.topUndoEntry) {
-        PlacesTransactions.undo().catch(Components.utils.reportError);
-      }
+    if (!performed &&
+        topUndoEntry != PlacesTransactions.topUndoEntry) {
+      PlacesTransactions.undo().catch(Components.utils.reportError);
     }
 
     return performed;
   },
 
   /**
    * set and fetch a favicon. Can only be used from the parent process.
    * @param browser   {Browser}   The XUL browser element for which we're fetching a favicon.
@@ -1525,16 +1273,14 @@ PlacesUIUtils.URI_FLAVORS = [PlacesUtils
 PlacesUIUtils.SUPPORTED_FLAVORS = [...PlacesUIUtils.PLACES_FLAVORS,
                                    ...PlacesUIUtils.URI_FLAVORS];
 
 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
   return Services.prefs.getComplexValue("intl.ellipsis",
                                         Ci.nsIPrefLocalizedString).data;
 });
 
-XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "useAsyncTransactions",
-                                      "browser.places.useAsyncTransactions", false);
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInBackground",
                                       PREF_LOAD_BOOKMARKS_IN_BACKGROUND, false);
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInTabs",
                                       PREF_LOAD_BOOKMARKS_IN_TABS, false);
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "openInTabClosesMenu",
   "browser.bookmarks.openInTabClosesMenu", false);
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -94,17 +94,16 @@ var BookmarkPropertiesPanel = {
   _keyword: "",
   _postData: null,
   _charSet: "",
   _feedURI: null,
   _siteURI: null,
 
   _defaultInsertionPoint: null,
   _hiddenRows: [],
-  _batching: false,
 
   /**
    * This method returns the correct label for the dialog's "accept"
    * button based on the variant of the dialog.
    */
   _getAcceptLabel: function BPP__getAcceptLabel() {
     if (this._action == ACTION_ADD) {
       if (this._URIs.length)
@@ -299,18 +298,16 @@ var BookmarkPropertiesPanel = {
                                    { subtree: true,
                                      attributeOldValue: true,
                                      attributeFilter: ["collapsed"] });
 
     // Some controls are flexible and we want to update their cached size when
     // the dialog is resized.
     window.addEventListener("resize", this);
 
-    this._beginBatch();
-
     switch (this._action) {
       case ACTION_EDIT:
         gEditItemOverlay.initPanel({ node: this._node,
                                      hiddenRows: this._hiddenRows,
                                      focusedElement: "first" });
         acceptButtonDisabled = gEditItemOverlay.readOnly;
         break;
       case ACTION_ADD:
@@ -367,39 +364,16 @@ var BookmarkPropertiesPanel = {
           let newHeight = document.getElementById(id).boxObject.height;
           this._height += -oldHeight + newHeight;
           elementsHeight.set(id, newHeight);
         }
         break;
     }
   },
 
-  // Hack for implementing batched-Undo around the editBookmarkOverlay
-  // instant-apply code. For all the details see the comment above beginBatch
-  // in browser-places.js
-  _batchBlockingDeferred: null,
-  _beginBatch() {
-    if (this._batching)
-      return;
-    if (!PlacesUIUtils.useAsyncTransactions) {
-      PlacesUtils.transactionManager.beginBatch(null);
-    }
-    this._batching = true;
-  },
-
-  _endBatch() {
-    if (!this._batching)
-      return;
-
-    if (!PlacesUIUtils.useAsyncTransactions) {
-      PlacesUtils.transactionManager.endBatch(false);
-    }
-    this._batching = false;
-  },
-
   // nsISupports
   QueryInterface: function BPP_QueryInterface(aIID) {
     if (aIID.equals(Ci.nsIDOMEventListener) ||
         aIID.equals(Ci.nsISupports))
       return this;
 
     throw Cr.NS_NOINTERFACE;
   },
@@ -419,32 +393,26 @@ var BookmarkPropertiesPanel = {
     // currently registered EventListener on the EventTarget has no effect.
     this._element("locationField")
         .removeEventListener("input", this);
   },
 
   onDialogAccept() {
     // We must blur current focused element to save its changes correctly
     document.commandDispatcher.focusedElement.blur();
-    // The order here is important! We have to uninit the panel first, otherwise
-    // late changes could force it to commit more transactions.
+    // We have to uninit the panel first, otherwise late changes could force it
+    // to commit more transactions.
     gEditItemOverlay.uninitPanel(true);
-    this._endBatch();
     window.arguments[0].performed = true;
   },
 
   onDialogCancel() {
-    // The order here is important! We have to uninit the panel first, otherwise
-    // changes done as part of Undo may change the panel contents and by
-    // that force it to commit more transactions.
+    // We have to uninit the panel first, otherwise late changes could force it
+    // to commit more transactions.
     gEditItemOverlay.uninitPanel(true);
-    this._endBatch();
-    if (!PlacesUIUtils.useAsyncTransactions) {
-      PlacesUtils.transactionManager.undoTransaction();
-    }
     window.arguments[0].performed = false;
   },
 
   /**
    * This method checks to see if the input fields are in a valid state.
    *
    * @returns  true if the input is valid, false otherwise
    */
@@ -488,142 +456,17 @@ var BookmarkPropertiesPanel = {
   async _getInsertionPointDetails() {
     return [
       this._defaultInsertionPoint.itemId,
       await this._defaultInsertionPoint.getIndex(),
       this._defaultInsertionPoint.guid,
     ];
   },
 
-  /**
-   * Returns a transaction for creating a new bookmark item representing the
-   * various fields and opening arguments of the dialog.
-   */
-  _getCreateNewBookmarkTransaction:
-  function BPP__getCreateNewBookmarkTransaction(aContainer, aIndex) {
-    var annotations = [];
-    var childTransactions = [];
-
-    if (this._description) {
-      let annoObj = { name: PlacesUIUtils.DESCRIPTION_ANNO,
-                      type: Ci.nsIAnnotationService.TYPE_STRING,
-                      flags: 0,
-                      value: this._description,
-                      expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
-      let editItemTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj);
-      childTransactions.push(editItemTxn);
-    }
-
-    if (this._loadInSidebar) {
-      let annoObj = { name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
-                      value: true };
-      let setLoadTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj);
-      childTransactions.push(setLoadTxn);
-    }
-
-    // XXX TODO: this should be in a transaction!
-    if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
-      PlacesUtils.setCharsetForURI(this._uri, this._charSet);
-
-    let createTxn = new PlacesCreateBookmarkTransaction(this._uri,
-                                                        aContainer,
-                                                        aIndex,
-                                                        this._title,
-                                                        this._keyword,
-                                                        annotations,
-                                                        childTransactions,
-                                                        this._postData);
-
-    return new PlacesAggregatedTransaction(this._getDialogTitle(),
-                                           [createTxn]);
-  },
-
-  /**
-   * Returns a childItems-transactions array representing the URIList with
-   * which the dialog has been opened.
-   */
-  _getTransactionsForURIList: function BPP__getTransactionsForURIList() {
-    var transactions = [];
-    for (let uri of this._URIs) {
-      let createTxn =
-        new PlacesCreateBookmarkTransaction(uri.uri, -1,
-                                            PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                            uri.title);
-      transactions.push(createTxn);
-    }
-    return transactions;
-  },
-
-  /**
-   * Returns a transaction for creating a new folder item representing the
-   * various fields and opening arguments of the dialog.
-   */
-  _getCreateNewFolderTransaction:
-  function BPP__getCreateNewFolderTransaction(aContainer, aIndex) {
-    var annotations = [];
-    var childItemsTransactions;
-    if (this._URIs.length)
-      childItemsTransactions = this._getTransactionsForURIList();
-
-    if (this._description)
-      annotations.push(this._getDescriptionAnnotation(this._description));
-
-    return new PlacesCreateFolderTransaction(this._title, aContainer,
-                                             aIndex, annotations,
-                                             childItemsTransactions);
-  },
-
-  async _createNewItem() {
-    let [container, index] = await this._getInsertionPointDetails();
-    let txn;
-    switch (this._itemType) {
-      case BOOKMARK_FOLDER:
-        txn = this._getCreateNewFolderTransaction(container, index);
-        break;
-      case LIVEMARK_CONTAINER:
-        txn = new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI,
-                                                  this._title, container, index);
-        break;
-      default: // BOOKMARK_ITEM
-        txn = this._getCreateNewBookmarkTransaction(container, index);
-    }
-
-    PlacesUtils.transactionManager.doTransaction(txn);
-    // This is a temporary hack until we use PlacesTransactions.jsm
-    if (txn._promise) {
-      await txn._promise;
-    }
-
-    let folderGuid = await PlacesUtils.promiseItemGuid(container);
-    let bm = await PlacesUtils.bookmarks.fetch({
-      parentGuid: folderGuid,
-      index
-    });
-    this._itemId = await PlacesUtils.promiseItemId(bm.guid);
-
-    return Object.freeze({
-      itemId: this._itemId,
-      bookmarkGuid: bm.guid,
-      title: this._title,
-      uri: this._uri ? this._uri.spec : "",
-      type: this._itemType == BOOKMARK_ITEM ?
-              Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
-              Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
-      parent: {
-        itemId: container,
-        bookmarkGuid: await PlacesUtils.promiseItemGuid(container),
-        type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
-      }
-    });
-  },
-
   async _promiseNewItem() {
-    if (!PlacesUIUtils.useAsyncTransactions)
-      return this._createNewItem();
-
     let [containerId, index, parentGuid] = await this._getInsertionPointDetails();
     let annotations = [];
     if (this._description) {
       annotations.push({ name: PlacesUIUtils.DESCRIPTION_ANNO,
                          value: this._description });
     }
     if (this._loadInSidebar) {
       annotations.push({ name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -115,28 +115,21 @@ PlacesController.prototype = {
     // filters out other commands that we do _not_ support (see 329587).
     const CMD_PREFIX = "placesCmd_";
     return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
   },
 
   isCommandEnabled: function PC_isCommandEnabled(aCommand) {
     switch (aCommand) {
     case "cmd_undo":
-      if (!PlacesUIUtils.useAsyncTransactions)
-        return PlacesUtils.transactionManager.numberOfUndoItems > 0;
-
       return PlacesTransactions.topUndoEntry != null;
     case "cmd_redo":
-      if (!PlacesUIUtils.useAsyncTransactions)
-        return PlacesUtils.transactionManager.numberOfRedoItems > 0;
-
       return PlacesTransactions.topRedoEntry != null;
     case "cmd_cut":
     case "placesCmd_cut":
-    case "placesCmd_moveBookmarks":
       for (let node of this._view.selectedNodes) {
         // If selection includes history nodes or tags-as-bookmark, disallow
         // cutting.
         if (node.itemId == -1 ||
             (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
           return false;
         }
       }
@@ -197,27 +190,19 @@ PlacesController.prototype = {
     default:
       return false;
     }
   },
 
   doCommand: function PC_doCommand(aCommand) {
     switch (aCommand) {
     case "cmd_undo":
-      if (!PlacesUIUtils.useAsyncTransactions) {
-        PlacesUtils.transactionManager.undoTransaction();
-        return;
-      }
       PlacesTransactions.undo().catch(Components.utils.reportError);
       break;
     case "cmd_redo":
-      if (!PlacesUIUtils.useAsyncTransactions) {
-        PlacesUtils.transactionManager.redoTransaction();
-        return;
-      }
       PlacesTransactions.redo().catch(Components.utils.reportError);
       break;
     case "cmd_cut":
     case "placesCmd_cut":
       this.cut();
       break;
     case "cmd_copy":
     case "placesCmd_copy":
@@ -263,19 +248,16 @@ PlacesController.prototype = {
       this.newItem("bookmark").catch(Components.utils.reportError);
       break;
     case "placesCmd_new:separator":
       this.newSeparator().catch(Components.utils.reportError);
       break;
     case "placesCmd_show:info":
       this.showBookmarkPropertiesForSelection();
       break;
-    case "placesCmd_moveBookmarks":
-      this.moveSelectedBookmarks().catch(Components.utils.reportError);
-      break;
     case "placesCmd_reload":
       this.reloadSelectedLivemark();
       break;
     case "placesCmd_sortBy:name":
       this.sortFolderByName().catch(Components.utils.reportError);
       break;
     case "placesCmd_createBookmark":
       let node = this._view.selectedNode;
@@ -746,82 +728,27 @@ PlacesController.prototype = {
    * Create a new Bookmark separator somewhere.
    */
   async newSeparator() {
     var ip = this._view.insertionPoint;
     if (!ip)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
 
     let index = await ip.getIndex();
-    if (!PlacesUIUtils.useAsyncTransactions) {
-      let txn = new PlacesCreateSeparatorTransaction(ip.itemId, index);
-      PlacesUtils.transactionManager.doTransaction(txn);
-      // Select the new item.
-      let insertedNodeId = PlacesUtils.bookmarks
-                                      .getIdForItemAt(ip.itemId, index);
-      this._view.selectItems([insertedNodeId], false);
-      return;
-    }
-
     let txn = PlacesTransactions.NewSeparator({ parentGuid: ip.guid, index });
     let guid = await txn.transact();
     // Select the new item.
     this._view.selectItems([guid], false);
   },
 
   /**
-   * Opens a dialog for moving the selected nodes.
-   */
-  async moveSelectedBookmarks() {
-    let args = {
-      // The guid of the folder to move bookmarks to. This will only be
-      // set in the useAsyncTransactions case.
-      moveToGuid: null,
-      // nodes is passed to support !useAsyncTransactions.
-      nodes: this._view.selectedNodes,
-    };
-    window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
-                      "", "chrome, modal",
-                      args);
-
-    if (!args.moveToGuid) {
-      return;
-    }
-
-    let transactions = [];
-
-    for (let node of this._view.selectedNodes) {
-      // Nothing to do if the node is already under the selected folder.
-      if (node.parent.bookmarkGuid == args.moveToGuid) {
-        continue;
-      }
-      transactions.push(PlacesTransactions.Move({
-        guid: node.bookmarkGuid,
-        newParentGuid: args.moveToGuid,
-      }));
-    }
-
-    if (transactions.length) {
-      await PlacesUIUtils.batchUpdatesForNode(this._view.result, transactions.length, async () => {
-        await PlacesTransactions.batch(transactions);
-      });
-    }
-  },
-
-  /**
    * Sort the selected folder by name
    */
   async sortFolderByName() {
-    let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
-    if (!PlacesUIUtils.useAsyncTransactions) {
-      var txn = new PlacesSortFolderByNameTransaction(itemId);
-      PlacesUtils.transactionManager.doTransaction(txn);
-      return;
-    }
-    let guid = await PlacesUtils.promiseItemGuid(itemId);
+    let guid = PlacesUtils.getConcreteItemGuid(this._view.selectedNode);
     await PlacesTransactions.SortByName(guid).transact();
   },
 
   /**
    * Walk the list of folders we're removing in this delete operation, and
    * see if the selected node specified is already implicitly being removed
    * because it is a child of that folder.
    * @param   node
@@ -880,47 +807,35 @@ PlacesController.prototype = {
       if (this._shouldSkipNode(node, removedFolders))
         continue;
 
       totalItems++;
 
       if (PlacesUtils.nodeIsTagQuery(node.parent)) {
         // This is a uri node inside a tag container.  It needs a special
         // untag transaction.
-        var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
-        var uri = NetUtil.newURI(node.uri);
-        if (PlacesUIUtils.useAsyncTransactions) {
-          let tag = node.parent.title;
-          if (!tag) {
-            let tagGuid = await PlacesUtils.promiseItemGuid(tagItemId);
-            tag = (await PlacesUtils.bookmarks.fetch(tagGuid)).title;
-          }
-          transactions.push(PlacesTransactions.Untag({ urls: [uri], tag }));
-        } else {
-          let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
-          transactions.push(txn);
+        let tag = node.parent.title;
+        if (!tag) {
+          // TODO: Bug 1432405 Try using getConcreteItemGuid.
+          let tagItemId = PlacesUtils.getConcreteItemId(node.parent);
+          let tagGuid = await PlacesUtils.promiseItemGuid(tagItemId);
+          tag = (await PlacesUtils.bookmarks.fetch(tagGuid)).title;
         }
+        transactions.push(PlacesTransactions.Untag({ urls: [node.uri], tag }));
       } else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
                PlacesUtils.nodeIsQuery(node.parent) &&
                PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
                  Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
         // This is a tag container.
         // Untag all URIs tagged with this tag only if the tag container is
         // child of the "Tags" query in the library, in all other places we
         // must only remove the query node.
         let tag = node.title;
         let URIs = PlacesUtils.tagging.getURIsForTag(tag);
-        if (PlacesUIUtils.useAsyncTransactions) {
-          transactions.push(PlacesTransactions.Untag({ tag, urls: URIs }));
-        } else {
-          for (var j = 0; j < URIs.length; j++) {
-            let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
-            transactions.push(txn);
-          }
-        }
+        transactions.push(PlacesTransactions.Untag({ tag, urls: URIs }));
       } else if (PlacesUtils.nodeIsURI(node) &&
                PlacesUtils.nodeIsQuery(node.parent) &&
                PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
                  Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
         // This is a uri node inside an history query.
         PlacesUtils.history.remove(node.uri).catch(Components.utils.reportError);
         // History deletes are not undoable, so we don't have a transaction.
       } else if (node.itemId == -1 &&
@@ -934,22 +849,17 @@ PlacesController.prototype = {
         // History deletes are not undoable, so we don't have a transaction.
       } else {
         // This is a common bookmark item.
         if (PlacesUtils.nodeIsFolder(node)) {
           // If this is a folder we add it to our array of folders, used
           // to skip nodes that are children of an already removed folder.
           removedFolders.push(node);
         }
-        if (PlacesUIUtils.useAsyncTransactions) {
-          bmGuidsToRemove.push(node.bookmarkGuid);
-        } else {
-          let txn = new PlacesRemoveItemTransaction(node.itemId);
-          transactions.push(txn);
-        }
+        bmGuidsToRemove.push(node.bookmarkGuid);
       }
     }
     if (bmGuidsToRemove.length) {
       transactions.push(PlacesTransactions.Remove({ guids: bmGuidsToRemove }));
     }
     return totalItems;
   },
 
@@ -964,24 +874,19 @@ PlacesController.prototype = {
     let removedFolders = [];
     let totalItems = 0;
 
     for (let range of ranges) {
       totalItems += await this._removeRange(range, transactions, removedFolders);
     }
 
     if (transactions.length > 0) {
-      if (PlacesUIUtils.useAsyncTransactions) {
-        await PlacesUIUtils.batchUpdatesForNode(this._view.result, totalItems, async () => {
-          await PlacesTransactions.batch(transactions);
-        });
-      } else {
-        var txn = new PlacesAggregatedTransaction(txnName, transactions);
-        PlacesUtils.transactionManager.doTransaction(txn);
-      }
+      await PlacesUIUtils.batchUpdatesForNode(this._view.result, totalItems, async () => {
+        await PlacesTransactions.batch(transactions);
+      });
     }
   },
 
   /**
    * Removes the set of selected ranges from history, asynchronously.
    *
    * @note history deletes are not undoable.
    */
@@ -1038,27 +943,21 @@ PlacesController.prototype = {
     if (!this._hasRemovableSelection())
       return;
 
     NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
 
     var root = this._view.result.root;
 
     if (PlacesUtils.nodeIsFolder(root)) {
-      if (PlacesUIUtils.useAsyncTransactions)
-        await this._removeRowsFromBookmarks(aTxnName);
-      else
-        this._removeRowsFromBookmarks(aTxnName);
+      await this._removeRowsFromBookmarks(aTxnName);
     } else if (PlacesUtils.nodeIsQuery(root)) {
       var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
       if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
-        if (PlacesUIUtils.useAsyncTransactions)
-          await this._removeRowsFromBookmarks(aTxnName);
-        else
-          this._removeRowsFromBookmarks(aTxnName);
+        await this._removeRowsFromBookmarks(aTxnName);
       } else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
         this._removeRowsFromHistory();
       } else {
         NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
       }
     } else
       NS_ASSERT(false, "unexpected root");
   },
@@ -1299,60 +1198,18 @@ PlacesController.prototype = {
       data = data.value.QueryInterface(Ci.nsISupportsString).data;
       type = type.value;
       items = PlacesUtils.unwrapNodes(data, type);
     } catch (ex) {
       // No supported data exists or nodes unwrap failed, just bail out.
       return;
     }
 
-    let itemsToSelect = [];
-    if (PlacesUIUtils.useAsyncTransactions) {
-      let doCopy = action == "copy";
-      itemsToSelect = await handleTransferItems(items, ip, doCopy, this._view);
-    } else {
-      let transactions = [];
<