Merge m-c to graphics
authorKartikaya Gupta <kgupta@mozilla.com>
Fri, 23 Dec 2016 10:14:44 -0500
changeset 341997 e0c38f8b6ebcc23320ede1928c09eda7526b71da
parent 341996 af6a4e3aad8100b726610d59c3c0eb13b69f7ed1 (current diff)
parent 327062 bbbd2f7539f224a482cc6d2dd10e6a5f31c8baf3 (diff)
child 341998 f241b4570f5396628738da4f0543f981bcc549d7
push id31345
push userkwierso@gmail.com
push dateFri, 10 Feb 2017 20:35:09 +0000
treeherdermozilla-central@a288fe35e494 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to graphics MozReview-Commit-ID: 6XqNGxF5EiQ
browser/installer/package-manifest.in
dom/animation/test/css-transitions/file_csstransition-events.html
dom/animation/test/css-transitions/file_event-dispatch.html
dom/animation/test/css-transitions/test_csstransition-events.html
dom/animation/test/css-transitions/test_event-dispatch.html
dom/base/nsDOMWindowUtils.cpp
dom/ipc/TabChild.cpp
dom/media/NextFrameSeekTask.cpp
dom/media/NextFrameSeekTask.h
dom/media/SeekTask.h
dom/media/test/test_source_media.html
gfx/gl/GLContextProviderEGL.cpp
gfx/gl/GLLibraryEGL.cpp
gfx/layers/Layers.h
gfx/layers/LayersTypes.h
gfx/layers/client/ClientLayerManager.cpp
gfx/layers/client/ClientLayerManager.h
gfx/layers/ipc/PCompositorBridge.ipdl
gfx/layers/ipc/PLayer.ipdl
gfx/layers/ipc/ShadowLayerChild.cpp
gfx/layers/ipc/ShadowLayerChild.h
gfx/layers/ipc/ShadowLayerParent.cpp
gfx/layers/ipc/ShadowLayerParent.h
gfx/layers/moz.build
js/src/jit/BaselineCacheIR.cpp
js/src/jit/BaselineCacheIR.h
js/src/jit/BaselineCacheIRCompiler.cpp
js/src/jit/BaselineCacheIRCompiler.h
layout/painting/FrameLayerBuilder.cpp
layout/painting/nsDisplayList.cpp
layout/reftests/svg/reftest.list
layout/tools/reftest/reftest.jsm
modules/libpref/init/all.js
taskcluster/ci/desktop-test/test-platforms.yml
taskcluster/ci/desktop-test/test-sets.yml
taskcluster/docker/README.md
taskcluster/docker/REGISTRY
taskcluster/docker/android-gradle-build/Dockerfile
taskcluster/docker/android-gradle-build/README.md
taskcluster/docker/android-gradle-build/REGISTRY
taskcluster/docker/android-gradle-build/VERSION
taskcluster/docker/android-gradle-build/bin/after.sh
taskcluster/docker/android-gradle-build/bin/before.sh
taskcluster/docker/android-gradle-build/bin/build.sh
taskcluster/docker/android-gradle-build/bin/checkout-script.sh
taskcluster/docker/android-gradle-build/bin/checkout-sources.sh
taskcluster/docker/android-gradle-build/buildprops.json
taskcluster/docker/android-gradle-build/dot-config/pip/pip.conf
taskcluster/docker/android-gradle-build/oauth.txt
taskcluster/docker/base-build/Dockerfile
taskcluster/docker/base-build/VERSION
taskcluster/docker/base-build/system-setup.sh
taskcluster/docker/base-test/Dockerfile
taskcluster/docker/base-test/REGISTRY
taskcluster/docker/base-test/VERSION
taskcluster/docker/base-test/sources.list
taskcluster/docker/centos6-build-upd/Dockerfile
taskcluster/docker/centos6-build-upd/REGISTRY
taskcluster/docker/centos6-build-upd/VERSION
taskcluster/docker/centos6-build/Dockerfile
taskcluster/docker/centos6-build/REGISTRY
taskcluster/docker/centos6-build/VERSION
taskcluster/docker/centos6-build/hgrc
taskcluster/docker/centos6-build/system-setup.sh
taskcluster/docker/decision/Dockerfile
taskcluster/docker/decision/README.md
taskcluster/docker/decision/REGISTRY
taskcluster/docker/decision/VERSION
taskcluster/docker/decision/system-setup.sh
taskcluster/docker/desktop-build/Dockerfile
taskcluster/docker/desktop-build/bin/build.sh
taskcluster/docker/desktop-build/bin/checkout-script.sh
taskcluster/docker/desktop-build/bin/checkout-sources.sh
taskcluster/docker/desktop-build/buildprops.json
taskcluster/docker/desktop-build/dot-config/pip/pip.conf
taskcluster/docker/desktop-build/oauth.txt
taskcluster/docker/desktop-test/Dockerfile
taskcluster/docker/desktop-test/apport
taskcluster/docker/desktop-test/buildprops.json
taskcluster/docker/desktop-test/deja-dup-monitor.desktop
taskcluster/docker/desktop-test/dot-files/config/pip/pip.conf
taskcluster/docker/desktop-test/dot-files/config/user-dirs.dirs
taskcluster/docker/desktop-test/dot-files/config/user-dirs.locale
taskcluster/docker/desktop-test/dot-files/pulse/default.pa
taskcluster/docker/desktop-test/fonts.conf
taskcluster/docker/desktop-test/jockey-gtk.desktop
taskcluster/docker/desktop-test/motd
taskcluster/docker/desktop-test/release-upgrades
taskcluster/docker/desktop-test/taskcluster-interactive-shell
taskcluster/docker/desktop-test/tc-vcs-config.yml
taskcluster/docker/desktop-test/tester.env
taskcluster/docker/desktop1604-test/Dockerfile
taskcluster/docker/desktop1604-test/apport
taskcluster/docker/desktop1604-test/buildprops.json
taskcluster/docker/desktop1604-test/deja-dup-monitor.desktop
taskcluster/docker/desktop1604-test/dot-files/config/pip/pip.conf
taskcluster/docker/desktop1604-test/dot-files/config/user-dirs.dirs
taskcluster/docker/desktop1604-test/dot-files/config/user-dirs.locale
taskcluster/docker/desktop1604-test/dot-files/pulse/default.pa
taskcluster/docker/desktop1604-test/fonts.conf
taskcluster/docker/desktop1604-test/jockey-gtk.desktop
taskcluster/docker/desktop1604-test/motd
taskcluster/docker/desktop1604-test/release-upgrades
taskcluster/docker/desktop1604-test/taskcluster-interactive-shell
taskcluster/docker/desktop1604-test/tc-vcs-config.yml
taskcluster/docker/desktop1604-test/tester.env
taskcluster/docker/image_builder/Dockerfile
taskcluster/docker/image_builder/REGISTRY
taskcluster/docker/image_builder/VERSION
taskcluster/docker/image_builder/build-image.sh
taskcluster/docker/image_builder/setup.sh
taskcluster/docker/lint/Dockerfile
taskcluster/docker/lint/system-setup.sh
taskcluster/docker/recipes/centos6-build-system-setup.sh
taskcluster/docker/recipes/common.sh
taskcluster/docker/recipes/install-mercurial.sh
taskcluster/docker/recipes/run-task
taskcluster/docker/recipes/tooltool.py
taskcluster/docker/recipes/ubuntu1204-test-system-setup.sh
taskcluster/docker/recipes/ubuntu1604-test-system-setup.sh
taskcluster/docker/recipes/xvfb.sh
taskcluster/docker/rust-build/Dockerfile
taskcluster/docker/rust-build/README.md
taskcluster/docker/rust-build/REGISTRY
taskcluster/docker/rust-build/VERSION
taskcluster/docker/rust-build/build_cargo.sh
taskcluster/docker/rust-build/build_rust.sh
taskcluster/docker/rust-build/build_rust_mac.sh
taskcluster/docker/rust-build/fetch_cargo.sh
taskcluster/docker/rust-build/fetch_rust.sh
taskcluster/docker/rust-build/package_rust.sh
taskcluster/docker/rust-build/repack_rust.py
taskcluster/docker/rust-build/splat_rust.py
taskcluster/docker/rust-build/task.json
taskcluster/docker/rust-build/tcbuild.py
taskcluster/docker/rust-build/upload_rust.sh
taskcluster/docker/tester/Dockerfile
taskcluster/docker/tester/REGISTRY
taskcluster/docker/tester/VERSION
taskcluster/docker/tester/bin/test.sh
taskcluster/docker/tester/dot-config/pip/pip.conf
taskcluster/docker/tester/dot-config/user-dirs.dirs
taskcluster/docker/tester/dot-config/user-dirs.locale
taskcluster/docker/tester/dot-pulse/default.pa
taskcluster/docker/tester/tc-vcs-config.yml
taskcluster/docker/tester/tester.env
taskcluster/docker/upload-symbols/Dockerfile
taskcluster/docker/upload-symbols/README.md
taskcluster/docker/upload-symbols/bin/checkout-script.sh
taskcluster/docker/upload-symbols/bin/upload.sh
taskcluster/docker/upload-symbols/test_exports.sh
testing/docker/README.md
testing/docker/REGISTRY
testing/docker/android-gradle-build/Dockerfile
testing/docker/android-gradle-build/README.md
testing/docker/android-gradle-build/REGISTRY
testing/docker/android-gradle-build/VERSION
testing/docker/android-gradle-build/bin/after.sh
testing/docker/android-gradle-build/bin/before.sh
testing/docker/android-gradle-build/bin/build.sh
testing/docker/android-gradle-build/bin/checkout-script.sh
testing/docker/android-gradle-build/bin/checkout-sources.sh
testing/docker/android-gradle-build/buildprops.json
testing/docker/android-gradle-build/dot-config/pip/pip.conf
testing/docker/android-gradle-build/oauth.txt
testing/docker/base-build/Dockerfile
testing/docker/base-build/VERSION
testing/docker/base-build/system-setup.sh
testing/docker/base-test/Dockerfile
testing/docker/base-test/REGISTRY
testing/docker/base-test/VERSION
testing/docker/base-test/sources.list
testing/docker/centos6-build-upd/Dockerfile
testing/docker/centos6-build-upd/REGISTRY
testing/docker/centos6-build-upd/VERSION
testing/docker/centos6-build/Dockerfile
testing/docker/centos6-build/REGISTRY
testing/docker/centos6-build/VERSION
testing/docker/centos6-build/hgrc
testing/docker/centos6-build/system-setup.sh
testing/docker/decision/Dockerfile
testing/docker/decision/README.md
testing/docker/decision/REGISTRY
testing/docker/decision/VERSION
testing/docker/decision/system-setup.sh
testing/docker/desktop-build/Dockerfile
testing/docker/desktop-build/bin/build.sh
testing/docker/desktop-build/bin/checkout-script.sh
testing/docker/desktop-build/bin/checkout-sources.sh
testing/docker/desktop-build/buildprops.json
testing/docker/desktop-build/dot-config/pip/pip.conf
testing/docker/desktop-build/oauth.txt
testing/docker/desktop-test/Dockerfile
testing/docker/desktop-test/apport
testing/docker/desktop-test/buildprops.json
testing/docker/desktop-test/deja-dup-monitor.desktop
testing/docker/desktop-test/dot-files/config/pip/pip.conf
testing/docker/desktop-test/dot-files/config/user-dirs.dirs
testing/docker/desktop-test/dot-files/config/user-dirs.locale
testing/docker/desktop-test/dot-files/pulse/default.pa
testing/docker/desktop-test/fonts.conf
testing/docker/desktop-test/jockey-gtk.desktop
testing/docker/desktop-test/motd
testing/docker/desktop-test/release-upgrades
testing/docker/desktop-test/taskcluster-interactive-shell
testing/docker/desktop-test/tc-vcs-config.yml
testing/docker/desktop-test/tester.env
testing/docker/desktop1604-test/Dockerfile
testing/docker/desktop1604-test/apport
testing/docker/desktop1604-test/buildprops.json
testing/docker/desktop1604-test/deja-dup-monitor.desktop
testing/docker/desktop1604-test/dot-files/config/pip/pip.conf
testing/docker/desktop1604-test/dot-files/config/user-dirs.dirs
testing/docker/desktop1604-test/dot-files/config/user-dirs.locale
testing/docker/desktop1604-test/dot-files/pulse/default.pa
testing/docker/desktop1604-test/fonts.conf
testing/docker/desktop1604-test/jockey-gtk.desktop
testing/docker/desktop1604-test/motd
testing/docker/desktop1604-test/release-upgrades
testing/docker/desktop1604-test/taskcluster-interactive-shell
testing/docker/desktop1604-test/tc-vcs-config.yml
testing/docker/desktop1604-test/tester.env
testing/docker/image_builder/Dockerfile
testing/docker/image_builder/REGISTRY
testing/docker/image_builder/VERSION
testing/docker/image_builder/build-image.sh
testing/docker/image_builder/setup.sh
testing/docker/lint/Dockerfile
testing/docker/lint/system-setup.sh
testing/docker/recipes/centos6-build-system-setup.sh
testing/docker/recipes/common.sh
testing/docker/recipes/install-mercurial.sh
testing/docker/recipes/run-task
testing/docker/recipes/tooltool.py
testing/docker/recipes/ubuntu1204-test-system-setup.sh
testing/docker/recipes/ubuntu1604-test-system-setup.sh
testing/docker/recipes/xvfb.sh
testing/docker/rust-build/Dockerfile
testing/docker/rust-build/README.md
testing/docker/rust-build/REGISTRY
testing/docker/rust-build/VERSION
testing/docker/rust-build/build_cargo.sh
testing/docker/rust-build/build_rust.sh
testing/docker/rust-build/build_rust_mac.sh
testing/docker/rust-build/fetch_cargo.sh
testing/docker/rust-build/fetch_rust.sh
testing/docker/rust-build/package_rust.sh
testing/docker/rust-build/repack_rust.py
testing/docker/rust-build/splat_rust.py
testing/docker/rust-build/task.json
testing/docker/rust-build/tcbuild.py
testing/docker/rust-build/upload_rust.sh
testing/docker/tester/Dockerfile
testing/docker/tester/REGISTRY
testing/docker/tester/VERSION
testing/docker/tester/bin/test.sh
testing/docker/tester/dot-config/pip/pip.conf
testing/docker/tester/dot-config/user-dirs.dirs
testing/docker/tester/dot-config/user-dirs.locale
testing/docker/tester/dot-pulse/default.pa
testing/docker/tester/tc-vcs-config.yml
testing/docker/tester/tester.env
testing/docker/upload-symbols/Dockerfile
testing/docker/upload-symbols/README.md
testing/docker/upload-symbols/bin/checkout-script.sh
testing/docker/upload-symbols/bin/upload.sh
testing/docker/upload-symbols/test_exports.sh
testing/runtimes/mochitest-e10s-browser-chrome.runtimes.json
testing/runtimes/mochitest-e10s-devtools-chrome.runtimes.json
testing/web-platform/meta/html/semantics/embedded-content/media-elements/loading-the-media-resource/resource-selection-invoke-insert-source-not-in-document.html.ini
testing/web-platform/meta/html/semantics/embedded-content/media-elements/loading-the-media-resource/resource-selection-invoke-insert-source.html.ini
testing/web-platform/meta/html/semantics/embedded-content/media-elements/playing-the-media-resource/pause-remove-from-document-networkState.html.ini
testing/web-platform/meta/html/semantics/embedded-content/media-elements/video_008.htm.ini
testing/web-platform/meta/web-animations/timing-model/animations/current-time.html.ini
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/library/gtest/rust/Cargo.lock
toolkit/library/moz.build
toolkit/library/rust/Cargo.lock
toolkit/moz.configure
widget/GfxInfoBase.cpp
widget/windows/nsWindow.cpp
widget/windows/nsWindowGfx.cpp
--- a/.eslintignore
+++ b/.eslintignore
@@ -35,19 +35,17 @@ netwerk/**
 nsprpub/**
 other-licenses/**
 parser/**
 probes/**
 python/**
 rdf/**
 startupcache/**
 testing/**
-!tools/lint/eslint/eslint-plugin-mozilla/
-tools/lint/eslint/eslint-plugin-mozilla/node_modules/**
-tools/**
+tools/update-packaging/**
 uriloader/**
 view/**
 widget/**
 xpcom/**
 xpfe/**
 xulrunner/**
 
 # b2g exclusions (pref files).
@@ -81,17 +79,16 @@ browser/extensions/mortar/**
 # devtools/ exclusions
 devtools/client/canvasdebugger/**
 devtools/client/commandline/**
 devtools/client/debugger/**
 devtools/client/framework/**
 !devtools/client/framework/selection.js
 !devtools/client/framework/toolbox.js
 devtools/client/jsonview/lib/**
-devtools/client/memory/**
 devtools/client/netmonitor/test/**
 devtools/client/netmonitor/har/test/**
 devtools/client/projecteditor/**
 devtools/client/promisedebugger/**
 devtools/client/responsivedesign/**
 devtools/client/scratchpad/**
 devtools/client/shadereditor/**
 devtools/client/shared/*.jsm
--- a/AUTHORS
+++ b/AUTHORS
@@ -874,16 +874,17 @@ Roy Yokoyama <yokoyama@netscape.com>
 RSA Security, Inc
 Russell King <rmk@arm.linux.org.uk>
 Rusty Lynch <rusty.lynch@intel.com>
 Ryan Cassin <rcassin@supernova.org>
 Ryan Flint <rflint@dslr.net>
 Ryan Jones <sciguyryan@gmail.com>
 Ryan VanderMeulen <ryanvm@gmail.com>
 Ryoichi Furukawa <oliver@1000cp.com>
+Sanyam Khurana <Sanyam.Khurana01@gmail.com>
 sagdjb@softwareag.com
 Samir Gehani <sgehani@netscape.com>
 Sammy Ford
 Samphan Raruenrom
 Samuel Sieb <samuel@sieb.net>
 Sarlos Tamas
 scole@planetweb.com
 Scooter Morris <scootermorris@comcast.net>
--- a/accessible/aom/AccessibleNode.cpp
+++ b/accessible/aom/AccessibleNode.cpp
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AccessibleNode.h"
 #include "mozilla/dom/AccessibleNodeBinding.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/DOMStringList.h"
+#include "nsIPersistentProperties2.h"
 
 #include "Accessible-inl.h"
 #include "nsAccessibilityService.h"
 #include "DocAccessible.h"
 
 using namespace mozilla;
 using namespace mozilla::a11y;
 using namespace mozilla::dom;
@@ -98,13 +99,51 @@ AccessibleNode::Is(const Sequence<nsStri
   for (const auto& flavor : aFlavors) {
     if (!flavor.Equals(role) && !mStates->Contains(flavor)) {
       return false;
     }
   }
   return true;
 }
 
+bool
+AccessibleNode::Has(const Sequence<nsString>& aAttributes)
+{
+  if (!mIntl) {
+    return false;
+  }
+  nsCOMPtr<nsIPersistentProperties> attrs = mIntl->Attributes();
+  for (const auto& attr : aAttributes) {
+    bool has = false;
+    attrs->Has(NS_ConvertUTF16toUTF8(attr).get(), &has);
+    if (!has) {
+      return false;
+    }
+  }
+  return true;
+}
+
+void
+AccessibleNode::Get(JSContext* aCX, const nsAString& aAttribute,
+                    JS::MutableHandle<JS::Value> aValue,
+                    ErrorResult& aRv)
+{
+  if (!mIntl) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+  }
+
+  nsCOMPtr<nsIPersistentProperties> attrs = mIntl->Attributes();
+  nsAutoString value;
+  attrs->GetStringProperty(NS_ConvertUTF16toUTF8(aAttribute), value);
+
+  JS::Rooted<JS::Value> jsval(aCX);
+  if (!ToJSValue(aCX, value, &jsval)) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+  }
+
+  aValue.set(jsval);
+}
+
 nsINode*
 AccessibleNode::GetDOMNode()
 {
   return mDOMNode;
 }
--- a/accessible/aom/AccessibleNode.h
+++ b/accessible/aom/AccessibleNode.h
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef A11Y_AOM_ACCESSIBLENODE_H
 #define A11Y_AOM_ACCESSIBLENODE_H
 
 #include "nsWrapperCache.h"
+#include "mozilla/ErrorResult.h"
 #include "mozilla/dom/BindingDeclarations.h"
 
 class nsINode;
 
 namespace mozilla {
 
 namespace a11y {
   class Accessible;
@@ -35,16 +36,20 @@ public:
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override final;
   virtual dom::ParentObject GetParentObject() const final;
 
   void GetRole(nsAString& aRole);
   void GetStates(nsTArray<nsString>& aStates);
   nsINode* GetDOMNode();
 
   bool Is(const Sequence<nsString>& aFlavors);
+  bool Has(const Sequence<nsString>& aAttributes);
+  void Get(JSContext* cx, const nsAString& aAttribute,
+           JS::MutableHandle<JS::Value> aValue,
+           ErrorResult& aRv);
 
   a11y::Accessible* Internal() const { return mIntl; }
 
 protected:
   AccessibleNode(const AccessibleNode& aCopy) = delete;
   AccessibleNode& operator=(const AccessibleNode& aCopy) = delete;
   virtual ~AccessibleNode();
 
--- a/accessible/base/nsAccessibilityService.cpp
+++ b/accessible/base/nsAccessibilityService.cpp
@@ -401,17 +401,17 @@ nsAccessibilityService::GetRootDocumentA
 static StaticAutoPtr<nsTArray<nsCOMPtr<nsIContent> > > sPendingPlugins;
 static StaticAutoPtr<nsTArray<nsCOMPtr<nsITimer> > > sPluginTimers;
 
 class PluginTimerCallBack final : public nsITimerCallback
 {
   ~PluginTimerCallBack() {}
 
 public:
-  PluginTimerCallBack(nsIContent* aContent) : mContent(aContent) {}
+  explicit PluginTimerCallBack(nsIContent* aContent) : mContent(aContent) {}
 
   NS_DECL_ISUPPORTS
 
   NS_IMETHOD Notify(nsITimer* aTimer) final
   {
     if (!mContent->IsInUncomposedDoc())
       return NS_OK;
 
--- a/accessible/tests/mochitest/aom/test_general.html
+++ b/accessible/tests/mochitest/aom/test_general.html
@@ -30,17 +30,17 @@
   }
 
   // WebIDL conditional annotations for an interface are evaluated once per
   // global, so we need to create an iframe to see the effects of calling
   // enablePref().
   function createIframe() {
     return new Promise((resolve) => {
       let iframe = document.createElement("iframe");
-      iframe.src = "about:blank";
+      iframe.src = `data:text/html,<html><body>hey</body></html>`;
       iframe.onload = () => resolve(iframe.contentDocument);
       document.body.appendChild(iframe);
     });
   }
 
   // Check that the WebIDL is as expected.
   function checkImplementation(ifrDoc) {
     let anode = ifrDoc.accessibleNode;
@@ -75,12 +75,18 @@
       for (var i = 0; i < states.length; i++) {
         is(anode.states[i], states[i], `${states[i]} state is expected at ${i}th index`);
       }
     }
 
     ok(anode.is('document', 'focusable'),
        'correct role and state on an accessible node');
 
+    is(anode.get('explicit-name'), 'true',
+       'correct object attribute value on an accessible node');
+
+    ok(anode.has('explicit-name'),
+       'object attributes are present');
+
     finish();
   }
   </script>
 </head>
--- a/accessible/windows/ProxyWrappers.h
+++ b/accessible/windows/ProxyWrappers.h
@@ -11,17 +11,17 @@
 #include "HyperTextAccessible.h"
 
 namespace mozilla {
 namespace a11y {
 
 class ProxyAccessibleWrap : public AccessibleWrap
 {
 public:
-  ProxyAccessibleWrap(ProxyAccessible* aProxy) :
+  explicit ProxyAccessibleWrap(ProxyAccessible* aProxy) :
     AccessibleWrap(nullptr, nullptr)
   {
     mType = eProxyType;
     mBits.proxy = aProxy;
   }
 
   virtual void Shutdown() override
   {
@@ -33,17 +33,17 @@ public:
   {
     mBits.proxy->GetCOMInterface(aOutAccessible);
   }
 };
 
 class HyperTextProxyAccessibleWrap : public HyperTextAccessibleWrap
 {
 public:
-  HyperTextProxyAccessibleWrap(ProxyAccessible* aProxy) :
+  explicit HyperTextProxyAccessibleWrap(ProxyAccessible* aProxy) :
     HyperTextAccessibleWrap(nullptr, nullptr)
   {
     mType = eProxyType;
     mBits.proxy = aProxy;
   }
 
   virtual void Shutdown() override
   {
@@ -55,17 +55,17 @@ public:
   {
     mBits.proxy->GetCOMInterface(aOutAccessible);
   }
 };
 
 class DocProxyAccessibleWrap : public HyperTextProxyAccessibleWrap
 {
 public:
-  DocProxyAccessibleWrap(ProxyAccessible* aProxy) :
+  explicit DocProxyAccessibleWrap(ProxyAccessible* aProxy) :
     HyperTextProxyAccessibleWrap(aProxy)
   { mGenericTypes |= eDocument; }
 
   void AddID(uint32_t aID, AccessibleWrap* aAcc)
     { mIDToAccessibleMap.Put(aID, aAcc); }
   void RemoveID(uint32_t aID) { mIDToAccessibleMap.Remove(aID); }
   AccessibleWrap* GetAccessibleByID(uint32_t aID) const
     { return mIDToAccessibleMap.Get(aID); }
--- a/accessible/windows/msaa/AccessibleWrap.cpp
+++ b/accessible/windows/msaa/AccessibleWrap.cpp
@@ -688,17 +688,17 @@ AccessibleWrap::get_accFocus(
 
 /**
  * This helper class implements IEnumVARIANT for a nsTArray containing
  * accessible objects.
  */
 class AccessibleEnumerator final : public IEnumVARIANT
 {
 public:
-  AccessibleEnumerator(const nsTArray<Accessible*>& aArray) :
+  explicit AccessibleEnumerator(const nsTArray<Accessible*>& aArray) :
     mArray(aArray), mCurIndex(0) { }
   AccessibleEnumerator(const AccessibleEnumerator& toCopy) :
     mArray(toCopy.mArray), mCurIndex(toCopy.mCurIndex) { }
   ~AccessibleEnumerator() { }
 
   // IUnknown
   DECL_IUNKNOWN
 
--- a/accessible/windows/msaa/EnumVariant.h
+++ b/accessible/windows/msaa/EnumVariant.h
@@ -14,17 +14,17 @@ namespace mozilla {
 namespace a11y {
 
 /**
  * Used to fetch accessible children.
  */
 class ChildrenEnumVariant final : public IEnumVARIANT
 {
 public:
-  ChildrenEnumVariant(AccessibleWrap* aAnchor) : mAnchorAcc(aAnchor),
+  explicit ChildrenEnumVariant(AccessibleWrap* aAnchor) : mAnchorAcc(aAnchor),
     mCurAcc(mAnchorAcc->GetChildAt(0)), mCurIndex(0) { }
 
   // IUnknown
   DECL_IUNKNOWN
 
   // IEnumVariant
   virtual /* [local] */ HRESULT STDMETHODCALLTYPE Next(
     /* [in] */ ULONG aCount,
--- a/accessible/windows/msaa/ServiceProvider.h
+++ b/accessible/windows/msaa/ServiceProvider.h
@@ -13,17 +13,17 @@
 #include "IUnknownImpl.h"
 
 namespace mozilla {
 namespace a11y {
 
 class ServiceProvider final : public IServiceProvider
 {
 public:
-  ServiceProvider(AccessibleWrap* aAcc) : mAccessible(aAcc) {}
+  explicit ServiceProvider(AccessibleWrap* aAcc) : mAccessible(aAcc) {}
   ~ServiceProvider() {}
 
   DECL_IUNKNOWN
 
   // IServiceProvider
   virtual HRESULT STDMETHODCALLTYPE QueryService(REFGUID aGuidService,
                                                  REFIID aIID,
                                                  void** aInstancePtr);
--- a/accessible/windows/sdn/sdnAccessible.h
+++ b/accessible/windows/sdn/sdnAccessible.h
@@ -14,17 +14,17 @@
 #include "mozilla/Attributes.h"
 
 namespace mozilla {
 namespace a11y {
 
 class sdnAccessible final : public ISimpleDOMNode
 {
 public:
-  sdnAccessible(nsINode* aNode) :
+  explicit sdnAccessible(nsINode* aNode) :
     mNode(aNode)
   {
     if (!mNode)
       MOZ_CRASH();
   }
   ~sdnAccessible() { }
 
   /**
--- a/accessible/windows/sdn/sdnDocAccessible.h
+++ b/accessible/windows/sdn/sdnDocAccessible.h
@@ -13,17 +13,17 @@
 #include "DocAccessibleWrap.h"
 
 namespace mozilla {
 namespace a11y {
 
 class sdnDocAccessible final : public ISimpleDOMDocument
 {
 public:
-  sdnDocAccessible(DocAccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
+  explicit sdnDocAccessible(DocAccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
   ~sdnDocAccessible() { };
 
   DECL_IUNKNOWN
 
   // ISimpleDOMDocument
   virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_URL(
     /* [out] */ BSTR __RPC_FAR *url);
 
--- a/accessible/windows/sdn/sdnTextAccessible.h
+++ b/accessible/windows/sdn/sdnTextAccessible.h
@@ -12,21 +12,21 @@
 
 #include "AccessibleWrap.h"
 
 class nsIFrame;
 struct nsPoint;
 
 namespace mozilla {
 namespace a11y {
- 
+
 class sdnTextAccessible final : public ISimpleDOMText
 {
 public:
-  sdnTextAccessible(AccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
+  explicit sdnTextAccessible(AccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
   ~sdnTextAccessible() {}
 
   DECL_IUNKNOWN
 
   // ISimpleDOMText
 
   virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_domText(
     /* [retval][out] */ BSTR __RPC_FAR *aText);
--- a/accessible/windows/uia/uiaRawElmProvider.cpp
+++ b/accessible/windows/uia/uiaRawElmProvider.cpp
@@ -51,18 +51,18 @@ uiaRawElmProvider::GetIAccessiblePair(__
 
   *aAcc = nullptr;
   *aIdChild = 0;
 
   if (mAcc->IsDefunct())
     return CO_E_OBJNOTCONNECTED;
 
   *aIdChild = CHILDID_SELF;
-  *aAcc = mAcc;
-  mAcc->AddRef();
+  RefPtr<AccessibleWrap> copy(mAcc);
+  copy.forget(aAcc);
 
   return S_OK;
 
   A11Y_TRYBLOCK_END
 }
 
 STDMETHODIMP
 uiaRawElmProvider::GetRuntimeId(__RPC__deref_out_opt SAFEARRAY** aRuntimeIds)
--- a/accessible/windows/uia/uiaRawElmProvider.h
+++ b/accessible/windows/uia/uiaRawElmProvider.h
@@ -19,17 +19,17 @@ class AccessibleWrap;
 
 /**
  * IRawElementProviderSimple implementation (maintains IAccessibleEx approach).
  */
 class uiaRawElmProvider final : public IAccessibleEx,
                                 public IRawElementProviderSimple
 {
 public:
-  uiaRawElmProvider(AccessibleWrap* aAcc) : mAcc(aAcc) { }
+  explicit uiaRawElmProvider(AccessibleWrap* aAcc) : mAcc(aAcc) { }
 
   // IUnknown
   DECL_IUNKNOWN
 
   // IAccessibleEx
   virtual HRESULT STDMETHODCALLTYPE GetObjectForChild(
     /* [in] */ long aIdChild,
     /* [retval][out] */ __RPC__deref_out_opt IAccessibleEx** aAccEx);
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<blocklist lastupdate="1480349195955" xmlns="http://www.mozilla.org/2006/addons-blocklist">
+<blocklist lastupdate="1480349215711" xmlns="http://www.mozilla.org/2006/addons-blocklist">
   <emItems>
     <emItem blockID="i988" id="{b12785f5-d8d0-4530-a3ea-5c4263b85bef}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i398" id="{377e5d4d-77e5-476a-8716-7e70a9272da0}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
@@ -805,28 +805,16 @@
     <emItem blockID="i998" id="meOYKQEbBBjH5Ml91z0p9Aosgus8P55bjTa4KPfl@jetpack">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="i434" id="afurladvisor@anchorfree.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
-    <emItem blockID="i872" id="search-snacks@search-snacks.com">
-      <prefs/>
-      <versionRange minVersion="0" maxVersion="*" severity="1"/>
-    </emItem>
-    <emItem blockID="i966" id="{5C655500-E712-41e7-9349-CE462F844B19}">
-      <prefs/>
-      <versionRange minVersion="0" maxVersion="1.0.1-signed" severity="1"/>
-    </emItem>
-    <emItem blockID="i90" id="videoplugin@player.com">
-      <prefs/>
-      <versionRange minVersion="0" maxVersion="*"/>
-    </emItem>
     <emItem blockID="i306" id="{ADFA33FD-16F5-4355-8504-DF4D664CFE10}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i1012" id="wxtui502n2xce9j@no14">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
@@ -1347,16 +1335,20 @@
     <emItem blockID="i70" id="psid-vhvxQHMZBOzUZA@jetpack">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i790" id="JMLv@njMaHh.org">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="i1425" id="/^(pdftoword@addingapps.com|jid0-EYTXLS0GyfQME5irGbnD4HksnbQ@jetpack|jid1-ZjJ7t75BAcbGCX@jetpack)$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="1"/>
+    </emItem>
     <emItem blockID="i674" id="crossriderapp12555@crossrider.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i1034" id="a88a77ahjjfjakckmmabsy278djasi@jetpack">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
@@ -1519,16 +1511,20 @@
     <emItem blockID="i514" id="/^(67314b39-24e6-4f05-99f3-3f88c7cddd17@6c5fa560-13a3-4d42-8e90-53d9930111f9\.com|ffxtlbr@visualbee\.com|{7aeae561-714b-45f6-ace3-4a8aed6e227b}|{7093ee04-f2e4-4637-a667-0f730797b3a0}|{53c4024f-5a2e-4f2a-b33e-e8784d730938})$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i531" id="/^(4cb61367-efbf-4aa1-8e3a-7f776c9d5763@cdece6e9-b2ef-40a9-b178-291da9870c59\.com|0efc9c38-1ec7-49ed-8915-53a48b6b7600@e7f17679-2a42-4659-83c5-7ba961fdf75a\.com|6be3335b-ef79-4b0b-a0ba-b87afbc6f4ad@6bbb4d2e-e33e-4fa5-9b37-934f4fb50182\.com)$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
+    <emItem blockID="i1423" id="/^(@pluginscribens_firefox|extension@vidscrab.com|firefox@jjj.ee|firefox@shop-reward.de|FxExtPasteNGoHtk@github.lostdj|himanshudotrai@gmail.com|jid0-bigoD0uivzAMmt07zrf3OHqa418@jetpack|jid0-iXbAR01tjT2BsbApyS6XWnjDhy8@jetpack)$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="1"/>
+    </emItem>
     <emItem blockID="i68" id="flashupdate@adobe.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*"/>
     </emItem>
     <emItem blockID="i492" id="{af95cc15-3b9b-45ae-8d9b-98d08eda3111}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
@@ -1670,16 +1666,20 @@
     <emItem blockID="i103" id="kdrgun@gmail.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*"/>
     </emItem>
     <emItem blockID="i1119" id="/^(test3@test.org|test2@test.org|test@test.org|support@mozilla.org)$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="i1424" id="/^(jid0-S9kkzfTvEmC985BVmf8ZOzA5nLM@jetpack|jid0-xGZYdxpAkROWMUMfWKINyrXigBA@jetpack|jid1-qps14pkDB6UDvA@jetpack|jid1-Tsr09YnAqIWL0Q@jetpack|shole@ats.ext|voipgrid@jetpack|{38a64ef0-7181-11e3-981f-0800200c9a66}|eochoa@ualberta.ca)$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="1"/>
+    </emItem>
     <emItem blockID="i519" id="703db0db-5fe9-44b6-9f53-c6a91a0ad5bd@7314bc82-969e-4d2a-921b-e5edd0b02cf1.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
     <emItem blockID="i484" id="plugin@getwebcake.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
@@ -1986,16 +1986,28 @@
     <emItem blockID="i788" id="{729c9605-0626-4792-9584-4cbe65b243e6}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="i548" id="/^firefox@(jumpflip|webconnect|browsesmart|mybuzzsearch|outobox|greygray|lemurleap|divapton|secretsauce|batbrowse|whilokii|linkswift|qualitink|browsefox|kozaka|diamondata|glindorus|saltarsmart|bizzybolt|websparkle)\.(com?|net|org|info|biz)$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
     </emItem>
+    <emItem blockID="i872" id="search-snacks@search-snacks.com">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="1"/>
+    </emItem>
+    <emItem blockID="i966" id="{5C655500-E712-41e7-9349-CE462F844B19}">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="1.0.1-signed" severity="1"/>
+    </emItem>
+    <emItem blockID="i90" id="videoplugin@player.com">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*"/>
+    </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>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -205,18 +205,24 @@ pref("general.autoScroll", false);
 #else
 pref("general.autoScroll", true);
 #endif
 
 // At startup, check if we're the default browser and prompt user if not.
 pref("browser.shell.checkDefaultBrowser", true);
 pref("browser.shell.shortcutFavicons",true);
 pref("browser.shell.mostRecentDateSetAsDefault", "");
+#ifdef RELEASE_OR_BETA
+pref("browser.shell.skipDefaultBrowserCheckOnFirstRun", false);
+#else
+pref("browser.shell.skipDefaultBrowserCheckOnFirstRun", true);
+#endif
 pref("browser.shell.skipDefaultBrowserCheck", true);
 pref("browser.shell.defaultBrowserCheckCount", 0);
+pref("browser.defaultbrowser.notificationbar", false);
 
 // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session
 // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore
 pref("browser.startup.page",                1);
 pref("browser.startup.homepage",            "chrome://branding/locale/browserconfig.properties");
 // Whether we should skip the homepage when opening the first-run page
 pref("browser.startup.firstrunSkipsHomepage", false);
 
@@ -643,22 +649,16 @@ pref("plugin.default.state", 1);
 // Plugins bundled in XPIs are enabled by default.
 pref("plugin.defaultXpi.state", 2);
 
 // Flash is enabled by default, and Java is click-to-activate by default on
 // all channels.
 pref("plugin.state.flash", 2);
 pref("plugin.state.java", 1);
 
-#ifdef XP_MACOSX
-pref("browser.preferences.animateFadeIn", true);
-#else
-pref("browser.preferences.animateFadeIn", false);
-#endif
-
 #ifdef XP_WIN
 pref("browser.preferences.instantApply", false);
 #else
 pref("browser.preferences.instantApply", true);
 #endif
 
 pref("browser.download.show_plugins_in_list", true);
 pref("browser.download.hide_plugins_without_extensions", true);
@@ -1441,18 +1441,16 @@ pref("browser.tabs.crashReporting.email"
 pref("extensions.interposition.enabled", true);
 pref("extensions.interposition.prefetching", true);
 
 // Enable blocking of e10s for add-on users on beta/release.
 #ifdef RELEASE_OR_BETA
 pref("extensions.e10sBlocksEnabling", true);
 #endif
 
-pref("browser.defaultbrowser.notificationbar", false);
-
 // How often to check for CPOW timeouts. CPOWs are only timed out by
 // the hang monitor.
 pref("dom.ipc.cpow.timeout", 500);
 
 // Causes access on unsafe CPOWs from browser code to throw by default.
 pref("dom.ipc.cpows.forbid-unsafe-from-browser", true);
 
 // Don't allow add-ons marked as multiprocessCompatible to use CPOWs.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1,22 +1,44 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cc = Components.classes;
+var Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/ContextualIdentityService.jsm");
 Cu.import("resource://gre/modules/NotificationDB.jsm");
 
 // lazy module getters
+
+/* global AboutHome:false, AddonWatcher:false, AppConstants: false,
+          BrowserUITelemetry:false, BrowserUsageTelemetry:false, BrowserUtils:false,
+          CastingApps:false, CharsetMenu:false, Color:false, ContentSearch:false,
+          Deprecated:false, E10SUtils:false, FormValidationHandler:false,
+          GMPInstallManager:false, LightweightThemeManager:false, Log:false,
+          LoginManagerParent:false, NewTabUtils:false, PageThumbs:false,
+          PluralForm:false, Preferences:false, PrivateBrowsingUtils:false,
+          ProcessHangMonitor:false, PromiseUtils:false, ReaderMode:false,
+          ReaderParent:false, RecentWindow:false, SessionStore:false,
+          ShortcutUtils:false, SimpleServiceDiscovery:false, SitePermissions:false,
+          Social:false, TabCrashHandler:false, Task:false, TelemetryStopwatch:false,
+          Translation:false, UITour:false, UpdateUtils:false, Weave:false,
+          fxAccounts:false, gDevTools:false, gDevToolsBrowser:false, webrtcUI:false
+ */
+
+/**
+ * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
+ * XXX Bug 1325373 is for making eslint detect these automatically.
+ */
 [
   ["AboutHome", "resource:///modules/AboutHome.jsm"],
   ["AddonWatcher", "resource://gre/modules/AddonWatcher.jsm"],
   ["AppConstants", "resource://gre/modules/AppConstants.jsm"],
   ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
   ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
   ["BrowserUtils", "resource://gre/modules/BrowserUtils.jsm"],
   ["CastingApps", "resource:///modules/CastingApps.jsm"],
@@ -62,16 +84,24 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/SafeBrowsing.jsm");
 
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyModuleGetter(this, "PluginCrashReporter",
     "resource:///modules/ContentCrashHandlers.jsm");
 }
 
 // lazy service getters
+
+/* global Favicons:false, WindowsUIUtils:false, gAboutNewTabService:false,
+          gDNSService:false
+*/
+/**
+ * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
+ * XXX Bug 1325373 is for making eslint detect these automatically.
+ */
 [
   ["Favicons", "@mozilla.org/browser/favicon-service;1", "mozIAsyncFavicons"],
   ["WindowsUIUtils", "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
   ["gAboutNewTabService", "@mozilla.org/browser/aboutnewtab-service;1", "nsIAboutNewTabService"],
   ["gDNSService", "@mozilla.org/network/dns-service;1", "nsIDNSService"],
 ].forEach(([name, cc, ci]) => XPCOMUtils.defineLazyServiceGetter(this, name, cc, ci));
 
 if (AppConstants.MOZ_CRASHREPORTER) {
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -273,89 +273,26 @@ Sanitizer.prototype = {
           let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"]
                                    .getService(Ci.nsIMediaManagerService);
           mediaMgr.sanitizeDeviceIds(range && range[0]);
         } catch (ex) {
           seenException = ex;
         }
 
         // Clear plugin data.
-        // As evidenced in bug 1253204, clearing plugin data can sometimes be
-        // very, very long, for mysterious reasons. Unfortunately, this is not
-        // something actionable by Mozilla, so crashing here serves no purpose.
-        //
-        // For this reason, instead of waiting for sanitization to always
-        // complete, we introduce a soft timeout. Once this timeout has
-        // elapsed, we proceed with the shutdown of Firefox.
-        let promiseClearPluginCookies;
         try {
-          // We don't want to wait for this operation to complete...
-          promiseClearPluginCookies = this.promiseClearPluginCookies(range);
-
-          // ... at least, not for more than 10 seconds.
-          yield Promise.race([
-            promiseClearPluginCookies,
-            new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
-          ]);
+          yield Sanitizer.clearPluginData(range);
         } catch (ex) {
           seenException = ex;
         }
 
-        // Detach waiting for plugin cookies to be cleared.
-        promiseClearPluginCookies.catch(() => {
-          // If this exception is raised before the soft timeout, it
-          // will appear in `seenException`. Otherwise, it's too late
-          // to do anything about it.
-        });
-
         if (seenException) {
           throw seenException;
         }
       }),
-
-      promiseClearPluginCookies: Task.async(function* (range) {
-        const FLAG_CLEAR_ALL = Ci.nsIPluginHost.FLAG_CLEAR_ALL;
-        let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-
-        // Determine age range in seconds. (-1 means clear all.) We don't know
-        // that range[1] is actually now, so we compute age range based
-        // on the lower bound. If range results in a negative age, do nothing.
-        let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1;
-        if (!range || age >= 0) {
-          let tags = ph.getPluginTags();
-          for (let tag of tags) {
-            let refObj = {};
-            let probe = "";
-            if (/\bFlash\b/.test(tag.name)) {
-              probe = tag.loaded ? "FX_SANITIZE_LOADED_FLASH"
-                                 : "FX_SANITIZE_UNLOADED_FLASH";
-              TelemetryStopwatch.start(probe, refObj);
-            }
-            try {
-              let rv = yield new Promise(resolve =>
-                ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
-              );
-              // If the plugin doesn't support clearing by age, clear everything.
-              if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
-                yield new Promise(resolve =>
-                  ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve)
-                );
-              }
-              if (probe) {
-                TelemetryStopwatch.finish(probe, refObj);
-              }
-            } catch (ex) {
-              // Ignore errors from plug-ins
-              if (probe) {
-                TelemetryStopwatch.cancel(probe, refObj);
-              }
-            }
-          }
-        }
-      })
     },
 
     offlineApps: {
       clear: Task.async(function* (range) {
         Components.utils.import("resource:///modules/offlineAppCache.jsm");
         // This doesn't wait for the cleanup to be complete.
         OfflineAppCacheHelper.clear();
       })
@@ -700,16 +637,22 @@ Sanitizer.prototype = {
         // Start the process of closing windows
         while (windowList.length) {
           windowList.pop().close();
         }
         newWindow.focus();
         yield promiseReady;
       })
     },
+
+    pluginData: {
+      clear: Task.async(function* (range) {
+        yield Sanitizer.clearPluginData(range);
+      }),
+    },
   }
 };
 
 // The preferences branch for the sanitizer.
 Sanitizer.PREF_DOMAIN = "privacy.sanitize.";
 // Whether we should sanitize on shutdown.
 Sanitizer.PREF_SANITIZE_ON_SHUTDOWN = "privacy.sanitize.sanitizeOnShutdown";
 // During a sanitization this is set to a json containing the array of items
@@ -769,16 +712,93 @@ Sanitizer.getClearRange = function(ts) {
       startDate = endDate - 86400000000; // 24*60*60*1000000
       break;
     default:
       throw "Invalid time span for clear private data: " + ts;
   }
   return [startDate, endDate];
 };
 
+Sanitizer.clearPluginData = Task.async(function* (range) {
+  // Clear plugin data.
+  // As evidenced in bug 1253204, clearing plugin data can sometimes be
+  // very, very long, for mysterious reasons. Unfortunately, this is not
+  // something actionable by Mozilla, so crashing here serves no purpose.
+  //
+  // For this reason, instead of waiting for sanitization to always
+  // complete, we introduce a soft timeout. Once this timeout has
+  // elapsed, we proceed with the shutdown of Firefox.
+  let seenException;
+
+  let promiseClearPluginData = Task.async(function* () {
+    const FLAG_CLEAR_ALL = Ci.nsIPluginHost.FLAG_CLEAR_ALL;
+    let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+
+    // Determine age range in seconds. (-1 means clear all.) We don't know
+    // that range[1] is actually now, so we compute age range based
+    // on the lower bound. If range results in a negative age, do nothing.
+    let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1;
+    if (!range || age >= 0) {
+      let tags = ph.getPluginTags();
+      for (let tag of tags) {
+        let refObj = {};
+        let probe = "";
+        if (/\bFlash\b/.test(tag.name)) {
+          probe = tag.loaded ? "FX_SANITIZE_LOADED_FLASH"
+                             : "FX_SANITIZE_UNLOADED_FLASH";
+          TelemetryStopwatch.start(probe, refObj);
+        }
+        try {
+          let rv = yield new Promise(resolve =>
+            ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
+          );
+          // If the plugin doesn't support clearing by age, clear everything.
+          if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
+            yield new Promise(resolve =>
+              ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve)
+            );
+          }
+          if (probe) {
+            TelemetryStopwatch.finish(probe, refObj);
+          }
+        } catch (ex) {
+          // Ignore errors from plug-ins
+          if (probe) {
+            TelemetryStopwatch.cancel(probe, refObj);
+          }
+        }
+      }
+    }
+  });
+
+  try {
+    // We don't want to wait for this operation to complete...
+    promiseClearPluginData = promiseClearPluginData(range);
+
+    // ... at least, not for more than 10 seconds.
+    yield Promise.race([
+      promiseClearPluginData,
+      new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
+    ]);
+  } catch (ex) {
+    seenException = ex;
+  }
+
+  // Detach waiting for plugin data to be cleared.
+  promiseClearPluginData.catch(() => {
+    // If this exception is raised before the soft timeout, it
+    // will appear in `seenException`. Otherwise, it's too late
+    // to do anything about it.
+  });
+
+  if (seenException) {
+    throw seenException;
+  }
+});
+
 Sanitizer._prefs = null;
 Sanitizer.__defineGetter__("prefs", function()
 {
   return Sanitizer._prefs ? Sanitizer._prefs
     : Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                          .getService(Components.interfaces.nsIPrefService)
                          .getBranch(Sanitizer.PREF_DOMAIN);
 });
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -602,16 +602,107 @@ addEventListener("unload", () => {
 }, false);
 
 addMessageListener("Browser:AppTab", function(message) {
   if (docShell) {
     docShell.isAppTab = message.data.isAppTab;
   }
 });
 
+let PrerenderContentHandler = {
+  init() {
+    this._pending = [];
+    this._idMonotonic = 0;
+    this._initialized = true;
+    addMessageListener("Prerender:Canceled", this);
+    addMessageListener("Prerender:Swapped", this);
+  },
+
+  get initialized() {
+    return !!this._initialized;
+  },
+
+  receiveMessage(aMessage) {
+    switch (aMessage.name) {
+      case "Prerender:Canceled": {
+        for (let i = 0; i < this._pending.length; ++i) {
+          if (this._pending[i].id === aMessage.data.id) {
+            if (this._pending[i].failure) {
+              this._pending[i].failure.run();
+            }
+            // Remove the item from the array
+            this._pending.splice(i, 1);
+            break;
+          }
+        }
+        break;
+      }
+      case "Prerender:Swapped": {
+        for (let i = 0; i < this._pending.length; ++i) {
+          if (this._pending[i].id === aMessage.data.id) {
+            if (this._pending[i].success) {
+              this._pending[i].success.run();
+            }
+            // Remove the item from the array
+            this._pending.splice(i, 1);
+            break;
+          }
+        }
+        break;
+      }
+    }
+  },
+
+  startPrerenderingDocument(aHref, aReferrer) {
+    // XXX: Make this constant a pref
+    if (this._pending.length >= 2) {
+      return;
+    }
+
+    let id = ++this._idMonotonic;
+    sendAsyncMessage("Prerender:Request", {
+      href: aHref.spec,
+      referrer: aReferrer ? aReferrer.spec : null,
+      id: id,
+    });
+
+    this._pending.push({
+      href: aHref,
+      referrer: aReferrer,
+      id: id,
+      success: null,
+      failure: null,
+    });
+  },
+
+  shouldSwitchToPrerenderedDocument(aHref, aReferrer, aSuccess, aFailure) {
+    // Check if we think there is a prerendering document pending for the given
+    // href and referrer. If we think there is one, we will send a message to
+    // the parent process asking it to do a swap, and hook up the success and
+    // failure listeners.
+    for (let i = 0; i < this._pending.length; ++i) {
+      let p = this._pending[i];
+      if (p.href.equals(aHref) && p.referrer.equals(aReferrer)) {
+        p.success = aSuccess;
+        p.failure = aFailure;
+        sendAsyncMessage("Prerender:Swap", {id: p.id});
+        return true;
+      }
+    }
+
+    return false;
+  }
+};
+
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+  // We only want to initialize the PrerenderContentHandler in the content
+  // process. Outside of the content process, this should be unused.
+  PrerenderContentHandler.init();
+}
+
 var WebBrowserChrome = {
   onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
     return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
   },
 
   // Check whether this URI should load in the current process
   shouldLoadURI: function(aDocShell, aURI, aReferrer) {
     if (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer)) {
@@ -625,16 +716,30 @@ var WebBrowserChrome = {
   shouldLoadURIInThisProcess: function(aURI) {
     return E10SUtils.shouldLoadURIInThisProcess(aURI);
   },
 
   // Try to reload the currently active or currently loading page in a new process.
   reloadInFreshProcess: function(aDocShell, aURI, aReferrer) {
     E10SUtils.redirectLoad(aDocShell, aURI, aReferrer, true);
     return true;
+  },
+
+  startPrerenderingDocument: function(aHref, aReferrer) {
+    if (PrerenderContentHandler.initialized) {
+      PrerenderContentHandler.startPrerenderingDocument(aHref, aReferrer);
+    }
+  },
+
+  shouldSwitchToPrerenderedDocument: function(aHref, aReferrer, aSuccess, aFailure) {
+    if (PrerenderContentHandler.initialized) {
+      return PrerenderContentHandler.shouldSwitchToPrerenderedDocument(
+        aHref, aReferrer, aSuccess, aFailure);
+    }
+    return false;
   }
 };
 
 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
   let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsITabChild);
   tabchild.webBrowserChrome = WebBrowserChrome;
 }
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1495,16 +1495,17 @@
 
       <method name="loadOneTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <parameter name="aPostData"/>
         <parameter name="aLoadInBackground"/>
         <parameter name="aAllowThirdPartyFixup"/>
+        <parameter name="aIsPrerendered"/>
         <body>
           <![CDATA[
             var aReferrerPolicy;
             var aFromExternal;
             var aRelatedToCurrent;
             var aAllowMixedContent;
             var aSkipAnimation;
             var aForceNotRemote;
@@ -1530,16 +1531,17 @@
               aSkipAnimation        = params.skipAnimation;
               aForceNotRemote       = params.forceNotRemote;
               aPreferredRemoteType  = params.preferredRemoteType;
               aNoReferrer           = params.noReferrer;
               aUserContextId        = params.userContextId;
               aRelatedBrowser       = params.relatedBrowser;
               aOriginPrincipal      = params.originPrincipal;
               aOpener               = params.opener;
+              aIsPrerendered        = params.isPrerendered;
             }
 
             var bgLoad = (aLoadInBackground != null) ? aLoadInBackground :
                          Services.prefs.getBoolPref("browser.tabs.loadInBackground");
             var owner = bgLoad ? null : this.selectedTab;
             var tab = this.addTab(aURI, {
                                   referrerURI: aReferrerURI,
                                   referrerPolicy: aReferrerPolicy,
@@ -1552,17 +1554,18 @@
                                   skipAnimation: aSkipAnimation,
                                   allowMixedContent: aAllowMixedContent,
                                   forceNotRemote: aForceNotRemote,
                                   preferredRemoteType: aPreferredRemoteType,
                                   noReferrer: aNoReferrer,
                                   userContextId: aUserContextId,
                                   originPrincipal: aOriginPrincipal,
                                   relatedBrowser: aRelatedBrowser,
-                                  opener: aOpener });
+                                  opener: aOpener,
+                                  isPrerendered: aIsPrerendered });
             if (!bgLoad)
               this.selectedTab = tab;
 
             return tab;
          ]]>
         </body>
       </method>
 
@@ -1930,28 +1933,32 @@
       </method>
 
       <method name="_createBrowser">
         <parameter name="aParams"/>
         <body>
           <![CDATA[
             // Supported parameters:
             // userContextId, remote, remoteType, isPreloadBrowser,
-            // uriIsAboutBlank, permanentKey
+            // uriIsAboutBlank, permanentKey, isPrerendered
 
             const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
             let b = document.createElementNS(NS_XUL, "browser");
             b.permanentKey = aParams.permanentKey || {};
             b.setAttribute("type", "content");
             b.setAttribute("message", "true");
             b.setAttribute("messagemanagergroup", "browsers");
             b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
             b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
 
+            if (aParams.isPrerendered) {
+              b.setAttribute("prerendered", "true");
+            }
+
             if (aParams.userContextId) {
               b.setAttribute("usercontextid", aParams.userContextId);
             }
 
             // remote parameter used by some addons, use default in this case.
             if (aParams.remote && !aParams.remoteType) {
               aParams.remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
             }
@@ -2030,17 +2037,17 @@
         <parameter name="aTab"/>
         <parameter name="aURI"/>
         <parameter name="aParams"/>
         <body>
           <![CDATA[
             "use strict";
 
             // Supported parameters:
-            // forceNotRemote, preferredRemoteType, userContextId
+            // forceNotRemote, preferredRemoteType, userContextId, isPrerendered
 
             let uriIsAboutBlank = !aURI || aURI == "about:blank";
 
             let remoteType =
               aParams.forceNotRemote ? E10SUtils.NOT_REMOTE
               : E10SUtils.getRemoteTypeForURI(aURI, gMultiProcessBrowser,
                                               aParams.preferredRemoteType);
 
@@ -2063,17 +2070,18 @@
 
             if (!browser) {
               // No preloaded browser found, create one.
               browser = this._createBrowser({permanentKey: aTab.permanentKey,
                                              remoteType,
                                              uriIsAboutBlank: uriIsAboutBlank,
                                              userContextId: aParams.userContextId,
                                              relatedBrowser: aParams.relatedBrowser,
-                                             opener: aParams.opener});
+                                             opener: aParams.opener,
+                                             isPrerendered: aParams.isPrerendered});
             }
 
             let notificationbox = this.getNotificationBox(browser);
             let uniqueId = this._generateUniquePanelID();
             notificationbox.id = uniqueId;
             aTab.linkedPanel = uniqueId;
             aTab.linkedBrowser = browser;
             aTab.hasBrowser = true;
@@ -2123,16 +2131,17 @@
 
       <method name="addTab">
         <parameter name="aURI"/>
         <parameter name="aReferrerURI"/>
         <parameter name="aCharset"/>
         <parameter name="aPostData"/>
         <parameter name="aOwner"/>
         <parameter name="aAllowThirdPartyFixup"/>
+        <parameter name="aIsPrerendered"/>
         <body>
           <![CDATA[
             "use strict";
 
             const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
             var aReferrerPolicy;
             var aFromExternal;
             var aRelatedToCurrent;
@@ -2165,16 +2174,17 @@
               aPreferredRemoteType      = params.preferredRemoteType;
               aNoReferrer               = params.noReferrer;
               aUserContextId            = params.userContextId;
               aEventDetail              = params.eventDetail;
               aRelatedBrowser           = params.relatedBrowser;
               aOriginPrincipal          = params.originPrincipal;
               aDisallowInheritPrincipal = params.disallowInheritPrincipal;
               aOpener                   = params.opener;
+              aIsPrerendered            = params.isPrerendered;
             }
 
             // if we're adding tabs, we're past interrupt mode, ditch the owner
             if (this.mCurrentTab.owner)
               this.mCurrentTab.owner = null;
 
             var t = document.createElementNS(NS_XUL, "tab");
 
@@ -2182,16 +2192,20 @@
 
             if (!aURI || isBlankPageURL(aURI)) {
               t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle"));
             } else if (aURI.toLowerCase().startsWith("javascript:")) {
               // This can go away when bug 672618 or bug 55696 are fixed.
               t.setAttribute("label", aURI);
             }
 
+            if (aIsPrerendered) {
+              t.setAttribute("hidden", "true");
+            }
+
             if (aUserContextId) {
               t.setAttribute("usercontextid", aUserContextId);
               ContextualIdentityService.setTabStyle(t);
             }
 
             t.setAttribute("onerror", "this.removeAttribute('image');");
             t.className = "tabbrowser-tab";
 
@@ -2234,16 +2248,17 @@
             // trigger SessionStore.jsm to run code that expects the existence
             // of tab.linkedBrowser.
             let browserParams = {
               forceNotRemote: aForceNotRemote,
               preferredRemoteType: aPreferredRemoteType,
               userContextId:  aUserContextId,
               relatedBrowser: aRelatedBrowser,
               opener: aOpener,
+              isPrerendered: aIsPrerendered,
             };
             let { usingPreloadedContent } = this._linkBrowserToTab(t, aURI, browserParams);
             let b = t.linkedBrowser;
 
             // Dispatch a new tab notification.  We do this once we're
             // entirely done, so that things are in a consistent state
             // even if the event listener opens or closes tabs.
             var detail = aEventDetail || {};
@@ -4785,16 +4800,84 @@
                 detail: data,
               });
 
               browser.dispatchEvent(event);
 
               break;
             }
 
+            case "Prerender:Request": {
+              let sendCancelPrerendering = () => {
+                browser.frameloader.messageManager.
+                  sendAsyncMessage("Prerender:Canceled", { id: data.id });
+              };
+
+              let tab = this.getTabForBrowser(browser);
+              if (!tab) {
+                // No tab?
+                sendCancelPrerendering();
+                break;
+              }
+
+              if (tab.hidden) {
+                // Skip prerender on hidden tab.
+                sendCancelPrerendering();
+                break;
+              }
+
+              if (browser.canGoForward) {
+                // Skip prerender on history navigation as we don't support it
+                // yet. Remove this check once bug 1323650 is implemented.
+                sendCancelPrerendering();
+                break;
+              }
+
+              if (!data.href) {
+                // If we don't have data.href, loadOneTab will load about:blank
+                // which is meaningless for prerendering.
+                sendCancelPrerendering();
+                break;
+              }
+
+              let groupedSHistory = browser.frameLoader.ensureGroupedSHistory();
+
+              let newTab = this.loadOneTab(data.href, {
+                referrerURI: (data.referrer ? makeURI(data.referrer) : null),
+                referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+                postData: null,
+                allowThirdPartyFixup: true,
+                relatedToCurrent: true,
+                isPrerendered: true,
+              });
+              let partialSHistory = newTab.linkedBrowser.frameLoader.partialSHistory;
+              groupedSHistory.addPrerenderingPartialSHistory(partialSHistory, data.id);
+              break;
+            }
+
+            case "Prerender:Cancel": {
+              let groupedSHistory = browser.frameLoader.groupedSHistory;
+              if (groupedSHistory) {
+                groupedSHistory.cancelPrerendering(data.id);
+              }
+              break;
+            }
+
+            case "Prerender:Swap": {
+              let frameloader = browser.frameLoader;
+              let groupedSHistory = browser.frameLoader.groupedSHistory;
+              if (groupedSHistory) {
+                groupedSHistory.activatePrerendering(data.id).then(
+                  () => frameloader.messageManager.sendAsyncMessage("Prerender:Swapped", data),
+                  () => frameloader.messageManager.sendAsyncMessage("Prerender:Canceled", data),
+                );
+              }
+              break;
+            }
+
           }
           return undefined;
         ]]></body>
       </method>
 
       <method name="observe">
         <parameter name="aSubject"/>
         <parameter name="aTopic"/>
@@ -4917,16 +5000,21 @@
           messageManager.addMessageListener("RefreshBlocker:Blocked", this);
           messageManager.addMessageListener("Browser:WindowCreated", this);
 
           // To correctly handle keypresses for potential FindAsYouType, while
           // the tab's find bar is not yet initialized.
           this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
           Services.prefs.addObserver("accessibility.typeaheadfind", this, false);
           messageManager.addMessageListener("Findbar:Keypress", this);
+
+          // Add listeners for prerender messages
+          messageManager.addMessageListener("Prerender:Request", this);
+          messageManager.addMessageListener("Prerender:Cancel", this);
+          messageManager.addMessageListener("Prerender:Swap", this);
         ]]>
       </constructor>
 
       <method name="_generateUniquePanelID">
         <body><![CDATA[
           if (!this._uniquePanelIDCounter) {
             this._uniquePanelIDCounter = 0;
           }
--- a/browser/base/content/test/general/browser_close_dependent_tabs.js
+++ b/browser/base/content/test/general/browser_close_dependent_tabs.js
@@ -1,51 +1,78 @@
 add_task(function* () {
   yield SpecialPowers.pushPrefEnv({
-    set: [["browser.groupedhistory.enabled", true]]
+    set: [["browser.groupedhistory.enabled", true],
+          ["dom.linkPrerender.enabled", true]]
   });
 
   // Wait for a process change and then fulfil the promise.
   function awaitProcessChange(browser) {
     return new Promise(resolve => {
       browser.addEventListener("BrowserChangedProcess", function bcp(e) {
         browser.removeEventListener("BrowserChangedProcess", bcp);
         ok(true, "The browser changed process!");
         resolve();
       });
     });
   }
 
-  let tab2;
+  // Wait for given number tabs being closed.
+  function awaitTabClose(number) {
+    return new Promise(resolve => {
+      let seen = 0;
+      gBrowser.tabContainer.addEventListener("TabClose", function f() {
+        if (++seen == number) {
+          gBrowser.tabContainer.removeEventListener("TabClose", f);
+          resolve();
+        }
+      });
+    });
+  }
 
   // Test 1: Create prerendered browser, and don't switch to it, then close the tab
+  let closed1 = awaitTabClose(2);
   yield BrowserTestUtils.withNewTab({ gBrowser, url: "data:text/html,a" }, function* (browser1) {
     // Set up the grouped SHEntry setup
-    tab2 = gBrowser.loadOneTab("data:text/html,b", {
-      referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
-      allowThirdPartyFixup: true,
-      relatedToCurrent: true,
-      isPrerendered: true,
+
+    let requestMade = new Promise(resolve => {
+      browser1.messageManager.addMessageListener("Prerender:Request", function f() {
+        browser1.messageManager.removeMessageListener("Prerender:Request", f);
+        ok(true, "Successfully received the prerender request");
+        resolve();
+      });
     });
-  });
 
-  // At this point tab2 should be closed
-  todo(!tab2.linkedBrowser, "The new tab should be closed");
-  yield BrowserTestUtils.removeTab(tab2); // XXX: Shouldn't be needed once the todo is fixed
+    is(gBrowser.tabs.length, 2);
+    yield ContentTask.spawn(browser1, null, function() {
+      let link = content.document.createElement("link");
+      link.setAttribute("rel", "prerender");
+      link.setAttribute("href", "data:text/html,b");
+      content.document.body.appendChild(link);
+    });
+    yield requestMade;
+
+    is(gBrowser.tabs.length, 3);
+  });
+  yield closed1;
+
+  // At this point prerendered tab should be closed
+  is(gBrowser.tabs.length, 1, "The new tab and the prerendered 'tab' should be closed");
 
   // Test 2: Create prerendered browser, switch to it, then close the tab
+  let closed2 = awaitTabClose(2);
   yield BrowserTestUtils.withNewTab({ gBrowser, url: "data:text/html,a" }, function* (browser1) {
     // Set up the grouped SHEntry setup
-    tab2 = gBrowser.loadOneTab("data:text/html,b", {
+    let tab2 = gBrowser.loadOneTab("data:text/html,b", {
       referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
       allowThirdPartyFixup: true,
       relatedToCurrent: true,
       isPrerendered: true,
     });
     yield BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
-    browser1.frameLoader.appendPartialSessionHistoryAndSwap(
-      tab2.linkedBrowser.frameLoader);
+    browser1.frameLoader.appendPartialSHistoryAndSwap(tab2.linkedBrowser.frameLoader);
     yield awaitProcessChange(browser1);
   });
+  yield closed2;
 
-  // At this point tab2 should be closed
-  ok(!tab2.linkedBrowser, "The new tab should be closed");
+  // At this point prerendered tab should be closed
+  is(gBrowser.tabs.length, 1, "The new tab and the prerendered 'tab' should be closed");
 });
--- a/browser/base/content/test/plugins/browser_clearplugindata.js
+++ b/browser/base/content/test/plugins/browser_clearplugindata.js
@@ -41,87 +41,79 @@ add_task(function* () {
     setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
     setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
     if (gTestBrowser) {
       gBrowser.removeCurrentTab();
     }
     window.focus();
     gTestBrowser = null;
   });
-});
 
-add_task(function* () {
   Services.prefs.setBoolPref("plugins.click_to_play", true);
 
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
+});
 
+function* setPrefs(cookies, pluginData) {
   sanitizer = new Sanitizer();
   sanitizer.ignoreTimespan = false;
   sanitizer.prefDomain = "privacy.cpd.";
   let itemPrefs = gPrefService.getBranch(sanitizer.prefDomain);
   itemPrefs.setBoolPref("history", false);
   itemPrefs.setBoolPref("downloads", false);
   itemPrefs.setBoolPref("cache", false);
-  itemPrefs.setBoolPref("cookies", true); // plugin data
+  itemPrefs.setBoolPref("cookies", cookies);
   itemPrefs.setBoolPref("formdata", false);
   itemPrefs.setBoolPref("offlineApps", false);
   itemPrefs.setBoolPref("passwords", false);
   itemPrefs.setBoolPref("sessions", false);
   itemPrefs.setBoolPref("siteSettings", false);
-});
+  itemPrefs.setBoolPref("pluginData", pluginData);
+}
 
-add_task(function* () {
+function* testClearingData(url) {
   // Load page to set data for the plugin.
   gBrowser.selectedTab = gBrowser.addTab();
   gTestBrowser = gBrowser.selectedBrowser;
 
-  yield promiseTabLoadEvent(gBrowser.selectedTab, testURL1);
+  yield promiseTabLoadEvent(gBrowser.selectedTab, url);
 
   yield promiseUpdatePluginBindings(gTestBrowser);
 
   ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
     "Data stored for sites");
 
-  // Clear 20 seconds ago
-  let now_uSec = Date.now() * 1000;
-  sanitizer.range = [now_uSec - 20 * 1000000, now_uSec];
-  yield sanitizer.sanitize();
-
-  ok(stored(["bar.com", "qux.com"]), "Data stored for sites");
-  ok(!stored(["foo.com"]), "Data cleared for foo.com");
-  ok(!stored(["baz.com"]), "Data cleared for baz.com");
-
-  // Clear everything
-  sanitizer.range = null;
-  yield sanitizer.sanitize();
-
-  ok(!stored(null), "All data cleared");
-
-  gBrowser.removeCurrentTab();
-  gTestBrowser = null;
-});
-
-add_task(function* () {
-  // Load page to set data for the plugin.
-  gBrowser.selectedTab = gBrowser.addTab();
-  gTestBrowser = gBrowser.selectedBrowser;
-
-  yield promiseTabLoadEvent(gBrowser.selectedTab, testURL2);
-
-  yield promiseUpdatePluginBindings(gTestBrowser);
-
-  ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
-    "Data stored for sites");
-
-  // Attempt to clear 20 seconds ago. The plugin will throw
+  // Clear 20 seconds ago.
+  // In the case of testURL2 the plugin will throw
   // NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED, which should result in us
   // clearing all data regardless of age.
   let now_uSec = Date.now() * 1000;
   sanitizer.range = [now_uSec - 20 * 1000000, now_uSec];
   yield sanitizer.sanitize();
 
+  if (url == testURL1) {
+    ok(stored(["bar.com", "qux.com"]), "Data stored for sites");
+    ok(!stored(["foo.com"]), "Data cleared for foo.com");
+    ok(!stored(["baz.com"]), "Data cleared for baz.com");
+
+    // Clear everything.
+    sanitizer.range = null;
+    yield sanitizer.sanitize();
+  }
+
   ok(!stored(null), "All data cleared");
 
   gBrowser.removeCurrentTab();
   gTestBrowser = null;
+}
+
+add_task(function* () {
+  // Test when santizing cookies.
+  yield setPrefs(true, false);
+  yield testClearingData(testURL1);
+  yield testClearingData(testURL2);
+
+  // Test when santizing pluginData.
+  yield setPrefs(false, true);
+  yield testClearingData(testURL1);
+  yield testClearingData(testURL2);
 });
-
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -668,80 +668,80 @@ extensions.registerSchemaAPI("tabs", "ad
         if (queryInfo.url !== null) {
           if (!extension.hasPermission("tabs")) {
             return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
           }
 
           pattern = new MatchPattern(queryInfo.url);
         }
 
-        function matches(window, tab) {
+        function matches(tab) {
           let props = ["active", "pinned", "highlighted", "status", "title", "index"];
           for (let prop of props) {
             if (queryInfo[prop] !== null && queryInfo[prop] != tab[prop]) {
               return false;
             }
           }
 
-          let lastFocused = window == WindowManager.topWindow;
-          if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow != lastFocused) {
-            return false;
-          }
-
-          let windowType = WindowManager.windowType(window);
-          if (queryInfo.windowType !== null && queryInfo.windowType != windowType) {
-            return false;
-          }
-
-          if (queryInfo.windowId !== null) {
-            if (queryInfo.windowId == WindowManager.WINDOW_ID_CURRENT) {
-              if (currentWindow(context) != window) {
-                return false;
-              }
-            } else if (queryInfo.windowId != tab.windowId) {
-              return false;
-            }
-          }
-
           if (queryInfo.audible !== null) {
             if (queryInfo.audible != tab.audible) {
               return false;
             }
           }
 
           if (queryInfo.muted !== null) {
             if (queryInfo.muted != tab.mutedInfo.muted) {
               return false;
             }
           }
 
-          if (queryInfo.currentWindow !== null) {
-            let eq = window == currentWindow(context);
-            if (queryInfo.currentWindow != eq) {
-              return false;
-            }
-          }
-
           if (queryInfo.cookieStoreId !== null &&
               tab.cookieStoreId != queryInfo.cookieStoreId) {
             return false;
           }
 
           if (pattern && !pattern.matches(Services.io.newURI(tab.url, null, null))) {
             return false;
           }
 
           return true;
         }
 
         let result = [];
         for (let window of WindowListManager.browserWindows()) {
+          let lastFocused = window === WindowManager.topWindow;
+          if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow !== lastFocused) {
+            continue;
+          }
+
+          let windowType = WindowManager.windowType(window);
+          if (queryInfo.windowType !== null && queryInfo.windowType !== windowType) {
+            continue;
+          }
+
+          if (queryInfo.windowId !== null) {
+            if (queryInfo.windowId === WindowManager.WINDOW_ID_CURRENT) {
+              if (currentWindow(context) !== window) {
+                continue;
+              }
+            } else if (queryInfo.windowId !== WindowManager.getId(window)) {
+              continue;
+            }
+          }
+
+          if (queryInfo.currentWindow !== null) {
+            let eq = window === currentWindow(context);
+            if (queryInfo.currentWindow != eq) {
+              continue;
+            }
+          }
+
           let tabs = TabManager.for(extension).getTabs(window);
           for (let tab of tabs) {
-            if (matches(window, tab)) {
+            if (matches(tab)) {
               result.push(tab);
             }
           }
         }
         return Promise.resolve(result);
       },
 
       captureVisibleTab: function(windowId, options) {
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -670,17 +670,17 @@ ExtensionTabManager.prototype = {
       id: TabManager.getId(tab),
       index: tab._tPos,
       windowId: WindowManager.getId(window),
       selected: tab.selected,
       highlighted: tab.selected,
       active: tab.selected,
       pinned: tab.pinned,
       status: TabManager.getStatus(tab),
-      incognito: PrivateBrowsingUtils.isBrowserPrivate(browser),
+      incognito: WindowManager.isBrowserPrivate(browser),
       width: browser.frameLoader.lazyWidth || browser.clientWidth,
       height: browser.frameLoader.lazyHeight || browser.clientHeight,
       audible: tab.soundPlaying,
       mutedInfo,
     };
     if (this.extension.hasPermission("cookies")) {
       result.cookieStoreId = getCookieStoreIdForTab(result, tab);
     }
@@ -920,16 +920,21 @@ TabManager.for = function(extension) {
 };
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("shutdown", (type, extension) => {
   tabManagers.delete(extension);
 });
 /* eslint-enable mozilla/balanced-listeners */
 
+function memoize(fn) {
+  let weakMap = new DefaultWeakMap(fn);
+  return weakMap.get.bind(weakMap);
+}
+
 // Manages mapping between XUL windows and extension window IDs.
 global.WindowManager = {
   // Note: These must match the values in windows.json.
   WINDOW_ID_NONE: -1,
   WINDOW_ID_CURRENT: -2,
 
   get topWindow() {
     return Services.wm.getMostRecentWindow("navigator:browser");
@@ -959,23 +964,26 @@ global.WindowManager = {
 
     if (options.width !== null || options.height !== null) {
       let width = options.width !== null ? options.width : window.outerWidth;
       let height = options.height !== null ? options.height : window.outerHeight;
       window.resizeTo(width, height);
     }
   },
 
-  getId(window) {
-    if (!window.QueryInterface) {
-      return null;
+  isBrowserPrivate: memoize(browser => {
+    return PrivateBrowsingUtils.isBrowserPrivate(browser);
+  }),
+
+  getId: memoize(window => {
+    if (window instanceof Ci.nsIInterfaceRequestor) {
+      return window.getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
     }
-    return window.QueryInterface(Ci.nsIInterfaceRequestor)
-                 .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
-  },
+    return null;
+  }),
 
   getWindow(id, context) {
     if (id == this.WINDOW_ID_CURRENT) {
       return currentWindow(context);
     }
 
     for (let window of WindowListManager.browserWindows(true)) {
       if (this.getId(window) == id) {
--- a/browser/components/migration/AutoMigrate.jsm
+++ b/browser/components/migration/AutoMigrate.jsm
@@ -6,39 +6,43 @@
 
 this.EXPORTED_SYMBOLS = ["AutoMigrate"];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 const kAutoMigrateEnabledPref = "browser.migrate.automigrate.enabled";
 const kUndoUIEnabledPref = "browser.migrate.automigrate.ui.enabled";
 
-const kAutoMigrateStartedPref = "browser.migrate.automigrate.started";
-const kAutoMigrateFinishedPref = "browser.migrate.automigrate.finished";
 const kAutoMigrateBrowserPref = "browser.migrate.automigrate.browser";
 
 const kAutoMigrateLastUndoPromptDateMsPref = "browser.migrate.automigrate.lastUndoPromptDateMs";
 const kAutoMigrateDaysToOfferUndoPref = "browser.migrate.automigrate.daysToOfferUndo";
 
-const kPasswordManagerTopic = "passwordmgr-storage-changed";
-const kPasswordManagerTopicTypes = new Set([
-  "addLogin",
-  "modifyLogin",
-]);
-
-const kSyncTopic = "fxaccounts:onlogin";
-
 const kNotificationId = "abouthome-automigration-undo";
 
 Cu.import("resource:///modules/MigrationUtils.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+                                  "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+                                  "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+
+Cu.importGlobalProperties(["URL"]);
+
+/* globals kUndoStateFullPath */
+XPCOMUtils.defineLazyGetter(this, "kUndoStateFullPath", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "initialMigrationMetadata.jsonlz4");
+});
 
 const AutoMigrate = {
   get resourceTypesToUse() {
     let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
     return BOOKMARKS | HISTORY | PASSWORDS;
   },
 
   _checkIfEnabled() {
@@ -58,49 +62,16 @@ const AutoMigrate = {
       return JSON.parse(parser.getString("Preferences", kAutoMigrateEnabledPref));
     } catch (ex) { /* ignore exceptions (file doesn't exist, invalid value, etc.) */ }
 
     return pref;
   },
 
   init() {
     this.enabled = this._checkIfEnabled();
-    if (this.enabled) {
-      this.maybeInitUndoObserver();
-    }
-  },
-
-  maybeInitUndoObserver() {
-    if (!this.canUndo()) {
-      return;
-    }
-    // Now register places, password and sync observers:
-    this.onItemAdded = this.onItemMoved = this.onItemChanged =
-      this.removeUndoOption.bind(this, this.UNDO_REMOVED_REASON_BOOKMARK_CHANGE);
-    PlacesUtils.addLazyBookmarkObserver(this, true);
-    for (let topic of [kSyncTopic, kPasswordManagerTopic]) {
-      Services.obs.addObserver(this, topic, true);
-    }
-  },
-
-  observe(subject, topic, data) {
-    if (topic == kPasswordManagerTopic) {
-      // As soon as any login gets added or modified, disable undo:
-      // (Note that this ignores logins being removed as that doesn't
-      //  impair the 'undo' functionality of the import.)
-      if (kPasswordManagerTopicTypes.has(data)) {
-        // Ignore chrome:// things like sync credentials:
-        let loginInfo = subject.QueryInterface(Ci.nsILoginInfo);
-        if (!loginInfo.hostname || !loginInfo.hostname.startsWith("chrome://")) {
-          this.removeUndoOption(this.UNDO_REMOVED_REASON_PASSWORD_CHANGE);
-        }
-      }
-    } else if (topic == kSyncTopic) {
-      this.removeUndoOption(this.UNDO_REMOVED_REASON_SYNC_SIGNIN);
-    }
   },
 
   /**
    * 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.
@@ -127,31 +98,28 @@ const AutoMigrate = {
         sawErrors = true;
       } else if (topic == "Migration:Ended") {
         histogram.add(25);
         if (sawErrors) {
           histogram.add(26);
         }
         Services.obs.removeObserver(migrationObserver, "Migration:Ended");
         Services.obs.removeObserver(migrationObserver, "Migration:ItemError");
-        Services.prefs.setCharPref(kAutoMigrateStartedPref, startTime.toString());
-        Services.prefs.setCharPref(kAutoMigrateFinishedPref, Date.now().toString());
         Services.prefs.setCharPref(kAutoMigrateBrowserPref, pickedKey);
-        // Need to manually start listening to new bookmarks/logins being created,
-        // because, when we were initialized, there wasn't the possibility to
-        // 'undo' anything.
-        this.maybeInitUndoObserver();
+        // Save the undo history and block shutdown on that save completing.
+        AsyncShutdown.profileBeforeChange.addBlocker(
+          "AutoMigrate Undo saving", this.saveUndoState(), () => {
+            return {state: this._saveUndoStateTrackerForShutdown};
+          });
       }
     };
 
+    MigrationUtils.initializeUndoData();
     Services.obs.addObserver(migrationObserver, "Migration:Ended", false);
     Services.obs.addObserver(migrationObserver, "Migration:ItemError", false);
-    // We'll save this when the migration has finished, at which point the pref
-    // service will be available.
-    let startTime = Date.now();
     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.
@@ -201,91 +169,63 @@ const AutoMigrate = {
       return suggestedProfile;
     }
     if (profiles && profiles.length > 1) {
       throw new Error("Don't know how to pick a profile when more than 1 profile is present.");
     }
     return profiles ? profiles[0] : null;
   },
 
-  getUndoRange() {
-    let start, finish;
+  canUndo: Task.async(function* () {
+    if (this._savingPromise) {
+      yield this._savingPromise;
+    }
+    let fileExists = false;
     try {
-      start = parseInt(Preferences.get(kAutoMigrateStartedPref, "0"), 10);
-      finish = parseInt(Preferences.get(kAutoMigrateFinishedPref, "0"), 10);
+      fileExists = yield OS.File.exists(kUndoStateFullPath);
     } catch (ex) {
       Cu.reportError(ex);
     }
-    if (!finish || !start) {
-      return null;
-    }
-    return [new Date(start), new Date(finish)];
-  },
-
-  canUndo() {
-    return !!this.getUndoRange();
-  },
+    return fileExists;
+  }),
 
   undo: Task.async(function* () {
     let histogram = Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_UNDO");
     histogram.add(0);
-    if (!this.canUndo()) {
+    if (!(yield this.canUndo())) {
       histogram.add(5);
       throw new Error("Can't undo!");
     }
 
     histogram.add(10);
 
-    yield PlacesUtils.bookmarks.eraseEverything();
+    let readPromise = OS.File.read(kUndoStateFullPath, {
+      encoding: "utf-8",
+      compression: "lz4",
+    });
+    let stateData = this._dejsonifyUndoState(yield readPromise);
+    yield this._removeUnchangedBookmarks(stateData.get("bookmarks"));
     histogram.add(15);
 
-    // NB: we drop the start time of the migration for now. This is because
-    // imported history will always end up being 'backdated' to the actual
-    // visit time recorded by the browser from which we imported. As a result,
-    // a lower bound on this item doesn't really make sense.
-    // Note that for form data this could be different, but we currently don't
-    // support form data import from any non-Firefox browser, so it isn't
-    // imported from other browsers by the automigration code, nor do we
-    // remove it here.
-    let range = this.getUndoRange();
-    yield PlacesUtils.history.removeVisitsByFilter({
-      beginDate: new Date(0),
-      endDate: range[1]
-    });
+    yield this._removeSomeVisits(stateData.get("visits"));
     histogram.add(20);
 
-    try {
-      Services.logins.removeAllLogins();
-    } catch (ex) {
-      // ignore failure.
-    }
+    yield this._removeUnchangedLogins(stateData.get("logins"));
     histogram.add(25);
+
     this.removeUndoOption(this.UNDO_REMOVED_REASON_UNDO_USED);
     histogram.add(30);
   }),
 
   removeUndoOption(reason) {
-    // Remove observers, and ensure that exceptions doing so don't break
-    // removing the pref.
-    for (let topic of [kSyncTopic, kPasswordManagerTopic]) {
-      try {
-        Services.obs.removeObserver(this, topic);
-      } catch (ex) {
-        Cu.reportError("Error removing observer for " + topic + ": " + ex);
-      }
-    }
-    try {
-      PlacesUtils.removeLazyBookmarkObserver(this);
-    } catch (ex) {
-      Cu.reportError("Error removing lazy bookmark observer: " + ex);
-    }
+    // We don't wait for the off-main-thread removal to complete. OS.File will
+    // ensure it happens before shutdown.
+    OS.File.remove(kUndoStateFullPath, {ignoreAbsent: true});
 
     let migrationBrowser = Preferences.get(kAutoMigrateBrowserPref, "unknown");
-    Services.prefs.clearUserPref(kAutoMigrateStartedPref);
-    Services.prefs.clearUserPref(kAutoMigrateFinishedPref);
     Services.prefs.clearUserPref(kAutoMigrateBrowserPref);
 
     let browserWindows = Services.wm.getEnumerator("navigator:browser");
     while (browserWindows.hasMoreElements()) {
       let win = browserWindows.getNext();
       if (!win.closed) {
         for (let browser of win.gBrowser.browsers) {
           let nb = win.gBrowser.getNotificationBox(browser);
@@ -304,19 +244,23 @@ const AutoMigrate = {
   getBrowserUsedForMigration() {
     let browserId = Services.prefs.getCharPref(kAutoMigrateBrowserPref);
     if (browserId) {
       return MigrationUtils.getBrowserName(browserId);
     }
     return null;
   },
 
-  maybeShowUndoNotification(target) {
+  maybeShowUndoNotification: Task.async(function* (target) {
+    if (!(yield this.canUndo())) {
+      return;
+    }
+
     // The tab might have navigated since we requested the undo state:
-    if (!this.canUndo() || target.currentURI.spec != "about:home" ||
+    if (target.currentURI.spec != "about:home" ||
         !Preferences.get(kUndoUIEnabledPref, false)) {
       return;
     }
 
     let win = target.ownerGlobal;
     let notificationBox = win.gBrowser.getNotificationBox(target);
     if (!notificationBox || notificationBox.getNotificationWithValue("abouthome-automigration-undo")) {
       return;
@@ -355,17 +299,17 @@ const AutoMigrate = {
         },
       },
     ];
     notificationBox.appendNotification(
       message, kNotificationId, null, notificationBox.PRIORITY_INFO_HIGH, buttons
     );
     let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 0);
     Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_UNDO_OFFERED").add(4 - remainingDays);
-  },
+  }),
 
   shouldStillShowUndoPrompt() {
     let today = new Date();
     // Round down to midnight:
     today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
     // We store the unix timestamp corresponding to midnight on the last day
     // on which we prompted. Fetch that and compare it to today's date.
     // (NB: stored as a string because int prefs are too small for unix
@@ -385,14 +329,145 @@ const AutoMigrate = {
 
   UNDO_REMOVED_REASON_UNDO_USED: 0,
   UNDO_REMOVED_REASON_SYNC_SIGNIN: 1,
   UNDO_REMOVED_REASON_PASSWORD_CHANGE: 2,
   UNDO_REMOVED_REASON_BOOKMARK_CHANGE: 3,
   UNDO_REMOVED_REASON_OFFER_EXPIRED: 4,
   UNDO_REMOVED_REASON_OFFER_REJECTED: 5,
 
+  _jsonifyUndoState(state) {
+    if (!state) {
+      return "null";
+    }
+    // Deal with date serialization.
+    let bookmarks = state.get("bookmarks");
+    for (let bm of bookmarks) {
+      bm.lastModified = bm.lastModified.getTime();
+    }
+    let serializableState = {
+      bookmarks,
+      logins: state.get("logins"),
+      visits: state.get("visits"),
+    };
+    return JSON.stringify(serializableState);
+  },
+
+  _dejsonifyUndoState(state) {
+    state = JSON.parse(state);
+    for (let bm of state.bookmarks) {
+      bm.lastModified = new Date(bm.lastModified);
+    }
+    return new Map([
+      ["bookmarks", state.bookmarks],
+      ["logins", state.logins],
+      ["visits", state.visits],
+    ]);
+  },
+
+  _saveUndoStateTrackerForShutdown: "not running",
+  saveUndoState: Task.async(function* () {
+    let resolveSavingPromise;
+    this._saveUndoStateTrackerForShutdown = "processing undo history";
+    this._savingPromise = new Promise(resolve => { resolveSavingPromise = resolve });
+    let state = yield MigrationUtils.stopAndRetrieveUndoData();
+    this._saveUndoStateTrackerForShutdown = "writing undo history";
+    this._undoSavePromise = OS.File.writeAtomic(
+      kUndoStateFullPath, this._jsonifyUndoState(state), {
+        encoding: "utf-8",
+        compression: "lz4",
+        tmpPath: kUndoStateFullPath + ".tmp",
+      });
+    this._undoSavePromise.then(
+      rv => {
+        resolveSavingPromise(rv);
+        delete this._savingPromise;
+      },
+      e => {
+        Cu.reportError("Could not write undo state for automatic migration.");
+        throw e;
+      });
+    return this._undoSavePromise;
+  }),
+
+  _removeUnchangedBookmarks: Task.async(function* (bookmarks) {
+    if (!bookmarks.length) {
+      return;
+    }
+
+    let guidToLMMap = new Map(bookmarks.map(b => [b.guid, b.lastModified]));
+    let bookmarksFromDB = [];
+    let bmPromises = Array.from(guidToLMMap.keys()).map(guid => {
+      // Ignore bookmarks where the promise doesn't resolve (ie that are missing)
+      // Also check that the bookmark fetch returns isn't null before adding it.
+      return PlacesUtils.bookmarks.fetch(guid).then(bm => bm && bookmarksFromDB.push(bm), () => {});
+    });
+    // We can't use the result of Promise.all because that would include nulls
+    // for bookmarks that no longer exist (which we're catching above).
+    yield Promise.all(bmPromises);
+    let unchangedBookmarks = bookmarksFromDB.filter(bm => {
+      return bm.lastModified.getTime() == guidToLMMap.get(bm.guid).getTime();
+    });
+
+    // We need to remove items with no ancestors first, followed by their
+    // parents, etc. In order to do this, find out how many ancestors each item
+    // has that also appear in our list of things to remove, and sort the items
+    // by those numbers. This ensures that children are always removed before
+    // their parents.
+    function determineAncestorCount(bm) {
+      if (bm._ancestorCount) {
+        return bm._ancestorCount;
+      }
+      let myCount = 0;
+      let parentBM = unchangedBookmarks.find(item => item.guid == bm.parentGuid);
+      if (parentBM) {
+        myCount = determineAncestorCount(parentBM) + 1;
+      }
+      bm._ancestorCount = myCount;
+      return myCount;
+    }
+    unchangedBookmarks.forEach(determineAncestorCount);
+    unchangedBookmarks.sort((a, b) => b._ancestorCount - a._ancestorCount);
+    for (let {guid} of unchangedBookmarks) {
+      yield PlacesUtils.bookmarks.remove(guid, {preventRemovalOfNonEmptyFolders: true}).catch(err => {
+        if (err && err.message != "Cannot remove a non-empty folder.") {
+          Cu.reportError(err);
+        }
+      });
+    }
+  }),
+
+  _removeUnchangedLogins: Task.async(function* (logins) {
+    for (let login of logins) {
+      let foundLogins = LoginHelper.searchLoginsWithObject({guid: login.guid});
+      if (foundLogins.length) {
+        let foundLogin = foundLogins[0];
+        foundLogin.QueryInterface(Ci.nsILoginMetaInfo);
+        if (foundLogin.timePasswordChanged == login.timePasswordChanged) {
+          Services.logins.removeLogin(foundLogin);
+        }
+      }
+    }
+  }),
+
+  _removeSomeVisits: Task.async(function* (visits) {
+    for (let urlVisits of visits) {
+      let urlObj;
+      try {
+        urlObj = new URL(urlVisits.url);
+      } catch (ex) {
+        continue;
+      }
+      yield PlacesUtils.history.removeVisitsByFilter({
+        url: urlObj,
+        beginDate: PlacesUtils.toDate(urlVisits.first),
+        endDate: PlacesUtils.toDate(urlVisits.last),
+        limit: urlVisits.visitCount,
+      });
+    }
+  }),
+
   QueryInterface: XPCOMUtils.generateQI(
     [Ci.nsIObserver, Ci.nsINavBookmarkObserver, Ci.nsISupportsWeakReference]
   ),
 };
 
 AutoMigrate.init();
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -443,28 +443,28 @@ function GetWindowsPasswordsResource(aPr
 
       for (let row of rows) {
         let loginInfo = {
           username: row.getResultByName("username_value"),
           password: crypto.
                     decryptData(crypto.arrayToString(row.getResultByName("password_value")),
                                                      null),
           hostname: NetUtil.newURI(row.getResultByName("origin_url")).prePath,
-          submitURL: null,
+          formSubmitURL: null,
           httpRealm: null,
           usernameElement: row.getResultByName("username_element"),
           passwordElement: row.getResultByName("password_element"),
           timeCreated: chromeTimeToDate(row.getResultByName("date_created") + 0).getTime(),
           timesUsed: row.getResultByName("times_used") + 0,
         };
 
         try {
           switch (row.getResultByName("scheme")) {
             case AUTH_TYPE.SCHEME_HTML:
-              loginInfo.submitURL = NetUtil.newURI(row.getResultByName("action_url")).prePath;
+              loginInfo.formSubmitURL = NetUtil.newURI(row.getResultByName("action_url")).prePath;
               break;
             case AUTH_TYPE.SCHEME_BASIC:
             case AUTH_TYPE.SCHEME_DIGEST:
               // signon_realm format is URIrealm, so we need remove URI
               loginInfo.httpRealm = row.getResultByName("signon_realm")
                                        .substring(loginInfo.hostname.length + 1);
               break;
             default:
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -11,16 +11,18 @@ const TOPIC_WILL_IMPORT_BOOKMARKS = "ini
 const TOPIC_DID_IMPORT_BOOKMARKS = "initial-migration-did-import-default-bookmarks";
 const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete";
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+Cu.importGlobalProperties(["URL"]);
+
 XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate",
                                   "resource:///modules/AutoMigrate.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
                                   "resource://gre/modules/BookmarkHTMLUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
@@ -33,16 +35,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
                                   "resource://gre/modules/WindowsRegistry.jsm");
 
 var gMigrators = null;
 var gProfileStartup = null;
 var gMigrationBundle = null;
 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"
     ];
   }
   if (AppConstants.platform == "macosx") {
@@ -943,27 +948,114 @@ this.MigrationUtils = Object.freeze({
   _importQuantities: {
     bookmarks: 0,
     logins: 0,
     history: 0,
   },
 
   insertBookmarkWrapper(bookmark) {
     this._importQuantities.bookmarks++;
-    return PlacesUtils.bookmarks.insert(bookmark);
+    let insertionPromise = PlacesUtils.bookmarks.insert(bookmark);
+    if (!gKeepUndoData) {
+      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
+      });
+      return bm;
+    });
   },
 
   insertVisitsWrapper(places, options) {
     this._importQuantities.history += places.length;
+    if (gKeepUndoData) {
+      this._updateHistoryUndo(places);
+    }
     return PlacesUtils.asyncHistory.updatePlaces(places, options);
   },
 
   insertLoginWrapper(login) {
     this._importQuantities.logins++;
-    return LoginHelper.maybeImportLogin(login);
+    let insertedLogin = LoginHelper.maybeImportLogin(login);
+    // Note that this means that if we import a login that has a newer password
+    // than we know about, we will update the login, and an undo of the import
+    // will not revert this. This seems preferable over removing the login
+    // outright or storing the old password in the undo file.
+    if (insertedLogin && gKeepUndoData) {
+      let {guid, timePasswordChanged} = insertedLogin;
+      gUndoData.get("logins").push({guid, timePasswordChanged});
+    }
+  },
+
+  initializeUndoData() {
+    gKeepUndoData = true;
+    gUndoData = new Map([["bookmarks", []], ["visits", []], ["logins", []]]);
+  },
+
+  _postProcessUndoData: Task.async(function*(state) {
+    if (!state) {
+      return state;
+    }
+    let bookmarkFolders = state.get("bookmarks").filter(b => b.type == PlacesUtils.bookmarks.TYPE_FOLDER);
+
+    let bookmarkFolderData = [];
+    let bmPromises = bookmarkFolders.map(({guid}) => {
+      // Ignore bookmarks where the promise doesn't resolve (ie that are missing)
+      // Also check that the bookmark fetch returns isn't null before adding it.
+      return PlacesUtils.bookmarks.fetch(guid).then(bm => bm && bookmarkFolderData.push(bm), () => {});
+    });
+
+    yield Promise.all(bmPromises);
+    let folderLMMap = new Map(bookmarkFolderData.map(b => [b.guid, b.lastModified]));
+    for (let bookmark of bookmarkFolders) {
+      let lastModified = folderLMMap.get(bookmark.guid);
+      // If the bookmark was deleted, the map will be returning null, so check:
+      if (lastModified) {
+        bookmark.lastModified = lastModified;
+      }
+    }
+    return state;
+  }),
+
+  stopAndRetrieveUndoData() {
+    let undoData = gUndoData;
+    gUndoData = null;
+    gKeepUndoData = false;
+    return this._postProcessUndoData(undoData);
+  },
+
+  _updateHistoryUndo(places) {
+    let visits = gUndoData.get("visits");
+    let visitMap = new Map(visits.map(v => [v.url, v]));
+    for (let place of places) {
+      let visitCount = place.visits.length;
+      let first = Math.min.apply(Math, place.visits.map(v => v.visitDate));
+      let last = Math.max.apply(Math, place.visits.map(v => v.visitDate));
+      let url = place.uri.spec;
+      try {
+        new URL(url);
+      } catch (ex) {
+        // This won't save and we won't need to 'undo' it, so ignore this URL.
+        continue;
+      }
+      if (!visitMap.has(url)) {
+        visitMap.set(url, {url, visitCount, first, last});
+      } else {
+        let currentData = visitMap.get(url);
+        currentData.visitCount += visitCount;
+        currentData.first = Math.min(currentData.first, first);
+        currentData.last = Math.max(currentData.last, last);
+      }
+    }
+    gUndoData.set("visits", Array.from(visitMap.values()));
   },
 
   /**
    * Cleans up references to migrators and nsIProfileInstance instances.
    */
   finishMigration: function MU_finishMigration() {
     gMigrators = null;
     gProfileStartup = null;
--- a/browser/components/migration/tests/unit/head_migration.js
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -1,25 +1,30 @@
 "use strict";
 
 /* exported gProfD, promiseMigration, registerFakePath */
 
 var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 Cu.importGlobalProperties([ "URL" ]);
 
+Cu.import("resource:///modules/MigrationUtils.jsm");
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/TestUtils.jsm");
+Cu.import("resource://testing-common/PlacesTestUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
-                                  "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils",
-                                  "resource:///modules/MigrationUtils.jsm");
 
 // Initialize profile.
 var gProfD = do_get_profile();
 
 Cu.import("resource://testing-common/AppInfo.jsm"); /* globals updateAppInfo */
 updateAppInfo();
 
 /**
--- a/browser/components/migration/tests/unit/test_automigration.js
+++ b/browser/components/migration/tests/unit/test_automigration.js
@@ -1,15 +1,10 @@
 "use strict";
 
-Cu.import("resource:///modules/MigrationUtils.jsm");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://testing-common/TestUtils.jsm");
-Cu.import("resource://testing-common/PlacesTestUtils.jsm");
 let AutoMigrateBackstage = Cu.import("resource:///modules/AutoMigrate.jsm"); /* globals AutoMigrate */
 
 let gShimmedMigratorKeyPicker = null;
 let gShimmedMigrator = null;
 
 const kUsecPerMin = 60 * 1000000;
 
 // This is really a proxy on MigrationUtils, but if we specify that directly,
@@ -27,16 +22,32 @@ AutoMigrateBackstage.MigrationUtils = ne
     return MigrationUtils[name];
   },
 });
 
 do_register_cleanup(function() {
   AutoMigrateBackstage.MigrationUtils = MigrationUtils;
 });
 
+// This should be replaced by using History.fetch with a fetchVisits option,
+// once that becomes available
+function* visitsForURL(url)
+{
+  let visitCount = 0;
+  let db = yield PlacesUtils.promiseDBConnection();
+  visitCount = yield db.execute(
+    `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;
+}
+
+
 /**
  * Test automatically picking a browser to migrate from
  */
 add_task(function* checkMigratorPicking() {
   Assert.throws(() => AutoMigrate.pickMigrator("firefox"),
                 /Can't automatically migrate from Firefox/,
                 "Should throw when explicitly picking Firefox.");
 
@@ -158,79 +169,93 @@ add_task(function* checkUndoPrecondition
                      "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");
 
   yield migrationFinishedPromise;
-  Assert.ok(Preferences.has("browser.migrate.automigrate.started"),
-            "Should have set start time pref");
-  Assert.ok(Preferences.has("browser.migrate.automigrate.finished"),
-            "Should have set finish time pref");
-  Assert.ok(AutoMigrate.canUndo(), "Should be able to undo migration");
-
-  let [beginRange, endRange] = AutoMigrate.getUndoRange();
-  let stringRange = `beginRange: ${beginRange}; endRange: ${endRange}`;
-  Assert.ok(beginRange <= endRange,
-            "Migration should have started before or when it ended " + stringRange);
+  Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
+            "Should have set browser pref");
+  Assert.ok((yield AutoMigrate.canUndo()), "Should be able to undo migration");
 
   yield AutoMigrate.undo();
   Assert.ok(true, "Should be able to finish an undo cycle.");
 });
 
 /**
  * Fake a migration and then try to undo it to verify all data gets removed.
  */
 add_task(function* checkUndoRemoval() {
-  let startTime = "" + Date.now();
-
+  MigrationUtils.initializeUndoData();
   // Insert a login and check that that worked.
-  let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
-  login.init("www.mozilla.org", "http://www.mozilla.org", null, "user", "pass", "userEl", "passEl");
-  Services.logins.addLogin(login);
+  MigrationUtils.insertLoginWrapper({
+    hostname: "www.mozilla.org",
+    formSubmitURL: "http://www.mozilla.org",
+    username: "user",
+    password: "pass",
+  });
   let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
                                                 "http://www.mozilla.org", null);
   Assert.equal(storedLogins.length, 1, "Should have 1 login");
 
   // Insert a bookmark and check that we have exactly 1 bookmark for that URI.
-  yield PlacesUtils.bookmarks.insert({
+  yield MigrationUtils.insertBookmarkWrapper({
     parentGuid: PlacesUtils.bookmarks.toolbarGuid,
     url: "http://www.example.org/",
     title: "Some example bookmark",
   });
 
   let bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
   Assert.ok(bookmark, "Should have a bookmark before undo");
   Assert.equal(bookmark.title, "Some example bookmark", "Should have correct bookmark before undo.");
 
-  // Insert 2 history visits - one in the current migration time, one from before.
+  // Insert 2 history visits
   let now_uSec = Date.now() * 1000;
   let visitedURI = Services.io.newURI("http://www.example.com/", null, null);
-  yield PlacesTestUtils.addVisits([
-    {uri: visitedURI, visitDate: now_uSec},
-    {uri: visitedURI, visitDate: now_uSec - 100 * kUsecPerMin},
-  ]);
+  let frecencyUpdatePromise = new Promise(resolve => {
+    let expectedChanges = 2;
+    let observer = {
+      onFrecencyChanged: function() {
+        if (!--expectedChanges) {
+          PlacesUtils.history.removeObserver(observer);
+          resolve();
+        }
+      },
+    };
+    PlacesUtils.history.addObserver(observer, false);
+  });
+  yield MigrationUtils.insertVisitsWrapper([{
+    uri: visitedURI,
+    visits: [
+      {
+        transitionType: PlacesUtils.history.TRANSITION_LINK,
+        visitDate: now_uSec,
+      },
+      {
+        transitionType: PlacesUtils.history.TRANSITION_LINK,
+        visitDate: now_uSec - 100 * kUsecPerMin,
+      },
+    ]
+  }]);
+  yield 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;
   let visits = PlacesUtils.history.executeQuery(query, opts);
   visits.root.containerOpen = true;
   Assert.equal(visits.root.childCount, 2, "Should have 2 visits");
   // Clean up:
   visits.root.containerOpen = false;
 
-  // Now set finished pref:
-  let endTime = "" + Date.now();
-  Preferences.set("browser.migrate.automigrate.started", startTime);
-  Preferences.set("browser.migrate.automigrate.finished", endTime);
+  yield AutoMigrate.saveUndoState();
 
   // Verify that we can undo, then undo:
   Assert.ok(AutoMigrate.canUndo(), "Should be possible to undo migration");
   yield AutoMigrate.undo();
 
   // Check that the undo removed the history visits:
   visits = PlacesUtils.history.executeQuery(query, opts);
   visits.root.containerOpen = true;
@@ -240,51 +265,348 @@ add_task(function* checkUndoRemoval() {
   // Check that the undo removed the bookmarks:
   bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
   Assert.ok(!bookmark, "Should have no bookmarks after undo");
 
   // Check that the undo removed the passwords:
   storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
                                             "http://www.mozilla.org", null);
   Assert.equal(storedLogins.length, 0, "Should have no logins");
+});
 
-  // Finally check prefs got cleared:
-  Assert.ok(!Preferences.has("browser.migrate.automigrate.started"),
-            "Should no longer have pref for migration start time.");
-  Assert.ok(!Preferences.has("browser.migrate.automigrate.finished"),
-            "Should no longer have pref for migration finish time.");
+add_task(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} = yield MigrationUtils.insertBookmarkWrapper({
+    title, url, parentGuid
+  });
+  Assert.deepEqual((yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"),
+      [{lastModified, parentGuid, guid, type: TYPE_BOOKMARK}]);
+
+  MigrationUtils.initializeUndoData();
+  ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
+    title, parentGuid, type: TYPE_FOLDER
+  }));
+  let folder = {guid, lastModified, parentGuid, type: TYPE_FOLDER};
+  let folderGuid = folder.guid;
+  ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
+    title, url, parentGuid: folderGuid
+  }));
+  let kid1 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
+  ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
+    title, url, parentGuid: folderGuid
+  }));
+  let kid2 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
+
+  let bookmarksUndo = (yield 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;
+  // Not just using deepEqual on the entire array (which should work) because
+  // the failure messages get truncated by xpcshell which is unhelpful.
+  Assert.deepEqual(bookmarksUndo[0], folder);
+  Assert.deepEqual(bookmarksUndo[1], kid1);
+  Assert.deepEqual(bookmarksUndo[2], kid2);
+  yield PlacesUtils.bookmarks.eraseEverything();
 });
 
-add_task(function* checkUndoDisablingByBookmarksAndPasswords() {
-  let startTime = "" + Date.now();
-  Services.prefs.setCharPref("browser.migrate.automigrate.started", startTime);
-  // Now set finished pref:
-  let endTime = "" + (Date.now() + 1000);
-  Services.prefs.setCharPref("browser.migrate.automigrate.finished", endTime);
-  AutoMigrate.maybeInitUndoObserver();
+add_task(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} = yield MigrationUtils.insertBookmarkWrapper({
+    title: "Some folder", parentGuid, type: TYPE_FOLDER
+  });
+  let folderGuid = guid;
+  let itemsToRemove = [];
+  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+    title: "Inner folder", parentGuid: folderGuid, type: TYPE_FOLDER
+  }));
+  let innerFolderGuid = guid;
+  itemsToRemove.push(innerFolderGuid);
 
-  ok(AutoMigrate.canUndo(), "Should be able to undo.");
+  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+    title: "Inner inner folder", parentGuid: innerFolderGuid, type: TYPE_FOLDER
+  }));
+  itemsToRemove.push(guid);
 
-  // Insert a login and check that that disabled undo.
-  let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
-  login.init("www.mozilla.org", "http://www.mozilla.org", null, "user", "pass", "userEl", "passEl");
-  Services.logins.addLogin(login);
+  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+    title: "Inner nested item", url: "http://inner-nested-example.com", parentGuid: guid
+  }));
+  itemsToRemove.push(guid);
 
-  ok(!AutoMigrate.canUndo(), "Should no longer be able to undo.");
-  Services.prefs.setCharPref("browser.migrate.automigrate.started", startTime);
-  Services.prefs.setCharPref("browser.migrate.automigrate.finished", endTime);
-  ok(AutoMigrate.canUndo(), "Should be able to undo.");
-  AutoMigrate.maybeInitUndoObserver();
+  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+    title, url, parentGuid: folderGuid
+  }));
+  itemsToRemove.push(guid);
 
-  // Insert a bookmark and check that that disabled undo.
-  yield PlacesUtils.bookmarks.insert({
-    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
-    url: "http://www.example.org/",
-    title: "Some example bookmark",
-  });
-  ok(!AutoMigrate.canUndo(), "Should no longer be able to undo.");
+  for (let toBeRemovedGuid of itemsToRemove) {
+    let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(toBeRemovedGuid);
+    Assert.ok(dbResultForGuid, "Should be able to find items that will be removed.");
+  }
+  let bookmarkUndoState = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
+  // Now insert a separate item into this folder, not related to the migration.
+  let newItem = yield PlacesUtils.bookmarks.insert(
+    {title: "Not imported", parentGuid: folderGuid, url: "http://www.example.com"}
+  );
 
-  try {
-    Services.logins.removeAllLogins();
-  } catch (ex) {}
+  yield AutoMigrate._removeUnchangedBookmarks(bookmarkUndoState);
+  Assert.ok(true, "Successfully removed imported items.");
+
+  let itemFromDB = yield PlacesUtils.bookmarks.fetch(newItem.guid);
+  Assert.ok(itemFromDB, "Item we inserted outside of migration is still there.");
+  itemFromDB = yield PlacesUtils.bookmarks.fetch(folderGuid);
+  Assert.ok(itemFromDB, "Folder we inserted in migration is still there because of new kids.");
+  for (let removedGuid of itemsToRemove) {
+    let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(removedGuid);
+    let dbgStr = dbResultForGuid && dbResultForGuid.title;
+    Assert.equal(null, dbResultForGuid, "Should not be able to find items that should have been removed, but found " + dbgStr);
+  }
   yield PlacesUtils.bookmarks.eraseEverything();
 });
 
+add_task(function* checkUndoLoginsState() {
+  MigrationUtils.initializeUndoData();
+  MigrationUtils.insertLoginWrapper({
+    username: "foo",
+    password: "bar",
+    hostname: "https://example.com",
+    formSubmitURL: "https://example.com/",
+    timeCreated: new Date(),
+  });
+  let storedLogins = Services.logins.findLogins({}, "https://example.com", "", "");
+  let storedLogin = storedLogins[0];
+  storedLogin.QueryInterface(Ci.nsILoginMetaInfo);
+  let {guid, timePasswordChanged} = storedLogin;
+  let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");
+  Assert.deepEqual([{guid, timePasswordChanged}], undoLoginData);
+  Services.logins.removeAllLogins();
+});
+
+add_task(function* testLoginsRemovalByUndo() {
+  MigrationUtils.initializeUndoData();
+  MigrationUtils.insertLoginWrapper({
+    username: "foo",
+    password: "bar",
+    hostname: "https://example.com",
+    formSubmitURL: "https://example.com/",
+    timeCreated: new Date(),
+  });
+  MigrationUtils.insertLoginWrapper({
+    username: "foo",
+    password: "bar",
+    hostname: "https://example.org",
+    formSubmitURL: "https://example.org/",
+    timeCreated: new Date(new Date().getTime() - 10000),
+  });
+  // This should update the existing login
+  LoginHelper.maybeImportLogin({
+    username: "foo",
+    password: "bazzy",
+    hostname: "https://example.org",
+    formSubmitURL: "https://example.org/",
+    timePasswordChanged: new Date(),
+  });
+  Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
+               "Should be only 1 login for example.org (that was updated)");
+  let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");
+
+  yield AutoMigrate._removeUnchangedLogins(undoLoginData);
+  Assert.equal(0, LoginHelper.searchLoginsWithObject({hostname: "https://example.com", formSubmitURL: "https://example.com/"}).length,
+                   "unchanged example.com entry should have been removed.");
+  Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
+                   "changed example.org entry should have persisted.");
+  Services.logins.removeAllLogins();
+});
+
+add_task(function* checkUndoVisitsState() {
+  MigrationUtils.initializeUndoData();
+  yield MigrationUtils.insertVisitsWrapper([{
+    uri: NetUtil.newURI("http://www.example.com/"),
+    title: "Example",
+    visits: [{
+      visitDate: new Date("2015-07-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }, {
+      visitDate: new Date("2015-09-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }, {
+      visitDate: new Date("2015-08-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }],
+  }, {
+    uri: NetUtil.newURI("http://www.example.org/"),
+    title: "Example",
+    visits: [{
+      visitDate: new Date("2016-04-03").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }, {
+      visitDate: new Date("2015-08-03").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }],
+  }, {
+    uri: NetUtil.newURI("http://www.example.com/"),
+    title: "Example",
+    visits: [{
+      visitDate: new Date("2015-10-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }],
+  }]);
+  let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");
+  Assert.deepEqual(Array.from(undoVisitData.map(v => v.url)).sort(),
+                   ["http://www.example.com/", "http://www.example.org/"]);
+  Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.com/"), {
+    url: "http://www.example.com/",
+    visitCount: 4,
+    first: new Date("2015-07-10").getTime() * 1000,
+    last: new Date("2015-10-10").getTime() * 1000,
+  });
+  Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.org/"), {
+    url: "http://www.example.org/",
+    visitCount: 2,
+    first: new Date("2015-08-03").getTime() * 1000,
+    last: new Date("2016-04-03").getTime() * 1000,
+  });
+
+  yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* checkUndoVisitsState() {
+  MigrationUtils.initializeUndoData();
+  yield MigrationUtils.insertVisitsWrapper([{
+    uri: NetUtil.newURI("http://www.example.com/"),
+    title: "Example",
+    visits: [{
+      visitDate: new Date("2015-07-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }, {
+      visitDate: new Date("2015-09-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }, {
+      visitDate: new Date("2015-08-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }],
+  }, {
+    uri: NetUtil.newURI("http://www.example.org/"),
+    title: "Example",
+    visits: [{
+      visitDate: new Date("2016-04-03").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }, {
+      visitDate: new Date("2015-08-03").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }],
+  }, {
+    uri: NetUtil.newURI("http://www.example.com/"),
+    title: "Example",
+    visits: [{
+      visitDate: new Date("2015-10-10").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }],
+  }, {
+    uri: NetUtil.newURI("http://www.mozilla.org/"),
+    title: "Example",
+    visits: [{
+      visitDate: new Date("2015-01-01").getTime() * 1000,
+      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+    }],
+  }]);
+
+  // 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({
+      onFrecencyChanged(aURI) {
+        if (aURI.spec == "http://www.unrelated.org/") {
+          PlacesUtils.history.removeObserver(this);
+          resolve();
+        }
+      }
+    }, false);
+  });
+  yield PlacesUtils.history.insertMany([{
+    url: "http://www.example.com/",
+    title: "Example",
+    visits: [{
+      date: new Date("2015-08-16"),
+    }],
+  }, {
+    url: "http://www.example.org/",
+    title: "Example",
+    visits: [{
+      date: new Date("2016-01-03"),
+    }, {
+      date: new Date("2015-05-03"),
+    }],
+  }, {
+    url: "http://www.unrelated.org/",
+    title: "Unrelated",
+    visits: [{
+      date: new Date("2015-09-01"),
+    }],
+  }]);
+  yield frecencyUpdatesHandled;
+  let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");
+
+  let frecencyChangesExpected = new Map([
+    ["http://www.example.com/", 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: function() {},
+    onEndUpdateBatch: function() {},
+    onVisit: function(uri) {
+      wrongMethodDeferred.reject(new Error("Unexpected call to onVisit " + uri.spec));
+    },
+    onTitleChanged: function(uri) {
+      wrongMethodDeferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
+    },
+    onClearHistory: function() {
+      wrongMethodDeferred.reject("Unexpected call to onClearHistory");
+    },
+    onPageChanged: function(uri) {
+      wrongMethodDeferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
+    },
+    onFrecencyChanged: function(aURI) {
+      do_print("frecency change");
+      Assert.ok(frecencyChangesExpected.has(aURI.spec),
+                "Should be expecting frecency change for " + aURI.spec);
+      frecencyChangesExpected.get(aURI.spec).resolve();
+    },
+    onManyFrecenciesChanged: function() {
+      do_print("Many frecencies changed");
+      wrongMethodDeferred.reject(new Error("This test can't deal with onManyFrecenciesChanged to be called"));
+    },
+    onDeleteURI: function(aURI) {
+      do_print("delete uri");
+      Assert.ok(uriDeletedExpected.has(aURI.spec),
+                "Should be expecting uri deletion for " + aURI.spec);
+      uriDeletedExpected.get(aURI.spec).resolve();
+    },
+  };
+  PlacesUtils.history.addObserver(observer, false);
+
+  yield AutoMigrate._removeSomeVisits(undoVisitData);
+  PlacesUtils.history.removeObserver(observer);
+  yield Promise.all(uriDeletedExpected.values());
+  yield Promise.all(frecencyChangesExpected.values());
+
+  Assert.equal(yield visitsForURL("http://www.example.com/"), 1,
+               "1 example.com visit (out of 5) should have persisted despite being within the range, due to limiting");
+  Assert.equal(yield visitsForURL("http://www.mozilla.org/"), 0,
+               "0 mozilla.org visits should have persisted (out of 1).");
+  Assert.equal(yield visitsForURL("http://www.example.org/"), 2,
+               "2 example.org visits should have persisted (out of 4).");
+  Assert.equal(yield visitsForURL("http://www.unrelated.org/"), 1,
+               "1 unrelated.org visits should have persisted as it's not involved in the import.");
+  yield PlacesTestUtils.clearHistory();
+});
+
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -13,16 +13,38 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/AsyncPrefs.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils", "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-service;1", "nsIAlertsService");
 
 // lazy module getters
+
+/* global AboutHome:false, AboutNewTab:false, AddonManager:false, AddonWatcher:false,
+          AsyncShutdown:false, AutoCompletePopup:false, BookmarkHTMLUtils:false,
+          BookmarkJSONUtils:false, BrowserUITelemetry:false, BrowserUsageTelemetry:false,
+          CaptivePortalWatcher:false, ContentClick:false, ContentPrefServiceParent:false,
+          ContentSearch:false, DateTimePickerHelper:false, DirectoryLinksProvider:false,
+          Feeds:false, FileUtils:false, FormValidationHandler:false, Integration:false,
+          LightweightThemeManager:false, LoginHelper:false, LoginManagerParent:false,
+          NetUtil:false, NewTabMessages:false, NewTabUtils:false, OS:false,
+          PageThumbs:false, PdfJs:false, PermissionUI:false, PlacesBackups:false,
+          PlacesUtils:false, PluralForm:false, PrivateBrowsingUtils:false,
+          ProcessHangMonitor:false, ReaderParent:false, RecentWindow:false,
+          RemotePrompt:false, SelfSupportBackend:false, SessionStore:false,
+          ShellService:false, SimpleServiceDiscovery:false, TabCrashHandler:false,
+          Task:false, UITour:false, URLBarZoom:false, WebChannel:false,
+          WindowsRegistry:false, webrtcUI:false */
+
+/**
+ * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
+ * XXX Bug 1325373 is for making eslint detect these automatically.
+ */
+
 [
   ["AboutHome", "resource:///modules/AboutHome.jsm"],
   ["AboutNewTab", "resource:///modules/AboutNewTab.jsm"],
   ["AddonManager", "resource://gre/modules/AddonManager.jsm"],
   ["AddonWatcher", "resource://gre/modules/AddonWatcher.jsm"],
   ["AsyncShutdown", "resource://gre/modules/AsyncShutdown.jsm"],
   ["AutoCompletePopup", "resource://gre/modules/AutoCompletePopup.jsm"],
   ["BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"],
@@ -358,86 +380,25 @@ BrowserGlue.prototype = {
           for (let addon of addons) {
             if (addon.type != "experiment") {
               this._notifyUnsignedAddonsDisabled();
               break;
             }
           }
         });
         break;
-      case "autocomplete-did-enter-text":
-        this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput));
-        break;
       case "test-initialize-sanitizer":
         this._sanitizer.onStartup();
         break;
       case AddonWatcher.TOPIC_SLOW_ADDON_DETECTED:
         this._notifySlowAddon(data);
         break;
     }
   },
 
-  _handleURLBarTelemetry(input) {
-    if (!input ||
-        input.id != "urlbar" ||
-        input.inPrivateContext ||
-        input.popup.selectedIndex < 0) {
-      return;
-    }
-    let controller =
-      input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
-    let idx = input.popup.selectedIndex;
-    let value = controller.getValueAt(idx);
-    let action = input._parseActionUrl(value);
-    let actionType;
-    if (action) {
-      actionType =
-        action.type == "searchengine" && action.params.searchSuggestion ?
-          "searchsuggestion" :
-        action.type;
-    }
-    if (!actionType) {
-      let styles = new Set(controller.getStyleAt(idx).split(/\s+/));
-      let style = ["autofill", "tag", "bookmark"].find(s => styles.has(s));
-      actionType = style || "history";
-    }
-
-    Services.telemetry
-            .getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX")
-            .add(idx);
-
-    // Ideally this would be a keyed histogram and we'd just add(actionType),
-    // but keyed histograms aren't currently shown on the telemetry dashboard
-    // (bug 1151756).
-    //
-    // You can add values but don't change any of the existing values.
-    // Otherwise you'll break our data.
-    let buckets = {
-      autofill: 0,
-      bookmark: 1,
-      history: 2,
-      keyword: 3,
-      searchengine: 4,
-      searchsuggestion: 5,
-      switchtab: 6,
-      tag: 7,
-      visiturl: 8,
-      remotetab: 9,
-      extension: 10,
-    };
-    if (actionType in buckets) {
-      Services.telemetry
-              .getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE")
-              .add(buckets[actionType]);
-    } else {
-      Cu.reportError("Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " +
-                     actionType);
-    }
-  },
-
   // initialization (called on application startup)
   _init: function BG__init() {
     let os = Services.obs;
     os.addObserver(this, "notifications-open-settings", false);
     os.addObserver(this, "prefservice:after-app-defaults", false);
     os.addObserver(this, "final-ui-startup", false);
     os.addObserver(this, "browser-delayed-startup-finished", false);
     os.addObserver(this, "sessionstore-windows-restored", false);
@@ -464,17 +425,16 @@ BrowserGlue.prototype = {
     os.addObserver(this, "profile-before-change", false);
     if (AppConstants.MOZ_TELEMETRY_REPORTING) {
       os.addObserver(this, "keyword-search", false);
     }
     os.addObserver(this, "browser-search-engine-modified", false);
     os.addObserver(this, "restart-in-safe-mode", false);
     os.addObserver(this, "flash-plugin-hang", false);
     os.addObserver(this, "xpi-signature-changed", false);
-    os.addObserver(this, "autocomplete-did-enter-text", false);
 
     if (AppConstants.NIGHTLY_BUILD) {
       os.addObserver(this, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false);
     }
 
     this._flashHangCount = 0;
     this._firstWindowReady = new Promise(resolve => this._firstWindowLoaded = resolve);
 
@@ -519,17 +479,16 @@ BrowserGlue.prototype = {
     os.removeObserver(this, "handle-xul-text-link");
     os.removeObserver(this, "profile-before-change");
     if (AppConstants.MOZ_TELEMETRY_REPORTING) {
       os.removeObserver(this, "keyword-search");
     }
     os.removeObserver(this, "browser-search-engine-modified");
     os.removeObserver(this, "flash-plugin-hang");
     os.removeObserver(this, "xpi-signature-changed");
-    os.removeObserver(this, "autocomplete-did-enter-text");
   },
 
   _onAppDefaults: function BG__onAppDefaults() {
     // apply distribution customizations (prefs)
     // other customizations are applied in _finalUIStartup()
     this._distributionCustomizer.applyPrefDefaults();
   },
 
@@ -1131,24 +1090,25 @@ BrowserGlue.prototype = {
         }
       });
     }
 
     // Perform default browser checking.
     if (ShellService) {
       let shouldCheck = AppConstants.DEBUG ? false :
                                              ShellService.shouldCheckDefaultBrowser;
-      let promptCount;
-      let skipDefaultBrowserCheck = false;
-      if (!AppConstants.RELEASE_OR_BETA) {
-        promptCount =
-          Services.prefs.getIntPref("browser.shell.defaultBrowserCheckCount");
-        skipDefaultBrowserCheck =
-          Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheck");
-      }
+
+      const skipDefaultBrowserCheck =
+        Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheckOnFirstRun") &&
+        Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheck");
+
+      const usePromptLimit = !AppConstants.RELEASE_OR_BETA;
+      let promptCount =
+        usePromptLimit ? Services.prefs.getIntPref("browser.shell.defaultBrowserCheckCount") : 0;
+
       let willRecoverSession = false;
       try {
         let ss = Cc["@mozilla.org/browser/sessionstartup;1"].
                  getService(Ci.nsISessionStartup);
         willRecoverSession =
           (ss.sessionType == Ci.nsISessionStartup.RECOVER_SESSION);
       }
       catch (ex) { /* never mind; suppose SessionStore is broken */ }
@@ -1173,26 +1133,23 @@ BrowserGlue.prototype = {
       // browser has been run a few times.
       if (willPrompt) {
         if (skipDefaultBrowserCheck) {
           Services.prefs.setBoolPref("browser.shell.skipDefaultBrowserCheck", false);
           willPrompt = false;
         } else {
           promptCount++;
         }
-        if (promptCount > 3) {
+        if (usePromptLimit && promptCount > 3) {
           willPrompt = false;
         }
       }
 
-      if (!AppConstants.RELEASE_OR_BETA) {
-        if (willPrompt) {
-          Services.prefs.setIntPref("browser.shell.defaultBrowserCheckCount",
-                                    promptCount);
-        }
+      if (usePromptLimit && willPrompt) {
+        Services.prefs.setIntPref("browser.shell.defaultBrowserCheckCount", promptCount);
       }
 
       try {
         // Report default browser status on startup to telemetry
         // so we can track whether we are the default.
         Services.telemetry.getHistogramById("BROWSER_IS_USER_DEFAULT")
                           .add(isDefault);
         Services.telemetry.getHistogramById("BROWSER_IS_USER_DEFAULT_ERROR")
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -650,16 +650,26 @@ PlacesController.prototype = {
             openContainerInTabsItem.disabled = true;
             // Ensure that we don't display the menu if nothing is enabled:
             usableItemCount--;
           }
         }
       }
     }
 
+    // Make sure correct PluralForms are diplayed when multiple pages are selected.
+    var deleteHistoryItem = document.getElementById("placesContext_delete_history");
+    deleteHistoryItem.label = PlacesUIUtils.getPluralString("cmd.deletePages.label",
+                                                            metadata.length);
+    deleteHistoryItem.accessKey = PlacesUIUtils.getString("cmd.deletePages.accesskey");
+    var createBookmarkItem = document.getElementById("placesContext_createBookmark");
+    createBookmarkItem.label = PlacesUIUtils.getPluralString("cmd.bookmarkPages.label",
+                                                             metadata.length);
+    createBookmarkItem.accessKey = PlacesUIUtils.getString("cmd.bookmarkPages.accesskey");
+
     return usableItemCount > 0;
   },
 
   /**
    * Select all links in the current view.
    */
   selectAll: function PC_selectAll() {
     this._view.selectAll();
--- a/browser/components/places/content/placesOverlay.xul
+++ b/browser/components/places/content/placesOverlay.xul
@@ -156,18 +156,16 @@
               label="&cmd.new_separator.label;"
               accesskey="&cmd.new_separator.accesskey;"
               closemenu="single"
               selectiontype="any"
               hideifnoinsertionpoint="true"/>
     <menuseparator id="placesContext_newSeparator"/>
     <menuitem id="placesContext_createBookmark"
               command="placesCmd_createBookmark"
-              label="&cmd.bookmarkLink.label;"
-              accesskey="&cmd.bookmarkLink.accesskey;"
               selection="link"
               forcehideselection="bookmark|tagChild"/>
     <menuitem id="placesContext_cut"
               command="placesCmd_cut"
               label="&cutCmd.label;"
               accesskey="&cutCmd.accesskey;" 
               closemenu="single"
               selection="bookmark|folder|separator|query"
@@ -189,18 +187,16 @@
     <menuitem id="placesContext_delete"
               command="placesCmd_delete"
               label="&deleteCmd.label;"
               accesskey="&deleteCmd.accesskey;"
               closemenu="single"
               selection="bookmark|tagChild|folder|query|dynamiccontainer|separator|host"/>
     <menuitem id="placesContext_delete_history"
               command="placesCmd_delete"
-              label="&cmd.delete.label;"
-              accesskey="&cmd.delete.accesskey;"
               closemenu="single"
               selection="link"
               forcehideselection="bookmark"/>
     <menuitem id="placesContext_deleteHost"
               command="placesCmd_deleteDataHost"
               label="&cmd.deleteDomainData.label;"
               accesskey="&cmd.deleteDomainData.accesskey;"
               closemenu="single"
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -1,15 +1,20 @@
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
   "resource://testing-common/PlacesTestUtils.jsm");
 
+// Imported via PlacesOverlay.xul
+/* global doGetPlacesControllerForCommand:false, PlacesControllerDragHelper:false,
+          PlacesUIUtils:false
+*/
+
 // We need to cache this before test runs...
 var cachedLeftPaneFolderIdGetter;
 var getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
 if (!cachedLeftPaneFolderIdGetter && typeof(getter) == "function") {
   cachedLeftPaneFolderIdGetter = getter;
 }
 
 // ...And restore it when test ends.
--- a/browser/components/places/tests/unit/head_bookmarks.js
+++ b/browser/components/places/tests/unit/head_bookmarks.js
@@ -7,16 +7,17 @@ var Ci = Components.interfaces;
 var Cc = Components.classes;
 var Cr = Components.results;
 var Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/LoadContextInfo.jsm");
 
 // Import common head.
+/* import-globals-from ../../../../../toolkit/components/places/tests/head_common.js */
 var commonFile = do_get_file("../../../../../toolkit/components/places/tests/head_common.js", false);
 if (commonFile) {
   let uri = Services.io.newFileURI(commonFile);
   Services.scriptloader.loadSubScript(uri.spec, this);
 }
 
 // Put any other stuff relative to this test folder below.
 
--- a/browser/components/preferences/cookies.xul
+++ b/browser/components/preferences/cookies.xul
@@ -27,20 +27,21 @@
   <keyset>
     <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
     <key key="&focusSearch1.key;" modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/>
     <key key="&focusSearch2.key;" modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/>
   </keyset>
 
   <vbox flex="1" class="contentPane largeDialogContainer">
     <hbox align="center">
-      <label accesskey="&filter.accesskey;" control="filter">&filter.label;</label>
       <textbox type="search" id="filter" flex="1"
                aria-controls="cookiesList"
-               oncommand="gCookiesWindow.filter();"/>
+               oncommand="gCookiesWindow.filter();"
+               placeholder="&searchFilter.label;"
+               accesskey="&searchFilter.accesskey;"/>
     </hbox>
     <separator class="thin"/>
     <label control="cookiesList" id="cookiesIntro" value="&cookiesonsystem.label;"/>
     <separator class="thin"/>
     <tree id="cookiesList" flex="1" style="height: 10em;"
           onkeypress="gCookiesWindow.onCookieKeyPress(event)"
           onselect="gCookiesWindow.onCookieSelected();"
           hidecolumnpicker="true" seltype="single">
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -345,17 +345,17 @@
         </vbox>
       </groupbox>
     </tabpanel>
 
     <!-- Update -->
     <tabpanel id="updatePanel" orient="vertical">
 #ifdef MOZ_UPDATER
       <groupbox id="updateApp" align="start">
-        <caption><label>&updateApp.label;</label></caption>
+        <caption><label>&updateApplication.label;</label></caption>
         <radiogroup id="updateRadioGroup" align="start">
           <radio id="autoDesktop"
                  value="auto"
                  label="&updateAuto1.label;"
                  accesskey="&updateAuto1.accesskey;"/>
           <radio value="checkOnly"
                 label="&updateCheck.label;"
                 accesskey="&updateCheck.accesskey;"/>
@@ -375,17 +375,17 @@
         <checkbox id="useService"
                   label="&useService.label;"
                   accesskey="&useService.accesskey;"
                   preference="app.update.service.enabled"/>
 #endif
       </groupbox>
 #endif
       <groupbox id="updateOthers" align="start">
-        <caption><label>&updateOthers.label;</label></caption>
+        <caption><label>&autoUpdateOthers.label;</label></caption>
         <checkbox id="enableSearchUpdate"
                   label="&enableSearchUpdate.label;"
                   accesskey="&enableSearchUpdate.accesskey;"
                   preference="browser.search.update"/>
       </groupbox>
     </tabpanel>
 
     <!-- Certificates -->
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -3636,17 +3636,17 @@ var SessionStoreInternal = {
     // browser, which removes it from the display list. We cannot
     // flip the remoteness of any browser that is not being displayed.
     this.markTabAsRestoring(aTab);
 
     // We need a new frameloader either if we are reloading into a fresh
     // process, or we have a browser with a grouped session history (as we don't
     // support restoring into browsers with grouped session histories directly).
     let newFrameloader =
-      aReloadInFreshProcess || !!browser.frameLoader.groupedSessionHistory;
+      aReloadInFreshProcess || !!browser.frameLoader.groupedSHistory;
     let isRemotenessUpdate =
       tabbrowser.updateBrowserRemotenessByURL(browser, uri, {
         freshProcess: aReloadInFreshProcess,
         newFrameloader: newFrameloader,
       });
 
     if (isRemotenessUpdate) {
       // We updated the remoteness, so we need to send the history down again.
--- a/browser/components/sessionstore/test/browser_grouped_session_store.js
+++ b/browser/components/sessionstore/test/browser_grouped_session_store.js
@@ -65,18 +65,17 @@ add_task(function* () {
     // Create a new hidden prerendered tab to swap to.
     let tab2 = gBrowser.loadOneTab(URIs[2], {
       referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
       allowThirdPartyFixup: true,
       relatedToCurrent: true,
       isPrerendered: true,
     });
     yield BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
-    browser1.frameLoader.appendPartialSessionHistoryAndSwap(
-      tab2.linkedBrowser.frameLoader);
+    browser1.frameLoader.appendPartialSHistoryAndSwap(tab2.linkedBrowser.frameLoader);
     yield awaitProcessChange(browser1);
     yield* validate(browser1, 3, 3);
 
     browser1.loadURI(URIs[3], null, null);
     yield BrowserTestUtils.browserLoaded(browser1);
     yield* validate(browser1, 4, 4);
 
     // In process navigate back.
@@ -108,18 +107,17 @@ add_task(function* () {
     // Create a new hidden prerendered tab to swap to
     let tab3 = gBrowser.loadOneTab(URIs[4], {
       referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
       allowThirdPartyFixup: true,
       relatedToCurrent: true,
       isPrerendered: true,
     });
     yield BrowserTestUtils.browserLoaded(tab3.linkedBrowser);
-    browser1.frameLoader.appendPartialSessionHistoryAndSwap(
-      tab3.linkedBrowser.frameLoader);
+    browser1.frameLoader.appendPartialSHistoryAndSwap(tab3.linkedBrowser.frameLoader);
     yield awaitProcessChange(browser1);
     yield* validate(browser1, 5, 5);
 
     browser1.gotoIndex(0);
     yield awaitProcessChange(browser1);
     yield* validate(browser1, 5, 1);
 
     browser1.gotoIndex(2);
--- a/browser/config/mozconfigs/linux32/artifact
+++ b/browser/config/mozconfigs/linux32/artifact
@@ -1,12 +1,8 @@
 MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/browser/config/mozconfigs/linux32/common-opt"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
-unset CC
-unset CXX
-unset RUSTC
-unset CARGO
--- a/browser/config/mozconfigs/linux32/debug-artifact
+++ b/browser/config/mozconfigs/linux32/debug-artifact
@@ -1,14 +1,10 @@
 MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/build/unix/mozconfig.linux32"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
-unset CC
-unset CXX
-unset RUSTC
-unset CARGO
 
 ac_add_options --enable-debug
--- a/browser/config/mozconfigs/linux64/artifact
+++ b/browser/config/mozconfigs/linux64/artifact
@@ -1,12 +1,8 @@
 MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/browser/config/mozconfigs/linux64/common-opt"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
-unset CC
-unset CXX
-unset RUSTC
-unset CARGO
--- a/browser/config/mozconfigs/linux64/debug-artifact
+++ b/browser/config/mozconfigs/linux64/debug-artifact
@@ -1,15 +1,11 @@
 MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/build/unix/mozconfig.linux"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
-unset CC
-unset CXX
-unset RUSTC
-unset CARGO
 
 ac_add_options --enable-debug
 
--- a/browser/config/mozconfigs/macosx64/artifact
+++ b/browser/config/mozconfigs/macosx64/artifact
@@ -2,14 +2,10 @@ MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 # Needed to set SourceRepository in application.ini (used by Talos)
 export MOZILLA_OFFICIAL=1
 
 . "$topsrcdir/build/macosx/mozconfig.common"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
-unset CC
-unset CXX
-unset RUSTC
-unset CARGO
--- a/browser/config/mozconfigs/macosx64/debug-artifact
+++ b/browser/config/mozconfigs/macosx64/debug-artifact
@@ -1,14 +1,10 @@
 MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/build/macosx/mozconfig.common"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
-unset CC
-unset CXX
-unset RUSTC
-unset CARGO
 
 ac_add_options --enable-debug
--- a/browser/config/mozconfigs/win32/artifact
+++ b/browser/config/mozconfigs/win32/artifact
@@ -4,10 +4,10 @@ MOZ_AUTOMATION_L10N_CHECK=0
 # Needed to set SourceRepository in application.ini (used by Talos)
 export MOZILLA_OFFICIAL=1
 
 . "$topsrcdir/browser/config/mozconfigs/common"
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/build/win32/mozconfig.vs-latest"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
--- a/browser/config/mozconfigs/win32/common-opt
+++ b/browser/config/mozconfigs/win32/common-opt
@@ -1,15 +1,14 @@
 # This file is sourced by the nightly, beta, and release mozconfigs.
 
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-jemalloc
-ac_add_options --enable-require-all-d3dc-versions
 
 if [ -f /c/builds/gapi.data ]; then
   _gapi_keyfile=/c/builds/gapi.data
 else
   _gapi_keyfile=/e/builds/gapi.data
 fi
 ac_add_options --with-google-api-keyfile=${_gapi_keyfile}
 
--- a/browser/config/mozconfigs/win32/debug
+++ b/browser/config/mozconfigs/win32/debug
@@ -1,17 +1,16 @@
 . "$topsrcdir/build/mozconfig.win-common"
 MOZ_AUTOMATION_L10N_CHECK=0
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-profiling  # needed for --enable-dmd to work on Windows
 ac_add_options --enable-verify-mar
-ac_add_options --enable-require-all-d3dc-versions
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 # Enable Telemetry
 export MOZ_TELEMETRY_REPORTING=1
 
 . $topsrcdir/build/win32/mozconfig.vs-latest
--- a/browser/config/mozconfigs/win32/debug-artifact
+++ b/browser/config/mozconfigs/win32/debug-artifact
@@ -1,12 +1,12 @@
 MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/browser/config/mozconfigs/common"
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/build/win32/mozconfig.vs-latest"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
 
 ac_add_options --enable-debug
--- a/browser/config/mozconfigs/win64/artifact
+++ b/browser/config/mozconfigs/win64/artifact
@@ -5,10 +5,10 @@ MOZ_AUTOMATION_L10N_CHECK=0
 export MOZILLA_OFFICIAL=1
 
 . "$topsrcdir/browser/config/mozconfigs/win64/common-win64"
 . "$topsrcdir/browser/config/mozconfigs/common"
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/build/win64/mozconfig.vs-latest"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
--- a/browser/config/mozconfigs/win64/debug-artifact
+++ b/browser/config/mozconfigs/win64/debug-artifact
@@ -2,12 +2,12 @@ MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/browser/config/mozconfigs/win64/common-win64"
 . "$topsrcdir/browser/config/mozconfigs/common"
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/build/win64/mozconfig.vs-latest"
 . "$topsrcdir/build/mozconfig.common.override"
 
-ac_add_options --enable-artifact-builds
+. "$topsrcdir/build/mozconfig.artifact"
 ac_add_options --enable-artifact-build-symbols
 
 ac_add_options --enable-debug
--- a/browser/config/tooltool-manifests/linux32/releng.manifest
+++ b/browser/config/tooltool-manifests/linux32/releng.manifest
@@ -1,13 +1,13 @@
 [
 {
-"version": "gcc 4.8.5 + PR64905",
-"size": 80160264,
-"digest": "c1a9dc9da289b8528874d16300b9d13a997cec99195bb0bc46ff665216d8535d6d6cb5af6b4b1f2749af6815dab12e703fdb3849014e5c23a70eff351a0baf4e",
+"version": "gcc 4.9.4 + PR64905",
+"size": 101297752,
+"digest": "42aa2e3fdd232b5e390472a788e7f7db71a1fee4221e260b6cb58c9a1d73e6cdd10afcbac137f7844290169cd6b561b424ecc92b159e9726b0ad5de3f478a8be",
 "algorithm": "sha512",
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "size": 11189216,
 "digest": "18bc52b0599b1308b667e282abb45f47597bfc98a5140cfcab8da71dacf89dd76d0dee22a04ce26fe7ad1f04e2d6596991f9e5b01fd2aaaab5542965f596b0e6",
 "algorithm": "sha512",
--- a/browser/config/tooltool-manifests/linux64/asan.manifest
+++ b/browser/config/tooltool-manifests/linux64/asan.manifest
@@ -1,13 +1,13 @@
 [
 {
-"version": "gcc 4.8.5 + PR64905",
-"size": 80160264,
-"digest": "c1a9dc9da289b8528874d16300b9d13a997cec99195bb0bc46ff665216d8535d6d6cb5af6b4b1f2749af6815dab12e703fdb3849014e5c23a70eff351a0baf4e",
+"version": "gcc 4.9.4 + PR64905",
+"size": 101297752,
+"digest": "42aa2e3fdd232b5e390472a788e7f7db71a1fee4221e260b6cb58c9a1d73e6cdd10afcbac137f7844290169cd6b561b424ecc92b159e9726b0ad5de3f478a8be",
 "algorithm": "sha512",
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "version": "clang 3.8.0, libgcc 4.8.5",
 "size": 139183100,
 "digest": "a056a151d4f25f415b6d905136c3fa8d51d12a5a815c3df37d5663c67d59571736641a4c990884a69f78ea6b5e37a6a7bfff0417dfe38936d842d6fa0776ae54",
--- a/browser/config/tooltool-manifests/linux64/hazard.manifest
+++ b/browser/config/tooltool-manifests/linux64/hazard.manifest
@@ -1,15 +1,15 @@
 [
 {
-"size": 102421980,
-"version": "gcc 4.9.3",
+"version": "gcc 4.9.4 + PR64905",
+"size": 101297752,
+"digest": "42aa2e3fdd232b5e390472a788e7f7db71a1fee4221e260b6cb58c9a1d73e6cdd10afcbac137f7844290169cd6b561b424ecc92b159e9726b0ad5de3f478a8be",
+"algorithm": "sha512",
 "filename": "gcc.tar.xz",
-"algorithm": "sha512",
-"digest": "f25292aa93dc449e0472eee511c0ac15b5f1a4272ab76cf53ce5d20dc57f29e83da49ae1a9d9e994192647f75e13ae60f75ba2ac3cb9d26d5f5d6cabf88de921",
 "unpack": true
 },
 {
 "digest": "36dc644e24c0aa824975ad8f5c15714445d5cb064d823000c3cb637e885199414d7df551e6b99233f0656dcf5760918192ef04113c486af37f3c489bb93ad029",
 "unpack": true,
 "algorithm": "sha512",
 "filename": "sixgill.tar.xz",
 "size": 2631908,
--- a/browser/config/tooltool-manifests/linux64/msan.manifest
+++ b/browser/config/tooltool-manifests/linux64/msan.manifest
@@ -1,13 +1,13 @@
 [
 {
-"version": "gcc 4.8.5 + PR64905",
-"size": 80160264,
-"digest": "c1a9dc9da289b8528874d16300b9d13a997cec99195bb0bc46ff665216d8535d6d6cb5af6b4b1f2749af6815dab12e703fdb3849014e5c23a70eff351a0baf4e",
+"version": "gcc 4.9.4 + PR64905",
+"size": 101297752,
+"digest": "42aa2e3fdd232b5e390472a788e7f7db71a1fee4221e260b6cb58c9a1d73e6cdd10afcbac137f7844290169cd6b561b424ecc92b159e9726b0ad5de3f478a8be",
 "algorithm": "sha512",
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "version": "clang 3.8.0, libgcc 4.8.5",
 "size": 139183100,
 "digest": "a056a151d4f25f415b6d905136c3fa8d51d12a5a815c3df37d5663c67d59571736641a4c990884a69f78ea6b5e37a6a7bfff0417dfe38936d842d6fa0776ae54",
--- a/browser/config/tooltool-manifests/linux64/releng.manifest
+++ b/browser/config/tooltool-manifests/linux64/releng.manifest
@@ -1,13 +1,13 @@
 [
 {
-"version": "gcc 4.8.5 + PR64905",
-"size": 80160264,
-"digest": "c1a9dc9da289b8528874d16300b9d13a997cec99195bb0bc46ff665216d8535d6d6cb5af6b4b1f2749af6815dab12e703fdb3849014e5c23a70eff351a0baf4e",
+"version": "gcc 4.9.4 + PR64905",
+"size": 101297752,
+"digest": "42aa2e3fdd232b5e390472a788e7f7db71a1fee4221e260b6cb58c9a1d73e6cdd10afcbac137f7844290169cd6b561b424ecc92b159e9726b0ad5de3f478a8be",
 "algorithm": "sha512",
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "size": 12072532,
 "digest": "3915f8ec396c56a8a92e6f9695b70f09ce9d1582359d1258e37e3fd43a143bc974410e4cfc27f500e095f34a8956206e0ebf799b7287f0f38def0d5e34ed71c9",
 "algorithm": "sha512",
--- a/browser/config/tooltool-manifests/linux64/tsan.manifest
+++ b/browser/config/tooltool-manifests/linux64/tsan.manifest
@@ -1,13 +1,13 @@
 [
 {
-"version": "gcc 4.8.5 + PR64905",
-"size": 80160264,
-"digest": "c1a9dc9da289b8528874d16300b9d13a997cec99195bb0bc46ff665216d8535d6d6cb5af6b4b1f2749af6815dab12e703fdb3849014e5c23a70eff351a0baf4e",
+"version": "gcc 4.9.4 + PR64905",
+"size": 101297752,
+"digest": "42aa2e3fdd232b5e390472a788e7f7db71a1fee4221e260b6cb58c9a1d73e6cdd10afcbac137f7844290169cd6b561b424ecc92b159e9726b0ad5de3f478a8be",
 "algorithm": "sha512",
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "version": "clang 3.8.0, libgcc 4.8.5",
 "size": 139183100,
 "digest": "a056a151d4f25f415b6d905136c3fa8d51d12a5a815c3df37d5663c67d59571736641a4c990884a69f78ea6b5e37a6a7bfff0417dfe38936d842d6fa0776ae54",
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'aushelper',
     'e10srollout',
     'pdfjs',
     'pocket',
     'webcompat',
+    'shield-recipe-client',
 ]
 
 # Only include the following system add-ons if building Aurora or Nightly
 if 'a' in CONFIG['GRE_MILESTONE']:
     DIRS += [
         'flyweb',
         'formautofill',
         'presentation',
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/bootstrap.js
@@ -0,0 +1,102 @@
+/* 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 {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const REASONS = {
+  APP_STARTUP: 1,      // The application is starting up.
+  APP_SHUTDOWN: 2,     // The application is shutting down.
+  ADDON_ENABLE: 3,     // The add-on is being enabled.
+  ADDON_DISABLE: 4,    // The add-on is being disabled. (Also sent during uninstallation)
+  ADDON_INSTALL: 5,    // The add-on is being installed.
+  ADDON_UNINSTALL: 6,  // The add-on is being uninstalled.
+  ADDON_UPGRADE: 7,    // The add-on is being upgraded.
+  ADDON_DOWNGRADE: 8,  //The add-on is being downgraded.
+};
+
+const PREF_BRANCH = "extensions.shield-recipe-client.";
+const PREFS = {
+  api_url: "https://self-repair.mozilla.org/api/v1",
+  dev_mode: false,
+  enabled: true,
+  startup_delay_seconds: 300,
+};
+const PREF_DEV_MODE = "extensions.shield-recipe-client.dev_mode";
+const PREF_SELF_SUPPORT_ENABLED = "browser.selfsupport.enabled";
+
+let shouldRun = true;
+
+this.install = function() {
+  // Self Repair only checks its pref on start, so if we disable it, wait until
+  // next startup to run, unless the dev_mode preference is set.
+  if (Preferences.get(PREF_SELF_SUPPORT_ENABLED, true)) {
+    Preferences.set(PREF_SELF_SUPPORT_ENABLED, false);
+    if (!Services.prefs.getBoolPref(PREF_DEV_MODE, false)) {
+      shouldRun = false;
+    }
+  }
+};
+
+this.startup = function() {
+  setDefaultPrefs();
+
+  if (!shouldRun) {
+    return;
+  }
+
+  Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm");
+  RecipeRunner.init();
+};
+
+this.shutdown = function(data, reason) {
+  Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
+
+  CleanupManager.cleanup();
+
+  if (reason === REASONS.ADDON_DISABLE || reason === REASONS.ADDON_UNINSTALL) {
+    Services.prefs.setBoolPref(PREF_SELF_SUPPORT_ENABLED, true);
+  }
+
+  const modules = [
+    "data/EventEmitter.js",
+    "lib/CleanupManager.jsm",
+    "lib/EnvExpressions.jsm",
+    "lib/Heartbeat.jsm",
+    "lib/NormandyApi.jsm",
+    "lib/NormandyDriver.jsm",
+    "lib/RecipeRunner.jsm",
+    "lib/Sampling.jsm",
+    "lib/SandboxManager.jsm",
+    "lib/Storage.jsm",
+  ];
+  for (const module in modules) {
+    Cu.unload(`resource://shield-recipe-client/${module}`);
+  }
+};
+
+this.uninstall = function() {
+};
+
+function setDefaultPrefs() {
+  const branch = Services.prefs.getDefaultBranch(PREF_BRANCH);
+  for (const [key, val] of Object.entries(PREFS)) {
+    // If someone beat us to setting a default, don't overwrite it.
+    if (branch.getPrefType(key) !== branch.PREF_INVALID)
+      continue;
+    switch (typeof val) {
+      case "boolean":
+        branch.setBoolPref(key, val);
+        break;
+      case "number":
+        branch.setIntPref(key, val);
+        break;
+      case "string":
+        branch.setCharPref(key, val);
+        break;
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/data/EventEmitter.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is meant to run inside action sandboxes
+
+"use strict";
+
+
+this.EventEmitter = function(driver) {
+  if (!driver) {
+    throw new Error("driver must be provided");
+  }
+
+  const listeners = {};
+
+  return {
+    emit(eventName, event) {
+      // Fire events async
+      Promise.resolve()
+        .then(() => {
+          if (!(eventName in listeners)) {
+            driver.log(`EventEmitter: Event fired with no listeners: ${eventName}`);
+            return;
+          }
+          // freeze event to prevent handlers from modifying it
+          const frozenEvent = Object.freeze(event);
+          // Clone callbacks array to avoid problems with mutation while iterating
+          const callbacks = Array.from(listeners[eventName]);
+          for (const cb of callbacks) {
+            cb(frozenEvent);
+          }
+        });
+    },
+
+    on(eventName, callback) {
+      if (!(eventName in listeners)) {
+        listeners[eventName] = [];
+      }
+      listeners[eventName].push(callback);
+    },
+
+    off(eventName, callback) {
+      if (eventName in listeners) {
+        const index = listeners[eventName].indexOf(callback);
+        if (index !== -1) {
+          listeners[eventName].splice(index, 1);
+        }
+      }
+    },
+
+    once(eventName, callback) {
+      const inner = event => {
+        callback(event);
+        this.off(eventName, inner);
+      };
+      this.on(eventName, inner);
+    },
+  };
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/install.rdf.in
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>shield-recipe-client@mozilla.org</em:id>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+    <em:unpack>false</em:unpack>
+    <em:version>1.0.0</em:version>
+    <em:name>Shield Recipe Client</em:name>
+    <em:description>Client to download and run recipes for SHIELD, Heartbeat, etc.</em:description>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[features/shield-recipe-client@mozilla.org] chrome.jar:
+% resource shield-recipe-client %content/
+  content/lib/ (./lib/*)
+  content/data/ (./data/*)
+  content/node_modules/jexl/ (./node_modules/jexl/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/CleanupManager.jsm
@@ -0,0 +1,21 @@
+/* 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 = ["CleanupManager"];
+
+const cleanupHandlers = [];
+
+this.CleanupManager = {
+  addCleanupHandler(handler) {
+    cleanupHandlers.push(handler);
+  },
+
+  cleanup() {
+    for (const handler of cleanupHandlers) {
+      handler();
+    }
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/EnvExpressions.jsm
@@ -0,0 +1,65 @@
+/* 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 {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/TelemetryArchive.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://shield-recipe-client/lib/Sampling.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+this.EXPORTED_SYMBOLS = ["EnvExpressions"];
+
+XPCOMUtils.defineLazyGetter(this, "nodeRequire", () => {
+  const {Loader, Require} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+  const loader = new Loader({
+    paths: {
+      "": "resource://shield-recipe-client/node_modules/",
+    },
+  });
+  return new Require(loader, {});
+});
+
+XPCOMUtils.defineLazyGetter(this, "jexl", () => {
+  const {Jexl} = nodeRequire("jexl/lib/Jexl.js");
+  const jexl = new Jexl();
+  jexl.addTransforms({
+    date: dateString => new Date(dateString),
+    stableSample: Sampling.stableSample,
+  });
+  return jexl;
+});
+
+const getLatestTelemetry = Task.async(function *() {
+  const pings = yield TelemetryArchive.promiseArchivedPingList();
+
+  // get most recent ping per type
+  const mostRecentPings = {};
+  for (const ping of pings) {
+    if (ping.type in mostRecentPings) {
+      if (mostRecentPings[ping.type].timeStampCreated < ping.timeStampCreated) {
+        mostRecentPings[ping.type] = ping;
+      }
+    } else {
+      mostRecentPings[ping.type] = ping;
+    }
+  }
+
+  const telemetry = {};
+  for (const key in mostRecentPings) {
+    const ping = mostRecentPings[key];
+    telemetry[ping.type] = yield TelemetryArchive.promiseArchivedPingById(ping.id);
+  }
+  return telemetry;
+});
+
+this.EnvExpressions = {
+  eval(expr, extraContext = {}) {
+    const context = Object.assign({telemetry: getLatestTelemetry()}, extraContext);
+    const onelineExpr = expr.replace(/[\t\n\r]/g, " ");
+    return jexl.eval(onelineExpr, context);
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
@@ -0,0 +1,346 @@
+/* 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 {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm");
+Cu.import("resource://gre/modules/Timer.jsm"); /* globals setTimeout, clearTimeout */
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
+
+Cu.importGlobalProperties(["URL"]); /* globals URL */
+
+this.EXPORTED_SYMBOLS = ["Heartbeat"];
+
+const log = Log.repository.getLogger("shield-recipe-client");
+const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration";
+const NOTIFICATION_TIME = 3000;
+
+/**
+ * Show the Heartbeat UI to request user feedback.
+ *
+ * @param chromeWindow
+ *        The chrome window that the heartbeat notification is displayed in.
+ * @param eventEmitter
+ *        An EventEmitter instance to report status to.
+ * @param sandboxManager
+ *        The manager for the sandbox this was called from. Heartbeat will
+ *        increment the hold counter on the manager.
+ * @param {Object} options Options object.
+ * @param {String} options.message
+ *        The message, or question, to display on the notification.
+ * @param {String} options.thanksMessage
+ *        The thank you message to display after user votes.
+ * @param {String} options.flowId
+ *        An identifier for this rating flow. Please note that this is only used to
+ *        identify the notification box.
+ * @param {String} [options.engagementButtonLabel=null]
+ *        The text of the engagement button to use instad of stars. If this is null
+ *        or invalid, rating stars are used.
+ * @param {String} [options.learnMoreMessage=null]
+ *        The label of the learn more link. No link will be shown if this is null.
+ * @param {String} [options.learnMoreUrl=null]
+ *        The learn more URL to open when clicking on the learn more link. No learn more
+ *        will be shown if this is an invalid URL.
+ * @param {String} [options.surveyId]
+ *        An ID for the survey, reflected in the Telemetry ping.
+ * @param {Number} [options.surveyVersion]
+ *        Survey's version number, reflected in the Telemetry ping.
+ * @param {boolean} [options.testing]
+ *        Whether this is a test survey, reflected in the Telemetry ping.
+ * @param {String} [options.postAnswerURL=null]
+ *        The url to visit after the user answers the question.
+ */
+this.Heartbeat = class {
+  constructor(chromeWindow, eventEmitter, sandboxManager, options) {
+    if (typeof options.flowId !== "string") {
+      throw new Error("flowId must be a string");
+    }
+
+    if (!options.flowId) {
+      throw new Error("flowId must not be an empty string");
+    }
+
+    if (typeof options.message !== "string") {
+      throw new Error("message must be a string");
+    }
+
+    if (!options.message) {
+      throw new Error("message must not be an empty string");
+    }
+
+    if (!sandboxManager) {
+      throw new Error("sandboxManager must be provided");
+    }
+
+    if (options.postAnswerUrl) {
+      options.postAnswerUrl = new URL(options.postAnswerUrl);
+    } else {
+      options.postAnswerUrl = null;
+    }
+
+    if (options.learnMoreUrl) {
+      try {
+        options.learnMoreUrl = new URL(options.learnMoreUrl);
+      } catch (e) {
+        options.learnMoreUrl = null;
+      }
+    }
+
+    this.chromeWindow = chromeWindow;
+    this.eventEmitter = eventEmitter;
+    this.sandboxManager = sandboxManager;
+    this.options = options;
+    this.surveyResults = {};
+    this.buttons = null;
+
+    // so event handlers are consistent
+    this.handleWindowClosed = this.handleWindowClosed.bind(this);
+
+    if (this.options.engagementButtonLabel) {
+      this.buttons = [{
+        label: this.options.engagementButtonLabel,
+        callback: () => {
+          // Let the consumer know user engaged.
+          this.maybeNotifyHeartbeat("Engaged");
+
+          this.userEngaged({
+            type: "button",
+            flowId: this.options.flowId,
+          });
+
+          // Return true so that the notification bar doesn't close itself since
+          // we have a thank you message to show.
+          return true;
+        },
+      }];
+    }
+
+    this.notificationBox = this.chromeWindow.document.querySelector("#high-priority-global-notificationbox");
+    this.notice = this.notificationBox.appendNotification(
+      this.options.message,
+      "heartbeat-" + this.options.flowId,
+      "chrome://browser/skin/heartbeat-icon.svg",
+      this.notificationBox.PRIORITY_INFO_HIGH,
+      this.buttons,
+      eventType => {
+        if (eventType !== "removed") {
+          return;
+        }
+        this.maybeNotifyHeartbeat("NotificationClosed");
+      }
+    );
+
+    // Holds the rating UI
+    const frag = this.chromeWindow.document.createDocumentFragment();
+
+    // Build the heartbeat stars
+    if (!this.options.engagementButtonLabel) {
+      const numStars = this.options.engagementButtonLabel ? 0 : 5;
+      const ratingContainer = this.chromeWindow.document.createElement("hbox");
+      ratingContainer.id = "star-rating-container";
+
+      for (let i = 0; i < numStars; i++) {
+        // create a star rating element
+        const ratingElement = this.chromeWindow.document.createElement("toolbarbutton");
+
+        // style it
+        const starIndex = numStars - i;
+        ratingElement.className = "plain star-x";
+        ratingElement.id = "star" + starIndex;
+        ratingElement.setAttribute("data-score", starIndex);
+
+        // Add the click handler
+        ratingElement.addEventListener("click", ev => {
+          const rating = parseInt(ev.target.getAttribute("data-score"));
+          this.maybeNotifyHeartbeat("Voted", {score: rating});
+          this.userEngaged({type: "stars", score: rating, flowId: this.options.flowId});
+        });
+
+        ratingContainer.appendChild(ratingElement);
+      }
+
+      frag.appendChild(ratingContainer);
+    }
+
+    this.messageImage = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageImage");
+    this.messageImage.classList.add("heartbeat", "pulse-onshow");
+
+    this.messageText = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageText");
+    this.messageText.classList.add("heartbeat");
+
+    // Make sure the stars are not pushed to the right by the spacer.
+    const rightSpacer = this.chromeWindow.document.createElement("spacer");
+    rightSpacer.flex = 20;
+    frag.appendChild(rightSpacer);
+
+    // collapse the space before the stars
+    this.messageText.flex = 0;
+    const leftSpacer = this.messageText.nextSibling;
+    leftSpacer.flex = 0;
+
+    // Add Learn More Link
+    if (this.options.learnMoreMessage && this.options.learnMoreUrl) {
+      const learnMore = this.chromeWindow.document.createElement("label");
+      learnMore.className = "text-link";
+      learnMore.href = this.options.learnMoreUrl.toString();
+      learnMore.setAttribute("value", this.options.learnMoreMessage);
+      learnMore.addEventListener("click", () => this.maybeNotifyHeartbeat("LearnMore"));
+      frag.appendChild(learnMore);
+    }
+
+    // Append the fragment and apply the styling
+    this.notice.appendChild(frag);
+    this.notice.classList.add("heartbeat");
+
+    // Let the consumer know the notification was shown.
+    this.maybeNotifyHeartbeat("NotificationOffered");
+    this.chromeWindow.addEventListener("SSWindowClosing", this.handleWindowClosed);
+
+    const surveyDuration = Preferences.get(PREF_SURVEY_DURATION, 300) * 1000;
+    this.surveyEndTimer = setTimeout(() => {
+      this.maybeNotifyHeartbeat("SurveyExpired");
+      this.close();
+    }, surveyDuration);
+
+    this.sandboxManager.addHold("heartbeat");
+    CleanupManager.addCleanupHandler(() => this.close());
+  }
+
+  maybeNotifyHeartbeat(name, data = {}) {
+    if (this.pingSent) {
+      log.warn("Heartbeat event recieved after Telemetry ping sent. name:", name, "data:", data);
+      return;
+    }
+
+    const timestamp = Date.now();
+    let sendPing = false;
+    let cleanup = false;
+
+    const phases = {
+      NotificationOffered: () => {
+        this.surveyResults.flowId = this.options.flowId;
+        this.surveyResults.offeredTS = timestamp;
+      },
+      LearnMore: () => {
+        if (!this.surveyResults.learnMoreTS) {
+          this.surveyResults.learnMoreTS = timestamp;
+        }
+      },
+      Engaged: () => {
+        this.surveyResults.engagedTS = timestamp;
+      },
+      Voted: () => {
+        this.surveyResults.votedTS = timestamp;
+        this.surveyResults.score = data.score;
+      },
+      SurveyExpired: () => {
+        this.surveyResults.expiredTS = timestamp;
+      },
+      NotificationClosed: () => {
+        this.surveyResults.closedTS = timestamp;
+        cleanup = true;
+        sendPing = true;
+      },
+      WindowClosed: () => {
+        this.surveyResults.windowClosedTS = timestamp;
+        cleanup = true;
+        sendPing = true;
+      },
+      default: () => {
+        log.error("Unrecognized Heartbeat event:", name);
+      },
+    };
+
+    (phases[name] || phases.default)();
+
+    data.timestamp = timestamp;
+    data.flowId = this.options.flowId;
+    this.eventEmitter.emit(name, Cu.cloneInto(data, this.sandboxManager.sandbox));
+
+    if (sendPing) {
+      // Send the ping to Telemetry
+      const payload = Object.assign({version: 1}, this.surveyResults);
+      for (const meta of ["surveyId", "surveyVersion", "testing"]) {
+        if (this.options.hasOwnProperty(meta)) {
+          payload[meta] = this.options[meta];
+        }
+      }
+
+      log.debug("Sending telemetry");
+      TelemetryController.submitExternalPing("heartbeat", payload, {
+        addClientId: true,
+        addEnvironment: true,
+      });
+
+      // only for testing
+      this.eventEmitter.emit("TelemetrySent", Cu.cloneInto(payload, this.sandboxManager.sandbox));
+
+      // Survey is complete, clear out the expiry timer & survey configuration
+      if (this.surveyEndTimer) {
+        clearTimeout(this.surveyEndTimer);
+        this.surveyEndTimer = null;
+      }
+
+      this.pingSent = true;
+      this.surveyResults = null;
+    }
+
+    if (cleanup) {
+      this.cleanup();
+    }
+  }
+
+  userEngaged(engagementParams) {
+    // Make the heartbeat icon pulse twice
+    this.notice.label = this.options.thanksMessage;
+    this.messageImage.classList.remove("pulse-onshow");
+    this.messageImage.classList.add("pulse-twice");
+
+    // Remove all the children of the notice (rating container, and the flex)
+    while (this.notice.firstChild) {
+      this.notice.firstChild.remove();
+    }
+
+    // Open the engagement tab if we have a valid engagement URL.
+    if (this.options.postAnswerUrl) {
+      for (const key in engagementParams) {
+        this.options.postAnswerUrl.searchParams.append(key, engagementParams[key]);
+      }
+      // Open the engagement URL in a new tab.
+      this.chromeWindow.gBrowser.selectedTab = this.chromeWindow.gBrowser.addTab(this.options.postAnswerUrl.toString());
+    }
+
+    if (this.surveyEndTimer) {
+      clearTimeout(this.surveyEndTimer);
+      this.surveyEndTimer = null;
+    }
+
+    setTimeout(() => this.close(), NOTIFICATION_TIME);
+  }
+
+  handleWindowClosed() {
+    this.maybeNotifyHeartbeat("WindowClosed");
+  }
+
+  close() {
+    this.notificationBox.removeNotification(this.notice);
+    this.cleanup();
+  }
+
+  cleanup() {
+    this.sandboxManager.removeHold("heartbeat");
+    // remove listeners
+    this.chromeWindow.removeEventListener("SSWindowClosing", this.handleWindowClosed);
+    // remove references for garbage collection
+    this.chromeWindow = null;
+    this.notificationBox = null;
+    this.notification = null;
+    this.eventEmitter = null;
+    this.sandboxManager = null;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/NormandyApi.jsm
@@ -0,0 +1,99 @@
+/* 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/. */
+/* globals URLSearchParams */
+
+"use strict";
+
+const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/CanonicalJSON.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+this.EXPORTED_SYMBOLS = ["NormandyApi"];
+
+const log = Log.repository.getLogger("extensions.shield-recipe-client");
+const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
+
+this.NormandyApi = {
+  apiCall(method, endpoint, data = {}) {
+    const api_url = prefs.getCharPref("api_url");
+    let url = `${api_url}/${endpoint}`;
+    method = method.toLowerCase();
+
+    if (method === "get") {
+      if (data === {}) {
+        const paramObj = new URLSearchParams();
+        for (const key in data) {
+          paramObj.append(key, data[key]);
+        }
+        url += "?" + paramObj.toString();
+      }
+      data = undefined;
+    }
+
+    const headers = {"Accept": "application/json"};
+    return fetch(url, {
+      body: JSON.stringify(data),
+      headers,
+    });
+  },
+
+  get(endpoint, data) {
+    return this.apiCall("get", endpoint, data);
+  },
+
+  post(endpoint, data) {
+    return this.apiCall("post", endpoint, data);
+  },
+
+  fetchRecipes: Task.async(function* (filters = {}) {
+    const recipeResponse = yield this.get("recipe/signed/", filters);
+    const rawText = yield recipeResponse.text();
+    const recipesWithSigs = JSON.parse(rawText);
+
+    const verifiedRecipes = [];
+
+    for (const {recipe, signature: {signature, x5u}} of recipesWithSigs) {
+      const serialized = CanonicalJSON.stringify(recipe);
+      if (!rawText.includes(serialized)) {
+        log.debug(rawText, serialized);
+        throw new Error("Canonical recipe serialization does not match!");
+      }
+
+      const certChainResponse = yield fetch(x5u);
+      const certChain = yield certChainResponse.text();
+      const builtSignature = `p384ecdsa=${signature}`;
+
+      const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
+        .createInstance(Ci.nsIContentSignatureVerifier);
+
+      if (!verifier.verifyContentSignature(serialized, builtSignature, certChain, "normandy.content-signature.mozilla.org")) {
+        throw new Error("Recipe signature is not valid");
+      }
+      verifiedRecipes.push(recipe);
+    }
+
+    log.debug(`Fetched ${verifiedRecipes.length} recipes from the server:`, verifiedRecipes.map(r => r.name).join(", "));
+
+    return verifiedRecipes;
+  }),
+
+  /**
+   * Fetch metadata about this client determined by the server.
+   * @return {object} Metadata specified by the server
+   */
+  classifyClient() {
+    return this.get("classify_client/")
+      .then(response => response.json())
+      .then(clientData => {
+        clientData.request_time = new Date(clientData.request_time);
+        return clientData;
+      });
+  },
+
+  fetchAction(name) {
+    return this.get(`action/${name}/`).then(req => req.json());
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
@@ -0,0 +1,141 @@
+/* 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";
+/* globals Components */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource:///modules/ShellService.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Timer.jsm"); /* globals setTimeout, clearTimeout */
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://shield-recipe-client/lib/Storage.jsm");
+Cu.import("resource://shield-recipe-client/lib/Heartbeat.jsm");
+
+const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+this.EXPORTED_SYMBOLS = ["NormandyDriver"];
+
+const log = Log.repository.getLogger("extensions.shield-recipe-client");
+const actionLog = Log.repository.getLogger("extensions.shield-recipe-client.actions");
+
+this.NormandyDriver = function(sandboxManager, extraContext = {}) {
+  if (!sandboxManager) {
+    throw new Error("sandboxManager is required");
+  }
+  const {sandbox} = sandboxManager;
+
+  return {
+    testing: false,
+
+    get locale() {
+      return Cc["@mozilla.org/chrome/chrome-registry;1"]
+        .getService(Ci.nsIXULChromeRegistry)
+        .getSelectedLocale("browser");
+    },
+
+    log(message, level = "debug") {
+      const levels = ["debug", "info", "warn", "error"];
+      if (levels.indexOf(level) === -1) {
+        throw new Error(`Invalid log level "${level}"`);
+      }
+      actionLog[level](message);
+    },
+
+    showHeartbeat(options) {
+      log.info(`Showing heartbeat prompt "${options.message}"`);
+      const aWindow = Services.wm.getMostRecentWindow("navigator:browser");
+
+      if (!aWindow) {
+        return sandbox.Promise.reject(new sandbox.Error("No window to show heartbeat in"));
+      }
+
+      const sandboxedDriver = Cu.cloneInto(this, sandbox, {cloneFunctions: true});
+      const ee = new sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
+      const internalOptions = Object.assign({}, options, {testing: this.testing});
+      new Heartbeat(aWindow, ee, sandboxManager, internalOptions);
+      return sandbox.Promise.resolve(ee);
+    },
+
+    saveHeartbeatFlow() {
+      // no-op required by spec
+    },
+
+    client() {
+      const appinfo = {
+        version: Services.appinfo.version,
+        channel: Services.appinfo.defaultUpdateChannel,
+        isDefaultBrowser: ShellService.isDefaultBrowser() || null,
+        searchEngine: null,
+        syncSetup: Preferences.isSet("services.sync.username"),
+        plugins: {},
+        doNotTrack: Preferences.get("privacy.donottrackheader.enabled", false),
+      };
+
+      const searchEnginePromise = new Promise(resolve => {
+        Services.search.init(rv => {
+          if (Components.isSuccessCode(rv)) {
+            appinfo.searchEngine = Services.search.defaultEngine.identifier;
+          }
+          resolve();
+        });
+      });
+
+      const pluginsPromise = new Promise(resolve => {
+        AddonManager.getAddonsByTypes(["plugin"], plugins => {
+          plugins.forEach(plugin => appinfo.plugins[plugin.name] = plugin);
+          resolve();
+        });
+      });
+
+      return new sandbox.Promise(resolve => {
+        Promise.all([searchEnginePromise, pluginsPromise]).then(() => {
+          resolve(Cu.cloneInto(appinfo, sandbox));
+        });
+      });
+    },
+
+    uuid() {
+      let ret = generateUUID().toString();
+      ret = ret.slice(1, ret.length - 1);
+      return ret;
+    },
+
+    createStorage(keyPrefix) {
+      let storage;
+      try {
+        storage = Storage.makeStorage(keyPrefix, sandbox);
+      } catch (e) {
+        log.error(e.stack);
+        throw e;
+      }
+      return storage;
+    },
+
+    location() {
+      const location = Cu.cloneInto({countryCode: extraContext.country}, sandbox);
+      return sandbox.Promise.resolve(location);
+    },
+
+    setTimeout(cb, time) {
+      if (typeof cb !== "function") {
+        throw new sandbox.Error(`setTimeout must be called with a function, got "${typeof cb}"`);
+      }
+      const token = setTimeout(() => {
+        cb();
+        sandboxManager.removeHold(`setTimeout-${token}`);
+      }, time);
+      sandboxManager.addHold(`setTimeout-${token}`);
+      return Cu.cloneInto(token, sandbox);
+    },
+
+    clearTimeout(token) {
+      clearTimeout(token);
+      sandboxManager.removeHold(`setTimeout-${token}`);
+    },
+  };
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm
@@ -0,0 +1,162 @@
+/* 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 {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm"); /* globals setTimeout */
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm");
+Cu.import("resource://shield-recipe-client/lib/EnvExpressions.jsm");
+Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm");
+Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm");
+Cu.importGlobalProperties(["fetch"]); /* globals fetch */
+
+this.EXPORTED_SYMBOLS = ["RecipeRunner"];
+
+const log = Log.repository.getLogger("extensions.shield-recipe-client");
+const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
+
+this.RecipeRunner = {
+  init() {
+    if (!this.checkPrefs()) {
+      return;
+    }
+
+    let delay;
+    if (prefs.getBoolPref("dev_mode")) {
+      delay = 0;
+    } else {
+      // startup delay is in seconds
+      delay = prefs.getIntPref("startup_delay_seconds") * 1000;
+    }
+
+    setTimeout(this.start.bind(this), delay);
+  },
+
+  checkPrefs() {
+    // Only run if Unified Telemetry is enabled.
+    if (!Services.prefs.getBoolPref("toolkit.telemetry.unified")) {
+      log.info("Disabling RecipeRunner because Unified Telemetry is disabled.");
+      return false;
+    }
+
+    if (!prefs.getBoolPref("enabled")) {
+      log.info("Recipe Client is disabled.");
+      return false;
+    }
+
+    const apiUrl = prefs.getCharPref("api_url");
+    if (!apiUrl || !apiUrl.startsWith("https://")) {
+      log.error(`Non HTTPS URL provided for extensions.shield-recipe-client.api_url: ${apiUrl}`);
+      return false;
+    }
+
+    return true;
+  },
+
+  start: Task.async(function* () {
+    let recipes;
+    try {
+      recipes = yield NormandyApi.fetchRecipes({enabled: true});
+    } catch (e) {
+      const apiUrl = prefs.getCharPref("api_url");
+      log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
+      return;
+    }
+
+    let extraContext;
+    try {
+      extraContext = yield this.getExtraContext();
+    } catch (e) {
+      log.warning(`Couldn't get extra filter context: ${e}`);
+      extraContext = {};
+    }
+
+    const recipesToRun = [];
+
+    for (const recipe of recipes) {
+      if (yield this.checkFilter(recipe, extraContext)) {
+        recipesToRun.push(recipe);
+      }
+    }
+
+    if (recipesToRun.length === 0) {
+      log.debug("No recipes to execute");
+    } else {
+      for (const recipe of recipesToRun) {
+        try {
+          log.debug(`Executing recipe "${recipe.name}" (action=${recipe.action})`);
+          yield this.executeRecipe(recipe, extraContext);
+        } catch (e) {
+          log.error(`Could not execute recipe ${recipe.name}:`, e);
+        }
+      }
+    }
+  }),
+
+  getExtraContext() {
+    return NormandyApi.classifyClient()
+      .then(clientData => ({normandy: clientData}));
+  },
+
+  /**
+   * Evaluate a recipe's filter expression against the environment.
+   * @param {object} recipe
+   * @param {string} recipe.filter The expression to evaluate against the environment.
+   * @param {object} extraContext Any extra context to provide to the filter environment.
+   * @return {boolean} The result of evaluating the filter, cast to a bool.
+   */
+  checkFilter(recipe, extraContext) {
+    return EnvExpressions.eval(recipe.filter_expression, extraContext)
+      .then(result => {
+        return !!result;
+      })
+      .catch(error => {
+        log.error(`Error checking filter for "${recipe.name}"`);
+        log.error(`Filter: "${recipe.filter_expression}"`);
+        log.error(`Error: "${error}"`);
+      });
+  },
+
+  /**
+   * Execute a recipe by fetching it action and executing it.
+   * @param  {Object} recipe A recipe to execute
+   * @promise Resolves when the action has executed
+   */
+  executeRecipe: Task.async(function* (recipe, extraContext) {
+    const sandboxManager = new SandboxManager();
+    const {sandbox} = sandboxManager;
+
+    const action = yield NormandyApi.fetchAction(recipe.action);
+    const response = yield fetch(action.implementation_url);
+
+    const actionScript = yield response.text();
+    const prepScript = `
+      var pendingAction = null;
+
+      function registerAction(name, Action) {
+        let a = new Action(sandboxedDriver, sandboxedRecipe);
+        pendingAction = a.execute()
+          .catch(err => sandboxedDriver.log(err, 'error'));
+      };
+
+      window.registerAction = registerAction;
+      window.setTimeout = sandboxedDriver.setTimeout;
+      window.clearTimeout = sandboxedDriver.clearTimeout;
+    `;
+
+    const driver = new NormandyDriver(sandboxManager, extraContext);
+    sandbox.sandboxedDriver = Cu.cloneInto(driver, sandbox, {cloneFunctions: true});
+    sandbox.sandboxedRecipe = Cu.cloneInto(recipe, sandbox);
+
+    Cu.evalInSandbox(prepScript, sandbox);
+    Cu.evalInSandbox(actionScript, sandbox);
+
+    sandboxManager.addHold("recipeExecution");
+    sandbox.pendingAction.then(() => sandboxManager.removeHold("recipeExecution"));
+  }),
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/Sampling.jsm
@@ -0,0 +1,81 @@
+/* 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 {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.importGlobalProperties(["crypto", "TextEncoder"]);
+
+this.EXPORTED_SYMBOLS = ["Sampling"];
+
+const log = Log.repository.getLogger("extensions.shield-recipe-client");
+
+/**
+ * Map from the range [0, 1] to [0, max(sha256)].
+ * @param  {number} frac A float from 0.0 to 1.0.
+ * @return {string} A 48 bit number represented in hex, padded to 12 characters.
+ */
+function fractionToKey(frac) {
+  const hashBits = 48;
+  const hashLength = hashBits / 4;
+
+  if (frac < 0 || frac > 1) {
+    throw new Error(`frac must be between 0 and 1 inclusive (got ${frac})`);
+  }
+
+  const mult = Math.pow(2, hashBits) - 1;
+  const inDecimal = Math.floor(frac * mult);
+  let hexDigits = inDecimal.toString(16);
+  if (hexDigits.length < hashLength) {
+    // Left pad with zeroes
+    // If N zeroes are needed, generate an array of nulls N+1 elements long,
+    // and inserts zeroes between each null.
+    hexDigits = Array(hashLength - hexDigits.length + 1).join("0") + hexDigits;
+  }
+
+  // Saturate at 2**48 - 1
+  if (hexDigits.length > hashLength) {
+    hexDigits = Array(hashLength + 1).join("f");
+  }
+
+  return hexDigits;
+}
+
+function bufferToHex(buffer) {
+  const hexCodes = [];
+  const view = new DataView(buffer);
+  for (let i = 0; i < view.byteLength; i += 4) {
+    // Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
+    const value = view.getUint32(i);
+    // toString(16) will give the hex representation of the number without padding
+    hexCodes.push(value.toString(16).padStart(8, "0"));
+  }
+
+  // Join all the hex strings into one
+  return hexCodes.join("");
+}
+
+this.Sampling = {
+  stableSample(input, rate) {
+    const hasher = crypto.subtle;
+
+    return hasher.digest("SHA-256", new TextEncoder("utf-8").encode(JSON.stringify(input)))
+      .then(hash => {
+        // truncate hash to 12 characters (2^48)
+        const inputHash = bufferToHex(hash).slice(0, 12);
+        const samplePoint = fractionToKey(rate);
+
+        if (samplePoint.length !== 12 || inputHash.length !== 12) {
+          throw new Error("Unexpected hash length");
+        }
+
+        return inputHash < samplePoint;
+
+      })
+      .catch(error => {
+        log.error(`Error: ${error}`);
+      });
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/SandboxManager.jsm
@@ -0,0 +1,65 @@
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["SandboxManager"];
+
+this.SandboxManager = class {
+  constructor() {
+    this._sandbox = makeSandbox();
+    this.holds = [];
+  }
+
+  get sandbox() {
+    if (this._sandbox) {
+      return this._sandbox;
+    }
+    throw new Error("Tried to use sandbox after it was nuked");
+  }
+
+  addHold(name) {
+    this.holds.push(name);
+  }
+
+  removeHold(name) {
+    const index = this.holds.indexOf(name);
+    if (index === -1) {
+      throw new Error(`Tried to remove non-existant hold "${name}"`);
+    }
+    this.holds.splice(index, 1);
+    this.tryCleanup();
+  }
+
+  tryCleanup() {
+    if (this.holds.length === 0) {
+      const sandbox = this._sandbox;
+      this._sandbox = null;
+      Cu.nukeSandbox(sandbox);
+    }
+  }
+
+  isNuked() {
+    // Do this in a promise, so other async things can resolve.
+    return new Promise((resolve, reject) => {
+      if (!this._sandbox) {
+        resolve();
+      } else {
+        reject(new Error(`Sandbox is not nuked. Holds left: ${this.holds}`));
+      }
+    });
+  }
+};
+
+
+function makeSandbox() {
+  const sandbox = new Cu.Sandbox(null, {
+    wantComponents: false,
+    wantGlobalProperties: ["URL", "URLSearchParams"],
+  });
+
+  sandbox.window = Cu.cloneInto({}, sandbox);
+
+  const url = "resource://shield-recipe-client/data/EventEmitter.js";
+  Services.scriptloader.loadSubScript(url, sandbox);
+
+  return sandbox;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/Storage.jsm
@@ -0,0 +1,134 @@
+/* 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 {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile", "resource://gre/modules/JSONFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+
+this.EXPORTED_SYMBOLS = ["Storage"];
+
+const log = Log.repository.getLogger("extensions.shield-recipe-client");
+let storePromise;
+
+function loadStorage() {
+  if (storePromise === undefined) {
+    const path = OS.Path.join(OS.Constants.Path.profileDir, "shield-recipe-client.json");
+    const storage = new JSONFile({path});
+    storePromise = Task.spawn(function* () {
+      yield storage.load();
+      return storage;
+    });
+  }
+  return storePromise;
+}
+
+this.Storage = {
+  makeStorage(prefix, sandbox) {
+    if (!sandbox) {
+      throw new Error("No sandbox passed");
+    }
+
+    const storageInterface = {
+      /**
+       * Sets an item in the prefixed storage.
+       * @returns {Promise}
+       * @resolves With the stored value, or null.
+       * @rejects Javascript exception.
+       */
+      getItem(keySuffix) {
+        return new sandbox.Promise((resolve, reject) => {
+          loadStorage()
+            .then(store => {
+              const namespace = store.data[prefix] || {};
+              const value = namespace[keySuffix] || null;
+              resolve(Cu.cloneInto(value, sandbox));
+            })
+            .catch(err => {
+              log.error(err);
+              reject(new sandbox.Error());
+            });
+        });
+      },
+
+      /**
+       * Sets an item in the prefixed storage.
+       * @returns {Promise}
+       * @resolves When the operation is completed succesfully
+       * @rejects Javascript exception.
+       */
+      setItem(keySuffix, value) {
+        return new sandbox.Promise((resolve, reject) => {
+          loadStorage()
+            .then(store => {
+              if (!(prefix in store.data)) {
+                store.data[prefix] = {};
+              }
+              store.data[prefix][keySuffix] = value;
+              store.saveSoon();
+              resolve();
+            })
+            .catch(err => {
+              log.error(err);
+              reject(new sandbox.Error());
+            });
+        });
+      },
+
+      /**
+       * Removes a single item from the prefixed storage.
+       * @returns {Promise}
+       * @resolves When the operation is completed succesfully
+       * @rejects Javascript exception.
+       */
+      removeItem(keySuffix) {
+        return new sandbox.Promise((resolve, reject) => {
+          loadStorage()
+            .then(store => {
+              if (!(prefix in store.data)) {
+                return;
+              }
+              delete store.data[prefix][keySuffix];
+              store.saveSoon();
+              resolve();
+            })
+            .catch(err => {
+              log.error(err);
+              reject(new sandbox.Error());
+            });
+        });
+      },
+
+      /**
+       * Clears all storage for the prefix.
+       * @returns {Promise}
+       * @resolves When the operation is completed succesfully
+       * @rejects Javascript exception.
+       */
+      clear() {
+        return new sandbox.Promise((resolve, reject) => {
+          return loadStorage()
+            .then(store => {
+              store.data[prefix] = {};
+              store.saveSoon();
+              resolve();
+            })
+            .catch(err => {
+              log.error(err);
+              reject(new sandbox.Error());
+            });
+        });
+      },
+    };
+
+    return Cu.cloneInto(storageInterface, sandbox, {
+      cloneFunctions: true,
+    });
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['shield-recipe-client@mozilla.org'] += [
+  'bootstrap.js',
+]
+
+FINAL_TARGET_PP_FILES.features['shield-recipe-client@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+BROWSER_CHROME_MANIFESTS += [
+    'test/browser.ini',
+]
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2015 TechnologyAdvice
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js
@@ -0,0 +1,225 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+var Evaluator = require('./evaluator/Evaluator'),
+	Lexer = require('./Lexer'),
+	Parser = require('./parser/Parser'),
+	defaultGrammar = require('./grammar').elements;
+
+/**
+ * Jexl is the Javascript Expression Language, capable of parsing and
+ * evaluating basic to complex expression strings, combined with advanced
+ * xpath-like drilldown into native Javascript objects.
+ * @constructor
+ */
+function Jexl() {
+	this._customGrammar = null;
+	this._lexer = null;
+	this._transforms = {};
+}
+
+/**
+ * Adds a binary operator to Jexl at the specified precedence. The higher the
+ * precedence, the earlier the operator is applied in the order of operations.
+ * For example, * has a higher precedence than +, because multiplication comes
+ * before division.
+ *
+ * Please see grammar.js for a listing of all default operators and their
+ * precedence values in order to choose the appropriate precedence for the
+ * new operator.
+ * @param {string} operator The operator string to be added
+ * @param {number} precedence The operator's precedence
+ * @param {function} fn A function to run to calculate the result. The function
+ *      will be called with two arguments: left and right, denoting the values
+ *      on either side of the operator. It should return either the resulting
+ *      value, or a Promise that resolves with the resulting value.
+ */
+Jexl.prototype.addBinaryOp = function(operator, precedence, fn) {
+	this._addGrammarElement(operator, {
+		type: 'binaryOp',
+		precedence: precedence,
+		eval: fn
+	});
+};
+
+/**
+ * Adds a unary operator to Jexl. Unary operators are currently only supported
+ * on the left side of the value on which it will operate.
+ * @param {string} operator The operator string to be added
+ * @param {function} fn A function to run to calculate the result. The function
+ *      will be called with one argument: the literal value to the right of the
+ *      operator. It should return either the resulting value, or a Promise
+ *      that resolves with the resulting value.
+ */
+Jexl.prototype.addUnaryOp = function(operator, fn) {
+	this._addGrammarElement(operator, {
+		type: 'unaryOp',
+		weight: Infinity,
+		eval: fn
+	});
+};
+
+/**
+ * Adds or replaces a transform function in this Jexl instance.
+ * @param {string} name The name of the transform function, as it will be used
+ *      within Jexl expressions
+ * @param {function} fn The function to be executed when this transform is
+ *      invoked.  It will be provided with two arguments:
+ *          - {*} value: The value to be transformed
+ *          - {{}} args: The arguments for this transform
+ *          - {function} cb: A callback function to be called with an error
+ *            if the transform fails, or a null first argument and the
+ *            transformed value as the second argument on success.
+ */
+Jexl.prototype.addTransform = function(name, fn) {
+	this._transforms[name] = fn;
+};
+
+/**
+ * Syntactic sugar for calling {@link #addTransform} repeatedly.  This function
+ * accepts a map of one or more transform names to their transform function.
+ * @param {{}} map A map of transform names to transform functions
+ */
+Jexl.prototype.addTransforms = function(map) {
+	for (var key in map) {
+		if (map.hasOwnProperty(key))
+			this._transforms[key] = map[key];
+	}
+};
+
+/**
+ * Retrieves a previously set transform function.
+ * @param {string} name The name of the transform function
+ * @returns {function} The transform function
+ */
+Jexl.prototype.getTransform = function(name) {
+	return this._transforms[name];
+};
+
+/**
+ * Evaluates a Jexl string within an optional context.
+ * @param {string} expression The Jexl expression to be evaluated
+ * @param {Object} [context] A mapping of variables to values, which will be
+ *      made accessible to the Jexl expression when evaluating it
+ * @param {function} [cb] An optional callback function to be executed when
+ *      evaluation is complete.  It will be supplied with two arguments:
+ *          - {Error|null} err: Present if an error occurred
+ *          - {*} result: The result of the evaluation
+ * @returns {Promise<*>} resolves with the result of the evaluation.  Note that
+ *      if a callback is supplied, the returned promise will already have
+ *      a '.catch' attached to it in order to pass the error to the callback.
+ */
+Jexl.prototype.eval = function(expression, context, cb) {
+	if (typeof context === 'function') {
+		cb = context;
+		context = {};
+	}
+	else if (!context)
+		context = {};
+	var valPromise = this._eval(expression, context);
+	if (cb) {
+		// setTimeout is used for the callback to break out of the Promise's
+		// try/catch in case the callback throws.
+		var called = false;
+		return valPromise.then(function(val) {
+			called = true;
+			setTimeout(cb.bind(null, null, val), 0);
+		}).catch(function(err) {
+			if (!called)
+				setTimeout(cb.bind(null, err), 0);
+		});
+	}
+	return valPromise;
+};
+
+/**
+ * Removes a binary or unary operator from the Jexl grammar.
+ * @param {string} operator The operator string to be removed
+ */
+Jexl.prototype.removeOp = function(operator) {
+	var grammar = this._getCustomGrammar();
+	if (grammar[operator] && (grammar[operator].type == 'binaryOp' ||
+			grammar[operator].type == 'unaryOp')) {
+		delete grammar[operator];
+		this._lexer = null;
+	}
+};
+
+/**
+ * Adds an element to the grammar map used by this Jexl instance, cloning
+ * the default grammar first if necessary.
+ * @param {string} str The key string to be added
+ * @param {{type: <string>}} obj A map of configuration options for this
+ *      grammar element
+ * @private
+ */
+Jexl.prototype._addGrammarElement = function(str, obj) {
+	var grammar = this._getCustomGrammar();
+	grammar[str] = obj;
+	this._lexer = null;
+};
+
+/**
+ * Evaluates a Jexl string in the given context.
+ * @param {string} exp The Jexl expression to be evaluated
+ * @param {Object} [context] A mapping of variables to values, which will be
+ *      made accessible to the Jexl expression when evaluating it
+ * @returns {Promise<*>} resolves with the result of the evaluation.
+ * @private
+ */
+Jexl.prototype._eval = function(exp, context) {
+	var self = this,
+		grammar = this._getGrammar(),
+		parser = new Parser(grammar),
+		evaluator = new Evaluator(grammar, this._transforms, context);
+	return Promise.resolve().then(function() {
+		parser.addTokens(self._getLexer().tokenize(exp));
+		return evaluator.eval(parser.complete());
+	});
+};
+
+/**
+ * Gets the custom grammar object, creating it first if necessary. New custom
+ * grammars are created by executing a shallow clone of the default grammar
+ * map. The returned map is available to be changed.
+ * @returns {{}} a customizable grammar map.
+ * @private
+ */
+Jexl.prototype._getCustomGrammar = function() {
+	if (!this._customGrammar) {
+		this._customGrammar = {};
+		for (var key in defaultGrammar) {
+			if (defaultGrammar.hasOwnProperty(key))
+				this._customGrammar[key] = defaultGrammar[key];
+		}
+	}
+	return this._customGrammar;
+};
+
+/**
+ * Gets the grammar map currently being used by Jexl; either the default map,
+ * or a locally customized version. The returned map should never be changed
+ * in any way.
+ * @returns {{}} the grammar map currently in use.
+ * @private
+ */
+Jexl.prototype._getGrammar = function() {
+	return this._customGrammar || defaultGrammar;
+};
+
+/**
+ * Gets a Lexer instance as a singleton in reference to this Jexl instance.
+ * @returns {Lexer} an instance of Lexer, initialized with a grammar
+ *      appropriate to this Jexl instance.
+ * @private
+ */
+Jexl.prototype._getLexer = function() {
+	if (!this._lexer)
+		this._lexer = new Lexer(this._getGrammar());
+	return this._lexer;
+};
+
+module.exports = new Jexl();
+module.exports.Jexl = Jexl;
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js
@@ -0,0 +1,244 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+var numericRegex = /^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/,
+	identRegex = /^[a-zA-Z_\$][a-zA-Z0-9_\$]*$/,
+	escEscRegex = /\\\\/,
+	preOpRegexElems = [
+		// Strings
+		"'(?:(?:\\\\')?[^'])*'",
+		'"(?:(?:\\\\")?[^"])*"',
+		// Whitespace
+		'\\s+',
+		// Booleans
+		'\\btrue\\b',
+		'\\bfalse\\b'
+	],
+	postOpRegexElems = [
+		// Identifiers
+		'\\b[a-zA-Z_\\$][a-zA-Z0-9_\\$]*\\b',
+		// Numerics (without negative symbol)
+		'(?:(?:[0-9]*\\.[0-9]+)|[0-9]+)'
+	],
+	minusNegatesAfter = ['binaryOp', 'unaryOp', 'openParen', 'openBracket',
+		'question', 'colon'];
+
+/**
+ * Lexer is a collection of stateless, statically-accessed functions for the
+ * lexical parsing of a Jexl string.  Its responsibility is to identify the
+ * "parts of speech" of a Jexl expression, and tokenize and label each, but
+ * to do only the most minimal syntax checking; the only errors the Lexer
+ * should be concerned with are if it's unable to identify the utility of
+ * any of its tokens.  Errors stemming from these tokens not being in a
+ * sensible configuration should be left for the Parser to handle.
+ * @type {{}}
+ */
+function Lexer(grammar) {
+	this._grammar = grammar;
+}
+
+/**
+ * Splits a Jexl expression string into an array of expression elements.
+ * @param {string} str A Jexl expression string
+ * @returns {Array<string>} An array of substrings defining the functional
+ *      elements of the expression.
+ */
+Lexer.prototype.getElements = function(str) {
+	var regex = this._getSplitRegex();
+	return str.split(regex).filter(function(elem) {
+		// Remove empty strings
+		return elem;
+	});
+};
+
+/**
+ * Converts an array of expression elements into an array of tokens.  Note that
+ * the resulting array may not equal the element array in length, as any
+ * elements that consist only of whitespace get appended to the previous
+ * token's "raw" property.  For the structure of a token object, please see
+ * {@link Lexer#tokenize}.
+ * @param {Array<string>} elements An array of Jexl expression elements to be
+ *      converted to tokens
+ * @returns {Array<{type, value, raw}>} an array of token objects.
+ */
+Lexer.prototype.getTokens = function(elements) {
+	var tokens = [],
+		negate = false;
+	for (var i = 0; i < elements.length; i++) {
+		if (this._isWhitespace(elements[i])) {
+			if (tokens.length)
+				tokens[tokens.length - 1].raw += elements[i];
+		}
+		else if (elements[i] === '-' && this._isNegative(tokens))
+			negate = true;
+		else {
+			if (negate) {
+				elements[i] = '-' + elements[i];
+				negate = false;
+			}
+			tokens.push(this._createToken(elements[i]));
+		}
+	}
+	// Catch a - at the end of the string. Let the parser handle that issue.
+	if (negate)
+		tokens.push(this._createToken('-'));
+	return tokens;
+};
+
+/**
+ * Converts a Jexl string into an array of tokens.  Each token is an object
+ * in the following format:
+ *
+ *     {
+ *         type: <string>,
+ *         [name]: <string>,
+ *         value: <boolean|number|string>,
+ *         raw: <string>
+ *     }
+ *
+ * Type is one of the following:
+ *
+ *      literal, identifier, binaryOp, unaryOp
+ *
+ * OR, if the token is a control character its type is the name of the element
+ * defined in the Grammar.
+ *
+ * Name appears only if the token is a control string found in
+ * {@link grammar#elements}, and is set to the name of the element.
+ *
+ * Value is the value of the token in the correct type (boolean or numeric as
+ * appropriate). Raw is the string representation of this value taken directly
+ * from the expression string, including any trailing spaces.
+ * @param {string} str The Jexl string to be tokenized
+ * @returns {Array<{type, value, raw}>} an array of token objects.
+ * @throws {Error} if the provided string contains an invalid token.
+ */
+Lexer.prototype.tokenize = function(str) {
+	var elements = this.getElements(str);
+	return this.getTokens(elements);
+};
+
+/**
+ * Creates a new token object from an element of a Jexl string. See
+ * {@link Lexer#tokenize} for a description of the token object.
+ * @param {string} element The element from which a token should be made
+ * @returns {{value: number|boolean|string, [name]: string, type: string,
+ *      raw: string}} a token object describing the provided element.
+ * @throws {Error} if the provided string is not a valid expression element.
+ * @private
+ */
+Lexer.prototype._createToken = function(element) {
+	var token = {
+		type: 'literal',
+		value: element,
+		raw: element
+	};
+	if (element[0] == '"' || element[0] == "'")
+		token.value = this._unquote(element);
+	else if (element.match(numericRegex))
+		token.value = parseFloat(element);
+	else if (element === 'true' || element === 'false')
+		token.value = element === 'true';
+	else if (this._grammar[element])
+		token.type = this._grammar[element].type;
+	else if (element.match(identRegex))
+		token.type = 'identifier';
+	else
+		throw new Error("Invalid expression token: " + element);
+	return token;
+};
+
+/**
+ * Escapes a string so that it can be treated as a string literal within a
+ * regular expression.
+ * @param {string} str The string to be escaped
+ * @returns {string} the RegExp-escaped string.
+ * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
+ * @private
+ */
+Lexer.prototype._escapeRegExp = function(str) {
+	str = str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+	if (str.match(identRegex))
+		str = '\\b' + str + '\\b';
+	return str;
+};
+
+/**
+ * Gets a RegEx object appropriate for splitting a Jexl string into its core
+ * elements.
+ * @returns {RegExp} An element-splitting RegExp object
+ * @private
+ */
+Lexer.prototype._getSplitRegex = function() {
+	if (!this._splitRegex) {
+		var elemArray = Object.keys(this._grammar);
+		// Sort by most characters to least, then regex escape each
+		elemArray = elemArray.sort(function(a ,b) {
+			return b.length - a.length;
+		}).map(function(elem) {
+			return this._escapeRegExp(elem);
+		}, this);
+		this._splitRegex = new RegExp('(' + [
+			preOpRegexElems.join('|'),
+			elemArray.join('|'),
+			postOpRegexElems.join('|')
+		].join('|') + ')');
+	}
+	return this._splitRegex;
+};
+
+/**
+ * Determines whether the addition of a '-' token should be interpreted as a
+ * negative symbol for an upcoming number, given an array of tokens already
+ * processed.
+ * @param {Array<Object>} tokens An array of tokens already processed
+ * @returns {boolean} true if adding a '-' should be considered a negative
+ *      symbol; false otherwise
+ * @private
+ */
+Lexer.prototype._isNegative = function(tokens) {
+	if (!tokens.length)
+		return true;
+	return minusNegatesAfter.some(function(type) {
+		return type === tokens[tokens.length - 1].type;
+	});
+};
+
+/**
+ * A utility function to determine if a string consists of only space
+ * characters.
+ * @param {string} str A string to be tested
+ * @returns {boolean} true if the string is empty or consists of only spaces;
+ *      false otherwise.
+ * @private
+ */
+Lexer.prototype._isWhitespace = function(str) {
+	for (var i = 0; i < str.length; i++) {
+		if (str[i] != ' ')
+			return false;
+	}
+	return true;
+};
+
+/**
+ * Removes the beginning and trailing quotes from a string, unescapes any
+ * escaped quotes on its interior, and unescapes any escaped escape characters.
+ * Note that this function is not defensive; it assumes that the provided
+ * string is not empty, and that its first and last characters are actually
+ * quotes.
+ * @param {string} str A string whose first and last characters are quotes
+ * @returns {string} a string with the surrounding quotes stripped and escapes
+ *      properly processed.
+ * @private
+ */
+Lexer.prototype._unquote = function(str) {
+	var quote = str[0],
+		escQuoteRegex = new RegExp('\\\\' + quote, 'g');
+	return str.substr(1, str.length - 2)
+		.replace(escQuoteRegex, quote)
+		.replace(escEscRegex, '\\');
+};
+
+module.exports = Lexer;
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js
@@ -0,0 +1,153 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+var handlers = require('./handlers');
+
+/**
+ * The Evaluator takes a Jexl expression tree as generated by the
+ * {@link Parser} and calculates its value within a given context. The
+ * collection of transforms, context, and a relative context to be used as the
+ * root for relative identifiers, are all specific to an Evaluator instance.
+ * When any of these things change, a new instance is required.  However, a
+ * single instance can be used to simultaneously evaluate many different
+ * expressions, and does not have to be reinstantiated for each.
+ * @param {{}} grammar A grammar map against which to evaluate the expression
+ *      tree
+ * @param {{}} [transforms] A map of transform names to transform functions. A
+ *      transform function takes two arguments:
+ *          - {*} val: A value to be transformed
+ *          - {{}} args: A map of argument keys to their evaluated values, as
+ *              specified in the expression string
+ *      The transform function should return either the transformed value, or
+ *      a Promises/A+ Promise object that resolves with the value and rejects
+ *      or throws only when an unrecoverable error occurs. Transforms should
+ *      generally return undefined when they don't make sense to be used on the
+ *      given value type, rather than throw/reject. An error is only
+ *      appropriate when the transform would normally return a value, but
+ *      cannot due to some other failure.
+ * @param {{}} [context] A map of variable keys to their values. This will be
+ *      accessed to resolve the value of each non-relative identifier. Any
+ *      Promise values will be passed to the expression as their resolved
+ *      value.
+ * @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
+ *      to resolve the value of a relative identifier.
+ * @constructor
+ */
+var Evaluator = function(grammar, transforms, context, relativeContext) {
+	this._grammar = grammar;
+	this._transforms = transforms || {};
+	this._context = context || {};
+	this._relContext = relativeContext || this._context;
+};
+
+/**
+ * Evaluates an expression tree within the configured context.
+ * @param {{}} ast An expression tree object
+ * @returns {Promise<*>} resolves with the resulting value of the expression.
+ */
+Evaluator.prototype.eval = function(ast) {
+	var self = this;
+	return Promise.resolve().then(function() {
+		return handlers[ast.type].call(self, ast);
+	});
+};
+
+/**
+ * Simultaneously evaluates each expression within an array, and delivers the
+ * response as an array with the resulting values at the same indexes as their
+ * originating expressions.
+ * @param {Array<string>} arr An array of expression strings to be evaluated
+ * @returns {Promise<Array<{}>>} resolves with the result array
+ */
+Evaluator.prototype.evalArray = function(arr) {
+	return Promise.all(arr.map(function(elem) {
+		return this.eval(elem);
+	}, this));
+};
+
+/**
+ * Simultaneously evaluates each expression within a map, and delivers the
+ * response as a map with the same keys, but with the evaluated result for each
+ * as their value.
+ * @param {{}} map A map of expression names to expression trees to be
+ *      evaluated
+ * @returns {Promise<{}>} resolves with the result map.
+ */
+Evaluator.prototype.evalMap = function(map) {
+	var keys = Object.keys(map),
+		result = {};
+	var asts = keys.map(function(key) {
+		return this.eval(map[key]);
+	}, this);
+	return Promise.all(asts).then(function(vals) {
+		vals.forEach(function(val, idx) {
+			result[keys[idx]] = val;
+		});
+		return result;
+	});
+};
+
+/**
+ * Applies a filter expression with relative identifier elements to a subject.
+ * The intent is for the subject to be an array of subjects that will be
+ * individually used as the relative context against the provided expression
+ * tree. Only the elements whose expressions result in a truthy value will be
+ * included in the resulting array.
+ *
+ * If the subject is not an array of values, it will be converted to a single-
+ * element array before running the filter.
+ * @param {*} subject The value to be filtered; usually an array. If this value is
+ *      not an array, it will be converted to an array with this value as the
+ *      only element.
+ * @param {{}} expr The expression tree to run against each subject. If the
+ *      tree evaluates to a truthy result, then the value will be included in
+ *      the returned array; otherwise, it will be eliminated.
+ * @returns {Promise<Array>} resolves with an array of values that passed the
+ *      expression filter.
+ * @private
+ */
+Evaluator.prototype._filterRelative = function(subject, expr) {
+	var promises = [];
+	if (!Array.isArray(subject))
+		subject = [subject];
+	subject.forEach(function(elem) {
+		var evalInst = new Evaluator(this._grammar, this._transforms,
+			this._context, elem);
+		promises.push(evalInst.eval(expr));
+	}, this);
+	return Promise.all(promises).then(function(values) {
+		var results = [];
+		values.forEach(function(value, idx) {
+			if (value)
+				results.push(subject[idx]);
+		});
+		return results;
+	});
+};
+
+/**
+ * Applies a static filter expression to a subject value.  If the filter
+ * expression evaluates to boolean true, the subject is returned; if false,
+ * undefined.
+ *
+ * For any other resulting value of the expression, this function will attempt
+ * to respond with the property at that name or index of the subject.
+ * @param {*} subject The value to be filtered.  Usually an Array (for which
+ *      the expression would generally resolve to a numeric index) or an
+ *      Object (for which the expression would generally resolve to a string
+ *      indicating a property name)
+ * @param {{}} expr The expression tree to run against the subject
+ * @returns {Promise<*>} resolves with the value of the drill-down.
+ * @private
+ */
+Evaluator.prototype._filterStatic = function(subject, expr) {
+	return this.eval(expr).then(function(res) {
+		if (typeof res === 'boolean')
+			return res ? subject : undefined;
+		return subject[res];
+	});
+};
+
+module.exports = Evaluator;
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js
@@ -0,0 +1,159 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+/**
+ * Evaluates an ArrayLiteral by returning its value, with each element
+ * independently run through the evaluator.
+ * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an
+ *      ObjectLiteral as the top node
+ * @returns {Promise.<[]>} resolves to a map contained evaluated values.
+ * @private
+ */
+exports.ArrayLiteral = function(ast) {
+	return this.evalArray(ast.value);
+};
+
+/**
+ * Evaluates a BinaryExpression node by running the Grammar's evaluator for
+ * the given operator.
+ * @param {{type: 'BinaryExpression', operator: <string>, left: {},
+ *      right: {}}} ast An expression tree with a BinaryExpression as the top
+ *      node
+ * @returns {Promise<*>} resolves with the value of the BinaryExpression.
+ * @private
+ */
+exports.BinaryExpression = function(ast) {
+	var self = this;
+	return Promise.all([
+		this.eval(ast.left),
+		this.eval(ast.right)
+	]).then(function(arr) {
+		return self._grammar[ast.operator].eval(arr[0], arr[1]);
+	});
+};
+
+/**
+ * Evaluates a ConditionalExpression node by first evaluating its test branch,
+ * and resolving with the consequent branch if the test is truthy, or the
+ * alternate branch if it is not. If there is no consequent branch, the test
+ * result will be used instead.
+ * @param {{type: 'ConditionalExpression', test: {}, consequent: {},
+ *      alternate: {}}} ast An expression tree with a ConditionalExpression as
+ *      the top node
+ * @private
+ */
+exports.ConditionalExpression = function(ast) {
+	var self = this;
+	return this.eval(ast.test).then(function(res) {
+		if (res) {
+			if (ast.consequent)
+				return self.eval(ast.consequent);
+			return res;
+		}
+		return self.eval(ast.alternate);
+	});
+};
+
+/**
+ * Evaluates a FilterExpression by applying it to the subject value.
+ * @param {{type: 'FilterExpression', relative: <boolean>, expr: {},
+ *      subject: {}}} ast An expression tree with a FilterExpression as the top
+ *      node
+ * @returns {Promise<*>} resolves with the value of the FilterExpression.
+ * @private
+ */
+exports.FilterExpression = function(ast) {
+	var self = this;
+	return this.eval(ast.subject).then(function(subject) {
+		if (ast.relative)
+			return self._filterRelative(subject, ast.expr);
+		return self._filterStatic(subject, ast.expr);
+	});
+};
+
+/**
+ * Evaluates an Identifier by either stemming from the evaluated 'from'
+ * expression tree or accessing the context provided when this Evaluator was
+ * constructed.
+ * @param {{type: 'Identifier', value: <string>, [from]: {}}} ast An expression
+ *      tree with an Identifier as the top node
+ * @returns {Promise<*>|*} either the identifier's value, or a Promise that
+ *      will resolve with the identifier's value.
+ * @private
+ */
+exports.Identifier = function(ast) {
+	if (ast.from) {
+		return this.eval(ast.from).then(function(context) {
+			if (context === undefined)
+				return undefined;
+			if (Array.isArray(context))
+				context = context[0];
+			return context[ast.value];
+		});
+	}
+	else {
+		return ast.relative ? this._relContext[ast.value] :
+			this._context[ast.value];
+	}
+};
+
+/**
+ * Evaluates a Literal by returning its value property.
+ * @param {{type: 'Literal', value: <string|number|boolean>}} ast An expression
+ *      tree with a Literal as its only node
+ * @returns {string|number|boolean} The value of the Literal node
+ * @private
+ */
+exports.Literal = function(ast) {
+	return ast.value;
+};
+
+/**
+ * Evaluates an ObjectLiteral by returning its value, with each key
+ * independently run through the evaluator.
+ * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an
+ *      ObjectLiteral as the top node
+ * @returns {Promise<{}>} resolves to a map contained evaluated values.
+ * @private
+ */
+exports.ObjectLiteral = function(ast) {
+	return this.evalMap(ast.value);
+};
+
+/**
+ * Evaluates a Transform node by applying a function from the transforms map
+ * to the subject value.
+ * @param {{type: 'Transform', name: <string>, subject: {}}} ast An
+ *      expression tree with a Transform as the top node
+ * @returns {Promise<*>|*} the value of the transformation, or a Promise that
+ *      will resolve with the transformed value.
+ * @private
+ */
+exports.Transform = function(ast) {
+	var transform = this._transforms[ast.name];
+	if (!transform)
+		throw new Error("Transform '" + ast.name + "' is not defined.");
+	return Promise.all([
+		this.eval(ast.subject),
+		this.evalArray(ast.args || [])
+	]).then(function(arr) {
+		return transform.apply(null, [arr[0]].concat(arr[1]));
+	});
+};
+
+/**
+ * Evaluates a Unary expression by passing the right side through the
+ * operator's eval function.
+ * @param {{type: 'UnaryExpression', operator: <string>, right: {}}} ast An
+ *      expression tree with a UnaryExpression as the top node
+ * @returns {Promise<*>} resolves with the value of the UnaryExpression.
+ * @constructor
+ */
+exports.UnaryExpression = function(ast) {
+	var self = this;
+	return this.eval(ast.right).then(function(right) {
+		return self._grammar[ast.operator].eval(right);
+	});
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js
@@ -0,0 +1,66 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+/**
+ * A map of all expression elements to their properties. Note that changes
+ * here may require changes in the Lexer or Parser.
+ * @type {{}}
+ */
+exports.elements = {
+	'.': {type: 'dot'},
+	'[': {type: 'openBracket'},
+	']': {type: 'closeBracket'},
+	'|': {type: 'pipe'},
+	'{': {type: 'openCurl'},
+	'}': {type: 'closeCurl'},
+	':': {type: 'colon'},
+	',': {type: 'comma'},
+	'(': {type: 'openParen'},
+	')': {type: 'closeParen'},
+	'?': {type: 'question'},
+	'+': {type: 'binaryOp', precedence: 30,
+		eval: function(left, right) { return left + right; }},
+	'-': {type: 'binaryOp', precedence: 30,
+		eval: function(left, right) { return left - right; }},
+	'*': {type: 'binaryOp', precedence: 40,
+		eval: function(left, right) { return left * right; }},
+	'/': {type: 'binaryOp', precedence: 40,
+		eval: function(left, right) { return left / right; }},
+	'//': {type: 'binaryOp', precedence: 40,
+		eval: function(left, right) { return Math.floor(left / right); }},
+	'%': {type: 'binaryOp', precedence: 50,
+		eval: function(left, right) { return left % right; }},
+	'^': {type: 'binaryOp', precedence: 50,
+		eval: function(left, right) { return Math.pow(left, right); }},
+	'==': {type: 'binaryOp', precedence: 20,
+		eval: function(left, right) { return left == right; }},
+	'!=': {type: 'binaryOp', precedence: 20,
+		eval: function(left, right) { return left != right; }},
+	'>': {type: 'binaryOp', precedence: 20,
+		eval: function(left, right) { return left > right; }},
+	'>=': {type: 'binaryOp', precedence: 20,
+		eval: function(left, right) { return left >= right; }},
+	'<': {type: 'binaryOp', precedence: 20,
+		eval: function(left, right) { return left < right; }},
+	'<=': {type: 'binaryOp', precedence: 20,
+		eval: function(left, right) { return left <= right; }},
+	'&&': {type: 'binaryOp', precedence: 10,
+		eval: function(left, right) { return left && right; }},
+	'||': {type: 'binaryOp', precedence: 10,
+		eval: function(left, right) { return left || right; }},
+	'in': {type: 'binaryOp', precedence: 20,
+		eval: function(left, right) {
+			if (typeof right === 'string')
+				return right.indexOf(left) !== -1;
+			if (Array.isArray(right)) {
+				return right.some(function(elem) {
+					return elem == left;
+				});
+			}
+			return false;
+		}},
+	'!': {type: 'unaryOp', precedence: Infinity,
+		eval: function(right) { return !right; }}
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js
@@ -0,0 +1,188 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+var handlers = require('./handlers'),
+	states = require('./states').states;
+
+/**
+ * The Parser is a state machine that converts tokens from the {@link Lexer}
+ * into an Abstract Syntax Tree (AST), capable of being evaluated in any
+ * context by the {@link Evaluator}.  The Parser expects that all tokens
+ * provided to it are legal and typed properly according to the grammar, but
+ * accepts that the tokens may still be in an invalid order or in some other
+ * unparsable configuration that requires it to throw an Error.
+ * @param {{}} grammar The grammar map to use to parse Jexl strings
+ * @param {string} [prefix] A string prefix to prepend to the expression string
+ *      for error messaging purposes.  This is useful for when a new Parser is
+ *      instantiated to parse an subexpression, as the parent Parser's
+ *      expression string thus far can be passed for a more user-friendly
+ *      error message.
+ * @param {{}} [stopMap] A mapping of token types to any truthy value. When the
+ *      token type is encountered, the parser will return the mapped value
+ *      instead of boolean false.
+ * @constructor
+ */
+function Parser(grammar, prefix, stopMap) {
+	this._grammar = grammar;
+	this._state = 'expectOperand';
+	this._tree = null;
+	this._exprStr = prefix || '';
+	this._relative = false;
+	this._stopMap = stopMap || {};
+}
+
+/**
+ * Processes a new token into the AST and manages the transitions of the state
+ * machine.
+ * @param {{type: <string>}} token A token object, as provided by the
+ *      {@link Lexer#tokenize} function.
+ * @throws {Error} if a token is added when the Parser has been marked as
+ *      complete by {@link #complete}, or if an unexpected token type is added.
+ * @returns {boolean|*} the stopState value if this parser encountered a token
+ *      in the stopState mapb; false if tokens can continue.
+ */
+Parser.prototype.addToken = function(token) {
+	if (this._state == 'complete')
+		throw new Error('Cannot add a new token to a completed Parser');
+	var state = states[this._state],
+		startExpr = this._exprStr;
+	this._exprStr += token.raw;
+	if (state.subHandler) {
+		if (!this._subParser)
+			this._startSubExpression(startExpr);
+		var stopState = this._subParser.addToken(token);
+		if (stopState) {
+			this._endSubExpression();
+			if (this._parentStop)
+				return stopState;
+			this._state = stopState;
+		}
+	}
+	else if (state.tokenTypes[token.type]) {
+		var typeOpts = state.tokenTypes[token.type],
+			handleFunc = handlers[token.type];
+		if (typeOpts.handler)
+			handleFunc = typeOpts.handler;
+		if (handleFunc)
+			handleFunc.call(this, token);
+		if (typeOpts.toState)
+			this._state = typeOpts.toState;
+	}
+	else if (this._stopMap[token.type])
+		return this._stopMap[token.type];
+	else {
+		throw new Error('Token ' + token.raw + ' (' + token.type +
+			') unexpected in expression: ' + this._exprStr);
+	}
+	return false;
+};
+
+/**
+ * Processes an array of tokens iteratively through the {@link #addToken}
+ * function.
+ * @param {Array<{type: <string>}>} tokens An array of tokens, as provided by
+ *      the {@link Lexer#tokenize} function.
+ */
+Parser.prototype.addTokens = function(tokens) {
+	tokens.forEach(this.addToken, this);
+};
+
+/**
+ * Marks this Parser instance as completed and retrieves the full AST.
+ * @returns {{}|null} a full expression tree, ready for evaluation by the
+ *      {@link Evaluator#eval} function, or null if no tokens were passed to
+ *      the parser before complete was called
+ * @throws {Error} if the parser is not in a state where it's legal to end
+ *      the expression, indicating that the expression is incomplete
+ */
+Parser.prototype.complete = function() {
+	if (this._cursor && !states[this._state].completable)
+		throw new Error('Unexpected end of expression: ' + this._exprStr);
+	if (this._subParser)
+		this._endSubExpression();
+	this._state = 'complete';
+	return this._cursor ? this._tree : null;
+};
+
+/**
+ * Indicates whether the expression tree contains a relative path identifier.
+ * @returns {boolean} true if a relative identifier exists; false otherwise.
+ */
+Parser.prototype.isRelative = function() {
+	return this._relative;
+};
+
+/**
+ * Ends a subexpression by completing the subParser and passing its result
+ * to the subHandler configured in the current state.
+ * @private
+ */
+Parser.prototype._endSubExpression = function() {
+	states[this._state].subHandler.call(this, this._subParser.complete());
+	this._subParser = null;
+};
+
+/**
+ * Places a new tree node at the current position of the cursor (to the 'right'
+ * property) and then advances the cursor to the new node. This function also
+ * handles setting the parent of the new node.
+ * @param {{type: <string>}} node A node to be added to the AST
+ * @private
+ */
+Parser.prototype._placeAtCursor = function(node) {
+	if (!this._cursor)
+		this._tree = node;
+	else {
+		this._cursor.right = node;
+		this._setParent(node, this._cursor);
+	}
+	this._cursor = node;
+};
+
+/**
+ * Places a tree node before the current position of the cursor, replacing
+ * the node that the cursor currently points to. This should only be called in
+ * cases where the cursor is known to exist, and the provided node already
+ * contains a pointer to what's at the cursor currently.
+ * @param {{type: <string>}} node A node to be added to the AST
+ * @private
+ */
+Parser.prototype._placeBeforeCursor = function(node) {
+	this._cursor = this._cursor._parent;
+	this._placeAtCursor(node);
+};
+
+/**
+ * Sets the parent of a node by creating a non-enumerable _parent property
+ * that points to the supplied parent argument.
+ * @param {{type: <string>}} node A node of the AST on which to set a new
+ *      parent
+ * @param {{type: <string>}} parent An existing node of the AST to serve as the
+ *      parent of the new node
+ * @private
+ */
+Parser.prototype._setParent = function(node, parent) {
+	Object.defineProperty(node, '_parent', {
+		value: parent,
+		writable: true
+	});
+};
+
+/**
+ * Prepares the Parser to accept a subexpression by (re)instantiating the
+ * subParser.
+ * @param {string} [exprStr] The expression string to prefix to the new Parser
+ * @private
+ */
+Parser.prototype._startSubExpression = function(exprStr) {
+	var endStates = states[this._state].endStates;
+	if (!endStates) {
+		this._parentStop = true;
+		endStates = this._stopMap;
+	}
+	this._subParser = new Parser(this._grammar, exprStr, endStates);
+};
+
+module.exports = Parser;
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js
@@ -0,0 +1,210 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+/**
+ * Handles a subexpression that's used to define a transform argument's value.
+ * @param {{type: <string>}} ast The subexpression tree
+ */
+exports.argVal = function(ast) {
+	this._cursor.args.push(ast);
+};
+
+/**
+ * Handles new array literals by adding them as a new node in the AST,
+ * initialized with an empty array.
+ */
+exports.arrayStart = function() {
+	this._placeAtCursor({
+		type: 'ArrayLiteral',
+		value: []
+	});
+};
+
+/**
+ * Handles a subexpression representing an element of an array literal.
+ * @param {{type: <string>}} ast The subexpression tree
+ */
+exports.arrayVal = function(ast) {
+	if (ast)
+		this._cursor.value.push(ast);
+};
+
+/**
+ * Handles tokens of type 'binaryOp', indicating an operation that has two
+ * inputs: a left side and a right side.
+ * @param {{type: <string>}} token A token object
+ */
+exports.binaryOp = function(token) {
+	var precedence = this._grammar[token.value].precedence || 0,
+		parent = this._cursor._parent;
+	while (parent && parent.operator &&
+			this._grammar[parent.operator].precedence >= precedence) {
+		this._cursor = parent;
+		parent = parent._parent;
+	}
+	var node = {
+		type: 'BinaryExpression',
+		operator: token.value,
+		left: this._cursor
+	};
+	this._setParent(this._cursor, node);
+	this._cursor = parent;
+	this._placeAtCursor(node);
+};
+
+/**
+ * Handles successive nodes in an identifier chain.  More specifically, it
+ * sets values that determine how the following identifier gets placed in the
+ * AST.
+ */
+exports.dot = function() {
+	this._nextIdentEncapsulate = this._cursor &&
+		(this._cursor.type != 'BinaryExpression' ||
+		(this._cursor.type == 'BinaryExpression' && this._cursor.right)) &&
+		this._cursor.type != 'UnaryExpression';
+	this._nextIdentRelative = !this._cursor ||
+		(this._cursor && !this._nextIdentEncapsulate);
+	if (this._nextIdentRelative)
+		this._relative = true;
+};
+
+/**
+ * Handles a subexpression used for filtering an array returned by an
+ * identifier chain.
+ * @param {{type: <string>}} ast The subexpression tree
+ */
+exports.filter = function(ast) {
+	this._placeBeforeCursor({
+		type: 'FilterExpression',
+		expr: ast,
+		relative: this._subParser.isRelative(),
+		subject: this._cursor
+	});
+};
+
+/**
+ * Handles identifier tokens by adding them as a new node in the AST.
+ * @param {{type: <string>}} token A token object
+ */
+exports.identifier = function(token) {
+	var node = {
+		type: 'Identifier',
+		value: token.value
+	};
+	if (this._nextIdentEncapsulate) {
+		node.from = this._cursor;
+		this._placeBeforeCursor(node);
+		this._nextIdentEncapsulate = false;
+	}
+	else {
+		if (this._nextIdentRelative)
+			node.relative = true;
+		this._placeAtCursor(node);
+	}
+};
+
+/**
+ * Handles literal values, such as strings, booleans, and numerics, by adding
+ * them as a new node in the AST.
+ * @param {{type: <string>}} token A token object
+ */
+exports.literal = function(token) {
+	this._placeAtCursor({
+		type: 'Literal',
+		value: token.value
+	});
+};
+
+/**
+ * Queues a new object literal key to be written once a value is collected.
+ * @param {{type: <string>}} token A token object
+ */
+exports.objKey = function(token) {
+	this._curObjKey = token.value;
+};
+
+/**
+ * Handles new object literals by adding them as a new node in the AST,
+ * initialized with an empty object.
+ */
+exports.objStart = function() {
+	this._placeAtCursor({
+		type: 'ObjectLiteral',
+		value: {}
+	});
+};
+
+/**
+ * Handles an object value by adding its AST to the queued key on the object
+ * literal node currently at the cursor.
+ * @param {{type: <string>}} ast The subexpression tree
+ */
+exports.objVal = function(ast) {
+	this._cursor.value[this._curObjKey] = ast;
+};
+
+/**
+ * Handles traditional subexpressions, delineated with the groupStart and
+ * groupEnd elements.
+ * @param {{type: <string>}} ast The subexpression tree
+ */
+exports.subExpression = function(ast) {
+	this._placeAtCursor(ast);
+};
+
+/**
+ * Handles a completed alternate subexpression of a ternary operator.
+ * @param {{type: <string>}} ast The subexpression tree
+ */
+exports.ternaryEnd = function(ast) {
+	this._cursor.alternate = ast;
+};
+
+/**
+ * Handles a completed consequent subexpression of a ternary operator.
+ * @param {{type: <string>}} ast The subexpression tree
+ */
+exports.ternaryMid = function(ast) {
+	this._cursor.consequent = ast;
+};
+
+/**
+ * Handles the start of a new ternary expression by encapsulating the entire
+ * AST in a ConditionalExpression node, and using the existing tree as the
+ * test element.
+ */
+exports.ternaryStart = function() {
+	this._tree = {
+		type: 'ConditionalExpression',
+		test: this._tree
+	};
+	this._cursor = this._tree;
+};
+
+/**
+ * Handles identifier tokens when used to indicate the name of a transform to
+ * be applied.
+ * @param {{type: <string>}} token A token object
+ */
+exports.transform = function(token) {
+	this._placeBeforeCursor({
+		type: 'Transform',
+		name: token.value,
+		args: [],
+		subject: this._cursor
+	});
+};
+
+/**
+ * Handles token of type 'unaryOp', indicating that the operation has only
+ * one input: a right side.
+ * @param {{type: <string>}} token A token object
+ */
+exports.unaryOp = function(token) {
+	this._placeAtCursor({
+		type: 'UnaryExpression',
+		operator: token.value
+	});
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/states.js
@@ -0,0 +1,154 @@
+/*
+ * Jexl
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+
+var h = require('./handlers');
+
+/**
+ * A mapping of all states in the finite state machine to a set of instructions
+ * for handling or transitioning into other states. Each state can be handled
+ * in one of two schemes: a tokenType map, or a subHandler.
+ *
+ * Standard expression elements are handled through the tokenType object. This
+ * is an object map of all legal token types to encounter in this state (and
+ * any unexpected token types will generate a thrown error) to an options
+ * object that defines how they're handled.  The available options are:
+ *
+ *      {string} toState: The name of the state to which to transition
+ *          immediately after handling this token
+ *      {string} handler: The handler function to call when this token type is
+ *          encountered in this state.  If omitted, the default handler
+ *          matching the token's "type" property will be called. If the handler
+ *          function does not exist, no call will be made and no error will be
+ *          generated.  This is useful for tokens whose sole purpose is to
+ *          transition to other states.
+ *
+ * States that consume a subexpression should define a subHandler, the
+ * function to be called with an expression tree argument when the
+ * subexpression is complete. Completeness is determined through the
+ * endStates object, which maps tokens on which an expression should end to the
+ * state to which to transition once the subHandler function has been called.
+ *
+ * Additionally, any state in which it is legal to mark the AST as completed
+ * should have a 'completable' property set to boolean true.  Attempting to
+ * call {@link Parser#complete} in any state without this property will result
+ * in a thrown Error.
+ *
+ * @type {{}}
+ */
+exports.states = {
+	expectOperand: {
+		tokenTypes: {
+			literal: {toState: 'expectBinOp'},
+			identifier: {toState: 'identifier'},
+			unaryOp: {},
+			openParen: {toState: 'subExpression'},
+			openCurl: {toState: 'expectObjKey', handler: h.objStart},
+			dot: {toState: 'traverse'},
+			openBracket: {toState: 'arrayVal', handler: h.arrayStart}
+		}
+	},
+	expectBinOp: {
+		tokenTypes: {
+			binaryOp: {toState: 'expectOperand'},
+			pipe: {toState: 'expectTransform'},
+			dot: {toState: 'traverse'},
+			question: {toState: 'ternaryMid', handler: h.ternaryStart}
+		},
+		completable: true
+	},
+	expectTransform: {
+		tokenTypes: {
+			identifier: {toState: 'postTransform', handler: h.transform}
+		}
+	},
+	expectObjKey: {
+		tokenTypes: {
+			identifier: {toState: 'expectKeyValSep', handler: h.objKey},
+			closeCurl: {toState: 'expectBinOp'}
+		}
+	},
+	expectKeyValSep: {
+		tokenTypes: {
+			colon: {toState: 'objVal'}
+		}
+	},
+	postTransform: {
+		tokenTypes: {
+			openParen: {toState: 'argVal'},
+			binaryOp: {toState: 'expectOperand'},
+			dot: {toState: 'traverse'},
+			openBracket: {toState: 'filter'},
+			pipe: {toState: 'expectTransform'}
+		},
+		completable: true
+	},
+	postTransformArgs: {
+		tokenTypes: {
+			binaryOp: {toState: 'expectOperand'},
+			dot: {toState: 'traverse'},
+			openBracket: {toState: 'filter'},
+			pipe: {toState: 'expectTransform'}
+		},
+		completable: true
+	},
+	identifier: {
+		tokenTypes: {
+			binaryOp: {toState: 'expectOperand'},
+			dot: {toState: 'traverse'},
+			openBracket: {toState: 'filter'},
+			pipe: {toState: 'expectTransform'},
+			question: {toState: 'ternaryMid', handler: h.ternaryStart}
+		},
+		completable: true
+	},
+	traverse: {
+		tokenTypes: {
+			'identifier': {toState: 'identifier'}
+		}
+	},
+	filter: {
+		subHandler: h.filter,
+		endStates: {
+			closeBracket: 'identifier'
+		}
+	},
+	subExpression: {
+		subHandler: h.subExpression,
+		endStates: {
+			closeParen: 'expectBinOp'
+		}
+	},
+	argVal: {
+		subHandler: h.argVal,
+		endStates: {
+			comma: 'argVal',
+			closeParen: 'postTransformArgs'
+		}
+	},
+	objVal: {
+		subHandler: h.objVal,
+		endStates: {
+			comma: 'expectObjKey',
+			closeCurl: 'expectBinOp'
+		}
+	},
+	arrayVal: {
+		subHandler: h.arrayVal,
+		endStates: {
+			comma: 'arrayVal',
+			closeBracket: 'expectBinOp'
+		}
+	},
+	ternaryMid: {
+		subHandler: h.ternaryMid,
+		endStates: {
+			colon: 'ternaryEnd'
+		}
+	},
+	ternaryEnd: {
+		subHandler: h.ternaryEnd,
+		completable: true
+	}
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/.eslintrc.js
@@ -0,0 +1,16 @@
+"use strict";
+
+module.exports = {
+  globals: {
+    Assert: false,
+    BrowserTestUtils: false,
+    add_task: false,
+    is: false,
+    isnot: false,
+    ok: false,
+  },
+  rules: {
+    "spaced-comment": 2,
+    "space-before-function-paren": 2,
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/TestUtils.jsm
@@ -0,0 +1,21 @@
+/* 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";
+
+/* eslint-disable no-console */
+this.EXPORTED_SYMBOLS = ["TestUtils"];
+
+this.TestUtils = {
+  promiseTest(test) {
+    return function(assert, done) {
+      test(assert)
+      .catch(err => {
+        console.error(err);
+        assert.ok(false, err);
+      })
+      .then(() => done());
+    };
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser.ini
@@ -0,0 +1,5 @@
+[browser_driver_uuids.js]
+[browser_env_expressions.js]
+[browser_EventEmitter.js]
+[browser_Storage.js]
+[browser_Heartbeat.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser_EventEmitter.js
@@ -0,0 +1,92 @@
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Log.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
+
+const sandboxManager = new SandboxManager();
+sandboxManager.addHold("test running");
+const driver = new NormandyDriver(sandboxManager);
+const sandboxedDriver = Cu.cloneInto(driver, sandboxManager.sandbox, {cloneFunctions: true});
+const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
+
+
+const evidence = {
+  a: 0,
+  b: 0,
+  c: 0,
+  log: "",
+};
+
+function listenerA(x = 1) {
+  evidence.a += x;
+  evidence.log += "a";
+}
+
+function listenerB(x = 1) {
+  evidence.b += x;
+  evidence.log += "b";
+}
+
+function listenerC(x = 1) {
+  evidence.c += x;
+  evidence.log += "c";
+}
+
+add_task(function* () {
+  // Fire an unrelated event, to make sure nothing goes wrong
+  eventEmitter.on("nothing");
+
+  // bind listeners
+  eventEmitter.on("event", listenerA);
+  eventEmitter.on("event", listenerB);
+  eventEmitter.once("event", listenerC);
+
+  // one event for all listeners
+  eventEmitter.emit("event");
+  // another event for a and b, since c should have turned off already
+  eventEmitter.emit("event", 10);
+
+  // make sure events haven't actually fired yet, just queued
+  Assert.deepEqual(evidence, {
+    a: 0,
+    b: 0,
+    c: 0,
+    log: "",
+  }, "events are fired async");
+
+  // Spin the event loop to run events, so we can safely "off"
+  yield Promise.resolve();
+
+  // Check intermediate event results
+  Assert.deepEqual(evidence, {
+    a: 11,
+    b: 11,
+    c: 1,
+    log: "abcab",
+  }, "intermediate events are fired");
+
+  // one more event for a
+  eventEmitter.off("event", listenerB);
+  eventEmitter.emit("event", 100);
+
+  // And another unrelated event
+  eventEmitter.on("nothing");
+
+  // Spin the event loop to run events
+  yield Promise.resolve();
+
+  Assert.deepEqual(evidence, {
+    a: 111,
+    b: 11,
+    c: 1,
+    log: "abcaba",  // events are in order
+  }, "events fired as expected");
+
+  sandboxManager.removeHold("test running");
+
+  yield sandboxManager.isNuked()
+    .then(() => ok(true, "sandbox is nuked"))
+    .catch(e => ok(false, "sandbox is nuked", e));
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser_Heartbeat.js
@@ -0,0 +1,188 @@
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/Heartbeat.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
+
+/**
+ * Assert an array is in non-descending order, and that every element is a number
+ */
+function assertOrdered(arr) {
+  for (let i = 0; i < arr.length; i++) {
+    Assert.equal(typeof arr[i], "number", `element ${i} is type "number"`);
+  }
+  for (let i = 0; i < arr.length - 1; i++) {
+    Assert.lessOrEqual(arr[i], arr[i + 1],
+      `element ${i} is less than or equal to element ${i + 1}`);
+  }
+}
+
+/* Close every notification in a target window and notification box */
+function closeAllNotifications(targetWindow, notificationBox) {
+  if (notificationBox.allNotifications.length === 0) {
+    return Promise.resolve();
+  }
+
+
+  return new Promise(resolve => {
+    const notificationSet = new Set(notificationBox.allNotifications);
+
+    const observer = new targetWindow.MutationObserver(mutations => {
+      for (const mutation of mutations) {
+        for (let i = 0; i < mutation.removedNodes.length; i++) {
+          const node = mutation.removedNodes.item(i);
+          if (notificationSet.has(node)) {
+            notificationSet.delete(node);
+          }
+        }
+      }
+      if (notificationSet.size === 0) {
+        Assert.equal(notificationBox.allNotifications.length, 0, "No notifications left");
+        observer.disconnect();
+        resolve();
+      }
+    });
+
+    observer.observe(notificationBox, {childList: true});
+
+    for (const notification of notificationBox.allNotifications) {
+      notification.close();
+    }
+  });
+}
+
+/* Check that the correct telmetry was sent */
+function assertTelemetrySent(hb, eventNames) {
+  return new Promise(resolve => {
+    hb.eventEmitter.once("TelemetrySent", payload => {
+      const events = [0];
+      for (const name of eventNames) {
+        Assert.equal(typeof payload[name], "number", `payload field ${name} is a number`);
+        events.push(payload[name]);
+      }
+      events.push(Date.now());
+
+      assertOrdered(events);
+      resolve();
+    });
+  });
+}
+
+
+const sandboxManager = new SandboxManager();
+const driver = new NormandyDriver(sandboxManager);
+sandboxManager.addHold("test running");
+const sandboxedDriver = Cu.cloneInto(driver, sandboxManager.sandbox, {cloneFunctions: true});
+
+
+// Several of the behaviors of heartbeat prompt are mutually exclusive, so checks are broken up
+// into three batches.
+
+/* Batch #1 - General UI, Stars, and telemetry data */
+add_task(function* () {
+  const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
+  const targetWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  const notificationBox = targetWindow.document.querySelector("#high-priority-global-notificationbox");
+
+  const preCount = notificationBox.childElementCount;
+  const hb = new Heartbeat(targetWindow, eventEmitter, sandboxManager, {
+    testing: true,
+    flowId: "test",
+    message: "test",
+    engagementButtonLabel: undefined,
+    learnMoreMessage: "Learn More",
+    learnMoreUrl: "https://example.org/learnmore",
+  });
+
+  // Check UI
+  const learnMoreEl = hb.notice.querySelector(".text-link");
+  const messageEl = targetWindow.document.getAnonymousElementByAttribute(hb.notice, "anonid", "messageText");
+  Assert.equal(notificationBox.childElementCount, preCount + 1, "Correct number of notifications open");
+  Assert.equal(hb.notice.querySelectorAll(".star-x").length, 5, "Correct number of stars");
+  Assert.equal(hb.notice.querySelectorAll(".notification-button").length, 0, "Engagement button not shown");
+  Assert.equal(learnMoreEl.href, "https://example.org/learnmore", "Learn more url correct");
+  Assert.equal(learnMoreEl.value, "Learn More", "Learn more label correct");
+  Assert.equal(messageEl.textContent, "test", "Message is correct");
+
+  // Check that when clicking the learn more link, a tab opens with the right URL
+  const tabOpenPromise = BrowserTestUtils.waitForNewTab(targetWindow.gBrowser);
+  learnMoreEl.click();
+  const tab = yield tabOpenPromise;
+  const tabUrl = yield BrowserTestUtils.browserLoaded(
+    tab.linkedBrowser, true, url => url && url !== "about:blank");
+
+  Assert.equal(tabUrl, "https://example.org/learnmore", "Learn more link opened the right url");
+
+  const telemetrySentPromise = assertTelemetrySent(hb, ["offeredTS", "learnMoreTS", "closedTS"]);
+  // Close notification to trigger telemetry to be sent
+  yield closeAllNotifications(targetWindow, notificationBox);
+  yield telemetrySentPromise;
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+
+// Batch #2 - Engagement buttons
+add_task(function* () {
+  const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
+  const targetWindow = Services.wm.getMostRecentWindow("navigator:browser");
+  const notificationBox = targetWindow.document.querySelector("#high-priority-global-notificationbox");
+  const hb = new Heartbeat(targetWindow, eventEmitter, sandboxManager, {
+    testing: true,
+    flowId: "test",
+    message: "test",
+    engagementButtonLabel: "Click me!",
+    postAnswerUrl: "https://example.org/postAnswer",
+    learnMoreMessage: "Learn More",
+    learnMoreUrl: "https://example.org/learnMore",
+  });
+  const engagementButton = hb.notice.querySelector(".notification-button");
+
+  Assert.equal(hb.notice.querySelectorAll(".star-x").length, 0, "Stars not shown");
+  Assert.ok(engagementButton, "Engagement button added");
+  Assert.equal(engagementButton.label, "Click me!", "Engagement button has correct label");
+
+  const engagementEl = hb.notice.querySelector(".notification-button");
+  const tabOpenPromise = BrowserTestUtils.waitForNewTab(targetWindow.gBrowser);
+  engagementEl.click();
+  const tab = yield tabOpenPromise;
+  const tabUrl = yield BrowserTestUtils.browserLoaded(
+        tab.linkedBrowser, true, url => url && url !== "about:blank");
+  // the postAnswer url gets query parameters appended onto the end, so use Assert.startsWith instead of Assert.equal
+  Assert.ok(tabUrl.startsWith("https://example.org/postAnswer"), "Engagement button opened the right url");
+
+  const telemetrySentPromise = assertTelemetrySent(hb, ["offeredTS", "engagedTS", "closedTS"]);
+  // Close notification to trigger telemetry to be sent
+  yield closeAllNotifications(targetWindow, notificationBox);
+  yield telemetrySentPromise;
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+// Batch 3 - Closing the window while heartbeat is open
+add_task(function* () {
+  const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
+  const targetWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+  const hb = new Heartbeat(targetWindow, eventEmitter, sandboxManager, {
+    testing: true,
+    flowId: "test",
+    message: "test",
+  });
+
+  const telemetrySentPromise = assertTelemetrySent(hb, ["offeredTS", "windowClosedTS"]);
+  // triggers sending ping to normandy
+  yield BrowserTestUtils.closeWindow(targetWindow);
+  yield telemetrySentPromise;
+});
+
+
+// Cleanup
+add_task(function* () {
+  // Make sure the sandbox is clean.
+  sandboxManager.removeHold("test running");
+  yield sandboxManager.isNuked()
+    .then(() => ok(true, "sandbox is nuked"))
+    .catch(e => ok(false, "sandbox is nuked", e));
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser_Storage.js
@@ -0,0 +1,37 @@
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://shield-recipe-client/lib/Storage.jsm", this);
+
+const fakeSandbox = {Promise};
+const store1 = Storage.makeStorage("prefix1", fakeSandbox);
+const store2 = Storage.makeStorage("prefix2", fakeSandbox);
+
+add_task(function* () {
+  // Make sure values return null before being set
+  Assert.equal(yield store1.getItem("key"), null);
+  Assert.equal(yield store2.getItem("key"), null);
+
+  // Set values to check
+  yield store1.setItem("key", "value1");
+  yield store2.setItem("key", "value2");
+
+  // Check that they are available
+  Assert.equal(yield store1.getItem("key"), "value1");
+  Assert.equal(yield store2.getItem("key"), "value2");
+
+  // Remove them, and check they are gone
+  yield store1.removeItem("key");
+  yield store2.removeItem("key");
+  Assert.equal(yield store1.getItem("key"), null);
+  Assert.equal(yield store2.getItem("key"), null);
+
+  // Check that numbers are stored as numbers (not strings)
+  yield store1.setItem("number", 42);
+  Assert.equal(yield store1.getItem("number"), 42);
+
+  // Check complex types work
+  const complex = {a: 1, b: [2, 3], c: {d: 4}};
+  yield store1.setItem("complex", complex);
+  Assert.deepEqual(yield store1.getItem("complex"), complex);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser_driver_uuids.js
@@ -0,0 +1,26 @@
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
+
+add_task(function* () {
+  const sandboxManager = new SandboxManager();
+  sandboxManager.addHold("test running");
+  let driver = new NormandyDriver(sandboxManager);
+
+  // Test that UUID look about right
+  const uuid1 = driver.uuid();
+  ok(/^[a-f0-9-]{36}$/.test(uuid1), "valid uuid format");
+
+  // Test that UUIDs are different each time
+  const uuid2 = driver.uuid();
+  isnot(uuid1, uuid2, "uuids are unique");
+
+  driver = null;
+  sandboxManager.removeHold("test running");
+
+  yield sandboxManager.isNuked()
+    .then(() => ok(true, "sandbox is nuked"))
+    .catch(e => ok(false, "sandbox is nuked", e));
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser_env_expressions.js
@@ -0,0 +1,56 @@
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+Cu.import("resource://shield-recipe-client/lib/EnvExpressions.jsm", this);
+Cu.import("resource://gre/modules/Log.jsm", this);
+
+add_task(function* () {
+  // setup
+  yield TelemetryController.submitExternalPing("testfoo", {foo: 1});
+  yield TelemetryController.submitExternalPing("testbar", {bar: 2});
+
+  let val;
+  // Test that basic expressions work
+  val = yield EnvExpressions.eval("2+2");
+  is(val, 4, "basic expression works");
+
+  // Test that multiline expressions work
+  val = yield EnvExpressions.eval(`
+    2
+    +
+    2
+  `);
+  is(val, 4, "multiline expression works");
+
+  // Test it can access telemetry
+  val = yield EnvExpressions.eval("telemetry");
+  is(typeof val, "object", "Telemetry is accesible");
+
+  // Test it reads different types of telemetry
+  val = yield EnvExpressions.eval("telemetry");
+  is(val.testfoo.payload.foo, 1, "value 'foo' is in mock telemetry");
+  is(val.testbar.payload.bar, 2, "value 'bar' is in mock telemetry");
+
+  // Test has a date transform
+  val = yield EnvExpressions.eval('"2016-04-22"|date');
+  const d = new Date(Date.UTC(2016, 3, 22)); // months are 0 based
+  is(val.toString(), d.toString(), "Date transform works");
+
+  // Test dates are comparable
+  const context = {someTime: Date.UTC(2016, 0, 1)};
+  val = yield EnvExpressions.eval('"2015-01-01"|date < someTime', context);
+  ok(val, "dates are comparable with less-than");
+  val = yield EnvExpressions.eval('"2017-01-01"|date > someTime', context);
+  ok(val, "dates are comparable with greater-than");
+
+  // Test stable sample returns true for matching samples
+  val = yield EnvExpressions.eval('["test"]|stableSample(1)');
+  is(val, true, "Stable sample returns true for 100% sample");
+
+  // Test stable sample returns true for matching samples
+  val = yield EnvExpressions.eval('["test"]|stableSample(0)');
+  is(val, false, "Stable sample returns false for 0% sample");
+});
--- a/browser/installer/Makefile.in
+++ b/browser/installer/Makefile.in
@@ -56,19 +56,16 @@ endif
 
 DEFINES += -DJAREXT=
 
 ifdef MOZ_ANGLE_RENDERER
 DEFINES += -DMOZ_ANGLE_RENDERER=$(MOZ_ANGLE_RENDERER)
 ifdef MOZ_D3DCOMPILER_VISTA_DLL
 DEFINES += -DMOZ_D3DCOMPILER_VISTA_DLL=$(MOZ_D3DCOMPILER_VISTA_DLL)
 endif
-ifdef MOZ_D3DCOMPILER_XP_DLL
-DEFINES += -DMOZ_D3DCOMPILER_XP_DLL=$(MOZ_D3DCOMPILER_XP_DLL)
-endif
 endif
 
 DEFINES += -DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME)
 
 # Set MSVC dlls version to package, if any.
 ifdef MOZ_NO_DEBUG_RTL
 ifdef WIN32_REDIST_DIR
 DEFINES += -DMOZ_PACKAGE_MSVC_DLLS=1
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -584,20 +584,16 @@
 ; ANGLE GLES-on-D3D rendering library
 #ifdef MOZ_ANGLE_RENDERER
 @BINPATH@/libEGL.dll
 @BINPATH@/libGLESv2.dll
 
 #ifdef MOZ_D3DCOMPILER_VISTA_DLL
 @BINPATH@/@MOZ_D3DCOMPILER_VISTA_DLL@
 #endif
-
-#ifdef MOZ_D3DCOMPILER_XP_DLL
-@BINPATH@/@MOZ_D3DCOMPILER_XP_DLL@
-#endif
 #endif # MOZ_ANGLE_RENDERER
 
 ; [Browser Chrome Files]
 @RESPATH@/browser/chrome.manifest
 @RESPATH@/browser/chrome/browser@JAREXT@
 @RESPATH@/browser/chrome/browser.manifest
 @RESPATH@/browser/chrome/pdfjs.manifest
 @RESPATH@/browser/chrome/pdfjs/*
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -727,22 +727,20 @@ decoder.noCodecs.message = To play video, you may need to install Microsoft’s Media Feature Pack.
 decoder.noCodecsVista.message = To play video, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
 decoder.noCodecsXP.message = To play video, you may need to enable Adobe’s Primetime Content Decryption Module.
 decoder.noCodecsLinux.message = To play video, you may need to install the required video codecs.
 decoder.noHWAcceleration.message = To improve video quality, you may need to install Microsoft’s Media Feature Pack.
 decoder.noHWAccelerationVista.message = To improve video quality, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
 decoder.noPulseAudio.message = To play audio, you may need to install the required PulseAudio software.
 decoder.unsupportedLibavcodec.message = libavcodec may be vulnerable or is not supported, and should be updated to play video.
 
-# LOCALIZATION NOTE (captivePortal.infoMessage,
-#                    captivePortal.infoMessage2):
+# LOCALIZATION NOTE (captivePortal.infoMessage2):
 # Shown in a notification bar when we detect a captive portal is blocking network access
-# and requires the user to log in before browsing. %1$S is replaced with brandShortName.
-captivePortal.infoMessage = This network may require you to login to use the internet. %1$S has opened the login page for you.
-captivePortal.infoMessage2 = This network may require you to login to use the internet.
+# and requires the user to log in before browsing.
+captivePortal.infoMessage2 = This network may require you to log in to use the internet.
 # LOCALIZATION NOTE (captivePortal.showLoginPage):
 # The label for a button shown in the info bar in all tabs except the login page tab.
 # The button shows the portal login page tab when clicked.
 captivePortal.showLoginPage = Show Login Page
 
 permissions.remove.tooltip = Clear this permission and ask again
 
 # LOCALIZATION NOTE (aboutDialog.architecture.*):
--- a/browser/locales/en-US/chrome/browser/places/places.dtd
+++ b/browser/locales/en-US/chrome/browser/places/places.dtd
@@ -36,20 +36,16 @@
 
 <!ENTITY cmd.backup.label               "Backup…">
 <!ENTITY cmd.backup.accesskey           "B">
 <!ENTITY cmd.restore2.label             "Restore">
 <!ENTITY cmd.restore2.accesskey         "R">
 <!ENTITY cmd.restoreFromFile.label      "Choose File…">
 <!ENTITY cmd.restoreFromFile.accesskey  "C">
 
-<!ENTITY cmd.bookmarkLink.label         "Bookmark This Page…">
-<!ENTITY cmd.bookmarkLink.accesskey     "B">
-<!ENTITY cmd.delete.label               "Delete This Page">
-<!ENTITY cmd.delete.accesskey           "D">
 <!ENTITY cmd.deleteDomainData.label     "Forget About This Site">
 <!ENTITY cmd.deleteDomainData.accesskey "F">
 
 <!ENTITY cmd.open.label                  "Open">
 <!ENTITY cmd.open.accesskey              "O">
 <!ENTITY cmd.open_window.label           "Open in a New Window">
 <!ENTITY cmd.open_window.accesskey       "N">
 <!ENTITY cmd.open_private_window.label     "Open in a New Private Window">
--- a/browser/locales/en-US/chrome/browser/places/places.properties
+++ b/browser/locales/en-US/chrome/browser/places/places.properties
@@ -85,8 +85,18 @@ searchengineResultLabel=Search
 
 
 # LOCALIZATION NOTE (lockPrompt.text)
 # %S will be replaced with the application name.
 lockPrompt.title=Browser Startup Error
 lockPrompt.text=The bookmarks and history system will not be functional because one of %S’s files is in use by another application. Some security software can cause this problem.
 lockPromptInfoButton.label=Learn More
 lockPromptInfoButton.accessKey=L
+
+# LOCALIZATION NOTE (deletePagesLabel): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+cmd.deletePages.label=Delete Page;Delete Pages
+cmd.deletePages.accesskey=D
+
+# LOCALIZATION NOTE (bookmarkPagesLabel): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+cmd.bookmarkPages.label=Bookmark Page;Bookmark Pages
+cmd.bookmarkPages.accesskey=B
--- a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
@@ -77,31 +77,31 @@
 <!ENTITY clearCacheNow.accesskey         "C">
 <!ENTITY clearOfflineAppCacheNow.label   "Clear Now">
 <!ENTITY clearOfflineAppCacheNow.accesskey "N">
 <!ENTITY overrideSmartCacheSize.label    "Override automatic cache management">
 <!ENTITY overrideSmartCacheSize.accesskey "O">
 
 <!ENTITY updateTab.label                 "Update">
 
-<!ENTITY updateApp.label                 "&brandShortName; updates:">
+<!ENTITY updateApplication.label         "&brandShortName; updates">
 <!ENTITY updateAuto1.label               "Automatically install updates (recommended: improved security)">
 <!ENTITY updateAuto1.accesskey           "A">
 <!ENTITY updateCheck.label               "Check for updates, but let me choose whether to install them">
 <!ENTITY updateCheck.accesskey           "C">
 <!ENTITY updateManual.label              "Never check for updates (not recommended: security risk)">
 <!ENTITY updateManual.accesskey          "N">
 
 <!ENTITY updateHistory.label             "Show Update History">
 <!ENTITY updateHistory.accesskey         "p">
 
 <!ENTITY useService.label                "Use a background service to install updates">
 <!ENTITY useService.accesskey            "b">
 
-<!ENTITY updateOthers.label              "Automatically update:">
+<!ENTITY autoUpdateOthers.label          "Automatically update">
 <!ENTITY enableSearchUpdate.label        "Search Engines">
 <!ENTITY enableSearchUpdate.accesskey    "E">
 
 <!ENTITY offlineNotify.label             "Tell me when a website asks to store data for offline use">
 <!ENTITY offlineNotify.accesskey         "T">
 <!ENTITY offlineNotifyExceptions.label   "Exceptions…">
 <!ENTITY offlineNotifyExceptions.accesskey "x">
 
--- a/browser/locales/en-US/chrome/browser/preferences/cookies.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/cookies.dtd
@@ -23,13 +23,13 @@
 <!ENTITY     props.expires.label            "Expires:">
 <!ENTITY     props.container.label          "Container:">
 
 <!ENTITY     window.title                   "Cookies">
 <!ENTITY     windowClose.key                "w">
 <!ENTITY     focusSearch1.key               "f">
 <!ENTITY     focusSearch2.key               "k">
 
-<!ENTITY     filter.label                   "Search:">
-<!ENTITY     filter.accesskey               "S">
+<!ENTITY     searchFilter.label             "Search">
+<!ENTITY     searchFilter.accesskey         "S">
 
 <!ENTITY     button.close.label             "Close">
 <!ENTITY     button.close.accesskey         "C">
--- a/browser/locales/en-US/chrome/overrides/netError.dtd
+++ b/browser/locales/en-US/chrome/overrides/netError.dtd
@@ -46,19 +46,19 @@
 </ul>
 ">
 
 <!ENTITY generic.title "Oops.">
 <!ENTITY generic.longDesc "
 <p>&brandShortName; can’t load this page for some reason.</p>
 ">
 
-<!ENTITY captivePortal.title "Login to network">
+<!ENTITY captivePortal.title "Log in to network">
 <!ENTITY captivePortal.longDesc "
-<p>This network may require you to login to access the internet.</p>
+<p>This network may require you to log in to access the internet.</p>
 ">
 
 <!ENTITY openPortalLoginPage.label "Open Login Page">
 
 <!ENTITY malformedURI.title "The address isn’t valid">
 <!ENTITY malformedURI.longDesc "
 <ul>
   <li>Web addresses are usually written like
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -1,16 +1,16 @@
 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
 /* 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 = ["BrowserUsageTelemetry"];
+this.EXPORTED_SYMBOLS = ["BrowserUsageTelemetry", "URLBAR_SELECTED_RESULT_TYPES"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -18,16 +18,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 // The upper bound for the count of the visited unique domain names.
 const MAX_UNIQUE_VISITED_DOMAINS = 100;
 
 // Observed topic names.
 const WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
 const TAB_RESTORING_TOPIC = "SSTabRestoring";
 const TELEMETRY_SUBSESSIONSPLIT_TOPIC = "internal-telemetry-after-subsession-split";
 const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
+const AUTOCOMPLETE_ENTER_TEXT_TOPIC = "autocomplete-did-enter-text";
 
 // Probe names.
 const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
 const MAX_WINDOW_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_window_count";
 const TAB_OPEN_EVENT_COUNT_SCALAR_NAME = "browser.engagement.tab_open_event_count";
 const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME = "browser.engagement.window_open_event_count";
 const UNIQUE_DOMAINS_COUNT_SCALAR_NAME = "browser.engagement.unique_domains_count";
 const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count";
@@ -43,16 +44,34 @@ const KNOWN_SEARCH_SOURCES = [
 ];
 
 const KNOWN_ONEOFF_SOURCES = [
   "oneoff-urlbar",
   "oneoff-searchbar",
   "unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
 ];
 
+/**
+ * The buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE
+ * histogram.
+ */
+const URLBAR_SELECTED_RESULT_TYPES = {
+  autofill: 0,
+  bookmark: 1,
+  history: 2,
+  keyword: 3,
+  searchengine: 4,
+  searchsuggestion: 5,
+  switchtab: 6,
+  tag: 7,
+  visiturl: 8,
+  remotetab: 9,
+  extension: 10,
+};
+
 function getOpenTabsAndWinsCounts() {
   let tabCount = 0;
   let winCount = 0;
 
   let browserEnum = Services.wm.getEnumerator("navigator:browser");
   while (browserEnum.hasMoreElements()) {
     let win = browserEnum.getNext();
     winCount++;
@@ -182,19 +201,92 @@ let URICountListener = {
   reset() {
     this._domainSet.clear();
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                          Ci.nsISupportsWeakReference]),
 };
 
+let urlbarListener = {
+  init() {
+    Services.obs.addObserver(this, AUTOCOMPLETE_ENTER_TEXT_TOPIC, true);
+  },
+
+  uninit() {
+    Services.obs.removeObserver(this, AUTOCOMPLETE_ENTER_TEXT_TOPIC, true);
+  },
+
+  observe(subject, topic, data) {
+    switch (topic) {
+      case AUTOCOMPLETE_ENTER_TEXT_TOPIC:
+        this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput));
+        break;
+    }
+  },
+
+  /**
+   * Used to log telemetry when the user enters text in the urlbar.
+   *
+   * @param {nsIAutoCompleteInput} input  The autocomplete element where the
+   *                                      text was entered.
+   */
+  _handleURLBarTelemetry(input) {
+    if (!input ||
+        input.id != "urlbar" ||
+        input.inPrivateContext ||
+        input.popup.selectedIndex < 0) {
+      return;
+    }
+    let controller =
+      input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
+    let idx = input.popup.selectedIndex;
+    let value = controller.getValueAt(idx);
+    let action = input._parseActionUrl(value);
+    let actionType;
+    if (action) {
+      actionType =
+        action.type == "searchengine" && action.params.searchSuggestion ?
+          "searchsuggestion" :
+        action.type;
+    }
+    if (!actionType) {
+      let styles = new Set(controller.getStyleAt(idx).split(/\s+/));
+      let style = ["autofill", "tag", "bookmark"].find(s => styles.has(s));
+      actionType = style || "history";
+    }
+
+    Services.telemetry
+            .getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX")
+            .add(idx);
+
+    // Ideally this would be a keyed histogram and we'd just add(actionType),
+    // but keyed histograms aren't currently shown on the telemetry dashboard
+    // (bug 1151756).
+    //
+    // You can add values but don't change any of the existing values.
+    // Otherwise you'll break our data.
+    if (actionType in URLBAR_SELECTED_RESULT_TYPES) {
+      Services.telemetry
+              .getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE")
+              .add(URLBAR_SELECTED_RESULT_TYPES[actionType]);
+    } else {
+      Cu.reportError("Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " +
+                     actionType);
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+};
+
 let BrowserUsageTelemetry = {
   init() {
     Services.obs.addObserver(this, WINDOWS_RESTORED_TOPIC, false);
+    urlbarListener.init();
   },
 
   /**
    * Handle subsession splits in the parent process.
    */
   afterSubsessionSplit() {
     // Scalars just got cleared due to a subsession split. We need to set the maximum
     // concurrent tab and window counts so that they reflect the correct value for the
@@ -206,16 +298,17 @@ let BrowserUsageTelemetry = {
     // Reset the URI counter.
     URICountListener.reset();
   },
 
   uninit() {
     Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC, false);
     Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, false);
     Services.obs.removeObserver(this, WINDOWS_RESTORED_TOPIC, false);
+    urlbarListener.uninit();
   },
 
   observe(subject, topic, data) {
     switch (topic) {
       case WINDOWS_RESTORED_TOPIC:
         this._setupAfterRestore();
         break;
       case DOMWINDOW_OPENED_TOPIC:
--- a/browser/modules/test/browser_UsageTelemetry_content.js
+++ b/browser/modules/test/browser_UsageTelemetry_content.js
@@ -28,16 +28,17 @@ add_task(function* setup() {
     ["toolkit.telemetry.enabled", true]  // And Extended Telemetry to be enabled.
   ]});
 
   // Make sure to restore the engine once we're done.
   registerCleanupFunction(function* () {
     Services.search.currentEngine = originalEngine;
     Services.search.removeEngine(engineDefault);
     Services.search.removeEngine(engineOneOff);
+    yield PlacesTestUtils.clearHistory();
   });
 });
 
 add_task(function* test_context_menu() {
   // Let's reset the Telemetry data.
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
   let search_hist = getSearchCountsHistogram();
--- a/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
+++ b/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
@@ -30,16 +30,17 @@ add_task(function* setup() {
   // Enable Extended Telemetry.
   yield SpecialPowers.pushPrefEnv({"set": [["toolkit.telemetry.enabled", true]]});
 
   // Make sure to restore the engine once we're done.
   registerCleanupFunction(function* () {
     Services.search.currentEngine = originalEngine;
     Services.search.removeEngine(engineDefault);
     Services.search.removeEngine(engineOneOff);
+    yield PlacesTestUtils.clearHistory();
   });
 });
 
 add_task(function* test_abouthome_simpleQuery() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
   let search_hist = getSearchCountsHistogram();
--- a/browser/modules/test/browser_UsageTelemetry_urlbar.js
+++ b/browser/modules/test/browser_UsageTelemetry_urlbar.js
@@ -3,16 +3,31 @@
 const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
 
 // The preference to enable suggestions in the urlbar.
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
 // The name of the search engine used to generate suggestions.
 const SUGGESTION_ENGINE_NAME = "browser_UsageTelemetry usageTelemetrySearchSuggestions.xml";
 const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
 
+XPCOMUtils.defineLazyModuleGetter(this, "URLBAR_SELECTED_RESULT_TYPES",
+                                  "resource:///modules/BrowserUsageTelemetry.jsm");
+
+function checkHistogramResults(resultIndexes, expected, histogram) {
+  for (let i = 0; i < resultIndexes.counts.length; i++) {
+    if (i == expected) {
+      Assert.equal(resultIndexes.counts[i], 1,
+        `expected counts should match for ${histogram} index ${i}`);
+    } else {
+      Assert.equal(resultIndexes.counts[i], 0,
+        `unexpected counts should be zero for ${histogram} index ${i}`);
+    }
+  }
+}
+
 let searchInAwesomebar = Task.async(function* (inputText, win = window) {
   yield new Promise(r => waitForFocus(r, win));
   // Write the search query in the urlbar.
   win.gURLBar.focus();
   win.gURLBar.value = inputText;
   win.gURLBar.controller.startSearch(inputText);
   // Wait for the popup to show.
   yield BrowserTestUtils.waitForEvent(win.gURLBar.popup, "popupshown");
@@ -57,29 +72,40 @@ add_task(function* setup() {
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
 
   // Enable the urlbar one-off buttons.
   Services.prefs.setBoolPref(ONEOFF_URLBAR_PREF, true);
 
   // Enable Extended Telemetry.
   yield SpecialPowers.pushPrefEnv({"set": [["toolkit.telemetry.enabled", true]]});
 
+  // Enable local telemetry recording for the duration of the tests.
+  let oldCanRecord = Services.telemetry.canRecordExtended;
+  Services.telemetry.canRecordExtended = true;
+
   // Make sure to restore the engine once we're done.
   registerCleanupFunction(function* () {
+    Services.telemetry.canRecordExtended = oldCanRecord;
     Services.search.currentEngine = originalEngine;
     Services.search.removeEngine(engine);
     Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF, true);
     Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
+    yield PlacesTestUtils.clearHistory();
   });
 });
 
 add_task(function* test_simpleQuery() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
+  let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
+  let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
+  resultIndexHist.clear();
+  resultTypeHist.clear();
+
   let search_hist = getSearchCountsHistogram();
 
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
 
   info("Simulate entering a simple search.");
   let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
   yield searchInAwesomebar("simple query");
   EventUtils.sendKey("return");
@@ -95,23 +121,37 @@ add_task(function* test_simpleQuery() {
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, 'other-MozSearch.urlbar', 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
   events = events.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "enter", {engine: "other-MozSearch"}]]);
 
+  // Check the histograms as well.
+  let resultIndexes = resultIndexHist.snapshot();
+  checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
+
+  let resultTypes = resultTypeHist.snapshot();
+  checkHistogramResults(resultTypes,
+    URLBAR_SELECTED_RESULT_TYPES.searchengine,
+    "FX_URLBAR_SELECTED_RESULT_TYPE");
+
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_searchAlias() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
+  let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
+  let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
+  resultIndexHist.clear();
+  resultTypeHist.clear();
+
   let search_hist = getSearchCountsHistogram();
 
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
 
   info("Search using a search alias.");
   let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
   yield searchInAwesomebar("mozalias query");
   EventUtils.sendKey("return");
@@ -127,23 +167,37 @@ add_task(function* test_searchAlias() {
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, 'other-MozSearch.urlbar', 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
   events = events.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "alias", {engine: "other-MozSearch"}]]);
 
+  // Check the histograms as well.
+  let resultIndexes = resultIndexHist.snapshot();
+  checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
+
+  let resultTypes = resultTypeHist.snapshot();
+  checkHistogramResults(resultTypes,
+    URLBAR_SELECTED_RESULT_TYPES.searchengine,
+    "FX_URLBAR_SELECTED_RESULT_TYPE");
+
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_oneOff() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
+  let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
+  let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
+  resultIndexHist.clear();
+  resultTypeHist.clear();
+
   let search_hist = getSearchCountsHistogram();
 
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
 
   info("Perform a one-off search using the first engine.");
   let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
   yield searchInAwesomebar("query");
 
@@ -162,23 +216,37 @@ add_task(function* test_oneOff() {
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, 'other-MozSearch.urlbar', 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
   events = events.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "oneoff", {engine: "other-MozSearch"}]]);
 
+  // Check the histograms as well.
+  let resultIndexes = resultIndexHist.snapshot();
+  checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
+
+  let resultTypes = resultTypeHist.snapshot();
+  checkHistogramResults(resultTypes,
+    URLBAR_SELECTED_RESULT_TYPES.searchengine,
+    "FX_URLBAR_SELECTED_RESULT_TYPE");
+
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_suggestion() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
+  let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
+  let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
+  resultIndexHist.clear();
+  resultTypeHist.clear();
+
   let search_hist = getSearchCountsHistogram();
 
   // Create an engine to generate search suggestions and add it as default
   // for this test.
   const url = getRootDirectory(gTestPath) + "usageTelemetrySearchSuggestions.xml";
   let suggestionEngine = yield new Promise((resolve, reject) => {
     Services.search.addEngine(url, null, "", false, {
       onSuccess(engine) { resolve(engine) },
@@ -209,12 +277,21 @@ add_task(function* test_suggestion() {
   let searchEngineId = 'other-' + suggestionEngine.name;
   checkKeyedHistogram(search_hist, searchEngineId + '.urlbar', 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
   events = events.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "suggestion", {engine: searchEngineId}]]);
 
+  // Check the histograms as well.
+  let resultIndexes = resultIndexHist.snapshot();
+  checkHistogramResults(resultIndexes, 3, "FX_URLBAR_SELECTED_RESULT_INDEX");
+
+  let resultTypes = resultTypeHist.snapshot();
+  checkHistogramResults(resultTypes,
+    URLBAR_SELECTED_RESULT_TYPES.searchsuggestion,
+    "FX_URLBAR_SELECTED_RESULT_TYPE");
+
   Services.search.currentEngine = previousEngine;
   Services.search.removeEngine(suggestionEngine);
   yield BrowserTestUtils.removeTab(tab);
 });
--- a/browser/modules/test/head.js
+++ b/browser/modules/test/head.js
@@ -1,10 +1,13 @@
 Cu.import("resource://gre/modules/Promise.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+                                  "resource://testing-common/PlacesTestUtils.jsm");
+
 const SINGLE_TRY_TIMEOUT = 100;
 const NUMBER_OF_TRIES = 30;
 
 function waitForConditionPromise(condition, timeoutMsg, tryCount = NUMBER_OF_TRIES) {
   let defer = Promise.defer();
   let tries = 0;
   function checkCondition() {
     if (tries >= tryCount) {
--- a/browser/themes/windows/preferences/preferences.css
+++ b/browser/themes/windows/preferences/preferences.css
@@ -57,22 +57,16 @@
   border: 1px solid ThreeDShadow;
   border-radius: 0;
   margin: 4px;
   padding: 0;
 }
 
 /* Advanced Pane */
 
-/* Adding padding-bottom prevents the bottom of the tabpanel from being cutoff
-   when browser.preferences.animateFadeIn = true */
-#advancedPrefs {
-  padding-bottom: 8px;
-}
-
 /* bottom-most box containing a groupbox in a prefpane. Prevents the bottom
    of the groupbox from being cutoff */
 .bottomBox {
   padding-bottom: 4px;
 }
 
 /* Sync Pane */
 
--- a/build/clang-plugin/clang-plugin.cpp
+++ b/build/clang-plugin/clang-plugin.cpp
@@ -293,17 +293,20 @@ bool isIgnoredPathForImplicitCtor(const 
                                     End = llvm::sys::path::rend(FileName);
   for (; Begin != End; ++Begin) {
     if (Begin->compare_lower(StringRef("skia")) == 0 ||
         Begin->compare_lower(StringRef("angle")) == 0 ||
         Begin->compare_lower(StringRef("harfbuzz")) == 0 ||
         Begin->compare_lower(StringRef("hunspell")) == 0 ||
         Begin->compare_lower(StringRef("scoped_ptr.h")) == 0 ||
         Begin->compare_lower(StringRef("graphite2")) == 0 ||
-        Begin->compare_lower(StringRef("icu")) == 0) {
+        Begin->compare_lower(StringRef("icu")) == 0 ||
+        Begin->compare_lower(StringRef("libcubeb")) == 0 ||
+        Begin->compare_lower(StringRef("libstagefright")) == 0 ||
+        Begin->compare_lower(StringRef("cairo")) == 0) {
       return true;
     }
     if (Begin->compare_lower(StringRef("chromium")) == 0) {
       // Ignore security/sandbox/chromium but not ipc/chromium.
       ++Begin;
       return Begin != End && Begin->compare_lower(StringRef("sandbox")) == 0;
     }
   }
--- a/build/clang-plugin/tests/TestCustomHeap.cpp
+++ b/build/clang-plugin/tests/TestCustomHeap.cpp
@@ -1,14 +1,16 @@
 #define MOZ_NONHEAP_CLASS __attribute__((annotate("moz_nonheap_class")))
+#ifndef MOZ_HEAP_ALLOCATOR
 #define MOZ_HEAP_ALLOCATOR \
   _Pragma("GCC diagnostic push") \
   _Pragma("GCC diagnostic ignored \"-Wgcc-compat\"") \
   __attribute__((annotate("moz_heap_allocator"))) \
   _Pragma("GCC diagnostic pop")
+#endif
 
 #include <stdlib.h>
 #include <memory>
 
 struct MOZ_NONHEAP_CLASS X {
 };
 
 void *operator new(size_t x, int qual) MOZ_HEAP_ALLOCATOR {
--- a/build/docs/rust.rst
+++ b/build/docs/rust.rst
@@ -6,21 +6,21 @@ Including Rust Code in Firefox
 
 The build system has support for building and linking Rust crates.
 Rust code is built using ``cargo`` in the typical way, so it is
 straightforward to take an existing Rust crate and integrate it
 into Firefox.
 
 .. important::
 
-   Rust code is not currently enabled by default in Firefox builds.
-   This should change soon (`bug 1283898 <https://bugzilla.mozilla.org/show_bug.cgi?id=1283898>`_),
-   but the option to build without Rust code will likely last a little longer
-   (`bug 1284816 <https://bugzilla.mozilla.org/show_bug.cgi?id=1284816>`_),
-   so Rust code cannot currently be used for required components.
+   Rust code is enabled by default in Firefox builds. Until we have
+   a required component written in Rust, you can build without by
+   setting ``ac_add_options --disable-rust`` in your mozconfig.
+   This option will be around for a little longer
+   (`bug 1284816 <https://bugzilla.mozilla.org/show_bug.cgi?id=1284816>`_).
 
 
 Linking Rust Crates into libxul
 ===============================
 
 Rust crates that you want to link into libxul should be listed in the
 ``dependencies`` section of `toolkit/library/rust/shared/Cargo.toml <https://dxr.mozilla.org/mozilla-central/source/toolkit/library/rust/shared/Cargo.toml>`_.
 You'll also need to add an ``extern crate`` reference to `toolkit/library/rust/shared/lib.rs <https://dxr.mozilla.org/mozilla-central/source/toolkit/library/rust/shared/lib.rs>`_.
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -199,16 +199,19 @@ def bootstrap(topsrcdir, mozilla_dir=Non
     # creation is much simpler for the "advanced" environment variable use
     # case. For default behavior, we educate users and give them an opportunity
     # to react. We always exit after creating the directory because users don't
     # like surprises.
     sys.path[0:0] = [os.path.join(mozilla_dir, path) for path in SEARCH_PATHS]
     import mach.main
     from mozboot.util import get_state_dir
 
+    from mozbuild.util import patch_main
+    patch_main()
+
     def telemetry_handler(context, data):
         # We have not opted-in to telemetry
         if 'BUILD_SYSTEM_TELEMETRY' not in os.environ:
             return
 
         telemetry_dir = os.path.join(get_state_dir()[0], 'telemetry')
         try:
             os.mkdir(telemetry_dir)
--- a/build/moz.configure/old.configure
+++ b/build/moz.configure/old.configure
@@ -209,17 +209,16 @@ def old_configure_options(*options):
     '--enable-png-arm-neon-support',
     '--enable-posix-nspr-emulation',
     '--enable-pref-extensions',
     '--enable-pulseaudio',
     '--enable-raw',
     '--enable-readline',
     '--enable-reflow-perf',
     '--enable-release',
-    '--enable-require-all-d3dc-versions',
     '--enable-safe-browsing',
     '--enable-sandbox',
     '--enable-signmar',
     '--enable-simulator',
     '--enable-small-chunk-size',
     '--enable-startup-notification',
     '--enable-startupcache',
     '--enable-stdcxx-compat',
--- a/build/moz.configure/rust.configure
+++ b/build/moz.configure/rust.configure
@@ -120,23 +120,25 @@ def rust_triple_alias(host_or_target):
             # NetBSD
             ('x86_64', 'NetBSD'): 'x86_64-unknown-netbsd',
             # OpenBSD
             ('x86_64', 'OpenBSD'): 'x86_64-unknown-openbsd',
             # Linux
             ('x86', 'Linux'): 'i686-unknown-linux-gnu',
             # Linux
             ('x86_64', 'Linux'): 'x86_64-unknown-linux-gnu',
+            ('aarch64', 'Linux'): 'aarch64-unknown-linux-gnu',
             # OS X and iOS
             ('x86', 'OSX'): 'i686-apple-darwin',
             ('x86', 'iOS'): 'i386-apple-ios',
             ('x86_64', 'OSX'): 'x86_64-apple-darwin',
             # Android
             ('x86', 'Android'): 'i686-linux-android',
             ('arm', 'Android'): 'armv7-linux-androideabi',
+            ('aarch64', 'Android'): 'aarch64-linux-android',
             # Windows
             # XXX better detection of CXX needed here, to figure out whether
             # we need i686-pc-windows-gnu instead, since mingw32 builds work.
             ('x86', 'WINNT'): 'i686-pc-windows-msvc',
             ('x86_64', 'WINNT'): 'x86_64-pc-windows-msvc',
         }.get((host_or_target.cpu, os_or_kernel), None)
 
         if rustc_target is None:
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure