Merge m-c to graphics
authorKartikaya Gupta <kgupta@mozilla.com>
Thu, 09 Feb 2017 10:12:03 -0500
changeset 342268 e79bf3dc89267b2e8bdcd349b0e8bab1c8134111
parent 342267 b9f45837d2986ac4a232d6f180aafa03a04c8a89 (current diff)
parent 341680 88d6a5ed101805514831a01ed539cb916e214aa2 (diff)
child 342269 b8a64223f86efbf5725ba740ee7eab952d841b6e
push id86826
push userkwierso@gmail.com
push dateFri, 10 Feb 2017 23:33:17 +0000
treeherdermozilla-inbound@2c7816419218 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone54.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: Ejcj1CD87t3
CLOBBER
browser/base/content/test/general/browser_extension_update.js
browser/config/tooltool-manifests/linux32/clang.manifest
browser/installer/package-manifest.in
config/rules.mk
dom/base/test/file_use_counter_svg_background.html
dom/base/test/file_use_counter_svg_list_style_image.html
gfx/layers/Layers.h
gfx/layers/LayersTypes.h
js/src/threading/posix/ConditionVariable.cpp
js/src/threading/posix/MutexImpl.cpp
js/src/threading/posix/MutexPlatformData.h
js/src/threading/windows/ConditionVariable.cpp
js/src/threading/windows/MutexImpl.cpp
js/src/threading/windows/MutexPlatformData.h
layout/base/nsLayoutUtils.cpp
layout/painting/nsDisplayList.cpp
modules/libpref/init/all.js
taskcluster/ci/build/linux.yml
taskcluster/ci/build/windows.yml
taskcluster/taskgraph/transforms/gecko_v2_whitelist.py
testing/mozharness/configs/multi_locale/release_mozilla-esr_android-armv6.json
testing/mozharness/configs/multi_locale/staging_release_mozilla-esr_android-armv6.json
testing/mozharness/configs/releases/postrelease_esr38.py
testing/mozharness/configs/releases/postrelease_firefox_esr45.py
testing/mozharness/configs/releases/updates_firefox_esr45.py
testing/mozharness/configs/single_locale/mozilla-esr45.py
testing/mozharness/mozharness/mozilla/building/buildbase.py
testing/web-platform/meta/XMLHttpRequest/XMLHttpRequest-withCredentials.any.js.ini
testing/web-platform/meta/console/console-is-a-namespace.any.js.ini
testing/web-platform/meta/html/browsers/history/the-location-interface/location-protocol-setter.html.ini
toolkit/components/satchel/nsFormHistory.js
toolkit/components/satchel/nsIFormHistory.idl
toolkit/crashreporter/google-breakpad/src/client/apple/Framework/BreakpadDefines.h
toolkit/crashreporter/google-breakpad/src/client/ios/Breakpad.h
toolkit/crashreporter/google-breakpad/src/client/ios/Breakpad.mm
toolkit/crashreporter/google-breakpad/src/client/ios/Breakpad.xcodeproj/project.pbxproj
toolkit/crashreporter/google-breakpad/src/client/ios/BreakpadController.h
toolkit/crashreporter/google-breakpad/src/client/ios/BreakpadController.mm
toolkit/crashreporter/google-breakpad/src/client/ios/Breakpad_Prefix.pch
toolkit/crashreporter/google-breakpad/src/client/ios/handler/ios_exception_minidump_generator.h
toolkit/crashreporter/google-breakpad/src/client/ios/handler/ios_exception_minidump_generator.mm
toolkit/crashreporter/google-breakpad/src/client/linux/crash_generation/client_info.h
toolkit/crashreporter/google-breakpad/src/client/linux/crash_generation/crash_generation_client.cc
toolkit/crashreporter/google-breakpad/src/client/linux/crash_generation/crash_generation_client.h
toolkit/crashreporter/google-breakpad/src/client/linux/crash_generation/crash_generation_server.cc
toolkit/crashreporter/google-breakpad/src/client/linux/crash_generation/crash_generation_server.h
toolkit/crashreporter/google-breakpad/src/client/linux/data/linux-gate-amd.sym
toolkit/crashreporter/google-breakpad/src/client/linux/data/linux-gate-intel.sym
toolkit/crashreporter/google-breakpad/src/client/linux/dump_writer_common/mapping_info.h
toolkit/crashreporter/google-breakpad/src/client/linux/dump_writer_common/raw_context_cpu.h
toolkit/crashreporter/google-breakpad/src/client/linux/dump_writer_common/thread_info.cc
toolkit/crashreporter/google-breakpad/src/client/linux/dump_writer_common/thread_info.h
toolkit/crashreporter/google-breakpad/src/client/linux/dump_writer_common/ucontext_reader.cc
toolkit/crashreporter/google-breakpad/src/client/linux/dump_writer_common/ucontext_reader.h
toolkit/crashreporter/google-breakpad/src/client/linux/handler/exception_handler.cc
toolkit/crashreporter/google-breakpad/src/client/linux/handler/exception_handler.h
toolkit/crashreporter/google-breakpad/src/client/linux/handler/exception_handler_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/handler/microdump_extra_info.h
toolkit/crashreporter/google-breakpad/src/client/linux/handler/minidump_descriptor.cc
toolkit/crashreporter/google-breakpad/src/client/linux/handler/minidump_descriptor.h
toolkit/crashreporter/google-breakpad/src/client/linux/log/log.cc
toolkit/crashreporter/google-breakpad/src/client/linux/log/log.h
toolkit/crashreporter/google-breakpad/src/client/linux/microdump_writer/microdump_writer.cc
toolkit/crashreporter/google-breakpad/src/client/linux/microdump_writer/microdump_writer.h
toolkit/crashreporter/google-breakpad/src/client/linux/microdump_writer/microdump_writer_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/cpu_set.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/cpu_set_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/directory_reader.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/directory_reader_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/line_reader.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/line_reader_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_core_dumper.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_core_dumper.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_core_dumper_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_dumper.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_dumper.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_dumper_unittest_helper.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_ptrace_dumper.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_ptrace_dumper.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/linux_ptrace_dumper_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/minidump_writer.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/minidump_writer.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/minidump_writer_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/minidump_writer_unittest_utils.cc
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/minidump_writer_unittest_utils.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/proc_cpuinfo_reader.h
toolkit/crashreporter/google-breakpad/src/client/linux/minidump_writer/proc_cpuinfo_reader_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/linux/moz.build
toolkit/crashreporter/google-breakpad/src/client/linux/sender/google_crash_report_sender.cc
toolkit/crashreporter/google-breakpad/src/client/mac/Breakpad.xcodeproj/project.pbxproj
toolkit/crashreporter/google-breakpad/src/client/mac/Framework/Breakpad.h
toolkit/crashreporter/google-breakpad/src/client/mac/Framework/Breakpad.mm
toolkit/crashreporter/google-breakpad/src/client/mac/Framework/Breakpad_Prefix.pch
toolkit/crashreporter/google-breakpad/src/client/mac/Framework/Info.plist
toolkit/crashreporter/google-breakpad/src/client/mac/Framework/OnDemandServer.h
toolkit/crashreporter/google-breakpad/src/client/mac/Framework/OnDemandServer.mm
toolkit/crashreporter/google-breakpad/src/client/mac/UnitTests-Info.plist
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/ConfigFile.h
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/ConfigFile.mm
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/Inspector.h
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/Inspector.mm
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/InspectorMain.mm
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/client_info.h
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/crash_generation_client.cc
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/crash_generation_client.h
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/crash_generation_server.cc
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/crash_generation_server.h
toolkit/crashreporter/google-breakpad/src/client/mac/crash_generation/moz.build
toolkit/crashreporter/google-breakpad/src/client/mac/gcov/libgcov.a
toolkit/crashreporter/google-breakpad/src/client/mac/handler/breakpad_nlist_64.cc
toolkit/crashreporter/google-breakpad/src/client/mac/handler/breakpad_nlist_64.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/dynamic_images.cc
toolkit/crashreporter/google-breakpad/src/client/mac/handler/dynamic_images.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/exception_handler.cc
toolkit/crashreporter/google-breakpad/src/client/mac/handler/exception_handler.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/mach_vm_compat.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/minidump_generator.cc
toolkit/crashreporter/google-breakpad/src/client/mac/handler/minidump_generator.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/minidump_test.xcodeproj/project.pbxproj
toolkit/crashreporter/google-breakpad/src/client/mac/handler/minidump_tests32-Info.plist
toolkit/crashreporter/google-breakpad/src/client/mac/handler/minidump_tests64-Info.plist
toolkit/crashreporter/google-breakpad/src/client/mac/handler/moz.build
toolkit/crashreporter/google-breakpad/src/client/mac/handler/obj-cTestCases-Info.plist
toolkit/crashreporter/google-breakpad/src/client/mac/handler/protected_memory_allocator.cc
toolkit/crashreporter/google-breakpad/src/client/mac/handler/protected_memory_allocator.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/DynamicImagesTests.cc
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/DynamicImagesTests.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/breakpad_nlist_test.cc
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/breakpad_nlist_test.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/dwarftests.h
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/dwarftests.mm
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/testdata/dump_syms_dwarf_data
toolkit/crashreporter/google-breakpad/src/client/mac/handler/testcases/testdata/dump_syms_i386_breakpad.sym
toolkit/crashreporter/google-breakpad/src/client/mac/handler/ucontext_compat.h
toolkit/crashreporter/google-breakpad/src/client/mac/sender/Breakpad.xib
toolkit/crashreporter/google-breakpad/src/client/mac/sender/English.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/English.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/ReporterIcon.graffle
toolkit/crashreporter/google-breakpad/src/client/mac/sender/crash_report_sender-Info.plist
toolkit/crashreporter/google-breakpad/src/client/mac/sender/crash_report_sender.h
toolkit/crashreporter/google-breakpad/src/client/mac/sender/crash_report_sender.icns
toolkit/crashreporter/google-breakpad/src/client/mac/sender/crash_report_sender.m
toolkit/crashreporter/google-breakpad/src/client/mac/sender/da.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/da.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/de.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/de.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/es.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/es.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/fr.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/fr.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/goArrow.png
toolkit/crashreporter/google-breakpad/src/client/mac/sender/it.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/it.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/ja.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/ja.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/nl.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/nl.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/no.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/no.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/sl.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/sl.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/sv.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/sv.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/tr.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/tr.lproj/Localizable.strings
toolkit/crashreporter/google-breakpad/src/client/mac/sender/uploader.h
toolkit/crashreporter/google-breakpad/src/client/mac/sender/uploader.mm
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/Controller.h
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/Controller.m
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/English.lproj/InfoPlist.strings
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/English.lproj/MainMenu.xib
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/Info.plist
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/TestClass.h
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/TestClass.mm
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/bomb.icns
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/crashInMain
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/crashduringload
toolkit/crashreporter/google-breakpad/src/client/mac/testapp/main.m
toolkit/crashreporter/google-breakpad/src/client/mac/tests/BreakpadFramework_Test.mm
toolkit/crashreporter/google-breakpad/src/client/mac/tests/crash_generation_server_test.cc
toolkit/crashreporter/google-breakpad/src/client/mac/tests/exception_handler_test.cc
toolkit/crashreporter/google-breakpad/src/client/mac/tests/minidump_generator_test.cc
toolkit/crashreporter/google-breakpad/src/client/mac/tests/minidump_generator_test_helper.cc
toolkit/crashreporter/google-breakpad/src/client/mac/tests/spawn_child_process.h
toolkit/crashreporter/google-breakpad/src/client/mac/tests/testlogging.h
toolkit/crashreporter/google-breakpad/src/client/minidump_file_writer-inl.h
toolkit/crashreporter/google-breakpad/src/client/minidump_file_writer.cc
toolkit/crashreporter/google-breakpad/src/client/minidump_file_writer.h
toolkit/crashreporter/google-breakpad/src/client/minidump_file_writer_unittest.cc
toolkit/crashreporter/google-breakpad/src/client/moz.build
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/Makefile
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/exception_handler.cc
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/exception_handler.h
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/exception_handler_test.cc
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/minidump_generator.cc
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/minidump_generator.h
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/minidump_test.cc
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/moz.build
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/solaris_lwp.cc
toolkit/crashreporter/google-breakpad/src/client/solaris/handler/solaris_lwp.h
toolkit/crashreporter/google-breakpad/src/client/windows/breakpad_client.gyp
toolkit/crashreporter/google-breakpad/src/client/windows/common/auto_critical_section.h
toolkit/crashreporter/google-breakpad/src/client/windows/common/ipc_protocol.h
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/ReadMe.txt
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/client_info.cc
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/client_info.h
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/crash_generation.gyp
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/crash_generation_client.cc
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/crash_generation_client.h
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/crash_generation_server.cc
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/crash_generation_server.h
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/minidump_generator.cc
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/minidump_generator.h
toolkit/crashreporter/google-breakpad/src/client/windows/crash_generation/objs.mozbuild
toolkit/crashreporter/google-breakpad/src/client/windows/handler/exception_handler.cc
toolkit/crashreporter/google-breakpad/src/client/windows/handler/exception_handler.gyp
toolkit/crashreporter/google-breakpad/src/client/windows/handler/exception_handler.h
toolkit/crashreporter/google-breakpad/src/client/windows/handler/objs.mozbuild
toolkit/crashreporter/google-breakpad/src/client/windows/sender/crash_report_sender.cc
toolkit/crashreporter/google-breakpad/src/client/windows/sender/crash_report_sender.gyp
toolkit/crashreporter/google-breakpad/src/client/windows/sender/crash_report_sender.h
toolkit/crashreporter/google-breakpad/src/client/windows/sender/objs.mozbuild
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/abstract_class.cc
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/abstract_class.h
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/crash_generation_app.cc
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/crash_generation_app.gyp
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/crash_generation_app.h
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/crash_generation_app.ico
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/crash_generation_app.rc
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/resource.h
toolkit/crashreporter/google-breakpad/src/client/windows/tests/crash_generation_app/small.ico
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/client_tests.gyp
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/crash_generation_server_test.cc
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/dump_analysis.cc
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/dump_analysis.h
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/exception_handler_death_test.cc
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/exception_handler_nesting_test.cc
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/exception_handler_test.cc
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/exception_handler_test.h
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/minidump_test.cc
toolkit/crashreporter/google-breakpad/src/client/windows/unittests/testing.gyp
toolkit/library/moz.build
--- a/.hgignore
+++ b/.hgignore
@@ -129,8 +129,11 @@ GPATH
 ^testing/talos/talos/tests/devtools/damp.manifest.develop
 ^talos-venv
 
 # Ignore files created when running a reftest.
 ^lextab.py$
 
 # tup database
 ^\.tup
+
+subinclude:servo/.hgignore
+
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Touch clobber because of bug 1336456
+Touch clobber again because of bug 1336456
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -564,35 +564,22 @@ var Output = {
         if (highlightBox) {
           highlightBox.classList.remove('show');
         }
         break;
       }
     }
   },
 
-  get androidBridge() {
-    delete this.androidBridge;
-    if (Utils.MozBuildApp === 'mobile/android') {
-      this.androidBridge = Services.androidBridge;
-    } else {
-      this.androidBridge = null;
-    }
-    return this.androidBridge;
-  },
-
   Android: function Android(aDetails, aBrowser) {
     const ANDROID_VIEW_TEXT_CHANGED = 0x10;
     const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
 
-    if (!this.androidBridge) {
-      return;
-    }
-
     for (let androidEvent of aDetails) {
+      androidEvent.type = 'Accessibility:Event';
       if (androidEvent.bounds) {
         androidEvent.bounds = AccessFu.adjustContentBounds(
           androidEvent.bounds, aBrowser);
       }
 
       switch(androidEvent.eventType) {
         case ANDROID_VIEW_TEXT_CHANGED:
           androidEvent.brailleOutput = this.brailleState.adjustText(
@@ -602,19 +589,18 @@ var Output = {
           androidEvent.brailleOutput = this.brailleState.adjustSelection(
             androidEvent.brailleOutput);
           break;
         default:
           androidEvent.brailleOutput = this.brailleState.init(
             androidEvent.brailleOutput);
           break;
       }
-      let win = Utils.win;
-      let view = win && win.QueryInterface(Ci.nsIAndroidView);
-      view.dispatch('Accessibility:Event', androidEvent);
+
+      Utils.win.WindowEventDispatcher.sendRequest(androidEvent);
     }
   },
 
   Braille: function Braille(aDetails) {
     Logger.debug('Braille output: ' + aDetails.output);
   }
 };
 
@@ -800,19 +786,17 @@ var Input = {
             return;
           } else {
             target.blur();
           }
         }
 
         if (Utils.MozBuildApp == 'mobile/android') {
           // Return focus to native Android browser chrome.
-          let win = Utils.win;
-          let view = win && win.QueryInterface(Ci.nsIAndroidView);
-          view.dispatch('ToggleChrome:Focus');
+          Utils.win.WindowEventDispatcher.dispatch('ToggleChrome:Focus');
         }
         break;
       case aEvent.DOM_VK_RETURN:
         if (this.editState.editing) {
           return;
         }
         this.activateCurrent();
         break;
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -310,17 +310,17 @@ AndroidPresenter.prototype.pivotChanged 
   };
 
 AndroidPresenter.prototype.actionInvoked =
   function AndroidPresenter_actionInvoked(aObject, aActionName) {
     let state = Utils.getState(aObject);
 
     // Checkable objects use TalkBack's text derived from the event state,
     // so we don't populate the text here.
-    let text = '';
+    let text = null;
     if (!state.contains(States.CHECKABLE)) {
       text = Utils.localize(UtteranceGenerator.genForAction(aObject,
         aActionName));
     }
 
     return {
       type: this.type,
       details: [{
--- a/accessible/tests/browser/e10s/browser.ini
+++ b/accessible/tests/browser/e10s/browser.ini
@@ -1,51 +1,76 @@
 [DEFAULT]
-skip-if = (e10s && os == 'win') # Bug 1269369: Document loaded event does not fire in Windows
 support-files =
   events.js
   head.js
   doc_treeupdate_ariadialog.html
   doc_treeupdate_ariaowns.html
   doc_treeupdate_imagemap.html
   doc_treeupdate_removal.xhtml
   doc_treeupdate_visibility.html
   doc_treeupdate_whitespace.html
   !/accessible/tests/browser/shared-head.js
   !/accessible/tests/mochitest/*.js
   !/accessible/tests/mochitest/letters.gif
   !/accessible/tests/mochitest/moz.png
 
 # Caching tests
 [browser_caching_attributes.js]
+skip-if = e10s && os == 'win' # Bug 1288839
 [browser_caching_description.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_caching_name.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_caching_relations.js]
+skip-if = e10s && os == 'win' # Bug 1288839
 [browser_caching_states.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_caching_value.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 
 # Events tests
 [browser_events_caretmove.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_events_hide.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_events_show.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_events_statechange.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_events_textchange.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 
 # Tree update tests
 [browser_treeupdate_ariadialog.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_ariaowns.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_canvas.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_cssoverflow.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_doc.js]
+skip-if = e10s && os == 'win' # Bug 1288839
 [browser_treeupdate_gencontent.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_hidden.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_imagemap.js]
-skip-if = e10s # Bug 1318569
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_list.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_list_editabledoc.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_listener.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_optgroup.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_removal.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_table.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_textleaf.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_visibility.js]
+skip-if = e10s && os == 'win' && os_version == '5.1'
 [browser_treeupdate_whitespace.js]
 skip-if = true # Failing due to incorrect index of test container children on document load.
--- a/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js
+++ b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js
@@ -165,12 +165,27 @@ function* testContainer(browser) {
   yield onReorder;
 
   tree = {
     SECTION: [ ]
   };
   testAccessibleTree(acc, tree);
 }
 
+function* waitForImageMap(browser, accDoc) {
+  const id = 'imgmap';
+  const acc = findAccessibleChildByID(accDoc, id);
+  if (acc.firstChild) {
+    return;
+  }
+
+  const onReorder = waitForEvent(EVENT_REORDER, id);
+  // Wave over image map
+  yield BrowserTestUtils.synthesizeMouse(`#${id}`, 10, 10,
+                                         { type: 'mousemove' }, browser);
+  yield onReorder;
+}
+
 addAccessibleTask('doc_treeupdate_imagemap.html', function*(browser, accDoc) {
+  yield waitForImageMap(browser, accDoc);
   yield testImageMap(browser, accDoc);
   yield testContainer(browser);
 });
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -487,17 +487,16 @@
 @RESPATH@/components/nsContentDispatchChooser.manifest
 @RESPATH@/components/nsContentDispatchChooser.js
 @RESPATH@/components/nsHandlerService.manifest
 @RESPATH@/components/nsHandlerService.js
 @RESPATH@/components/nsWebHandlerApp.manifest
 @RESPATH@/components/nsWebHandlerApp.js
 @RESPATH@/components/satchel.manifest
 @RESPATH@/components/nsFormAutoComplete.js
-@RESPATH@/components/nsFormHistory.js
 @RESPATH@/components/FormHistoryStartup.js
 @RESPATH@/components/nsInputListAutoComplete.js
 @RESPATH@/components/formautofill.manifest
 @RESPATH@/components/FormAutofillContentService.js
 @RESPATH@/components/FormAutofillStartup.js
 @RESPATH@/components/contentAreaDropListener.manifest
 @RESPATH@/components/contentAreaDropListener.js
 @RESPATH@/components/messageWakeupService.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2873,34 +2873,16 @@ var BrowserOnClick = {
       case "captive-portal-login-success":
         // Broadcast when a captive portal is freed so that error pages
         // can refresh themselves.
         window.messageManager.broadcastAsyncMessage("Browser:CaptivePortalFreed");
       break;
     }
   },
 
-  handleEvent(event) {
-    if (!event.isTrusted || // Don't trust synthetic events
-        event.button == 2) {
-      return;
-    }
-
-    let originalTarget = event.originalTarget;
-    let ownerDoc = originalTarget.ownerDocument;
-    if (!ownerDoc) {
-      return;
-    }
-
-    if (gMultiProcessBrowser &&
-        ownerDoc.documentURI.toLowerCase() == "about:newtab") {
-      this.onE10sAboutNewTab(event, ownerDoc);
-    }
-  },
-
   receiveMessage(msg) {
     switch (msg.name) {
       case "Browser:CertExceptionError":
         this.onCertError(msg.target, msg.data.elementId,
                          msg.data.isTopFrame, msg.data.location,
                          msg.data.securityInfoAsString);
       break;
       case "Browser:OpenCaptivePortalPage":
@@ -3077,38 +3059,16 @@ var BrowserOnClick = {
             secHistogram.add(nsISecTel[bucketName + "IGNORE_WARNING"]);
           }
           this.ignoreWarningButton(reason);
         }
         break;
     }
   },
 
-  /**
-   * This functions prevents navigation from happening directly through the <a>
-   * link in about:newtab (which is loaded in the parent and therefore would load
-   * the next page also in the parent) and instructs the browser to open the url
-   * in the current tab which will make it update the remoteness of the tab.
-   */
-  onE10sAboutNewTab(event, ownerDoc) {
-    let isTopFrame = (ownerDoc.defaultView.parent === ownerDoc.defaultView);
-    if (!isTopFrame) {
-      return;
-    }
-
-    let anchorTarget = event.originalTarget.parentNode;
-
-    if (anchorTarget instanceof HTMLAnchorElement &&
-        anchorTarget.classList.contains("newtab-link")) {
-      event.preventDefault();
-      let where = whereToOpenLink(event, false, false);
-      openLinkIn(anchorTarget.href, where, { charset: ownerDoc.characterSet, referrerURI: ownerDoc.documentURIObject });
-    }
-  },
-
   ignoreWarningButton(reason) {
     // Allow users to override and continue through to the site,
     // but add a notify bar as a reminder, so that they don't lose
     // track after, e.g., tab switching.
     gBrowser.loadURIWithFlags(gBrowser.currentURI.spec,
                               nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
                               null, null, null);
 
@@ -4919,42 +4879,36 @@ var TabsProgressListener = {
       } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
                  aStatus == Cr.NS_BINDING_ABORTED &&
                  this._startedLoadTimer.has(aBrowser)) {
         this._startedLoadTimer.delete(aBrowser);
         TelemetryStopwatch.cancel("FX_PAGE_LOAD_MS", aBrowser);
       }
     }
 
-    // Attach a listener to watch for "click" events bubbling up from error
-    // pages and other similar pages (like about:newtab). This lets us fix bugs
-    // like 401575 which require error page UI to do privileged things, without
-    // letting error pages have any privilege themselves.
-    // We can't look for this during onLocationChange since at that point the
-    // document URI is not yet the about:-uri of the error page.
-
+    // We used to listen for clicks in the browser here, but when that
+    // became unnecessary, removing the code below caused focus issues.
+    // This code should be removed. Tracked in bug 1337794.
     let isRemoteBrowser = aBrowser.isRemoteBrowser;
     // We check isRemoteBrowser here to avoid requesting the doc CPOW
     let doc = isRemoteBrowser ? null : aWebProgress.DOMWindow.document;
 
     if (!isRemoteBrowser &&
         aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
         Components.isSuccessCode(aStatus) &&
         doc.documentURI.startsWith("about:") &&
         !doc.documentURI.toLowerCase().startsWith("about:blank") &&
         !doc.documentURI.toLowerCase().startsWith("about:home") &&
         !doc.documentElement.hasAttribute("hasBrowserHandlers")) {
       // STATE_STOP may be received twice for documents, thus store an
       // attribute to ensure handling it just once.
       doc.documentElement.setAttribute("hasBrowserHandlers", "true");
-      aBrowser.addEventListener("click", BrowserOnClick, true);
       aBrowser.addEventListener("pagehide", function onPageHide(event) {
         if (event.target.defaultView.frameElement)
           return;
-        aBrowser.removeEventListener("click", BrowserOnClick, true);
         aBrowser.removeEventListener("pagehide", onPageHide, true);
         if (event.target.documentElement)
           event.target.documentElement.removeAttribute("hasBrowserHandlers");
       }, true);
     }
   },
 
   onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI,
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -76,16 +76,18 @@ support-files =
   zoom_test.html
   file_install_extensions.html
   browser_webext_permissions.xpi
   browser_webext_nopermissions.xpi
   browser_webext_update1.xpi
   browser_webext_update2.xpi
   browser_webext_update_icon1.xpi
   browser_webext_update_icon2.xpi
+  browser_webext_update_perms1.xpi
+  browser_webext_update_perms2.xpi
   browser_webext_update.json
   !/image/test/mochitest/blue.png
   !/toolkit/content/tests/browser/common/mockTransfer.js
   !/toolkit/modules/tests/browser/metadata_*.html
   !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
@@ -247,17 +249,18 @@ skip-if = os == "mac" # decoder doctor i
 [browser_discovery.js]
 [browser_double_close_tab.js]
 [browser_documentnavigation.js]
 [browser_duplicateIDs.js]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
 [browser_extension_permissions.js]
 [browser_extension_sideloading.js]
-[browser_extension_update.js]
+[browser_extension_update_background.js]
+[browser_extension_update_interactive.js]
 [browser_favicon_change.js]
 [browser_favicon_change_not_in_document.js]
 [browser_findbarClose.js]
 [browser_focusonkeydown.js]
 [browser_fullscreen-window-open.js]
 tags = fullscreen
 skip-if = os == "linux" # Linux: Intermittent failures - bug 941575.
 [browser_fxaccounts.js]
@@ -344,17 +347,17 @@ skip-if = os == "linux" # Bug 1329991 - 
 [browser_syncui.js]
 skip-if = os == 'linux' # Bug 1304272
 [browser_tab_close_dependent_window.js]
 [browser_tabDrop.js]
 [browser_tabReorder.js]
 [browser_tab_detach_restore.js]
 [browser_tab_drag_drop_perwindow.js]
 [browser_tab_dragdrop.js]
-skip-if = buildapp == 'mulet' || (e10s && (debug || asan)) # Bug 1312436
+skip-if = buildapp == 'mulet' || (e10s && (debug || os == 'linux')) # Bug 1312436
 [browser_tab_dragdrop2.js]
 [browser_tabbar_big_widgets.js]
 skip-if = os == "linux" || os == "mac" # No tabs in titlebar on linux
                                        # Disabled on OS X because of bug 967917
 [browser_tabfocus.js]
 [browser_tabkeynavigation.js]
 skip-if = (os == "mac" && !e10s) # Bug 1237713 - OSX eats keypresses for some reason
 [browser_tabopen_reflows.js]
--- a/browser/base/content/test/general/browser_bookmark_popup.js
+++ b/browser/base/content/test/general/browser_bookmark_popup.js
@@ -22,16 +22,21 @@ function* test_bookmarks_popup({isNewBoo
       if (!isNewBookmark) {
         yield PlacesUtils.bookmarks.insert({
           parentGuid: PlacesUtils.bookmarks.unfiledGuid,
           url: "about:home",
           title: "Home Page"
         });
       }
 
+      info(`BookmarkingUI.status is ${BookmarkingUI.status}`);
+      yield BrowserTestUtils.waitForCondition(
+        () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING,
+        "BookmarkingUI should not be updating");
+
       is(bookmarkStar.hasAttribute("starred"), !isNewBookmark,
          "Page should only be starred prior to popupshown if editing bookmark");
       is(bookmarkPanel.state, "closed", "Panel should be 'closed' to start test");
       let shownPromise = promisePopupShown(bookmarkPanel);
       yield popupShowFn(browser);
       yield shownPromise;
       is(bookmarkPanel.state, "open", "Panel should be 'open' after shownPromise is resolved");
 
--- a/browser/base/content/test/general/browser_extension_sideloading.js
+++ b/browser/base/content/test/general/browser_extension_sideloading.js
@@ -66,35 +66,16 @@ class MockProvider {
     let addons = [];
     if (!types || types.includes("extension")) {
       addons = [...this.addons];
     }
     callback(addons);
   }
 }
 
-function promiseViewLoaded(tab, viewid) {
-  let win = tab.linkedBrowser.contentWindow;
-  if (win.gViewController && !win.gViewController.isLoading &&
-      win.gViewController.currentViewId == viewid) {
-     return Promise.resolve();
-  }
-
-  return new Promise(resolve => {
-    function listener() {
-      if (win.gViewController.currentViewId != viewid) {
-        return;
-      }
-      win.document.removeEventListener("ViewChanged", listener);
-      resolve();
-    }
-    win.document.addEventListener("ViewChanged", listener);
-  });
-}
-
 function promisePopupNotificationShown(name) {
   return new Promise(resolve => {
     function popupshown() {
       let notification = PopupNotifications.getNotification(name);
       if (!notification) {
         return;
       }
 
@@ -164,16 +145,27 @@ add_task(function* () {
     AddonManagerPrivate.unregisterProvider(provider);
 
     // clear out ExtensionsUI state about sideloaded extensions so
     // subsequent tests don't get confused.
     ExtensionsUI.sideloaded.clear();
     ExtensionsUI.emit("change");
   });
 
+  // Navigate away from the starting page to force about:addons to load
+  // in a new tab during the tests below.
+  gBrowser.selectedBrowser.loadURI("about:robots");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+  registerCleanupFunction(function*() {
+    // Return to about:blank when we're done
+    gBrowser.selectedBrowser.loadURI("about:blank");
+    yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  });
+
   let changePromise = new Promise(resolve => {
     ExtensionsUI.on("change", function listener() {
       ExtensionsUI.off("change", listener);
       resolve();
     });
   });
   ExtensionsUI._checkForSideloaded();
   yield changePromise;
@@ -184,77 +176,73 @@ add_task(function* () {
 
   // Find the menu entries for sideloaded extensions
   yield PanelUI.show();
 
   let addons = document.getElementById("PanelUI-footer-addons");
   is(addons.children.length, 2, "Have 2 menu entries for sideloaded extensions");
 
   // Click the first sideloaded extension
-  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
   let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
 
-  // about:addons should load and go to the list of extensions
-  let tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons", "Newly opened tab is at about:addons");
+  // When we get the permissions prompt, we should be at the extensions
+  // list in about:addons
+  let panel = yield popupPromise;
+  is(gBrowser.currentURI.spec, "about:addons", "Foreground tab is at about:addons");
 
   const VIEW = "addons://list/extension";
-  yield promiseViewLoaded(tab, VIEW);
-  let win = tab.linkedBrowser.contentWindow;
+  let win = gBrowser.selectedBrowser.contentWindow;
   ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
   is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
 
-  // Wait for the permission prompt and cancel it
-  let panel = yield popupPromise;
+  // Check the contents of the notification, then choose "Cancel"
   let icon = panel.getAttribute("icon");
   is(icon, ICON_URL, "Permissions notification has the addon icon");
 
   let disablePromise = promiseSetDisabled(mock1);
   panel.secondaryButton.click();
 
   let value = yield disablePromise;
   is(value, true, "Addon should remain disabled");
 
   let [addon1, addon2] = yield AddonManager.getAddonsByIDs([ID1, ID2]);
   ok(addon1.seen, "Addon should be marked as seen");
   is(addon1.userDisabled, true, "Addon 1 should still be disabled");
   is(addon2.userDisabled, true, "Addon 2 should still be disabled");
 
-  yield BrowserTestUtils.removeTab(tab);
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
   // Should still have 1 entry in the hamburger menu
   yield PanelUI.show();
 
   addons = document.getElementById("PanelUI-footer-addons");
   is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions");
 
-  // Click the second sideloaded extension
-  tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
+  // Click the second sideloaded extension and wait for the notification
   popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
-
-  tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons", "Newly opened tab is at about:addons");
+  panel = yield popupPromise;
 
   isnot(menuButton.getAttribute("badge-status"), "addon-alert", "Should no longer have addon alert badge");
 
-  yield promiseViewLoaded(tab, VIEW);
-  win = tab.linkedBrowser.contentWindow;
+  // Again we should be at the extentions list in about:addons
+  is(gBrowser.currentURI.spec, "about:addons", "Foreground tab is at about:addons");
+
+  win = gBrowser.selectedBrowser.contentWindow;
   ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
   is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
 
-  // Wait for the permission prompt and accept it this time
-  panel = yield popupPromise;
+  // Check the notification contents, this time accept the install
   icon = panel.getAttribute("icon");
   is(icon, DEFAULT_ICON_URL, "Permissions notification has the default icon");
   disablePromise = promiseSetDisabled(mock2);
   panel.button.click();
 
   value = yield disablePromise;
   is(value, false, "Addon should be set to enabled");
 
   [addon1, addon2] = yield AddonManager.getAddonsByIDs([ID1, ID2]);
   is(addon1.userDisabled, true, "Addon 1 should still be disabled");
   is(addon2.userDisabled, false, "Addon 2 should now be enabled");
 
-  yield BrowserTestUtils.removeTab(tab);
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
rename from browser/base/content/test/general/browser_extension_update.js
rename to browser/base/content/test/general/browser_extension_update_background.js
--- a/browser/base/content/test/general/browser_extension_update.js
+++ b/browser/base/content/test/general/browser_extension_update_background.js
@@ -69,25 +69,36 @@ function promiseInstallEvent(addon, even
         resolve(...args);
       }
     };
     AddonManager.addInstallListener(listener);
   });
 }
 
 // Set some prefs that apply to all the tests in this file
-add_task(function setup() {
-  return SpecialPowers.pushPrefEnv({set: [
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({set: [
     // We don't have pre-pinned certificates for the local mochitest server
     ["extensions.install.requireBuiltInCerts", false],
     ["extensions.update.requireBuiltInCerts", false],
 
     // XXX remove this when prompts are enabled by default
     ["extensions.webextPermissionPrompts", true],
   ]});
+
+  // Navigate away from the initial page so that about:addons always
+  // opens in a new tab during tests
+  gBrowser.selectedBrowser.loadURI("about:robots");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+  registerCleanupFunction(function*() {
+    // Return to about:blank when we're done
+    gBrowser.selectedBrowser.loadURI("about:blank");
+    yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+  });
 });
 
 // Helper function to test background updates.
 function* backgroundUpdateTest(url, id, checkIconFn) {
   yield SpecialPowers.pushPrefEnv({set: [
     // Turn on background updates
     ["extensions.update.enabled", true],
 
@@ -118,17 +129,17 @@ function* backgroundUpdateTest(url, id, 
 
   // Click the menu item
   let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
   let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
   addons.children[0].click();
 
   // about:addons should load and go to the list of extensions
   let tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons");
+  is(tab.linkedBrowser.currentURI.spec, "about:addons", "Browser is at about:addons");
 
   const VIEW = "addons://list/extension";
   yield promiseViewLoaded(tab, VIEW);
   let win = tab.linkedBrowser.contentWindow;
   ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
   is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
 
   // Wait for the permission prompt, check the contents, then cancel the update
@@ -206,119 +217,50 @@ function checkNonDefaultIcon(icon) {
   // inside the jar.
   ok(icon.startsWith("jar:file://"), "Icon is a jar url");
   ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
 }
 
 add_task(() => backgroundUpdateTest(`${URL_BASE}/browser_webext_update_icon1.xpi`,
                                     ID_ICON, checkNonDefaultIcon));
 
-// Helper function to test a specific scenario for interactive updates.
-// `checkFn` is a callable that triggers a check for updates.
-// `autoUpdate` specifies whether the test should be run with
-// updates applied automatically or not.
-function* interactiveUpdateTest(autoUpdate, checkFn) {
+// Test that an update that adds new non-promptable permissions is just
+// applied without showing a notification dialog.
+add_task(function*() {
   yield SpecialPowers.pushPrefEnv({set: [
-    ["extensions.update.autoUpdateDefault", autoUpdate],
+    // Turn on background updates
+    ["extensions.update.enabled", true],
 
     // Point updates to the local mochitest server
-    ["extensions.update.url", `${URL_BASE}/browser_webext_update.json`],
+    ["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
   ]});
 
-  // Trigger an update check, manually applying the update if we're testing
-  // without auto-update.
-  function* triggerUpdate(win, addon) {
-    let manualUpdatePromise;
-    if (!autoUpdate) {
-      manualUpdatePromise = new Promise(resolve => {
-        let listener = {
-          onNewInstall() {
-            AddonManager.removeInstallListener(listener);
-            resolve();
-          },
-        };
-        AddonManager.addInstallListener(listener);
-      });
-    }
-
-    checkFn(win, addon);
-
-    if (manualUpdatePromise) {
-      yield manualUpdatePromise;
-
-      let item = win.document.getElementById("addon-list")
-                    .children.find(_item => _item.value == ID);
-      EventUtils.synthesizeMouseAtCenter(item._updateBtn, {}, win);
-    }
-  }
-
   // Install version 1.0 of the test extension
-  let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update1.xpi`);
+  let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update_perms1.xpi`);
+
   ok(addon, "Addon was installed");
-  is(addon.version, "1.0", "Version 1 of the addon is installed");
 
-  // Open add-ons manager and navigate to extensions list
-  let loadPromise = new Promise(resolve => {
-    let listener = (subject, topic) => {
-      if (subject.location.href == "about:addons") {
-        Services.obs.removeObserver(listener, topic);
-        resolve(subject);
-      }
-    };
-    Services.obs.addObserver(listener, "EM-loaded", false);
-  });
-  let tab = gBrowser.addTab("about:addons");
-  gBrowser.selectedTab = tab;
-  let win = yield loadPromise;
+  let sawPopup = false;
+  PopupNotifications.panel.addEventListener("popupshown",
+                                            () => sawPopup = true,
+                                            {once: true});
 
-  const VIEW = "addons://list/extension";
-  let viewPromise = promiseViewLoaded(tab, VIEW);
-  win.loadView(VIEW);
-  yield viewPromise;
+  // Trigger an update check and wait for the update to be applied.
+  let updatePromise = promiseInstallEvent(addon, "onInstallEnded");
+  AddonManagerPrivate.backgroundUpdateCheck();
+  yield updatePromise;
 
-  // Trigger an update check
-  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
-  yield triggerUpdate(win, addon);
-  let panel = yield popupPromise;
-
-  // Click the cancel button, wait to see the cancel event
-  let cancelPromise = promiseInstallEvent(addon, "onInstallCancelled");
-  panel.secondaryButton.click();
-  yield cancelPromise;
-
-  addon = yield AddonManager.getAddonByID(ID);
-  is(addon.version, "1.0", "Should still be running the old version");
+  // There should be no notifications about the update
+  is(getBadgeStatus(), "", "Should not have addon alert badge");
 
-  // Trigger a new update check
-  popupPromise = promisePopupNotificationShown("addon-webext-permissions");
-  yield triggerUpdate(win, addon);
+  yield PanelUI.show();
+  let addons = document.getElementById("PanelUI-footer-addons");
+  is(addons.children.length, 0, "Have 0 updates in the PanelUI menu");
+  yield PanelUI.hide();
 
-  // This time, accept the upgrade
-  let updatePromise = promiseInstallEvent(addon, "onInstallEnded");
-  panel = yield popupPromise;
-  panel.button.click();
+  ok(!sawPopup, "Should not have seen permissions notification");
 
-  addon = yield updatePromise;
-  is(addon.version, "2.0", "Should have upgraded");
+  addon = yield AddonManager.getAddonByID("update_perms@tests.mozilla.org");
+  is(addon.version, "2.0", "Update should have applied");
 
-  yield BrowserTestUtils.removeTab(tab);
   addon.uninstall();
   yield SpecialPowers.popPrefEnv();
-}
-
-// Invoke the "Check for Updates" menu item
-function checkAll(win) {
-  win.gViewController.doCommand("cmd_findAllUpdates");
-}
-
-// Test "Check for Updates" with both auto-update settings
-add_task(() => interactiveUpdateTest(true, checkAll));
-add_task(() => interactiveUpdateTest(false, checkAll));
-
-
-// Invoke an invidual extension's "Find Updates" menu item
-function checkOne(win, addon) {
-  win.gViewController.doCommand("cmd_findItemUpdates", addon);
-}
-
-// Test "Find Updates" with both auto-update settings
-add_task(() => interactiveUpdateTest(true, checkOne));
-add_task(() => interactiveUpdateTest(false, checkOne));
+});
copy from browser/base/content/test/general/browser_extension_update.js
copy to browser/base/content/test/general/browser_extension_update_interactive.js
--- a/browser/base/content/test/general/browser_extension_update.js
+++ b/browser/base/content/test/general/browser_extension_update_interactive.js
@@ -1,13 +1,12 @@
 const {AddonManagerPrivate} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 
 const URL_BASE = "https://example.com/browser/browser/base/content/test/general";
 const ID = "update@tests.mozilla.org";
-const ID_ICON = "update_icon@tests.mozilla.org";
 
 function promiseInstallAddon(url) {
   return AddonManager.getInstallForURL(url, null, "application/x-xpinstall")
                      .then(install => {
                        ok(install, "Created install");
                        return new Promise(resolve => {
                          install.addListener({
                            onInstallEnded(_install, addon) {
@@ -50,172 +49,41 @@ function promisePopupNotificationShown(n
       PopupNotifications.panel.removeEventListener("popupshown", popupshown);
       resolve(PopupNotifications.panel.firstChild);
     }
 
     PopupNotifications.panel.addEventListener("popupshown", popupshown);
   });
 }
 
-function getBadgeStatus() {
-  let menuButton = document.getElementById("PanelUI-menu-button");
-  return menuButton.getAttribute("badge-status");
-}
-
 function promiseInstallEvent(addon, event) {
   return new Promise(resolve => {
     let listener = {};
     listener[event] = (install, ...args) => {
       if (install.addon.id == addon.id) {
         AddonManager.removeInstallListener(listener);
         resolve(...args);
       }
     };
     AddonManager.addInstallListener(listener);
   });
 }
 
 // Set some prefs that apply to all the tests in this file
-add_task(function setup() {
-  return SpecialPowers.pushPrefEnv({set: [
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({set: [
     // We don't have pre-pinned certificates for the local mochitest server
     ["extensions.install.requireBuiltInCerts", false],
     ["extensions.update.requireBuiltInCerts", false],
 
     // XXX remove this when prompts are enabled by default
     ["extensions.webextPermissionPrompts", true],
   ]});
 });
 
-// Helper function to test background updates.
-function* backgroundUpdateTest(url, id, checkIconFn) {
-  yield SpecialPowers.pushPrefEnv({set: [
-    // Turn on background updates
-    ["extensions.update.enabled", true],
-
-    // Point updates to the local mochitest server
-    ["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
-  ]});
-
-  // Install version 1.0 of the test extension
-  let addon = yield promiseInstallAddon(url);
-
-  ok(addon, "Addon was installed");
-  is(getBadgeStatus(), "", "Should not start out with an addon alert badge");
-
-  // Trigger an update check and wait for the update for this addon
-  // to be downloaded.
-  let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
-
-  AddonManagerPrivate.backgroundUpdateCheck();
-  yield updatePromise;
-
-  is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
-
-  // Find the menu entry for the update
-  yield PanelUI.show();
-
-  let addons = document.getElementById("PanelUI-footer-addons");
-  is(addons.children.length, 1, "Have a menu entry for the update");
-
-  // Click the menu item
-  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
-  let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
-  addons.children[0].click();
-
-  // about:addons should load and go to the list of extensions
-  let tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons");
-
-  const VIEW = "addons://list/extension";
-  yield promiseViewLoaded(tab, VIEW);
-  let win = tab.linkedBrowser.contentWindow;
-  ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
-  is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
-
-  // Wait for the permission prompt, check the contents, then cancel the update
-  let panel = yield popupPromise;
-  checkIconFn(panel.getAttribute("icon"));
-  panel.secondaryButton.click();
-
-  addon = yield AddonManager.getAddonByID(id);
-  is(addon.version, "1.0", "Should still be running the old version");
-
-  yield BrowserTestUtils.removeTab(tab);
-
-  // Alert badge and hamburger menu items should be gone
-  is(getBadgeStatus(), "", "Addon alert badge should be gone");
-
-  yield PanelUI.show();
-  addons = document.getElementById("PanelUI-footer-addons");
-  is(addons.children.length, 0, "Update menu entries should be gone");
-  yield PanelUI.hide();
-
-  // Re-check for an update
-  updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
-  yield AddonManagerPrivate.backgroundUpdateCheck();
-  yield updatePromise;
-
-  is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
-
-  // Find the menu entry for the update
-  yield PanelUI.show();
-
-  addons = document.getElementById("PanelUI-footer-addons");
-  is(addons.children.length, 1, "Have a menu entry for the update");
-
-  // Click the menu item
-  tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
-  popupPromise = promisePopupNotificationShown("addon-webext-permissions");
-  addons.children[0].click();
-
-  // Wait for about:addons to load
-  tab = yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "about:addons");
-
-  yield promiseViewLoaded(tab, VIEW);
-  win = tab.linkedBrowser.contentWindow;
-  ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
-  is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
-
-  // Wait for the permission prompt and accept it this time
-  updatePromise = promiseInstallEvent(addon, "onInstallEnded");
-  panel = yield popupPromise;
-  panel.button.click();
-
-  addon = yield updatePromise;
-  is(addon.version, "2.0", "Should have upgraded to the new version");
-
-  yield BrowserTestUtils.removeTab(tab);
-
-  is(getBadgeStatus(), "", "Addon alert badge should be gone");
-
-  addon.uninstall();
-  yield SpecialPowers.popPrefEnv();
-}
-
-function checkDefaultIcon(icon) {
-  is(icon, "chrome://mozapps/skin/extensions/extensionGeneric.svg",
-     "Popup has the default extension icon");
-}
-
-add_task(() => backgroundUpdateTest(`${URL_BASE}/browser_webext_update1.xpi`,
-                                    ID, checkDefaultIcon));
-
-function checkNonDefaultIcon(icon) {
-  // The icon should come from the extension, don't bother with the precise
-  // path, just make sure we've got a jar url pointing to the right path
-  // inside the jar.
-  ok(icon.startsWith("jar:file://"), "Icon is a jar url");
-  ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
-}
-
-add_task(() => backgroundUpdateTest(`${URL_BASE}/browser_webext_update_icon1.xpi`,
-                                    ID_ICON, checkNonDefaultIcon));
-
 // Helper function to test a specific scenario for interactive updates.
 // `checkFn` is a callable that triggers a check for updates.
 // `autoUpdate` specifies whether the test should be run with
 // updates applied automatically or not.
 function* interactiveUpdateTest(autoUpdate, checkFn) {
   yield SpecialPowers.pushPrefEnv({set: [
     ["extensions.update.autoUpdateDefault", autoUpdate],
 
--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -89,16 +89,28 @@ const PAGECONTENT_COLORS =
   "</style>" +
   "<body><select id='one'>" +
   '  <option value="One" style="color: #fff; background-color: #f00;">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(255, 0, 0)"}</option>' +
   '  <option value="Two" class="blue">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 255)"}</option>' +
   '  <option value="Three" class="green">{"color": "rgb(128, 0, 128)", "backgroundColor": "rgb(0, 128, 0)"}</option>' +
   '  <option value="Four" class="defaultColor defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "transparent", "unstyled": "true"}</option>' +
   '  <option value="Five" class="defaultColor">{"color": "-moz-ComboboxText", "backgroundColor": "transparent", "unstyled": "true"}</option>' +
   '  <option value="Six" class="defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "transparent", "unstyled": "true"}</option>' +
+  '  <option value="Seven" selected="true">{"unstyled": "true"}</option>' +
+  "</select></body></html>";
+
+const PAGECONTENT_COLORS_ON_SELECT =
+  "<html><head><style>" +
+  "  #one { background-color: #7E3A3A; color: #fff }" +
+  "</style>" +
+  "<body><select id='one'>" +
+  '  <option value="One">{"color": "rgb(255, 255, 255)", "backgroundColor": "transparent"}</option>' +
+  '  <option value="Two">{"color": "rgb(255, 255, 255)", "backgroundColor": "transparent"}</option>' +
+  '  <option value="Three">{"color": "rgb(255, 255, 255)", "backgroundColor": "transparent"}</option>' +
+  '  <option value="Four" selected="true">{"end": "true"}</option>' +
   "</select></body></html>";
 
 function openSelectPopup(selectPopup, mode = "key", selector = "select", win = window) {
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
 
   if (mode == "click" || mode == "mousedown") {
     let mousePromise;
     if (mode == "click") {
@@ -145,16 +157,48 @@ function getChangeEvents() {
 }
 
 function getClickEvents() {
   return ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     return content.wrappedJSObject.gClickEvents;
   });
 }
 
+function testOptionColors(index, item, menulist) {
+  let expected = JSON.parse(item.label);
+
+  for (let color of Object.keys(expected)) {
+    if (color.toLowerCase().includes("color") &&
+        !expected[color].startsWith("rgb")) {
+      // Need to convert system color to RGB color.
+      let textarea = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea");
+      textarea.style.color = expected[color];
+      expected[color] = getComputedStyle(textarea).color;
+    }
+  }
+
+  // Press Down to move the selected item to the next item in the
+  // list and check the colors of this item when it's not selected.
+  EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
+
+  if (expected.end) {
+    return;
+  }
+
+  if (expected.unstyled) {
+    ok(!item.hasAttribute("customoptionstyling"),
+      `Item ${index} should not have any custom option styling`);
+  } else {
+    is(getComputedStyle(item).color, expected.color,
+       "Item " + (index) + " has correct foreground color");
+    is(getComputedStyle(item).backgroundColor, expected.backgroundColor,
+       "Item " + (index) + " has correct background color");
+  }
+}
+
 function* doSelectTests(contentType, dtd) {
   const pageUrl = "data:" + contentType + "," + escape(dtd + "\n" + PAGECONTENT);
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
   let menulist = document.getElementById("ContentSelectDropdown");
   let selectPopup = menulist.menupopup;
 
   yield openSelectPopup(selectPopup);
@@ -740,59 +784,70 @@ add_task(function* test_somehidden() {
        "Item " + (idx++) + " is visible");
     child = child.nextSibling;
   }
 
   yield hideSelectPopup(selectPopup, "escape");
   yield BrowserTestUtils.removeTab(tab);
 });
 
-add_task(function* test_colors_applied_to_popup() {
+// This test checks when a <select> element has styles applied to <option>s within it.
+add_task(function* test_colors_applied_to_popup_items() {
   const pageUrl = "data:text/html," + escape(PAGECONTENT_COLORS);
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
-  let selectPopup = document.getElementById("ContentSelectDropdown").menupopup;
+  let menulist = document.getElementById("ContentSelectDropdown");
+  let selectPopup = menulist.menupopup;
 
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
   yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
   yield popupShownPromise;
 
   // The label contains a JSON string of the expected colors for
   // `color` and `background-color`.
-  is(selectPopup.parentNode.itemCount, 6, "Correct number of items");
+  is(selectPopup.parentNode.itemCount, 7, "Correct number of items");
   let child = selectPopup.firstChild;
   let idx = 1;
 
-  ok(child.selected, "The first child should be selected");
+  ok(!child.selected, "The first child should not be selected");
   while (child) {
-    let expected = JSON.parse(child.label);
-
-    for (let color of Object.keys(expected)) {
-      if (color.toLowerCase().includes("color") &&
-          !expected[color].startsWith("rgb")) {
-        // Need to convert system color to RGB color.
-        let textarea = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea");
-        textarea.style.color = expected[color];
-        expected[color] = getComputedStyle(textarea).color;
-      }
-    }
-
-    // Press Down to move the selected item to the next item in the
-    // list and check the colors of this item when it's not selected.
-    EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
-
-    if (expected.unstyled) {
-      ok(!child.hasAttribute("customoptionstyling"),
-        `Item ${idx} should not have any custom option styling`);
-    } else {
-      is(getComputedStyle(child).color, expected.color,
-         "Item " + (idx) + " has correct foreground color");
-      is(getComputedStyle(child).backgroundColor, expected.backgroundColor,
-         "Item " + (idx) + " has correct background color");
-    }
-
+    testOptionColors(idx, child, menulist);
     idx++;
     child = child.nextSibling;
   }
 
   yield hideSelectPopup(selectPopup, "escape");
   yield BrowserTestUtils.removeTab(tab);
 });
+
+// This test checks when a <select> element has styles applied to itself.
+add_task(function* test_colors_applied_to_popup() {
+  const pageUrl = "data:text/html," + escape(PAGECONTENT_COLORS_ON_SELECT);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+  let menulist = document.getElementById("ContentSelectDropdown");
+  let selectPopup = menulist.menupopup;
+
+  let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
+  yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
+  yield popupShownPromise;
+
+  // The label contains a JSON string of the expected colors for
+  // `color` and `background-color`.
+  is(selectPopup.parentNode.itemCount, 4, "Correct number of items");
+  let child = selectPopup.firstChild;
+  let idx = 1;
+
+  is(getComputedStyle(selectPopup).color, "rgb(255, 255, 255)",
+    "popup has expected foreground color");
+  is(getComputedStyle(selectPopup).backgroundColor, "rgb(126, 58, 58)",
+    "popup has expected background color");
+
+  ok(!child.selected, "The first child should not be selected");
+  while (child) {
+    testOptionColors(idx, child, menulist);
+    idx++;
+    child = child.nextSibling;
+  }
+
+  yield hideSelectPopup(selectPopup, "escape");
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/browser/base/content/test/general/browser_webext_update.json
+++ b/browser/base/content/test/general/browser_webext_update.json
@@ -22,11 +22,25 @@
           "applications": {
             "gecko": {
               "strict_min_version": "1",
               "advisory_max_version": "55.0"
             }
           }
         }
       ]
+    },
+    "update_perms@tests.mozilla.org": {
+      "updates": [
+        {
+          "version": "2.0",
+          "update_link": "https://example.com/browser/browser/base/content/test/general/browser_webext_update_perms2.xpi",
+          "applications": {
+            "gecko": {
+              "strict_min_version": "1",
+              "advisory_max_version": "55.0"
+            }
+          }
+        }
+      ]
     }
   }
 }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5887987844f5324fae82908b7ff66b524e01b747
GIT binary patch
literal 309
zc$^FHW@Zs#U|`^2a1D3&dZntcr4`6K3&gw(G7Pzid6{Xc#U*-K#rb)mA)E}%2PI}j
zfN*IAHv=QfS4IW~uy)^`(_Bo3Jg(pWc3s*kQu^}Le*sA+>Gp(pldG-H|L>N#UGaS|
zueexz>DJB(FWT-bQx97<=ZvR(xwA@##zB*bX{vHJL$z|V|90MAZFytw2F8U`9QfR3
zAJsY3a_+^upVF)o<n#L<EqoZV!OgBeahu_O4VDc%&!y>>@2tHoXPflTCw|?k)#d@-
rj7)OOxO^kQ00KY<Gc0KYvC#a+3h^77zXH5j*&x~&8G?cI8L%M$3!h}H
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..71f613ca77c64128cb9fe2a29f5152ee4cf1f9eb
GIT binary patch
literal 317
zc$^FHW@Zs#U|`^2hzfW2>N@^jwHL^{3&gw(G7Pzid6{Xc#U*-K#rb)mA)E}%uO((h
zTms_K3T_5QmamKq3}EfPJ*T-2Iq<lC|JyaGRC0-T_`h}!&Rm^?m4=eB5x?I1Mg^KT
z&MAH$J8N5O!^HP0r4@mKeuC$6@6YUz@Q`1jm8dR!GQ#4z@>lh`&=+?-XF8s_`MG17
zm}$4sQsdtKFSVi{Qnseg+i+>ZN|v2BHtd*i?~U!QkhB|zSKs@3W1CEO&9e0ka$7m(
zo~>+(-fSiv;LXS+$BfHY5)2>!bU4G3Mi2|lkE{?sqWLeto0ScsjgcW3NM8XP0syBC
BYkvR$
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
@@ -143,19 +143,21 @@ var tests = [
       checkPopup(popup, this.notifyObj);
       let notification = popup.childNodes[0];
       let checkbox = notification.checkbox;
       checkCheckbox(checkbox, "This is a checkbox");
       yield promiseElementVisible(checkbox);
       EventUtils.synthesizeMouseAtCenter(checkbox, {});
       dismissNotification(popup);
     },
-    onHidden(popup) {
+    *onHidden(popup) {
       let icon = document.getElementById("default-notification-icon");
+      let shown = waitForNotificationPanel();
       EventUtils.synthesizeMouseAtCenter(icon, {});
+      yield shown;
       let notification = popup.childNodes[0];
       let checkbox = notification.checkbox;
       checkCheckbox(checkbox, "This is a checkbox", true);
       checkMainAction(notification, true);
       gNotification.remove();
     }
   },
 ];
--- a/browser/base/content/test/popupNotifications/head.js
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -96,18 +96,18 @@ function* runNextTest() {
     onPopupEvent("popupshown", function() {
       shownState = true;
       info("[" + nextTest.id + "] popup shown");
       Task.spawn(() => nextTest.onShown(this))
           .then(undefined, ex => Assert.ok(false, "onShown failed: " + ex));
     });
     onPopupEvent("popuphidden", function() {
       info("[" + nextTest.id + "] popup hidden");
-      nextTest.onHidden(this);
-      goNext();
+      Task.spawn(() => nextTest.onHidden(this))
+          .then(() => goNext(), ex => Assert.ok(false, "onHidden failed: " + ex));
     }, () => shownState);
     info("[" + nextTest.id + "] added listeners; panel is open: " + PopupNotifications.isPanelOpen);
   }
 
   info("[" + nextTest.id + "] running test");
   yield nextTest.run();
 }
 
--- a/browser/base/content/test/referrer/browser.ini
+++ b/browser/base/content/test/referrer/browser.ini
@@ -1,19 +1,17 @@
 [DEFAULT]
 support-files =
   file_referrer_policyserver.sjs
   file_referrer_policyserver_attr.sjs
   file_referrer_testserver.sjs
   head.js
 
 [browser_referrer_middle_click.js]
-skip-if = true # Bug 1315042
 [browser_referrer_middle_click_in_container.js]
-skip-if = true # Bug 1315042
 [browser_referrer_open_link_in_private.js]
 skip-if = os == 'linux' # Bug 1145199
 [browser_referrer_open_link_in_tab.js]
 skip-if = os == 'linux' # Bug 1144816
 [browser_referrer_open_link_in_window.js]
 skip-if = os == 'linux' # Bug 1145199
 [browser_referrer_open_link_in_window_in_container.js]
 skip-if = os == 'linux' # Bug 1145199
--- a/browser/base/content/test/referrer/browser_referrer_middle_click.js
+++ b/browser/base/content/test/referrer/browser_referrer_middle_click.js
@@ -1,18 +1,19 @@
 // Tests referrer on middle-click navigation.
 // Middle-clicks on the link, which opens it in a new tab.
 
 function startMiddleClickTestCase(aTestNumber) {
   info("browser_referrer_middle_click: " +
        getReferrerTestDescription(aTestNumber));
   someTabLoaded(gTestWindow).then(function(aNewTab) {
-    gTestWindow.gBrowser.selectedTab = aNewTab;
-    checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
-                                  startMiddleClickTestCase);
+    BrowserTestUtils.switchTab(gTestWindow.gBrowser, aNewTab).then(() => {
+      checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+                                    startMiddleClickTestCase);
+    });
   });
 
   clickTheLink(gTestWindow, "testlink", {button: 1});
 }
 
 function test() {
   requestLongerTimeout(10);  // slowwww shutdown on e10s
   startReferrerTest(startMiddleClickTestCase);
--- a/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js
+++ b/browser/base/content/test/referrer/browser_referrer_middle_click_in_container.js
@@ -1,19 +1,20 @@
 // Tests referrer on middle-click navigation.
 // Middle-clicks on the link, which opens it in a new tab, same container.
 
 function startMiddleClickTestCase(aTestNumber) {
   info("browser_referrer_middle_click: " +
        getReferrerTestDescription(aTestNumber));
   someTabLoaded(gTestWindow).then(function(aNewTab) {
-    gTestWindow.gBrowser.selectedTab = aNewTab;
-    checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
-                                  startMiddleClickTestCase,
-                                  { userContextId: 3 });
+    BrowserTestUtils.switchTab(gTestWindow.gBrowser, aNewTab).then(() => {
+      checkReferrerAndStartNextTest(aTestNumber, null, aNewTab,
+                                    startMiddleClickTestCase,
+                                    { userContextId: 3 });
+    });
   });
 
   clickTheLink(gTestWindow, "testlink", {button: 1});
 }
 
 function test() {
   waitForExplicitFinish();
 
--- a/browser/base/content/test/referrer/head.js
+++ b/browser/base/content/test/referrer/head.js
@@ -137,16 +137,19 @@ function delayedStartupFinished(aWindow)
  * Waits for some (any) tab to load. The caller triggers the load.
  * @param aWindow The window where to wait for a tab to load.
  * @return {Promise}
  * @resolves With the tab once it's loaded.
  */
 function someTabLoaded(aWindow) {
   return new Promise(function(resolve) {
     aWindow.gBrowser.addEventListener("load", function onLoad(aEvent) {
+      if (aWindow.location === "about:blank") {
+        return;
+      }
       let tab = aWindow.gBrowser._getTabForContentWindow(
           aEvent.target.defaultView.top);
       if (tab) {
         aWindow.gBrowser.removeEventListener("load", onLoad, true);
         resolve(tab);
       }
     }, true);
   });
--- a/browser/components/migration/MSMigrationUtils.jsm
+++ b/browser/components/migration/MSMigrationUtils.jsm
@@ -536,38 +536,42 @@ Cookies.prototype = {
       this.ctypesKernelHelpers.finalize();
 
       aCallback(success);
     }).apply(this);
     cookiesGenerator.next();
   },
 
   _readCookieFile(aFile, aCallback) {
-    let fileReader = new FileReader();
-    let onLoadEnd = () => {
-      fileReader.removeEventListener("loadend", onLoadEnd);
+    File.createFromNsIFile(aFile).then(aFile => {
+      let fileReader = new FileReader();
+      let onLoadEnd = () => {
+        fileReader.removeEventListener("loadend", onLoadEnd);
 
-      if (fileReader.readyState != fileReader.DONE) {
-        Cu.reportError("Could not read cookie contents: " + fileReader.error);
-        aCallback(false);
-        return;
-      }
+        if (fileReader.readyState != fileReader.DONE) {
+          Cu.reportError("Could not read cookie contents: " + fileReader.error);
+          aCallback(false);
+          return;
+        }
 
-      let success = true;
-      try {
-        this._parseCookieBuffer(fileReader.result);
-      } catch (ex) {
-        Components.utils.reportError("Unable to migrate cookie: " + ex);
-        success = false;
-      } finally {
-        aCallback(success);
-      }
-    };
-    fileReader.addEventListener("loadend", onLoadEnd);
-    fileReader.readAsText(File.createFromNsIFile(aFile));
+        let success = true;
+        try {
+          this._parseCookieBuffer(fileReader.result);
+        } catch (ex) {
+          Components.utils.reportError("Unable to migrate cookie: " + ex);
+          success = false;
+        } finally {
+          aCallback(success);
+        }
+      };
+      fileReader.addEventListener("loadend", onLoadEnd);
+      fileReader.readAsText(aFile);
+    }, () => {
+      aCallback(false);
+    });
   },
 
   /**
    * Parses a cookie file buffer and returns an array of the contained cookies.
    *
    * The cookie file format is a newline-separated-values with a "*" used as
    * delimeter between multiple records.
    * Each cookie has the following fields:
--- a/browser/components/preferences/in-content/tests/browser_advanced_siteData.js
+++ b/browser/components/preferences/in-content/tests/browser_advanced_siteData.js
@@ -7,16 +7,17 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 /* import-globals-from ../../../../../testing/modules/sinon-1.16.1.js */
 Services.scriptloader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
 
 const TEST_HOST = "example.com";
 const TEST_ORIGIN = "http://" + TEST_HOST;
 const TEST_BASE_URL = TEST_ORIGIN + "/browser/browser/components/preferences/in-content/tests/";
+const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
 
 const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 const { SiteDataManager } = Cu.import("resource:///modules/SiteDataManager.jsm", {});
 const { OfflineAppCacheHelper } = Cu.import("resource:///modules/offlineAppCache.jsm", {});
 
 const mockOfflineAppCacheHelper = {
   clear: null,
 
@@ -182,16 +183,32 @@ function promiseSitesUpdated() {
 }
 
 function promiseCookiesCleared() {
   return TestUtils.topicObserved("cookie-changed", (subj, data) => {
     return data === "cleared";
   });
 }
 
+function assertSitesListed(doc, origins) {
+  let frameDoc = doc.getElementById("dialogFrame").contentDocument;
+  let removeBtn = frameDoc.getElementById("removeSelected");
+  let removeAllBtn = frameDoc.getElementById("removeAll");
+  let sitesList = frameDoc.getElementById("sitesList");
+  let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+  is(totalSitesNumber, origins.length, "Should list the right sites number");
+  origins.forEach(origin => {
+    let site = sitesList.querySelector(`richlistitem[data-origin="${origin}"]`);
+    let host = site.getAttribute("host");
+    ok(origin.includes(host), `Should list the site of ${origin}`);
+  });
+  is(removeBtn.disabled, false, "Should enable the removeSelected button");
+  is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+}
+
 registerCleanupFunction(function() {
   delete window.sinon;
   delete window.setImmediate;
   delete window.clearImmediate;
   mockOfflineAppCacheHelper.unregister();
 });
 
 add_task(function* () {
@@ -365,38 +382,28 @@ add_task(function* () {
 
   let doc = gBrowser.selectedBrowser.contentDocument;
   let frameDoc = doc.getElementById("dialogFrame").contentDocument;
   let searchBox = frameDoc.getElementById("searchBox");
   let mockOrigins = Array.from(mockSiteDataManager.sites.keys());
 
   searchBox.value = "xyz";
   searchBox.doCommand();
-  assertSitesListed(mockOrigins.filter(o => o.includes("xyz")));
+  assertSitesListed(doc, mockOrigins.filter(o => o.includes("xyz")));
 
   searchBox.value = "bar";
   searchBox.doCommand();
-  assertSitesListed(mockOrigins.filter(o => o.includes("bar")));
+  assertSitesListed(doc, mockOrigins.filter(o => o.includes("bar")));
 
   searchBox.value = "";
   searchBox.doCommand();
-  assertSitesListed(mockOrigins);
+  assertSitesListed(doc, mockOrigins);
 
   mockSiteDataManager.unregister();
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
-
-  function assertSitesListed(origins) {
-    let sitesList = frameDoc.getElementById("sitesList");
-    let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
-    is(totalSitesNumber, origins.length, "Should list the right sites number");
-    origins.forEach(origin => {
-      let site = sitesList.querySelector(`richlistitem[data-origin="${origin}"]`);
-      ok(site instanceof XULElement, `Should list the site of ${origin}`);
-    });
-  }
 });
 
 // Test selecting and removing all sites one by one
 add_task(function* () {
   yield SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
   let fakeOrigins = [
     "https://news.foo.com/",
     "https://mails.bar.com/",
@@ -473,29 +480,33 @@ add_task(function* () {
       sites[i].click();
       removeBtn.doCommand();
     }
   }
 
   function assertAllSitesListed() {
     frameDoc = doc.getElementById("dialogFrame").contentDocument;
     let removeBtn = frameDoc.getElementById("removeSelected");
+    let removeAllBtn = frameDoc.getElementById("removeAll");
     let sitesList = frameDoc.getElementById("sitesList");
     let sites = sitesList.getElementsByTagName("richlistitem");
     is(sites.length, fakeOrigins.length, "Should list all sites");
     is(removeBtn.disabled, false, "Should enable the removeSelected button");
+    is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
   }
 
   function assertAllSitesNotListed() {
     frameDoc = doc.getElementById("dialogFrame").contentDocument;
     let removeBtn = frameDoc.getElementById("removeSelected");
+    let removeAllBtn = frameDoc.getElementById("removeAll");
     let sitesList = frameDoc.getElementById("sitesList");
     let sites = sitesList.getElementsByTagName("richlistitem");
     is(sites.length, 0, "Should not list all sites");
     is(removeBtn.disabled, true, "Should disable the removeSelected button");
+    is(removeAllBtn.disabled, true, "Should disable the removeAllBtn button");
   }
 });
 
 // Test selecting and removing partial sites
 add_task(function* () {
   yield SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
   let fakeOrigins = [
     "https://news.foo.com/",
@@ -507,63 +518,62 @@ add_task(function* () {
   ];
   fakeOrigins.forEach(origin => addPersistentStoragePerm(origin));
 
   let updatePromise = promiseSitesUpdated();
   yield openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
   yield updatePromise;
   yield openSettingsDialog();
 
-  const removeDialogURL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
   let doc = gBrowser.selectedBrowser.contentDocument;
   let frameDoc = null;
   let saveBtn = null;
   let cancelBtn = null;
   let removeDialogOpenPromise = null;
   let settingsDialogClosePromise = null;
 
   // Test the initial state
-  assertSitesListed(fakeOrigins);
+  assertSitesListed(doc, fakeOrigins);
 
   // Test the "Cancel" button
   settingsDialogClosePromise = promiseSettingsDialogClose();
   frameDoc = doc.getElementById("dialogFrame").contentDocument;
   cancelBtn = frameDoc.getElementById("cancel");
   removeSelectedSite(fakeOrigins.slice(0, 4));
-  assertSitesListed(fakeOrigins.slice(4));
+  assertSitesListed(doc, fakeOrigins.slice(4));
   cancelBtn.doCommand();
   yield settingsDialogClosePromise;
   yield openSettingsDialog();
-  assertSitesListed(fakeOrigins);
+  assertSitesListed(doc, fakeOrigins);
 
   // Test the "Save Changes" button but canceling save
-  removeDialogOpenPromise = promiseWindowDialogOpen("cancel", removeDialogURL);
+  removeDialogOpenPromise = promiseWindowDialogOpen("cancel", REMOVE_DIALOG_URL);
   settingsDialogClosePromise = promiseSettingsDialogClose();
   frameDoc = doc.getElementById("dialogFrame").contentDocument;
   saveBtn = frameDoc.getElementById("save");
   removeSelectedSite(fakeOrigins.slice(0, 4));
-  assertSitesListed(fakeOrigins.slice(4));
+  assertSitesListed(doc, fakeOrigins.slice(4));
   saveBtn.doCommand();
   yield removeDialogOpenPromise;
   yield settingsDialogClosePromise;
   yield openSettingsDialog();
-  assertSitesListed(fakeOrigins);
+  assertSitesListed(doc, fakeOrigins);
 
   // Test the "Save Changes" button and accepting save
-  removeDialogOpenPromise = promiseWindowDialogOpen("accept", removeDialogURL);
+  removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
   settingsDialogClosePromise = promiseSettingsDialogClose();
   frameDoc = doc.getElementById("dialogFrame").contentDocument;
   saveBtn = frameDoc.getElementById("save");
   removeSelectedSite(fakeOrigins.slice(0, 4));
-  assertSitesListed(fakeOrigins.slice(4));
+  assertSitesListed(doc, fakeOrigins.slice(4));
   saveBtn.doCommand();
   yield removeDialogOpenPromise;
   yield settingsDialogClosePromise;
   yield openSettingsDialog();
-  assertSitesListed(fakeOrigins.slice(4));
+  assertSitesListed(doc, fakeOrigins.slice(4));
 
   // Always clean up the fake origins
   fakeOrigins.forEach(origin => removePersistentStoragePerm(origin));
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
   function removeSelectedSite(origins) {
     frameDoc = doc.getElementById("dialogFrame").contentDocument;
     let removeBtn = frameDoc.getElementById("removeSelected");
@@ -573,22 +583,53 @@ add_task(function* () {
       if (site) {
         site.click();
         removeBtn.doCommand();
       } else {
         ok(false, `Should not select and remove inexisted site of ${origin}`);
       }
     });
   }
+});
 
-  function assertSitesListed(origins) {
-    frameDoc = doc.getElementById("dialogFrame").contentDocument;
-    let removeBtn = frameDoc.getElementById("removeSelected");
-    let sitesList = frameDoc.getElementById("sitesList");
-    let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
-    is(totalSitesNumber, origins.length, "Should list the right sites number");
-    origins.forEach(origin => {
-      let site = sitesList.querySelector(`richlistitem[data-origin="${origin}"]`);
-      ok(!!site, `Should list the site of ${origin}`);
-    });
-    is(removeBtn.disabled, false, "Should enable the removeSelected button");
-  }
+add_task(function* () {
+  yield SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+  let fakeOrigins = [
+    "https://news.foo.com/",
+    "https://books.foo.com/",
+    "https://mails.bar.com/",
+    "https://account.bar.com/",
+    "https://videos.xyz.com/",
+    "https://shopping.xyz.com/"
+  ];
+  fakeOrigins.forEach(origin => addPersistentStoragePerm(origin));
+
+  let updatePromise = promiseSitesUpdated();
+  yield openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+  yield updatePromise;
+  yield openSettingsDialog();
+
+  // Search "foo" to only list foo.com sites
+  let doc = gBrowser.selectedBrowser.contentDocument;
+  let frameDoc = doc.getElementById("dialogFrame").contentDocument;
+  let searchBox = frameDoc.getElementById("searchBox");
+  searchBox.value = "foo";
+  searchBox.doCommand();
+  assertSitesListed(doc, fakeOrigins.slice(0, 2));
+
+  // Test only removing all visible sites listed
+  updatePromise = promiseSitesUpdated();
+  let acceptRemovePromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
+  let settingsDialogClosePromise = promiseSettingsDialogClose();
+  let removeAllBtn = frameDoc.getElementById("removeAll");
+  let saveBtn = frameDoc.getElementById("save");
+  removeAllBtn.doCommand();
+  saveBtn.doCommand();
+  yield acceptRemovePromise;
+  yield settingsDialogClosePromise;
+  yield updatePromise;
+  yield openSettingsDialog();
+  assertSitesListed(doc, fakeOrigins.slice(2));
+
+  // Always clean up the fake origins
+  fakeOrigins.forEach(origin => removePersistentStoragePerm(origin));
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
--- a/browser/components/preferences/siteDataSettings.js
+++ b/browser/components/preferences/siteDataSettings.js
@@ -35,33 +35,35 @@ let gSiteDataSettings = {
 
     this._list = document.getElementById("sitesList");
     this._searchBox = document.getElementById("searchBox");
     SiteDataManager.getSites().then(sites => {
       this._sites = sites;
       let sortCol = document.getElementById("hostCol");
       this._sortSites(this._sites, sortCol);
       this._buildSitesList(this._sites);
-      this._updateButtonsState();
       Services.obs.notifyObservers(null, "sitedata-settings-init", null);
     });
 
     setEventListener("hostCol", "click", this.onClickTreeCol);
     setEventListener("usageCol", "click", this.onClickTreeCol);
     setEventListener("statusCol", "click", this.onClickTreeCol);
-    setEventListener("searchBox", "command", this.onCommandSearch);
     setEventListener("cancel", "command", this.close);
     setEventListener("save", "command", this.saveChanges);
-    setEventListener("removeSelected", "command", this.removeSelected);
+    setEventListener("searchBox", "command", this.onCommandSearch);
+    setEventListener("removeAll", "command", this.onClickRemoveAll);
+    setEventListener("removeSelected", "command", this.onClickRemoveSelected);
   },
 
   _updateButtonsState() {
     let items = this._list.getElementsByTagName("richlistitem");
-    let removeBtn = document.getElementById("removeSelected");
-    removeBtn.disabled = !(items.length > 0);
+    let removeSelectedBtn = document.getElementById("removeSelected");
+    let removeAllBtn = document.getElementById("removeAll");
+    removeSelectedBtn.disabled = items.length == 0;
+    removeAllBtn.disabled = removeSelectedBtn.disabled;
   },
 
   /**
    * @param sites {Array}
    * @param col {XULElement} the <treecol> being sorted on
    */
   _sortSites(sites, col) {
     let isCurrentSortCol = col.getAttribute("data-isCurrentSortCol")
@@ -131,40 +133,32 @@ let gSiteDataSettings = {
       let size = DownloadUtils.convertByteUnits(data.usage);
       let item = document.createElement("richlistitem");
       item.setAttribute("data-origin", data.uri.spec);
       item.setAttribute("host", host);
       item.setAttribute("status", prefStrBundle.getString(statusStrId));
       item.setAttribute("usage", prefStrBundle.getFormattedString("siteUsage", size));
       this._list.appendChild(item);
     }
-  },
-
-  onClickTreeCol(e) {
-    this._sortSites(this._sites, e.target);
-    this._buildSitesList(this._sites);
+    this._updateButtonsState();
   },
 
-  onCommandSearch() {
-    this._buildSitesList(this._sites);
-  },
-
-  removeSelected() {
-    let selected = this._list.selectedItem;
-    if (selected) {
-      let origin = selected.getAttribute("data-origin");
+  _removeSiteItems(items) {
+    for (let i = items.length - 1; i >= 0; --i) {
+      let item = items[i];
+      let origin = item.getAttribute("data-origin");
       for (let site of this._sites) {
         if (site.uri.spec === origin) {
           site.userAction = "remove";
           break;
         }
       }
-      this._list.removeChild(selected);
-      this._updateButtonsState();
+      item.remove();
     }
+    this._updateButtonsState();
   },
 
   saveChanges() {
     let allowed = true;
 
     // Confirm user really wants to remove site data starts
     let removals = [];
     this._sites = this._sites.filter(site => {
@@ -230,10 +224,33 @@ let gSiteDataSettings = {
     }
     // Confirm user really wants to remove site data ends
 
     this.close();
   },
 
   close() {
     window.close();
+  },
+
+  onClickTreeCol(e) {
+    this._sortSites(this._sites, e.target);
+    this._buildSitesList(this._sites);
+  },
+
+  onCommandSearch() {
+    this._buildSitesList(this._sites);
+  },
+
+  onClickRemoveSelected() {
+    let selected = this._list.selectedItem;
+    if (selected) {
+      this._removeSiteItems([selected]);
+    }
+  },
+
+  onClickRemoveAll() {
+    let siteItems = this._list.getElementsByTagName("richlistitem");
+    if (siteItems.length > 0) {
+      this._removeSiteItems(siteItems);
+    }
   }
 };
--- a/browser/components/preferences/siteDataSettings.xul
+++ b/browser/components/preferences/siteDataSettings.xul
@@ -39,16 +39,17 @@
         <treecol flex="2" width="50" label="&statusCol.label;" id="statusCol"/>
         <treecol flex="1" width="50" label="&usageCol.label;" id="usageCol"/>
       </listheader>
     </richlistbox>
   </vbox>
 
   <hbox align="start">
     <button id="removeSelected" label="&removeSelected.label;" accesskey="&removeSelected.accesskey;"/>
+    <button id="removeAll" label="&removeAll.label;" accesskey="&removeAll.accesskey;"/>
   </hbox>
 
   <vbox align="end">
     <hbox>
         <button id="cancel" label="&cancel.label;" accesskey="&cancel.accesskey;"/>
         <button id="save" label="&save.label;" accesskey="&save.accesskey;"/>
     </hbox>
   </vbox>
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -1556,20 +1556,32 @@
             list.insertBefore(button, settingsButton);
           }
 
           if (this.compact) {
             this.settingsButton.setAttribute("width", buttonWidth);
             if (rowCount == 1 && hasDummyItems) {
               // When there's only one row, make the compact settings button
               // hug the right edge of the panel.  It may not due to the panel's
-              // width not being an integral factor of the button width.  (See
+              // width not being an integral multiple of the button width.  (See
               // the "There will be an emtpy area" comment above.)  Increase the
               // width of the last dummy item by the remainder.
+              //
+              // There's one weird thing to guard against.  When layout pixels
+              // aren't an integral multiple of device pixels, the calculated
+              // remainder can end up being ~1px too big, at least on Windows,
+              // which pushes the settings button to a new row.  The remainder
+              // is integral, not a fraction, so that's not the problem.  To
+              // work around that, unscale the remainder, floor it, scale it
+              // back, and then floor that.
+              let scale = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIDOMWindowUtils)
+                                .screenPixelsPerCSSPixel;
               let remainder = panelWidth - (enginesPerRow * buttonWidth);
+              remainder = Math.floor(Math.floor(remainder * scale) / scale);
               let width = remainder + buttonWidth;
               let lastDummyItem = this.settingsButton.previousSibling;
               lastDummyItem.setAttribute("width", width);
             }
           }
         ]]></body>
       </method>
 
--- a/browser/components/search/test/browser.ini
+++ b/browser/components/search/test/browser.ini
@@ -22,19 +22,22 @@ support-files =
 [browser_bing.js]
 [browser_bing_behavior.js]
 [browser_contextmenu.js]
 [browser_contextSearchTabPosition.js]
 skip-if = os == "mac" # bug 967013
 [browser_ddg.js]
 [browser_ddg_behavior.js]
 [browser_google.js]
+skip-if = artifact # bug 1315953
 [browser_google_codes.js]
+skip-if = artifact # bug 1315953
 [browser_google_nocodes.js]
 [browser_google_behavior.js]
+skip-if = artifact # bug 1315953
 [browser_healthreport.js]
 [browser_hiddenOneOffs_cleanup.js]
 [browser_hiddenOneOffs_diacritics.js]
 [browser_oneOffContextMenu.js]
 [browser_oneOffContextMenu_setDefault.js]
 [browser_oneOffHeader.js]
 [browser_private_search_perwindowpb.js]
 [browser_yahoo.js]
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/linux32/debug-qr
@@ -0,0 +1,3 @@
+. "$topsrcdir/browser/config/mozconfigs/linux32/debug"
+
+ac_add_options --enable-webrender
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/linux32/opt-qr
@@ -0,0 +1,3 @@
+. "$topsrcdir/browser/config/mozconfigs/linux32/nightly"
+
+ac_add_options --enable-webrender
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/win32/debug-qr
@@ -0,0 +1,3 @@
+. "$topsrcdir/browser/config/mozconfigs/win32/debug"
+
+ac_add_options --enable-webrender
new file mode 100644
--- /dev/null
+++ b/browser/config/mozconfigs/win32/opt-qr
@@ -0,0 +1,3 @@
+. "$topsrcdir/browser/config/mozconfigs/win32/nightly"
+
+ac_add_options --enable-webrender
deleted file mode 100644
--- a/browser/config/tooltool-manifests/linux32/clang.manifest
+++ /dev/null
@@ -1,10 +0,0 @@
-[
-{
-"version": "clang 3.8.0, libgcc 4.8.5",
-"size": 140319580,
-"digest": "34e219d7e8eaffa81710631c34d21355563d06335b3c00851e94c1f42f9098788fded8463dd0f67dd699f77b47a0245dd7aff754943a7a03fb5fd145a808254f",
-"algorithm": "sha512",
-"filename": "clang.tar.xz",
-"unpack": true
-}
-]
--- a/browser/config/tooltool-manifests/macosx64/cross-releng.manifest
+++ b/browser/config/tooltool-manifests/macosx64/cross-releng.manifest
@@ -1,24 +1,24 @@
 [
 {
-"version": "clang 3.8.0, libgcc 4.8.5",
-"size": 140319580,
-"digest": "34e219d7e8eaffa81710631c34d21355563d06335b3c00851e94c1f42f9098788fded8463dd0f67dd699f77b47a0245dd7aff754943a7a03fb5fd145a808254f",
+"version": "clang 3.9.0",
+"size": 168062128,
+"digest": "2a5458a25792fcade86a56ff0f4acdfa284d2b62966991a7c34a92c2e8c0b4a162ce00512d4467754e7f74598d64c56e91517e1606ed3fba011f7c10e8ad3288",
 "algorithm": "sha512",
 "filename": "clang.tar.xz",
 "unpack": true
 },
 {
-"size": 3008804,
+"size": 6075028,
 "visibility": "public",
-"digest": "ba6937f14f3d8b26dcb2d39490dee6b0a8afb60f672f5debb71d7b62c1ec52103201b4b1a3d258f945567de531384b36ddb2ce4aa73dc63d72305b11c146847c",
+"digest": "0b962ba55a5a2fbae44218683bdf6ea0dfe8165aba436173a065f7190976184586b9acf4c23478bc5b6d81a3e00f681bf16df0536c9c9718ad0570d064f69027",
 "algorithm": "sha512",
 "unpack": true,
-"filename": "cctools.tar.gz"
+"filename": "cctools.tar.xz"
 },
 {
 "size": 35215976,
 "visibility": "internal",
 "digest": "8be736545ddab25ebded188458ce974d5c9a7e29f3c50d2ebfbcb878f6aff853dd2ff5a3528bdefc64396a10101a1b50fd2fe52000140df33643cebe1ea759da",
 "algorithm": "sha512",
 "unpack": true,
 "filename": "MacOSX10.7.sdk.tar.bz2"
@@ -36,24 +36,24 @@
 "size": 57060,
 "visibility": "public",
 "digest": "9649ca595f4cf088d118da26201f92cc94cda7af49c7c48112ee31cd13c83b2935b3e145de9dd78060cff2480b4c2e7ff5fb24235876956fed13c87852071998",
 "algorithm": "sha512",
 "unpack": true,
 "filename": "dmg.tar.xz"
 },
 {
-"size": 188880,
-"visibility": "public",
-"digest": "1ffddd43efb03aed897ee42035d9d8d758a8d66ab6c867599ef755e1a586768fc22011ce03698af61454920b00fe8bed08c9a681e7bd324d7f8f78c026c83943",
-"algorithm": "sha512",
-"unpack": true,
-"filename": "genisoimage.tar.xz"
-},
-{
 "version": "rustc 1.14.0 (e8a012324 2016-12-16) repack",
 "size": 152573516,
 "digest": "eef2f10bf57005d11c34b9b49eb76d0f09d026295055039fea89952a3be51580abdab29366278ed4bfa393b33c5cee4d51d3af4221e9e7d7d833d0fc1347597c",
 "algorithm": "sha512",
 "filename": "rustc.tar.xz",
 "unpack": true
+},
+{
+"size": 281576,
+"visibility": "public",
+"digest": "71616564533d138fb12f08e761c2638d054814fdf9c9439638ec57b201e100445c364d73d8d7a4f0e3b784898d5fe6264e8242863fc5ac40163f1791468bbc46",
+"algorithm": "sha512",
+"filename": "hfsplus-tools.tar.xz",
+"unpack": true
 }
 ]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/mortar/host/pdf/chrome/js/password-prompt.js
@@ -0,0 +1,89 @@
+'use strict';
+
+class PasswordPrompt {
+  /**
+   * @constructs PasswordPrompt
+   */
+  constructor(viewport) {
+    this.overlayName = 'passwordOverlay';
+    this.container = document.getElementById('passwordOverlay');
+    this.overlaysParent = document.getElementById('overlayContainer');
+    this.label = document.getElementById('passwordText');
+    this.input = document.getElementById('password');
+    this.submitButton = document.getElementById('passwordSubmit');
+    this.cancelButton = document.getElementById('passwordCancel');
+
+    this.updateCallback = null;
+    this.reason = null;
+    this.active = false;
+    this._viewport = viewport;
+    // PDFium doesn't return the result of password check. We count the retries
+    // to determine whether to show "invalid password" prompt instead.
+    // PDFium allows at most 3 times of tries.
+    this._passwordTries = 0;
+    this._viewport.onPasswordRequest = this.open.bind(this);
+
+    // Attach the event listeners.
+    this.submitButton.addEventListener('click', this);
+    this.cancelButton.addEventListener('click', this);
+    this.input.addEventListener('keydown', this);
+  }
+
+  handleEvent(e) {
+    switch(e.type) {
+    case 'keydown':
+      if (e.target == this.input && e.keyCode === KeyEvent.DOM_VK_RETURN) {
+        this.verify();
+        e.stopPropagation();
+      } else if (e.currentTarget == window &&
+                 e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
+        this.close();
+        e.preventDefault();
+        e.stopImmediatePropagation();
+      }
+      break;
+    case 'click':
+      if (e.target == this.submitButton) {
+        this.verify();
+      } else if (e.target == this.cancelButton) {
+        this.close();
+      }
+      break;
+    }
+  }
+
+  open() {
+    this.container.classList.remove('hidden');
+    this.overlaysParent.classList.remove('hidden');
+    window.addEventListener('keydown', this);
+    this.active = true;
+    let promptKey = this._passwordTries ? 'password_invalid' : 'password_label';
+    this._passwordTries++;
+
+    this.input.type = 'password';
+    this.input.focus();
+
+    document.l10n.formatValue(promptKey).then(promptString => {
+      this.label.textContent = promptString;
+    });
+
+  }
+
+  close() {
+    this.container.classList.add('hidden');
+    this.overlaysParent.classList.add('hidden');
+    window.removeEventListener('keydown', this);
+    this.active = false;
+
+    this.input.value = '';
+    this.input.type = '';
+  }
+
+  verify() {
+    let password = this.input.value;
+    if (password && password.length > 0) {
+      this.close();
+      this._viewport.verifyPassword(password);
+    }
+  }
+}
--- a/browser/extensions/mortar/host/pdf/chrome/js/viewer.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/viewer.js
@@ -2,16 +2,17 @@
  * 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';
 
 window.addEventListener('DOMContentLoaded', function() {
   let viewport = new Viewport();
   let toolbar = new Toolbar(viewport);
+  let passwordPrompt = new PasswordPrompt(viewport);
 
   // Expose the custom viewport object to runtime
   window.createCustomViewport = function(actionHandler) {
     viewport.registerActionHandler(actionHandler);
 
     return {
       addView: viewport.addView.bind(viewport),
       clearView: viewport.clearView.bind(viewport),
--- a/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
@@ -31,16 +31,17 @@ class Viewport {
     // gets a different value.
     this._runtimeSize = this.getBoundingClientRect();
     this._runtimeOnResizedListener = [];
 
     this.onProgressChanged = null;
     this.onZoomChanged = null;
     this.onDimensionChanged = null;
     this.onPageChanged = null;
+    this.onPasswordRequest = null;
 
     this._viewportController.addEventListener('scroll', this);
     window.addEventListener('resize', this);
   }
 
   get zoom() {
     return this._zoom;
   }
@@ -118,24 +119,24 @@ class Viewport {
     this._fullscreenStatus = 'changing';
     this._doAction({
       type: 'setFullscreen',
       fullscreen: enable
     });
   }
 
   _getScrollbarWidth() {
-    var div = document.createElement('div');
+    let div = document.createElement('div');
     div.style.visibility = 'hidden';
     div.style.overflow = 'scroll';
     div.style.width = '50px';
     div.style.height = '50px';
     div.style.position = 'absolute';
     document.body.appendChild(div);
-    var result = div.offsetWidth - div.clientWidth;
+    let result = div.offsetWidth - div.clientWidth;
     div.remove();
     return result;
   }
 
   _documentHasVisibleScrollbars(zoom) {
     let zoomedDimensions = this._getZoomedDocumentDimensions(zoom);
     if (!zoomedDimensions || !this._scrollbarWidth) {
       return {
@@ -463,16 +464,23 @@ class Viewport {
     if (newPage != this._page) {
       this._page = newPage;
       if (typeof this.onPageChanged === 'function') {
         this.onPageChanged(newPage);
       }
     }
   }
 
+  verifyPassword(password) {
+    this._doAction({
+      type: 'getPasswordComplete',
+      password: password
+    });
+  }
+
   handleEvent(evt) {
     switch(evt.type) {
       case 'resize':
         this.invokeResize();
         break;
       case 'scroll':
         this._nextPosition = null;
         let position = this.getScrollOffset();
@@ -586,11 +594,14 @@ class Viewport {
         this._updateProgress(message.progress);
         break;
       case 'documentDimensions':
         this._setDocumentDimensions(message);
         break;
       case 'fullscreenChange':
         this._handleFullscreenChange(message.fullscreen);
         break;
+      case 'getPassword':
+        this.onPasswordRequest && this.onPasswordRequest();
+        break;
     }
   }
 }
--- a/browser/extensions/mortar/host/pdf/chrome/viewer.html
+++ b/browser/extensions/mortar/host/pdf/chrome/viewer.html
@@ -13,16 +13,17 @@
     <link rel="localization" href="locale/viewer.{locale}.properties">
 
     <link rel="stylesheet" href="style/viewer.css">
 
     <script src="js/l20n.js"></script>
     <script src="js/polyfill.js"></script>
     <script src="js/toolbar.js"></script>
     <script src="js/viewport.js"></script>
+    <script src="js/password-prompt.js"></script>
     <script src="js/viewer.js"></script>
   </head>
 
   <body tabindex="1" class="loadingInProgress">
     <div id="outerContainer">
 
       <div id="sidebarContainer">
         <div id="toolbarSidebar">
--- a/browser/extensions/shield-recipe-client/bootstrap.js
+++ b/browser/extensions/shield-recipe-client/bootstrap.js
@@ -2,26 +2,34 @@
  * 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");
 Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LogManager",
+  "resource://shield-recipe-client/lib/LogManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecipeRunner",
+  "resource://shield-recipe-client/lib/RecipeRunner.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager",
+  "resource://shield-recipe-client/lib/CleanupManager.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.
+  ADDON_DOWNGRADE: 8,  // The add-on is being downgraded.
 };
 
 const PREF_BRANCH = "extensions.shield-recipe-client.";
 const DEFAULT_PREFS = {
   api_url: "https://self-repair.mozilla.org/api/v1",
   dev_mode: false,
   enabled: true,
   startup_delay_seconds: 300,
@@ -48,29 +56,25 @@ this.install = function() {
 this.startup = function() {
   setDefaultPrefs();
 
   if (!shouldRun) {
     return;
   }
 
   // Setup logging and listen for changes to logging prefs
-  Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
   LogManager.configure(Services.prefs.getIntPref(PREF_LOGGING_LEVEL));
   Preferences.observe(PREF_LOGGING_LEVEL, LogManager.configure);
+  CleanupManager.addCleanupHandler(
+    () => Preferences.ignore(PREF_LOGGING_LEVEL, LogManager.configure));
 
-  Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm");
   RecipeRunner.init();
 };
 
 this.shutdown = function(data, reason) {
-  Preferences.ignore(PREF_LOGGING_LEVEL, LogManager.configure);
-
-  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 = [
     "lib/CleanupManager.jsm",
--- a/browser/extensions/shield-recipe-client/lib/CleanupManager.jsm
+++ b/browser/extensions/shield-recipe-client/lib/CleanupManager.jsm
@@ -1,16 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {utils: Cu} = Components;
-
 this.EXPORTED_SYMBOLS = ["CleanupManager"];
 
 const cleanupHandlers = new Set();
 
 this.CleanupManager = {
   addCleanupHandler(handler) {
     cleanupHandlers.add(handler);
   },
--- a/browser/extensions/shield-recipe-client/lib/EnvExpressions.jsm
+++ b/browser/extensions/shield-recipe-client/lib/EnvExpressions.jsm
@@ -1,16 +1,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/. */
 
 "use strict";
 
 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://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");
 
 const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 
 this.EXPORTED_SYMBOLS = ["EnvExpressions"];
@@ -69,19 +70,21 @@ this.EnvExpressions = {
       id = generateUUID().toString().slice(1, -1);
       prefs.setCharPref("user_id", id);
     }
     return id;
   },
 
   eval(expr, extraContext = {}) {
     // First clone the extra context
-    const context = Object.assign({}, extraContext);
+    const context = Object.assign({normandy: {}}, extraContext);
     // jexl handles promises, so it is fine to include them in this data.
     context.telemetry = EnvExpressions.getLatestTelemetry();
-    context.normandy = context.normandy || {};
-    context.normandy.userId = EnvExpressions.getUserId();
+
+    context.normandy = Object.assign(context.normandy, {
+      userId: EnvExpressions.getUserId(),
+      distribution: Preferences.get("distribution.id", "default"),
+    });
 
     const onelineExpr = expr.replace(/[\t\n\r]/g, " ");
-
     return jexl.eval(onelineExpr, context);
   },
 };
--- a/browser/extensions/shield-recipe-client/lib/LogManager.jsm
+++ b/browser/extensions/shield-recipe-client/lib/LogManager.jsm
@@ -4,17 +4,17 @@
 
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/Log.jsm");
 
 this.EXPORTED_SYMBOLS = ["LogManager"];
 
-const ROOT_LOGGER_NAME = "extensions.shield-recipe-client"
+const ROOT_LOGGER_NAME = "extensions.shield-recipe-client";
 let rootLogger = null;
 
 this.LogManager = {
   /**
    * Configure the root logger for the Recipe Client. Must be called at
    * least once before using any loggers created via getLogger.
    * @param {Number} loggingLevel
    *        Logging level to use as defined in Log.jsm
--- a/browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
+++ b/browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
@@ -70,18 +70,22 @@ this.NormandyDriver = function(sandboxMa
 
     client() {
       const appinfo = {
         version: Services.appinfo.version,
         channel: Services.appinfo.defaultUpdateChannel,
         isDefaultBrowser: ShellService.isDefaultBrowser() || null,
         searchEngine: null,
         syncSetup: Preferences.isSet("services.sync.username"),
+        syncDesktopDevices: Preferences.get("services.sync.clients.devices.desktop", 0),
+        syncMobileDevices: Preferences.get("services.sync.clients.devices.mobile", 0),
+        syncTotalDevices: Preferences.get("services.sync.numClients", 0),
         plugins: {},
         doNotTrack: Preferences.get("privacy.donottrackheader.enabled", false),
+        distribution: Preferences.get("distribution.id", "default"),
       };
 
       const searchEnginePromise = new Promise(resolve => {
         Services.search.init(rv => {
           if (Components.isSuccessCode(rv)) {
             appinfo.searchEngine = Services.search.defaultEngine.identifier;
           }
           resolve();
--- a/browser/extensions/shield-recipe-client/test/browser/browser_EnvExpressions.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_EnvExpressions.js
@@ -77,9 +77,18 @@ add_task(function* () {
   Assert.equal(val, "fake id", "userId is pulled from preferences");
 
   // test that it merges context correctly, `userId` comes from the default context, and
   // `injectedValue` comes from us. Expect both to be on the final `normandy` object.
   val = yield EnvExpressions.eval(
     "[normandy.userId, normandy.injectedValue]",
     {normandy: {injectedValue: "injected"}});
   Assert.deepEqual(val, ["fake id", "injected"], "context is correctly merged");
+
+  // distribution id defaults to "default"
+  val = yield EnvExpressions.eval("normandy.distribution");
+  Assert.equal(val, "default", "distribution has a default value");
+
+  // distribution id is in the context
+  yield SpecialPowers.pushPrefEnv({set: [["distribution.id", "funnelcake"]]});
+  val = yield EnvExpressions.eval("normandy.distribution");
+  Assert.equal(val, "funnelcake", "distribution is read from preferences");
 });
--- a/browser/extensions/shield-recipe-client/test/browser/browser_NormandyDriver.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_NormandyDriver.js
@@ -13,8 +13,37 @@ add_task(Utils.withDriver(Assert, functi
   const uuid2 = driver.uuid();
   isnot(uuid1, uuid2, "uuids are unique");
 }));
 
 add_task(Utils.withDriver(Assert, function* userId(driver) {
   // Test that userId is a UUID
   ok(Utils.UUID_REGEX.test(driver.userId), "userId is a uuid");
 }));
+
+add_task(Utils.withDriver(Assert, function* syncDeviceCounts(driver) {
+  let client = yield driver.client();
+  is(client.syncMobileDevices, 0, "syncMobileDevices defaults to zero");
+  is(client.syncDesktopDevices, 0, "syncDesktopDevices defaults to zero");
+  is(client.syncTotalDevices, 0, "syncTotalDevices defaults to zero");
+
+  yield SpecialPowers.pushPrefEnv({
+    set: [
+      ["services.sync.numClients", 9],
+      ["services.sync.clients.devices.mobile", 5],
+      ["services.sync.clients.devices.desktop", 4],
+    ],
+  });
+
+  client = yield driver.client();
+  is(client.syncMobileDevices, 5, "syncMobileDevices is read when set");
+  is(client.syncDesktopDevices, 4, "syncDesktopDevices is read when set");
+  is(client.syncTotalDevices, 9, "syncTotalDevices is read when set");
+}));
+
+add_task(Utils.withDriver(Assert, function* distribution(driver) {
+  let client = yield driver.client();
+  is(client.distribution, "default", "distribution has a default value");
+
+  yield SpecialPowers.pushPrefEnv({set: [["distribution.id", "funnelcake"]]});
+  client = yield driver.client();
+  is(client.distribution, "funnelcake", "distribution is read from preferences");
+}));
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -460,17 +460,16 @@
 @RESPATH@/components/nsContentDispatchChooser.manifest
 @RESPATH@/components/nsContentDispatchChooser.js
 @RESPATH@/components/nsHandlerService.manifest
 @RESPATH@/components/nsHandlerService.js
 @RESPATH@/components/nsWebHandlerApp.manifest
 @RESPATH@/components/nsWebHandlerApp.js
 @RESPATH@/components/satchel.manifest
 @RESPATH@/components/nsFormAutoComplete.js
-@RESPATH@/components/nsFormHistory.js
 @RESPATH@/components/FormHistoryStartup.js
 @RESPATH@/components/nsInputListAutoComplete.js
 @RESPATH@/components/formautofill.manifest
 @RESPATH@/components/FormAutofillContentService.js
 @RESPATH@/components/FormAutofillStartup.js
 @RESPATH@/components/contentAreaDropListener.manifest
 @RESPATH@/components/contentAreaDropListener.js
 @RESPATH@/browser/components/BrowserProfileMigrators.manifest
--- a/browser/installer/windows/nsis/stub.nsi
+++ b/browser/installer/windows/nsis/stub.nsi
@@ -103,16 +103,17 @@ Var OpenedDownloadPage
 Var DownloadServerIP
 Var PostSigningData
 Var PreviousInstallDir
 Var PreviousInstallArch
 
 Var ControlHeightPX
 Var ControlRightPX
 Var ControlTopAdjustment
+Var OptionsItemWidthPX
 
 ; Uncomment the following to prevent pinging the metrics server when testing
 ; the stub installer
 ;!define STUB_DEBUG
 
 !define StubURLVersion "v7"
 
 ; Successful install exit code
@@ -900,16 +901,26 @@ Function createOptions
     ${AndIf} "$HasRequiredSpaceAvailable" == "true"
       Abort ; Skip the options page
     ${EndIf}
   ${EndIf}
 
   StrCpy $ExistingTopDir ""
   StrCpy $ControlTopAdjustment 0
 
+  ; Convert the options item width to pixels, so we can tell when a text string
+  ; exceeds this width and needs multiple lines.
+  StrCpy $2 "${OPTIONS_ITEM_WIDTH_DU}" -1
+  IntOp $2 $2 - 14 ; subtract approximate width of a checkbox
+  System::Call "*(i r2,i,i,i) p .r3"
+  System::Call "user32::MapDialogRect(p $HWNDPARENT, p r3)"
+  System::Call "*$3(i .s,i,i,i)"
+  Pop $OptionsItemWidthPX
+  System::Free $3
+
   nsDialogs::Create /NOUNLOAD 1018
   Pop $Dialog
   ; Since the text color for controls is set in this Dialog the foreground and
   ; background colors of the Dialog must also be hardcoded.
   SetCtlColors $Dialog ${COMMON_TEXT_COLOR_NORMAL} ${COMMON_BKGRD_COLOR}
 
   ${NSD_CreateLabel} ${OPTIONS_ITEM_EDGE_DU} 25u ${OPTIONS_ITEM_WIDTH_DU} \
                      12u "$(DEST_FOLDER)"
@@ -994,17 +1005,17 @@ Function createOptions
   ${Else}
     StrCpy $0 "$(ADD_SC_DESKTOP_QUICKLAUNCHBAR)"
   ${EndIf}
 
   ; In some locales, this string may be too long to fit on one line.
   ; In that case, we'll need to give the control two lines worth of height.
   StrCpy $1 12 ; single line height
   ${GetTextExtent} $0 $FontNormal $R1 $R2
-  ${If} $R1 > ${OPTIONS_ITEM_WIDTH_DU}
+  ${If} $R1 > $OptionsItemWidthPX
     ; Add a second line to the control height.
     IntOp $1 $1 + 12
     ; The rest of the controls will have to be lower to account for this label
     ; needing two lines worth of height.
     IntOp $ControlTopAdjustment $ControlTopAdjustment + 12
   ${EndIf}
   ${NSD_CreateCheckbox} ${OPTIONS_ITEM_EDGE_DU} 100u \
                         ${OPTIONS_ITEM_WIDTH_DU} "$1u" "$0"
@@ -1016,24 +1027,24 @@ Function createOptions
   SendMessage $CheckboxShortcuts ${WM_SETFONT} $FontNormal 0
   ${NSD_Check} $CheckboxShortcuts
 
   IntOp $0 116 + $ControlTopAdjustment
   ; In some locales, this string may be too long to fit on one line.
   ; In that case, we'll need to give the control two lines worth of height.
   StrCpy $1 12 ; single line height
   ${GetTextExtent} "$(SEND_PING)" $FontNormal $R1 $R2
-  ${If} $R1 > ${OPTIONS_ITEM_WIDTH_DU}
+  ${If} $R1 > $OptionsItemWidthPX
     ; Add a second line to the control height.
     IntOp $1 $1 + 12
     ; The rest of the controls will have to be lower to account for this label
     ; needing two lines worth of height.
     IntOp $ControlTopAdjustment $ControlTopAdjustment + 12
   ${EndIf}
-  ${NSD_CreateCheckbox} ${OPTIONS_ITEM_EDGE_DU} "$0u" ${OPTIONS_SUBITEM_WIDTH_DU} \
+  ${NSD_CreateCheckbox} ${OPTIONS_ITEM_EDGE_DU} "$0u" ${OPTIONS_ITEM_WIDTH_DU} \
                         "$1u" "$(SEND_PING)"
   Pop $CheckboxSendPing
   ; The uxtheme must be disabled on checkboxes in order to override the system
   ; font color.
   System::Call 'uxtheme::SetWindowTheme(i $CheckboxSendPing, w " ", w " ")'
   SetCtlColors $CheckboxSendPing ${COMMON_TEXT_COLOR_NORMAL} ${COMMON_BKGRD_COLOR}
   SendMessage $CheckboxSendPing ${WM_SETFONT} $FontNormal 0
   ${NSD_Check} $CheckboxSendPing
@@ -1053,18 +1064,29 @@ Function createOptions
     ${IfNot} ${Errors}
       DeleteRegValue HKLM "Software\Mozilla" "${BrandShortName}InstallerTest"
       ; Read the registry instead of using ServicesHelper::IsInstalled so the
       ; plugin isn't included in the stub installer to lessen its size.
       ClearErrors
       ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\services\MozillaMaintenance" "ImagePath"
       ${If} ${Errors}
         IntOp $0 132 + $ControlTopAdjustment
+        ; In some locales, this string may be too long to fit on one line.
+        ; In that case, we'll need to give the control two lines worth of height.
+        StrCpy $1 12 ; single line height
+        ${GetTextExtent} "$(INSTALL_MAINT_SERVICE)" $FontNormal $R1 $R2
+        ${If} $R1 > $OptionsItemWidthPX
+          ; Add a second line to the control height.
+          IntOp $1 $1 + 12
+          ; The rest of the controls will have to be lower to account for this label
+          ; needing two lines worth of height.
+          IntOp $ControlTopAdjustment $ControlTopAdjustment + 12
+        ${EndIf}
         ${NSD_CreateCheckbox} ${OPTIONS_ITEM_EDGE_DU} "$0u" ${OPTIONS_ITEM_WIDTH_DU} \
-                              12u "$(INSTALL_MAINT_SERVICE)"
+                              "$1u" "$(INSTALL_MAINT_SERVICE)"
         Pop $CheckboxInstallMaintSvc
         System::Call 'uxtheme::SetWindowTheme(i $CheckboxInstallMaintSvc, w " ", w " ")'
         SetCtlColors $CheckboxInstallMaintSvc ${COMMON_TEXT_COLOR_NORMAL} ${COMMON_BKGRD_COLOR}
         SendMessage $CheckboxInstallMaintSvc ${WM_SETFONT} $FontNormal 0
         ${NSD_Check} $CheckboxInstallMaintSvc
         ; Since we're adding in an optional control, remember the lower the ones
         ; that come after it.
         IntOp $ControlTopAdjustment 20 + $ControlTopAdjustment
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -56,22 +56,22 @@ webextPerms.cancel.accessKey=C
 webextPerms.sideloadMenuItem=%1$S added to %2$S
 
 # LOCALIZATION NOTE (webextPerms.sideloadHeader)
 # This string is used as a header in the webextension permissions dialog
 # when the extension is side-loaded.
 # %S is replaced with the localized name of the extension being installed.
 # Note, this string will be used as raw markup. Avoid characters like <, >, &
 webextPerms.sideloadHeader=%S added
-webextPerms.sideloadText=Another program on your computer installed an add-on that may affect your browser. Please review this add-on’s permissions requests and choose to Enable or Disable.
+webextPerms.sideloadText2=Another program on your computer installed an add-on that may affect your browser. Please review this add-on’s permissions requests and choose to Enable or Cancel (to leave it disabled).
 
 webextPerms.sideloadEnable.label=Enable
 webextPerms.sideloadEnable.accessKey=E
-webextPerms.sideloadDisable.label=Disable
-webextPerms.sideloadDisable.accessKey=D
+webextPerms.sideloadCancel.label=Cancel
+webextPerms.sideloadCancel.accessKey=C
 
 # LOCALIZATION NOTE (webextPerms.updateMenuItem)
 # %S will be replaced with the localized name of the extension which
 # has been updated.
 webextPerms.updateMenuItem=%S requires new permissions
 
 # LOCALIZATION NOTE (webextPerms.updateText)
 # %S is replaced with the localized name of the updated extension.
--- a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
@@ -6,15 +6,17 @@
 <!ENTITY     settings.description          "The following websites asked to store site data in your disk. You can specify which websites are allowed to store site data. Default site data is temporary and could be deleted automatically.">
 <!ENTITY     hostCol.label                 "Site">
 <!ENTITY     statusCol.label               "Status">
 <!ENTITY     usageCol.label                "Storage">
 <!ENTITY     search.label                  "Search:">
 <!ENTITY     search.accesskey              "S">
 <!ENTITY     removeSelected.label          "Remove Selected">
 <!ENTITY     removeSelected.accesskey      "r">
+<!ENTITY     removeAll.label               "Remove All">
+<!ENTITY     removeAll.accesskey           "e">
 <!ENTITY     save.label                    "Save Changes">
 <!ENTITY     save.accesskey                "a">
 <!ENTITY     cancel.label                  "Cancel">
 <!ENTITY     cancel.accesskey              "C">
 <!ENTITY     removingDialog.title          "Removing Site Data">
 <!ENTITY     removingDialog.description    "Removing site data will also remove cookies. This may log you out of websites and remove offline web content. Are you sure you want to make the changes?">
 <!ENTITY     siteTree.label                "The following website cookies will be removed:">
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -19,16 +19,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
                                       "extensions.webextPermissionPrompts", false);
 
 const DEFAULT_EXTENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 
+const BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties";
+const BRAND_PROPERTIES = "chrome://browser/locale/brand.properties";
+
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 this.ExtensionsUI = {
   sideloaded: new Set(),
   updates: new Set(),
 
   init() {
     Services.obs.addObserver(this, "webextension-permission-prompt", false);
@@ -60,65 +63,54 @@ this.ExtensionsUI = {
         let win = RecentWindow.getMostRecentBrowserWindow();
         for (let addon of sideloaded) {
           win.openUILinkIn(`about:newaddon?id=${addon.id}`, "tab");
         }
       }
     });
   },
 
-  showAddonsManager(browser, info) {
-    let loadPromise = new Promise(resolve => {
-      let listener = (subject, topic) => {
-        if (subject.location.href == "about:addons") {
-          Services.obs.removeObserver(listener, topic);
-          resolve(subject);
-        }
-      };
-      Services.obs.addObserver(listener, "EM-loaded", false);
-    });
-    let tab = browser.addTab("about:addons");
-    browser.selectedTab = tab;
-
-    return loadPromise.then(win => {
-      win.loadView("addons://list/extension");
-      return this.showPermissionsPrompt(browser.selectedBrowser, info);
+  showAddonsManager(browser, strings, icon) {
+    let global = browser.selectedBrowser.ownerGlobal;
+    return global.BrowserOpenAddonsMgr("addons://list/extension").then(aomWin => {
+      let aomBrowser = aomWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDocShell)
+                             .chromeEventHandler;
+      return this.showPermissionsPrompt(aomBrowser, strings, icon);
     });
   },
 
   showSideloaded(browser, addon) {
     addon.markAsSeen();
     this.sideloaded.delete(addon);
     this.emit("change");
 
-    let info = {
+    let strings = this._buildStrings({
       addon,
       permissions: addon.userPermissions,
-      icon: addon.iconURL,
       type: "sideload",
-    };
-    this.showAddonsManager(browser, info).then(answer => {
+    });
+    this.showAddonsManager(browser, strings, addon.iconURL).then(answer => {
       addon.userDisabled = !answer;
     });
   },
 
   showUpdate(browser, info) {
-    info.icon = info.addon.iconURL;
-    info.type = "update";
-    this.showAddonsManager(browser, info).then(answer => {
-      if (answer) {
-        info.resolve();
-      } else {
-        info.reject();
-      }
-      // At the moment, this prompt will re-appear next time we do an update
-      // check.  See bug 1332360 for proposal to avoid this.
-      this.updates.delete(info);
-      this.emit("change");
-    });
+    this.showAddonsManager(browser, info.strings, info.addon.iconURL)
+        .then(answer => {
+          if (answer) {
+            info.resolve();
+          } else {
+            info.reject();
+          }
+          // At the moment, this prompt will re-appear next time we do an update
+          // check.  See bug 1332360 for proposal to avoid this.
+          this.updates.delete(info);
+          this.emit("change");
+        });
   },
 
   observe(subject, topic, data) {
     if (topic == "webextension-permission-prompt") {
       let {target, info} = subject.wrappedJSObject;
 
       // Dismiss the progress notification.  Note that this is bad if
       // there are multiple simultaneous installs happening, see
@@ -136,20 +128,37 @@ this.ExtensionsUI = {
         }
       };
 
       let perms = info.addon.userPermissions;
       if (!perms) {
         reply(true);
       } else {
         info.permissions = perms;
-        this.showPermissionsPrompt(target, info).then(reply);
+        let strings = this._buildStrings(info);
+        this.showPermissionsPrompt(target, strings, info.icon).then(reply);
       }
     } else if (topic == "webextension-update-permissions") {
-      this.updates.add(subject.wrappedJSObject);
+      let info = subject.wrappedJSObject;
+      let strings = this._buildStrings(info);
+
+      // If we don't prompt for any new permissions, just apply it
+      if (strings.msgs.length == 0) {
+        info.resolve();
+        return;
+      }
+
+      let update = {
+        strings,
+        addon: info.addon,
+        resolve: info.resolve,
+        reject: info.reject,
+      };
+
+      this.updates.add(update);
       this.emit("change");
     } else if (topic == "webextension-install-notify") {
       let {target, addon, callback} = subject.wrappedJSObject;
       this.showInstallNotification(target, addon).then(() => {
         if (callback) {
           callback();
         }
       });
@@ -159,66 +168,64 @@ this.ExtensionsUI = {
   // Escape &, <, and > characters in a string so that it may be
   // injected as part of raw markup.
   _sanitizeName(name) {
     return name.replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;");
   },
 
-  showPermissionsPrompt(target, info) {
-    let perms = info.permissions;
-    if (!perms) {
-      return Promise.resolve();
-    }
+  // Create a set of formatted strings for a permission prompt
+  _buildStrings(info) {
+    let result = {};
 
-    let win = target.ownerGlobal;
+    let bundle = Services.strings.createBundle(BROWSER_PROPERTIES);
 
     let name = info.addon.name;
     if (name.length > 50) {
       name = name.slice(0, 49) + "…";
     }
     name = this._sanitizeName(name);
-
     let addonName = `<span class="addon-webext-name">${name}</span>`;
-    let bundle = win.gNavigatorBundle;
 
-    let header = bundle.getFormattedString("webextPerms.header", [addonName]);
-    let text = "";
-    let listIntro = bundle.getString("webextPerms.listIntro");
+    result.header = bundle.formatStringFromName("webextPerms.header", [addonName], 1);
+    result.text = "";
+    result.listIntro = bundle.GetStringFromName("webextPerms.listIntro");
 
-    let acceptText = bundle.getString("webextPerms.add.label");
-    let acceptKey = bundle.getString("webextPerms.add.accessKey");
-    let cancelText = bundle.getString("webextPerms.cancel.label");
-    let cancelKey = bundle.getString("webextPerms.cancel.accessKey");
+    result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
+    result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
+    result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
+    result.cancelKey = bundle.GetStringFromName("webextPerms.cancel.accessKey");
 
     if (info.type == "sideload") {
-      header = bundle.getFormattedString("webextPerms.sideloadHeader", [addonName]);
-      text = bundle.getString("webextPerms.sideloadText");
-      acceptText = bundle.getString("webextPerms.sideloadEnable.label");
-      acceptKey = bundle.getString("webextPerms.sideloadEnable.accessKey");
-      cancelText = bundle.getString("webextPerms.sideloadDisable.label");
-      cancelKey = bundle.getString("webextPerms.sideloadDisable.accessKey");
+      result.header = bundle.formatStringFromName("webextPerms.sideloadHeader", [addonName], 1);
+      result.text = bundle.GetStringFromName("webextPerms.sideloadText2");
+      result.acceptText = bundle.GetStringFromName("webextPerms.sideloadEnable.label");
+      result.acceptKey = bundle.GetStringFromName("webextPerms.sideloadEnable.accessKey");
+      result.cancelText = bundle.GetStringFromName("webextPerms.sideloadCancel.label");
+      result.cancelKey = bundle.GetStringFromName("webextPerms.sideloadCancel.accessKey");
     } else if (info.type == "update") {
-      header = "";
-      text = bundle.getFormattedString("webextPerms.updateText", [addonName]);
-      acceptText = bundle.getString("webextPerms.updateAccept.label");
-      acceptKey = bundle.getString("webextPerms.updateAccept.accessKey");
+      result.header = "";
+      result.text = bundle.formatStringFromName("webextPerms.updateText", [addonName], 1);
+      result.acceptText = bundle.GetStringFromName("webextPerms.updateAccept.label");
+      result.acceptKey = bundle.GetStringFromName("webextPerms.updateAccept.accessKey");
     }
 
-    let msgs = [];
+    let perms = info.permissions || {hosts: [], permissions: []};
+
+    result.msgs = [];
     for (let permission of perms.permissions) {
       let key = `webextPerms.description.${permission}`;
       if (permission == "nativeMessaging") {
-        let brandBundle = win.document.getElementById("bundle_brand");
-        let appName = brandBundle.getString("brandShortName");
-        msgs.push(bundle.getFormattedString(key, [appName]));
+        let brandBundle = Services.strings.createBundle(BRAND_PROPERTIES);
+        let appName = brandBundle.GetStringFromName("brandShortName");
+        result.msgs.push(bundle.formatStringFromName(key, [appName], 1));
       } else {
         try {
-          msgs.push(bundle.getString(key));
+          result.msgs.push(bundle.GetStringFromName(key));
         } catch (err) {
           // We deliberately do not include all permissions in the prompt.
           // So if we don't find one then just skip it.
         }
       }
     }
 
     let allUrls = false, wildcards = [], sites = [];
@@ -236,92 +243,100 @@ this.ExtensionsUI = {
       } else if (match[1].startsWith("*.")) {
         wildcards.push(match[1].slice(2));
       } else {
         sites.push(match[1]);
       }
     }
 
     if (allUrls) {
-      msgs.push(bundle.getString("webextPerms.hostDescription.allUrls"));
+      result.msgs.push(bundle.GetStringFromName("webextPerms.hostDescription.allUrls"));
     } else {
       // Formats a list of host permissions.  If we have 4 or fewer, display
       // them all, otherwise display the first 3 followed by an item that
       // says "...plus N others"
       function format(list, itemKey, moreKey) {
         function formatItems(items) {
-          msgs.push(...items.map(item => bundle.getFormattedString(itemKey, [item])));
+          result.msgs.push(...items.map(item => bundle.formatStringFromName(itemKey, [item], 1)));
         }
         if (list.length < 5) {
           formatItems(list);
         } else {
           formatItems(list.slice(0, 3));
 
           let remaining = list.length - 3;
-          msgs.push(PluralForm.get(remaining, bundle.getString(moreKey))
-                              .replace("#1", remaining));
+          result.msgs.push(PluralForm.get(remaining, bundle.GetStringFromName(moreKey))
+                                     .replace("#1", remaining));
         }
       }
 
       format(wildcards, "webextPerms.hostDescription.wildcard",
              "webextPerms.hostDescription.tooManyWildcards");
       format(sites, "webextPerms.hostDescription.oneSite",
              "webextPerms.hostDescription.tooManySites");
     }
 
+    return result;
+  },
+
+  showPermissionsPrompt(browser, strings, icon) {
+    function eventCallback(topic) {
+      if (topic == "showing") {
+        let doc = this.browser.ownerDocument;
+        doc.getElementById("addon-webext-perm-header").innerHTML = strings.header;
+
+        let textEl = doc.getElementById("addon-webext-perm-text");
+        textEl.innerHTML = strings.text;
+        textEl.hidden = !strings.text;
+
+        let listIntroEl = doc.getElementById("addon-webext-perm-intro");
+        listIntroEl.value = strings.listIntro;
+        listIntroEl.hidden = (strings.msgs.length == 0);
+
+        let list = doc.getElementById("addon-webext-perm-list");
+        while (list.firstChild) {
+          list.firstChild.remove();
+        }
+
+        for (let msg of strings.msgs) {
+          let item = doc.createElementNS(HTML_NS, "li");
+          item.textContent = msg;
+          list.appendChild(item);
+        }
+      } else if (topic == "swapping") {
+        return true;
+      }
+      return false;
+    }
+
     let popupOptions = {
       hideClose: true,
-      popupIconURL: info.icon || DEFAULT_EXTENSION_ICON,
+      popupIconURL: icon || DEFAULT_EXTENSION_ICON,
       persistent: true,
-
-      eventCallback(topic) {
-        if (topic == "showing") {
-          let doc = this.browser.ownerDocument;
-          doc.getElementById("addon-webext-perm-header").innerHTML = header;
-
-          let textEl = doc.getElementById("addon-webext-perm-text");
-          textEl.innerHTML = text;
-          textEl.hidden = !text;
-
-          let listIntroEl = doc.getElementById("addon-webext-perm-intro");
-          listIntroEl.value = listIntro;
-          listIntroEl.hidden = (msgs.length == 0);
-
-          let list = doc.getElementById("addon-webext-perm-list");
-          while (list.firstChild) {
-            list.firstChild.remove();
-          }
-
-          for (let msg of msgs) {
-            let item = doc.createElementNS(HTML_NS, "li");
-            item.textContent = msg;
-            list.appendChild(item);
-          }
-        } else if (topic == "swapping") {
-          return true;
-        }
-        return false;
-      },
+      eventCallback,
     };
 
+    let win = browser.ownerGlobal;
     return new Promise(resolve => {
-      win.PopupNotifications.show(target, "addon-webext-permissions", "",
+      let action = {
+        label: strings.acceptText,
+        accessKey: strings.acceptKey,
+        callback: () => resolve(true),
+      };
+      let secondaryActions = [
+        {
+          label: strings.cancelText,
+          accessKey: strings.cancelKey,
+          callback: () => resolve(false),
+        },
+      ];
+
+      win.PopupNotifications.show(browser, "addon-webext-permissions", "",
                                   "addons-notification-icon",
-                                  {
-                                    label: acceptText,
-                                    accessKey: acceptKey,
-                                    callback: () => resolve(true),
-                                  },
-                                  [
-                                    {
-                                      label: cancelText,
-                                      accessKey: cancelKey,
-                                      callback: () => resolve(false),
-                                    },
-                                  ], popupOptions);
+                                  action, secondaryActions, popupOptions);
     });
   },
 
   showInstallNotification(target, addon) {
     let win = target.ownerGlobal;
     let popups = win.PopupNotifications;
 
     let name = this._sanitizeName(addon.name);
--- a/browser/themes/windows/compacttheme.css
+++ b/browser/themes/windows/compacttheme.css
@@ -107,16 +107,30 @@
 @media (min-resolution: 1.1dppx) {
   .findbar-closebutton:-moz-lwtheme-brighttext,
   #sidebar-header > .close-icon:-moz-lwtheme-brighttext,
   .tab-close-button[selected=true] {
     list-style-image: url("chrome://global/skin/icons/close-inverted@2x.png");
   }
 }
 
+/* Override tab close icon (to disable inversion) for better contrast with
+   light theme on Windows 7 Classic theme. */
+@media not all and (min-resolution: 1.1dppx) {
+  #TabsToolbar[brighttext] .tab-close-button:-moz-lwtheme-darktext:not([selected="true"]) {
+    list-style-image: url("chrome://global/skin/icons/close.png");
+  }
+}
+
+@media (min-resolution: 1.1dppx) {
+  #TabsToolbar[brighttext] .tab-close-button:-moz-lwtheme-darktext:not([selected="true"]) {
+    list-style-image: url("chrome://global/skin/icons/close@2x.png");
+  }
+}
+
 @media (-moz-os-version: windows-win7),
        (-moz-os-version: windows-win8) {
   :root {
     --space-above-tabbar: 15px;
   }
 
   /* It'd be nice if there was an element in the scrollbox's inner content
      that collapsed to the current width of the tabs. Since there isn't we
--- a/build/build-clang/clang-static-analysis-linux64.json
+++ b/build/build-clang/clang-static-analysis-linux64.json
@@ -10,11 +10,13 @@
     "libcxx_repo": "https://llvm.org/svn/llvm-project/libcxx/tags/RELEASE_390/final",
     "libcxxabi_repo": "https://llvm.org/svn/llvm-project/libcxxabi/tags/RELEASE_390/final",
     "python_path": "/usr/bin/python2.7",
     "gcc_dir": "/home/worker/workspace/build/src/gcc",
     "cc": "/home/worker/workspace/build/src/gcc/bin/gcc",
     "cxx": "/home/worker/workspace/build/src/gcc/bin/g++",
     "as": "/home/worker/workspace/build/src/gcc/bin/gcc",
     "patches": [
-      "llvm-debug-frame.patch"
+      "llvm-debug-frame.patch",
+      "r277806.patch",
+      "r285657.patch"
     ]
 }
--- a/build/macosx/cross-mozconfig.common
+++ b/build/macosx/cross-mozconfig.common
@@ -17,27 +17,28 @@ CROSS_CCTOOLS_PATH=$topsrcdir/cctools
 # This SDK was copied from a local XCode install and uploaded to tooltool.
 # Generate the tarball by running this command with the proper SDK version:
 #   sdk_path=$(xcrun --sdk macosx10.12 --show-sdk-path)
 #   tar -C $(dirname ${sdk_path}) -cHjf /tmp/$(basename ${sdk_path}).tar.bz2 $(basename ${sdk_path})
 # Upload the resulting tarball from /tmp to tooltool, and change the entry in
 # `browser/config/tooltool-manifests/macosx64/cross-releng.manifest`.
 CROSS_SYSROOT=$topsrcdir/MacOSX10.7.sdk
 CROSS_PRIVATE_FRAMEWORKS=$CROSS_SYSROOT/System/Library/PrivateFrameworks
-FLAGS="-target x86_64-apple-darwin10 -mlinker-version=136 -B $CROSS_CCTOOLS_PATH/bin -isysroot $CROSS_SYSROOT"
+FLAGS="-target x86_64-apple-darwin11 -B $CROSS_CCTOOLS_PATH/bin -isysroot $CROSS_SYSROOT"
 
 export CC="$topsrcdir/clang/bin/clang $FLAGS"
 export CXX="$topsrcdir/clang/bin/clang++ $FLAGS"
 export CPP="$topsrcdir/clang/bin/clang $FLAGS -E"
 export LLVMCONFIG=$topsrcdir/clang/bin/llvm-config
 export LDFLAGS="-Wl,-syslibroot,$CROSS_SYSROOT -Wl,-dead_strip"
-export TOOLCHAIN_PREFIX=$CROSS_CCTOOLS_PATH/bin/x86_64-apple-darwin10-
+export TOOLCHAIN_PREFIX=$CROSS_CCTOOLS_PATH/bin/x86_64-apple-darwin11-
 export DSYMUTIL=$topsrcdir/clang/bin/llvm-dsymutil
-export GENISOIMAGE=$topsrcdir/genisoimage/genisoimage
+export MKFSHFS=$topsrcdir/hfsplus-tools/newfs_hfs
 export DMG_TOOL=$topsrcdir/dmg/dmg
+export HFS_TOOL=$topsrcdir/dmg/hfsplus
 
 export HOST_CC="$topsrcdir/clang/bin/clang"
 export HOST_CXX="$topsrcdir/clang/bin/clang++"
 export HOST_CPP="$topsrcdir/clang/bin/clang -E"
 export HOST_CFLAGS="-g"
 export HOST_CXXFLAGS="-g"
 export HOST_LDFLAGS="-g"
 
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -758,16 +758,18 @@ def compiler(language, host_or_target, c
     # and the flags that were part of the user input for those variables to
     # be provided.
     add_old_configure_assignment(var, depends_if(valid_compiler)(
         lambda x: list(x.wrapper) + [x.compiler] + list(x.flags)))
 
     # Set CC_TYPE/CC_VERSION/HOST_CC_TYPE/HOST_CC_VERSION to allow
     # old-configure to do some of its still existing checks.
     if language == 'C':
+        set_config(
+            '%s_TYPE' % var, delayed_getattr(valid_compiler, 'type'))
         add_old_configure_assignment(
             '%s_TYPE' % var, delayed_getattr(valid_compiler, 'type'))
         add_old_configure_assignment(
             '%s_VERSION' % var, delayed_getattr(valid_compiler, 'version'))
 
     valid_compiler = compiler_class(valid_compiler)
 
     def compiler_error():
new file mode 100644
--- /dev/null
+++ b/build/unix/build-gcc/07F3DBBECC1A39605078094D980C197698C3739D.key
@@ -0,0 +1,53 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGiBDpu6yARBACaqhVMzAymKhrUcY1uR1pjYxM5LSuYq6mmCPTNwlGRV5RqQL0p
+uXrYlfofu8xsKiVuUKk+Dx5aJT6SDxMNkfogPGMgHK8iCaHiMrw4nTtvrJDaoxDo
+k0k62fBa8pGv7N7G0FqfkpBS/x+SDNcgWGgsJugFgqetAiaHIVD4A2tRawCgt72R
+OX0StnDnwQFxovV0pIy5ka8D/14GxPLs4qTGWWA6B8mycT67/isaAshq9eJKxZVq
+M+0rjSRmhMO0/Ajl4PjzjJXA3PH0H8dTyYSkERjEKQ0McjVLmiTM9SYBtCdkra8Q
+Fc+zTPqwjX3AayK5DocfHJ2GRhBXNb2DCdznX4A9zFCssb3FLYE/ZCDqwvrQWH6i
+dobAA/0ftbhPLtpZnpgGq1InjDzsvEqHEEt97k/iiQxsRH0/52vLD6ZQaENOlDVt
+WulDu3gI+TjI1YgGQq8B7VzW6wRR5JW3Gx9emjP3oTVjTz0bmyuaICyetldfu+yZ
+A92SU7Wm4NiMMORB+KkMDfveEWT/XW35mMTJdjpgkQH9KgrEI7QkVmluY2VudCBM
+ZWZldnJlIDx2aW5jZW50QHZpbmMxNy5uZXQ+iGIEExECACIFAksWVb0CGyMGCwkI
+BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJgMGXaYw3OdKBwAn1gsYIqfmX7cFPVP
+bRrQo44e7rZFAJ0RqZAd7PDqT0WectbqGWuaugerf4hlBBMRAgAlAhsjBgsJCAcD
+AgYVCAIJCgsEFgIDAQIeAQIXgAUCS/PnRQIZAQAKCRCYDBl2mMNznXR7AJ9gDnrA
+LCJfyqRjfVBP6aF4JfzxbQCfTXAAEbnlEhBECqgYF/S8ZjNJD8WIVgQTEQIAFgQL
+CgQDAxUDAgMWAgECF4AFAkvz5lEACgkQmAwZdpjDc50eRwCgsQeoNoSgrDpFmfIy
+gsU7a5qhqR0AoLQWp2fHpmlNhYua+A8HVxBjoyKJiFYEExECABYFAjpu6yAECwoE
+AwMVAwIDFgIBAheAAAoJEJgMGXaYw3OdSgQAnRfkXJVySd9AhQYiMX0iIDqfiGRj
+AJ4pLPdp4VvVBPloIt4SN2E559kNRIhZBBMRAgAZBAsKBAMDFQMCAxYCAQIXgAUC
+SCGibQIZAQAKCRCYDBl2mMNznduQAJoCD5vaJOLGEO605eNKXTXRt2ygvwCfSNHR
+RgaYU+5YIWf3zteNWBxC0K6IYgQTEQIAIgIbIwYLCQgHAwIGFQgCCQoLBBYCAwEC
+HgECF4AFAkvz50AACgkQmAwZdpjDc534tACggJHDY3pXzW1T8vDLeysKNIVBkukA
+nj6WfWlDjvVSGkZDfcJyhvBXDzsZiGIEExECACIFAksWVd8CGyMGCwkIBwMCBhUI
+AgkKCwQWAgMBAh4BAheAAAoJEJgMGXaYw3Od6mYAn0JipNlCsSpyet3FelnGFWS0
+2eDzAJ9SFzy6w0IgIdJJdO0Y6/BAzq+jsIhgBBMRAgAgBQJIIaFtAhsjBgsJCAcD
+AgQVAggDBBYCAwECHgECF4AACgkQmAwZdpjDc53gqACffa9gv0J/e9JEt6IFLkYY
+fRmbt/YAnirKbsByzSvS0csLhOFx/uOA+qB5iGAEExECACACGyMGCwkIBwMCBBUC
+CAMEFgIDAQIeAQIXgAUCSCGiaAAKCRCYDBl2mMNznfLyAKCqhRZQegYMDYoJ9Po+
+5RxOHteSlwCfVARE7QYuaEPWdRGE3hEI6l1rhRqIYAQTEQIAIAUCSCGenwIbIwYL
+CQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEJgMGXaYw3OdNQYAn2/gJ1CdC6tTo1O3
+cc4GD+MG9227AJsEi9hD8xkIJqS9J/7KCpy6Cm+h9IhgBBMRAgAgBQJIIaGEAhsj
+BgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQmAwZdpjDc52c8gCeNpU/yisNGveb
+z10ifoz6d03XvyAAn3hNIG8aCemLdPgmHGdhATqTJcGmiGAEExECACAFAkghnsAC
+GyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCYDBl2mMNznbGYAJ42N2JMtPSn
+kVn4qVPHUc7WOU3YCACdFgBS10cg1wzkTF40k8PKy5IKnVOIYAQTEQIAIAUCSCGh
+oAIbIwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEJgMGXaYw3OdvAwAn3Lux4sL
++FNQGaFKviI+4GG+1BlIAKCGu8WiBKIsUjxC98SjMVG+4xN16rkCDQQ6butMEAgA
+gUyl/BQ0OA7B/GSDdx6J/wjS/S4QDx7ZehgigOhJAA74e1rUqeFykb1sqxxkKnCy
+AOSqHu2BQXqk7G7ozor5bU8eE6Rki7H6Vf734TprsQgYqPrztgcVxL2InRHcMw8I
+GMZZKhWbSzKST6XaEg7Yxy7pkvNhl29bc9scWNjOCxkUt6L9wtp2UEZQf5bL41k1
+A7B1/dGOAe+DOX64x2lNYAlry3f7WV7Yq99YgcFy+V+o2wW5OBb/404x8DIm7bKT
+zBiOO1QNNe8vGJAEf1lAhldPE03T9aNNXr0tHytLcDsQbHkbnsJELtY6C2AQiAKy
+thMo1OVC+y0+Kr3JMFfumwADBQf8CiymrdhZGEZYsgJfpih+eaoBVgnlY6lHx1bQ
+Ovfol4x7B+szlNtHjA+r3PV9uPsrxa6J5qT31iPPHgwu1utTJ8tQov9OpXvEB/2J
+8DV8lYzTMpAB/GKoDUFZEGc4q+BQAvTfYYv+6WKoFjRL6iKt+Qb6WyonjG6ViPeb
+IURoMP6eE7wPFCVwK8xWHvB32jdf+ni9a2XuE9bLkF8pHcC2pz0gi7vIk88FPo8E
+ypKTL5MjC0/7+nYK9K45PZwmWNO0m5BooyP6ddGP0xJq8gisZuSWAFW3I+SW5DyP
+nvxpOXCzSj0vCHuHvDbdsUArdNWUTpxw5k3XvAIxPLMBsFK3qIhGBBgRAgAGBQI6
+butMAAoJEJgMGXaYw3OdiYYAn2SsLZg3Cj2Rg7ZziZ01NE5QpP5CAKCLyZeqvx28
+Lt44/DBv052TOb47tw==
+=ERlK
+-----END PGP PUBLIC KEY BLOCK-----
new file mode 100644
--- /dev/null
+++ b/build/unix/build-gcc/13975A70E63C361C73AE69EF6EEB81F8981C74C7.key
@@ -0,0 +1,76 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGiBDs4dV0RBACZII57dgbfnCC7RTrJ1yc0F1ofEZJJ/x4tAtSHMDNj2zTnLR25
+5AHmxN85namwJdn7ixXSZv1FMPCeTs6jDk98YuA9r5uuCNPqCNZsuQtREpN7h+wO
+IeRrhvg9/F11mty/5NthXNh8P2ELnkWXSHu6DvTQyGppAtxueOL0CjRrpwCggVYu
+vxui5mqNq9+lILbMi2Zm3UkD/0T/0HupthZFXbuzY/h/nyqzoPOxnSAAAx6N7SiE
+2w9OQ1w3K8WOFoPH9P0cnIQ+KnMSGQV4C2WY/d8YtShnKkXRYZVvlK+aiwmvf1kU
+yNyUqaA/GhW5FWN26zFQc3G5Y9TDjgBqjd6SequZztK5M5cknJGJn+otpdQtA1Dx
+2KEABACSYjdRNT3OvQJ7OSz4x4C58JKz/P69WsNZxqYVo66P7PGxM7V2GykFPbG7
+agyEMWP1alvUK551IamVtXN+mD7h3uwi5Er0cFBBfV8bSLjmhSchVpyQpiMe2iAr
+IFeWox7IUp3zoT35/CP4xMu5l8pza61U5+hK3G7ud5ZQzVvh8bQrUmljaGFyZCBH
+dWVudGhlciAoV29yaykgPHJndWVudGhlckBzdXNlLmRlPohfBBMRAgAfBQJDJvJg
+AhsDBwsJCAcDAgEDFQIDAxYCAQIeAQIXgAAKCRBu64H4mBx0x0IPAJ9OiDKdHqdX
+2ETKcxD78PcKDCcg6gCfWuJ6TizPW0n5vV16NMKl74j528aIYgQTEQIAIgIbAwIe
+AQIXgAUCVmLemAYLCQgHAwIGFQgCCQoLBBYCAwEACgkQbuuB+JgcdMdosgCeLZi7
+4DbKYbK6Sinww8ldLc0eRbgAnjcppbTLIHxcr6Lngb44v4fh8jm5iF8EExECAB8F
+AkMm8uECGwMHCwkIBwMCAQMVAgMDFgIBAh4BAheAAAoJEG7rgfiYHHTHTcoAn0/u
+FvF25feqywtGPSpL6gQ+VQZiAJ42Q8zMLMqHxd5g0e3L7mrag7EgVIhiBBMRAgAi
+AhsDAh4BAheABQJWYt6YBgsJCAcDAgYVCAIJCgsEFgIDAQAKCRBu64H4mBx0x14N
+AJ9lFQUMIHywsroHrCpGbAKxvcQrowCeNIbpm2Ct0SNJBKZ8BwhX/1bfrsyIXwQT
+EQIAHwUCQybzCQIbAwcLCQgHAwIBAxUCAwMWAgECHgECF4AACgkQbuuB+JgcdMeg
+1gCff0P5UUkRXbj/0n0ron/Xh3ji0isAnRZOtUOA2ILSNd9PNCLea9jstf6hiGIE
+ExECACICGwMCHgECF4AFAlZi3pgGCwkIBwMCBhUIAgkKCwQWAgMBAAoJEG7rgfiY
+HHTH1PAAnj/1LWl3pxLYweV1ZClR0i44GJQcAJoCM0+92pI3VIsSMfkYaUVmOjVz
+f4hfBBMRAgAfBQJDJvKmAhsDBwsJCAcDAgEDFQIDAxYCAQIeAQIXgAAKCRBu64H4
+mBx0xyAgAJwN2SASDJN9Y2H9iMjRSCkEftC7PgCeOTjpR3vyDnM7QL8bjwEiR5l7
+l3qIYgQTEQIAIgIbAwcLCQgHAwIBAxUCAwMWAgECHgECF4AFAkm3jjkCGQEACgkQ
+buuB+JgcdMcXrACfVTEyxl0EqQN+FpmssqVUXMuGIPkAnjuh0lk4rlWnFHuRPKFP
+aLNcn7TbiGUEExECACUCGwMCHgECF4ACGQEFAlZi3pMGCwkIBwMCBhUIAgkKCwQW
+AgMBAAoJEG7rgfiYHHTHIBIAn20wZDYF0KrfbJNzK4/VwAEAzN+wAJ9Dpbhtq4sR
+oH3cbadBsD2mXXthOohXBBMRAgAXBQI7OHVdBQsHCgMEAxUDAgMWAgECF4AACgkQ
+buuB+JgcdMexIACfUdyOhJRqUp4ENf5WMF7zbVVLryoAn2cNiUWC2u4za4NDyde6
++JGW3yo4iFoEMBECABoFAkm3je4THSBBY2NvdW50IGRpc2FibGVkLgAKCRBu64H4
+mBx0xw8pAJ9f38BHfCYcFBFrzasWJ50aYiq9agCeJc39ixXix4rnOa8vzBvSqILU
+3J2IXwQTEQIAHwUCPvYc2wIbAwcLCQgHAwIBAxUCAwMWAgECHgECF4AACgkQbuuB
++JgcdMcsEACfQPXptVqB3lVdH8NmJq9988UjdugAnjc51tLV7wP/omMaG6zxqOBe
+bByGiFcEExECABcFAjs4dV0FCwcKAwQDFQMCAxYCAQIXgAAKCRBu64H4mBx0x7Eg
+AJ9R3I6ElGpSngQ1/lYwXvNtVUuvKgCfZw2JRYLa7jNrg0PJ17r4kZbfKjiIVwQT
+EQIAFwULBwoDBAMVAwIDFgIBAheABQJJt44zAAoJEG7rgfiYHHTHt1oAmwfqV/fy
+BQtuo6iVwyrLTrv6SH8WAJ9+vQxODP5nLEVv0VDkPe9YDmnHIohaBBMRAgAaBQsH
+CgMEAxUDAgMWAgECF4AFAkMm92MCGQEACgkQbuuB+JgcdMf9FgCffJBUSQIPBPWC
+zQvDLdCCQKj1gS0AnjY8bbEU+8j9MJdoyti8VQqc063IiFoEMBECABoFAkm3jboT
+HSBBY2NvdW50IGRpc2FibGVkLgAKCRBu64H4mBx0x3w4AJ9uJb1MnaB4XL2W4/ur
+kpvbRPiNrgCfRnEpymRfBRjuqSZpLr6t2548MFaIWgQwEQIAGgUCSbeOjRMdIEFj
+Y291bnQgZGlzYWJsZWQuAAoJEG7rgfiYHHTHsjkAn3kJ+cwIuWjR07f/1L87hC1x
+MGmAAJ45JUNoUgl45+JYUVamI+Sno02roLkBCwRDJvHRAQgA+McP+S2zoZBu2xX7
+r5pmB8IroxVl7Xgw5cUbrQWacc/NfKaivO7sPFJA6QqIpTj2ZSSVMhDUSsYivycL
+OOZUeabsIfnd3Lz86SU+Cl5wEsZI/1aKpDxMnE1SINZADSvYdZUCyLzo34Td725s
+3hVIrjJ3okxHUynYqDJLYsrY+NGj6jua6U4VoACjGaLyBYhVHqy/l2SHeD/r8N8q
+DfZTwJaMWnkhcqaTIw9Ifl45kvh4F/HghrVwVxZ8Mll2xhD4QH5q7MerKv8NLmif
+hpLvZYCmlaTAfUy799ic4RjfvIXgbBg9v8zkujPbBMzF2N9+XMIx19DnoK4yV9zz
+gx5P8wAGKYkBZwQYEQIACQIbAgUCSXscKQEpwF0gBBkBAgAGBQJDJvHRAAoJEDqw
+CZb8JqZBuHQIAOoXgUMEyxCHz6+SEW9c5NC+1eRAy5B52vJoIYdxL97n8nTFvm4v
+JsyecXKH20jLxyP2xzv3J5NO5dJAsmBTTZeHoQviiwal7klZa8VtjhLI2TJHdRyl
+eDOQfzRyuwcXmLHALHLs9MSNDjzJPT2GKDh6IMdDV9LijHQXlpRDiraaThs21TpY
+cQ//yXoErBJQL3+V8VCYyeTtJ4hCpPCAL1NqA9mEJDP+01kGj63cROVFx89nZ2MI
+ZEmbmZswb+nATLUv1+t2inMFiTnrISm4D3seOYgO+3fhhsA6U9g9IKy+eHNl2hWt
+G/+oFwbE2F1gDvPCIYOWuNF/tGDUpPyLO+gJEG7rgfiYHHTHVYMAlROqNeZ/TalC
+mF9ijupqU65WvW0AnRRSCD49emCs/SWngtDxJuTG8FGFuQELBEMm8fwBCAC3KLX6
+91TOFiizmWZTOeRNREUEZYy89I6cHrYjYyrRkBrOHJGNvoS5JO4Zy6wlc9bNGWxQ
+U92bJCMiqE8n1mRRIs6J4gExThWqBZzsZlcrs/gu6HxPFCvPlg62emPkd6//KPrc
+AIMshvNKGLMFK15n5Nkv5ofv/xcr/fqjisISnk4fr1GI9wJQUQdCTEXu9o92erIf
+zb8m1Q7FJbXNhyv7tcekdr5Q20jrZDgxX3H1aLq8EG8nrNlJqulWLtWIh/k9Uwa5
+ZvmcDVhKES1BUqdCefqkGpFQXiItzKu6cgs8anXeG1RRqFoOvipZQ/lUqYQtP0iK
+05NHQFfp7cTaHo2fAAYpiEkEGBECAAkFAkMm8fwCGwwACgkQbuuB+JgcdMcy4QCd
+Fw3ipNDVX3Z77ZHMmbYhhtUmM8EAnA1jqzeVutwLtlzYT+Tl/HDB6dJOuQENBDs4
+dV4QBACXdavIYhl+L248s1mUi9EUESu9QovNzuf79zUZpRUzFdwX8hq56BuWHjU6
+hXYpzPWwXHnYwsNINNXUPAOfh83PA/sNg572HgQGkx48bUNLstDQugPrzau97LoK
+/DD54WYEFd2ISoJe8+5bh3dYyc6xCovkGJJAf4aLAissU3vKPwADBQP+P0U7OJ/U
+Yt2hIbx+wSL/9rGrSxcj421FQ6u+auRMIbejmtk4k3DP4oFCk/jkt3Oiw7hX+Q9W
+4nlTgSmsQ9Gp6N9JNb6gr4GCbSZ8iaDDsm9p2Q15d8l3BiJ263IXWOOuhV2qmtKM
+ABqhmBKLazDTcIXHVaR0v4YJxzA3ohWXk4iIRgQYEQIABgUCOzh1XgAKCRBu64H4
+mBx0x6rMAJ94eNPY8Vh3Lzw8c1UbDmx70+oY9gCcCulffZ8N27/GG6qOoes2TDI7
+9ig=
+=2zZQ
+-----END PGP PUBLIC KEY BLOCK-----
new file mode 100644
--- /dev/null
+++ b/build/unix/build-gcc/33C235A34C46AA3FFB293709A328C3A2C3C45C06.key
@@ -0,0 +1,33 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGiBECGYZsRBAC9VE8N8vHAG87MTC2wbtnmbSD8Wc2xYCaLofZAH+vXyio3Dnwx
+jQLlj7IgwRWNAVq13uL0wn0WAsGop5Cs7nA/JD4MEBBNSdnvq1bMYitch2PTtAU+
+h6HaI9JXBDUh4AKZz2rllKgbigMHlgIugxnKTAMJIhS63lCTHWEDlnycJwCgqSX9
+hDs9eBC5coearGDhc0BDvTsD/A05YkZkQBgsYD6cjWFwNLJIcaHORKlLLZ9gRJO5
+LVcKaCEgYSWAM7dadJeqIFi9RkXdv+cWozxTgrGlY4T7/PakIBB7wWj2Zl72mW5a
+NHT2vAemB8IFV1saiFXZM+qDhCHbV4yKSmNOQHY1VnSCUrgINiM0qlTz08yjUazK
+fm2BBACDF3ZfUQNeHC9zwfsgCzKnqOm7FSlwOyI0f+j83B5PH2+KuzuyEqYoxGp+
+2d1zTxvbOeBBaX8T1M4n5d9ixiFMhgbTzuyit3nn6cp5j2L0IAS9pw0kaWpPMhpQ
+zydNgnaBxHs1Y+cP4iM/4FWFCvfjUdR7xULdEzkgGxevu8pNEbQgSmFrdWIgSmVs
+aW5layA8amFrdWJAcmVkaGF0LmNvbT6IZAQTEQIAJAIbAwYLCQgHAwIDFQIDAxYC
+AQIeAQIXgAUCSe3VIgUJEs109wAKCRCjKMOiw8RcBqANAJ0VlFMTtevlkEM+ym4k
+yE3YOrGZ+wCeP7lZGc2jVLHJfrOKxXsTM5YPWhqIZAQTEQIAJAIbAwYLCQgHAwID
+FQIDAxYCAQIeAQIXgAUCTI3tMgUJHtOOlwAKCRCjKMOiw8RcBjySAJ9ApMXF3+gW
+Ir0zpMxvWb53/oxsHgCaAl6V5JS9GJUnrPiHKdR+sMFPkd6IZAQTEQIAJAUCQIZh
+mwIbAwUJCWYBgAYLCQgHAwIDFQIDAxYCAQIeAQIXgAAKCRCjKMOiw8RcBrC+AJ9d
+mQcWoZHFGoinHck309KD0m2FegCeMBjr/M6Ec1myCYMUhtpl5DI7zY25Ag0EQIZh
+ohAIALrI1X59CM30/Ufg+O9FFRRyM8GefACfItrIvp6jx+0ZMY+/ZbYnlMzI7Gz4
+xNXc+83Zsz7zE5xogNcq9LILdhB7Ta1ZRkRttM8AdfyakRQTjzCPtxSPgSao/Dcu
+CL09BZdaeeqMAxLmw9DnY3xmZqQtCau8PlgIiClq2db9Wy0bpQ+DDfQV4MlX6eoI
+33TG9Moy59QQUG5reQ2JNkQZRebPxJAPiAgHoF/Q+XO1pLeCccIN7SApe7yVd/4A
+sS3Y9lZj2JvEvutLojsRGL0E/CAwH8cJqPAt65qbOgQzCILhcc9aYZ234g9n7Kpx
+Ck1h2QMtXfsmaA7GsrXo1Ddfra8ABA0H/0sa4SCQhWQ14tOFkN15xzuaqGOxUD+O
+uAsgRdKaFdIhZnj0MRmvOfBSP7hONw7fE0m9DVq9NDPqFcMeyCuBNIMpGIuN6CAK
+/G0K2UgzoCxMXUEYGncFfVnOoNURV9u2lGq7ZMNJmuzt0BhxXtUYRlH3WRPqPyGv
+s/OrIqvgN+Kf9+i0kQSObWz6CeYnBKzCc++MPkVhYj8KR5Y6n3zPZpnOfmO3c0rY
+C+KiNoMwchlZmiOh7zgcTybv4zuOU7bppEidreIq2/o4nBNTao/5uzYdDX9FBpDT
+hhU9ErdO8Vd7Vf2I1/WQdt6dHUXPLfkwI8+ODE/4R/Oz8opFC5L22kSITwQYEQIA
+DwIbDAUCTI3tTQUJHtOOqwAKCRCjKMOiw8RcBrBvAKCTFx5FOuuxM2VoQka8iBGj
+f1vcugCdHV/JIhOwETTqOQEbkw3y9ng2+4U=
+=K9Jj
+-----END PGP PUBLIC KEY BLOCK-----
new file mode 100644
--- /dev/null
+++ b/build/unix/build-gcc/343C2FF0FBEE5EC2EDBEF399F3599FF828C67298.key
@@ -0,0 +1,35 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQFNBFDrIWMBCgCyyYoTAD/aL6Yl90eSJ1xuFpODTcwyRZsNSUZKSmKwnqXo9LgS
+2B00yVZ2nO2OrSmWPiYikTciitv04bAqFaggSstx6hlni6n3h2PL0jXpf9EI6qOO
+oKwi2IVtbBnJAhWpfRcAce6WEqvnav6KjuBM3lr8/5GzDV8tm6+X/G/paTnBqTB9
+pBxrH7smB+iRjDt/6ykWkbYLd6uBKzIkAp4HqAZb/aZMvxI28PeWGjZJQYq2nVPf
+LroM6Ub/sNlXpv/bmHJusFQjUL368njhZD1+aVLCUfBCCDzvZc3EYt3wBkbmuCiA
+xOb9ramHgiVkNENtzXR+sbQHtKRQv/jllY1qxROM2/rWmL+HohdxL5E0VPple2bg
+U/zqX0Hg2byb8FbpzPJO5PnBD+1PME3Uirsly4N7XT80OvhXlYe4t+9X0QARAQAB
+tCROaWVscyBNw7ZsbGVyIDxuaXNzZUBseXNhdG9yLmxpdS5zZT6JAX4EEwECACgF
+AlDrIWMCGwMFCRLMAwAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPNZn/go
+xnKYqm0J/A4b6TE5qPWiWj0kriUBSmpys3qUz93gR6Ft7w2f478KJuzbSadvyn0u
+PcnP26AGTOQq75RhtgCJgdYbvRocTjlMh9jOX584Hx8hi/QSrpCSYMnj6dQKbu0Y
+QIFjZx8gPeYvzG8t34FCNEzZ09RQZqy/ukRyN99LkwEuP4FWq486b7dpgv7GC+SH
+lZcMco6VW8FLOT7KMalH06cmdhFPrFSYAIHDu3CsYhC8knIQV99Xzno/KeSkEwkq
+tYDOdz0x4HWdOwHrl2S2X6Ex1q3QRXcq84EYQwHz2WEGaPR7Vd76P5J1wiHN6rwO
+4exfgsRyTvc6NDQPTFqmoCzwuPviYk6JNnHr9E5TkLT7lAnESEhMLyyIG/7Uwpgu
+5C71IMaTpOpf8DEU9NU/zuxgHoMaKBZaeYKs0S26s1zwGOlQX0T9uQFNBFDrIWMB
+CgDKlONI+5Bqcu69+72fmLZPizzEUsIRA2Y0w2RE7+uJ5Es9/YTp5PnWANpPT7GS
+8JJnc6NJJeh6GkMkGGwq5Op7CDsjW9pQZ0vAW90XjnyniDa9W0W+m5+X/LPOzh+n
+is9Zcf17P91tprLCLi+TOOb35xt396pZ+S+PwuV0dLiIYdVYV3e6LNCV0LjhEqp5
+3TRwTrLTNPQVnt0DPYTh/Kn1x6d5zOS0MK4QybKN1WJU6nYIQRXyWKkixjbs++jc
+gV/juck96Ve0blvn6DfqfpG8YzbmqRCufLo683LtlBUZ0c+znrD1nouqX2Eb/Cyl
+G8Q8ZUHXimCJ+g6RfH9kOmtVH/208u/nDofVL/Q0dvAXfU5MX49c7XYy7B2rTlk+
+4nuNeaHM0aU2Y14+SQy+sR6zydu7eGLdqjzV0CX/ekgrjQARAQABiQFlBBgBAgAP
+BQJQ6yFjAhsMBQkSzAMAAAoJEPNZn/goxnKYGUcJ/j+L0/uzfwCR1aTBZ6FBT9Od
+NyatVjmz20ahskF3BySmkT1R06K08YOGJ//LPajj0eKqU8WKgxMc7pWi5SG+yMFn
+2db5HnJDGiSmSjCXW/BzsSt1786LtO0m0ehatj9kl6JrxQNXazOkRJ2ww13P6/91
+RBaV6R08BmFTrUco2P6w+djCF4NlnkOLa7fM6QtNZM+yB+EzaPjSBFjZG52BVWZk
+cXEVN0cEjPuznuQOmx8Dny7lQikp49NumrbamaxZEilx2Bi9gSbovNaKBuncKi9X
+boiEiNbAarGxP40Qvlk2AuXWvq+fiBnU1e1nU2oV7/7nAWH7kj/Vr/JxcBeOpsND
+GkW7Yrd3mkJCrhG+jMs1V2qNb9Uhr5ZLOA40sIz2PHfDrR+gc8THm2p5OvCWEAeu
+kYJ22XTUIt6XoPO0ERYD
+=MH4q
+-----END PGP PUBLIC KEY BLOCK-----
new file mode 100644
--- /dev/null
+++ b/build/unix/build-gcc/AD17A21EF8AED8F1CC02DBD9F7D5C9BF765C61E3.key
@@ -0,0 +1,57 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQMuBEvtHBoRCACUnk4CbRKM5SsykvTko30oeZqmzDF4bS/usOEcZBjtpudsZBC4
+Po7zfIQAvRyCyEsXtBHCM9KhUNgIbfToDfb9quXvH0KR5D/lcHL3eOHfFPX+Yr34
+ouHj/+2yFQNNrsmEmteOFJVM+zX1KBx2I8XQWDNbnMbEbPj/DdCvsk7+3uoQCepG
+bFD07pk7iFb1ny6DXgvM4fItJbY5z7+IQSJCv9blRNy55oCkOdGm1FE4Q/SPgbT4
+quZoec2IxGlFGt9ThUDpuYPcdejyjaC5eFDozhqXwMDh17yBDS53XF6lV02Djs7L
+e6QbUJv4B3rqvOGV+eLfRxFuy6X6XEOh8FgrAQCzj7dNslwWI9nTwp5GCr7IO7jz
+Ynmw+keMcaOUu0Gd2wf/f/uonF/RVy+Gp+PGHnPhi20xaKZ9unf3l3KWELTpizI9
+Of4R+N9AOpVR4Bf1MgkCV4VH8cpOUQOxQQUEYOpYYYH0EeuDlBItVgvcdG40bnQA
+PUwWdqbHUh1cXjD0kGQLv8B2+O31GfnjDQhnNJ5C9KdhKf2sLRkNJtMLU5XsPFMF
+qoAW7I0cak2XCuHokiOdJq3bhOX4FdxRGlFPOXNOQA53nYRb0kHv4gfKBHwPJbPT
+T3MFgoqO23q+om2cFqwVRTVLW4Cg+Ki5dvFkJrufE/NNaCRuSlj3G2WF5K3OOZct
+O7xsDsp5wPMQu1tkuwoZcnp+EmvI8QQkPl722eWf3wf7BFjLCIqi1ivu0GVVMLOM
+DMGRZeSkjVrLj1xw5BbWsQ8jOAGvnrqC5zpQoMQLzYyPGb6KzXX8Df1kbQEys7M/
+FoLVIhSE/Elr4e5epNW+8zpmLSW61PlDNraHYHcCxf9RY9aZrxtzEXxdCpPZ+bk3
+8sh4kvAv6XUsmweAu2RRY97u5KNyWkIEhhJJcd96cK6FNc9GeOLCiXQPJqK1ORSj
+bCBX8HL1U1r8iOo7Hh+Y25flZ0vRSE/6Fsw1X+seTakelh8EWQtIr+i+oClHgmrT
+su9NhhQFFvAUFNdN0K1TcADhfj5nPTImet1x9oAUsU//lOXBFWYhs9sitE879uQs
+d7QeQW5kcmVhcyBFbmdlIDxhbmRyZWFzQGVuZ2UuZnI+iIAEExEIACgCGwMGCwkI
+BwMCBhUIAgkKCwQWAgMBAh4BAheABQJVWjYMBQkLTk1nAAoJEPfVyb92XGHjOqEB
+AJsOI48xKPLh09bAzvzSOqS7H/KR6zWIfvLvu1gDhZVrAP92LZoj7qcgnZ15tY2Y
+yqHYHk87zl3vRlMLJXizEz64xIiABBMRCAAoAhsDBgsJCAcDAgYVCAIJCgsEFgID
+AQIeAQIXgAUCVqUDRgUJDJkamgAKCRD31cm/dlxh42vPAPoDs4RuOS7YWYM7gKiC
+3oNVTTIDKz9foDlOIXUhlWf6dwD/S1ofL5UNLLubCdK3UYNHNj+8r4ynz3YezHaR
+MDCTtGmIgAQTEQgAKAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AFAldHXPUF
+CQ07dFMACgkQ99XJv3ZcYeOc7wD/eE9W2sl2zI6h1LXTA6tVharyhP8cOAtzuuw7
+auZaE3wA/jaKo0HYrSnhrg8bF2zMnf9LQQdPdW99jZNVFIMcnOrniIAEExEIACgF
+AlIWO54CGwMFCQlmAYAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPfVyb92
+XGHj9VkBAJe2uRxafZnUWpkTMD2CGg2EQgIP0R4bH3lykKtNKiZ/AQChGBkQWref
+Z4eGsXhO205DYKq8TXKmAxuSVYv3UahXXIiABBMRCAAoAhsDBgsJCAcDAgYVCAIJ
+CgsEFgIDAQIeAQIXgAUCVVo2GgUJC05NZwAKCRD31cm/dlxh4yb4AP9PxhxI7yE/
+PiCa9hmrl5rvilMGXNBzA80re3+G8un6EgD7BQPdd9hBlC98uC6WtYtB9xFgny3M
+mNPpcUM7NHDjdYKIgAQTEQgAKAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AF
+AlalAz0FCQyZGpoACgkQ99XJv3ZcYeMR7gEAlSYGcUywSjjXJ+kjz6n3wddHZFGl
+q3Z4zmdVeIJctv8A/R0qGx73rFDNN1aEB36RZmjf6s3OKEtZ+sFNPEXOWwpAiIAE
+ExEIACgCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheABQJXR10BBQkNO3RTAAoJ
+EPfVyb92XGHjgN4BAKeBkmxrmrSPU9HUDlE7L/ecR7rUlF2Go4ibuDvOWp0BAP9X
+wXSHKxDlL2lh/IeiZSqIW09GXBItfQACaeoJz4s4oYiABBMRCAAoBQJL7RwaAhsD
+BQkJZgGABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRD31cm/dlxh4zhsAQCf
+pbJqrGh6rGBAW1L3jCHNeYt9ughb6wxtlwFclThG/QD/bccAIkDT1lem8Bhf66d5
+sYEx+d27d2rvyBNblP3Urwa5Ag0ES+0cGhAIAI7fBR4UWKVQ8t5A0hPXbOhQkxyt
+ztcIRo8rpGGMq//STIa4gBZjuyomkOGss8bElWFYeco09+OqGimD4fDEHXVpD/ev
+IYiLq9U2sAUHZaKQAM3vE5LBfWa6zeuQwQj0/t9+cDyNCLTEjPsFQ5AdWyXxxO2c
+XetgOHbKwtyjEEsjbJNms6ysjsmXzQGkDRCarGpWrqhAE+jweykpJLoCpCI8AmTv
+1/dA5AOcDfsNlTDJnKwWsIaEnvscE4YMwcbCxwHUbhlzzEs8uS7Bk1LaQKQFUcvQ
+Bt1nFiHD3uTHZLX5RjL2VTRArQFWN3PefAW1T5Ws+Fs+JwBy/VeKbuBud5sAAwYH
+/167fa00yFiCtloWPJ/Xv7Marh/CIpAG0GOuPIJ4IqdEl/ZZ76A0KalUbrSL+fj1
+Eq/0auiNi9CbtlKI8lebn0AkKRYZe9j6JwIHJGomn1hgFhPGMKUToE4iUXmv+ZWN
+BbH4iJz87xcrmtV9mLHiVZHGMwMBv5VVSnBoGcxcHHYnC3iAP8h+yaFt4pVIxQXR
+NNfbXsUFvZaW2Tgat8knupmxOZfJfdesIf+n1X36OvhsZgFw6rHTSf2mAfkiBl47
+uYbB8v8BR2nDXbtpNlg2ssPbmPIfOE0Ft7pZ5VN1YiNY60w+Sbh5wD0A4mr7OZ/t
+2NP0yxDMCLYN3jY5R+P/e4OIZwQYEQgADwIbDAUCV0dd7gUJDTt1RgAKCRD31cm/
+dlxh4xPFAQCXDeJBh1YPVkD8rgFlmMIEtorkzK0tHfCap6j1cG4iFAD/SCXCufA7
+8GOBvibrC/azKvoBKLY1/stpKCrecZdRFkk=
+=SDN9
+-----END PGP PUBLIC KEY BLOCK-----
new file mode 100644
--- /dev/null
+++ b/build/unix/build-gcc/EAF1C276A747E9ED86210CBAC3126D3B4AE55E93.key
@@ -0,0 +1,29 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGiBEj3S14RBACUE+e2hRWwM6AFWaNKsLgDg6ebDNCI6z/Pk38t6JUeM+D5MAvq
+fnL45bF/3CUrZRK+/qLg5iwRWehKh08VQ7GqDxMerZkPmfvirVxLwpc5ngCOGJwv
+ba13xdaxfTLMkHxaQyGWiUqHIwdzFoNBgjq9XTY0GGqHwVA1Hb+xTAL8PwCg+wru
+41p/aOq9cfPN1U1BjulWCSMD/2YP23pI19o9Hr26ltyJcd/xkSRiCUk84efIw5JH
+7QlxoMoW/SdJQAGi2pZN9o4I/fPDB3Gna9M9rZfacKda857dwkALPK8xTsfHiZzH
+40g+eYUvl9nloNidneSFcxbLO/euURcCJ6Ri6nb4QWWLVH1XF6wQxEGyn7ojYMOn
+ihlkA/9KATtSt+T0zgWskqckgV1ZQg9Ysqwp3GAvezJuyUTXlB02ApVFEsRTQtLZ
+2WPvo/gzfih/EdNLrq7UeeB4dr/nlpANn4IyBRN6EmBHaJ4MMKN77B7bB0GaBfGJ
+rgNNp7W0xG0FLjaMoQzP62qqAtZHNlW1qCmkyJGUpAa6tJw9CbQlVHJpc3RhbiBH
+aW5nb2xkIDxnaW5nb2xkQGFkYWNvcmUuY29tPohgBBMRAgAgBQJI90teAhsDBgsJ
+CAcDAgQVAggDBBYCAwECHgECF4AACgkQwxJtO0rlXpMVcwCgoQ91OLI+m2bsu2SS
+d5MsRQH3FWsAnRFG2YGk5o5zuoLzdZd6KlL9xJ+uuQINBEj3S2MQCACnDo5dHujc
+u7QHRPnxNwiKhMP6eIZaEm9tavab3UxsRufMyVC8nQ8+EmCOwfBrqstfRVoQnoDI
+s5UY1XAM3mBFXYqfY9wR6NISUlzK/HPyFhGE7t3lVjOkiqbWOftDt6GgRETeqYsW
+XkDV/dL4+P3eSaOSP6KMZwdjgXPOciN59KIiii9NK4icxP0lJHDk5WJFwfucEyUt
+Sz7uwuUFcajHZmMxxHAnWT3uJ+ZasSijduZevsHhKTTXaZRideqf+ur1/TcUaZDQ
+O3wist1qc03NkL+oGu6HYPx9ZV40p/axdTaUXMcBjtAZIzvy984HF9EsFQnvbiXt
+R8zg6SYXLRmbAAMGB/9JMKWsCuxUzXmU1jyJvMXdRBZ4YQYkKFYWrEXwjYlBEGx6
+01PkR//4QJVR4zFjy4zVnaUrOxtR+65Eedf+9fNZzSNeI24TGaqyVM0OYYQtp9cH
+kRDu3wif1k2NW3BnrmTjVefdAWVH6zKT9lP9m6RPHCwVGyORhVQtB3+ZXOehNJwL
+9NBU4MUpGKpoQCuODdgZ8iQXbo+plg0eCxcpNaYzSnq9DMAU+2qnP6d3x4DeWzlL
+wvJ2K2Mw89gvCImy/JDe05EXqKowR6aiIPvw5ou9xSHmjT6rcaIBiROCe+1hh4XC
+djCdb4kOskWCEXfFKcHax4N5fI9vmk3P5068BELMiEkEGBECAAkFAkj3S2MCGwwA
+CgkQwxJtO0rlXpNxdwCgjr4sQRf2cyDkCSWe4AElbI74BREAoPdet3XvE6ZcZJGl
+UIZySRkdpk/A
+=dGh0
+-----END PGP PUBLIC KEY BLOCK-----
--- a/build/unix/build-gcc/build-gcc.sh
+++ b/build/unix/build-gcc/build-gcc.sh
@@ -1,52 +1,120 @@
 #!/bin/bash
 
+set -e
+
+set -x
+
 gcc_version=4.9.4
 binutils_version=2.25.1
 this_path=$(readlink -f $(dirname $0))
 make_flags='-j12'
 
 root_dir="$1"
 if [ -z "$root_dir" -o ! -d "$root_dir" ]; then
   root_dir=$(mktemp -d)
 fi
 cd $root_dir
 
 if test -z $TMPDIR; then
   TMPDIR=/tmp/
 fi
 
-wget -c -P $TMPDIR ftp://ftp.gnu.org/gnu/binutils/binutils-$binutils_version.tar.bz2 || exit 1
+mkdir gpg
+GPG="gpg --homedir $root_dir/gpg"
+# GPG key used to sign GCC
+$GPG --import $this_path/13975A70E63C361C73AE69EF6EEB81F8981C74C7.key
+# GPG key used to sign binutils
+$GPG --import $this_path/EAF1C276A747E9ED86210CBAC3126D3B4AE55E93.key
+# GPG key used to sign GMP
+$GPG --import $this_path/343C2FF0FBEE5EC2EDBEF399F3599FF828C67298.key
+# GPG key used to sign MPFR
+$GPG --import $this_path/07F3DBBECC1A39605078094D980C197698C3739D.key
+# GPG key used to sign MPC
+$GPG --import $this_path/AD17A21EF8AED8F1CC02DBD9F7D5C9BF765C61E3.key
+
+> $root_dir/downloads
+
+download() {
+  wget -c -P $TMPDIR $1/$2
+  (cd $TMPDIR; sha256sum $2) >> $root_dir/downloads
+}
+
+download_and_check() {
+  download $1 ${2%.*}
+  wget -c -P $TMPDIR $1/$2
+  $GPG --verify $TMPDIR/$2 $TMPDIR/${2%.*}
+}
+
+download_and_check ftp://ftp.gnu.org/gnu/binutils binutils-$binutils_version.tar.bz2.sig
 tar xjf $TMPDIR/binutils-$binutils_version.tar.bz2
 mkdir binutils-objdir
 cd binutils-objdir
 # gold is disabled because we don't use it on automation, and also we ran into
 # some issues with it using this script in build-clang.py.
-../binutils-$binutils_version/configure --prefix /tools/gcc/ --disable-gold --enable-plugins --disable-nls || exit 1
-make $make_flags || exit 1
-make install $make_flags DESTDIR=$root_dir || exit 1
+../binutils-$binutils_version/configure --prefix /tools/gcc/ --disable-gold --enable-plugins --disable-nls
+make $make_flags
+make install $make_flags DESTDIR=$root_dir
 cd ..
 
 case "$gcc_version" in
 *-*)
-  wget -c -P $TMPDIR ftp://gcc.gnu.org/pub/gcc/snapshots/$gcc_version/gcc-$gcc_version.tar.bz2 || exit 1
+  download ftp://gcc.gnu.org/pub/gcc/snapshots/$gcc_version/gcc-$gcc_version.tar.bz2
   ;;
 *)
-  wget -c -P $TMPDIR ftp://ftp.gnu.org/gnu/gcc/gcc-$gcc_version/gcc-$gcc_version.tar.bz2 || exit 1
+  download_and_check ftp://ftp.gnu.org/gnu/gcc/gcc-$gcc_version gcc-$gcc_version.tar.bz2.sig
   ;;
 esac
 tar xjf $TMPDIR/gcc-$gcc_version.tar.bz2
 cd gcc-$gcc_version
 
-./contrib/download_prerequisites
+(
+  # Divert commands that download_prerequisites use
+  ln() { :; }
+  tar() { :; }
+  sed() { :; }
+  wget() {
+    echo $1
+  }
 
-patch -p1 < "${this_path}/PR64905.patch" || exit 1
+  . ./contrib/download_prerequisites
+) | while read url; do
+  file=$(basename $url)
+  case "$file" in
+  gmp-*.tar.*)
+    # If download_prerequisites wants 4.3.2, use 5.1.3 instead.
+    file=${file/4.3.2/5.1.3}
+    download_and_check https://gmplib.org/download/gmp $file.sig
+    ;;
+  mpfr-*.tar.*)
+    # If download_prerequisites wants 2.4.2, use 3.1.5 instead.
+    file=${file/2.4.2/3.1.5}
+    download_and_check http://www.mpfr.org/${file%.tar.*} $file.asc
+    ;;
+  mpc-*.tar.*)
+    # If download_prerequisites wants 0.8.1, use 0.8.2 instead.
+    file=${file/0.8.1/0.8.2}
+    download_and_check http://www.multiprecision.org/mpc/download $file.asc
+    ;;
+  *)
+    download $(dirname $url) $file
+    ;;
+  esac
+  tar xaf $TMPDIR/$file
+  ln -sf ${file%.tar.*} ${file%-*}
+done
+
+# Check all the downloads we did are in the checksums list, and that the
+# checksums match.
+diff -u <(sort -k 2 $root_dir/downloads) $this_path/checksums
+
+patch -p1 < "${this_path}/PR64905.patch"
 
 cd ..
 mkdir gcc-objdir
 cd gcc-objdir
-../gcc-$gcc_version/configure --prefix=/tools/gcc --enable-languages=c,c++  --disable-nls --disable-gnu-unique-object --enable-__cxa_atexit --with-arch-32=pentiumpro || exit 1
-make $make_flags || exit 1
-make $make_flags install DESTDIR=$root_dir || exit 1
+../gcc-$gcc_version/configure --prefix=/tools/gcc --enable-languages=c,c++  --disable-nls --disable-gnu-unique-object --enable-__cxa_atexit --with-arch-32=pentiumpro
+make $make_flags
+make $make_flags install DESTDIR=$root_dir
 
 cd $root_dir/tools
 tar caf $root_dir/gcc.tar.xz gcc/
new file mode 100644
--- /dev/null
+++ b/build/unix/build-gcc/checksums
@@ -0,0 +1,7 @@
+b5b14added7d78a8d1ca70b5cb75fef57ce2197264f4f5835326b0df22ac9f22  binutils-2.25.1.tar.bz2
+02500a4edd14875f94fe84cbeda4290425cb0c1c2474c6f75d75a303d64b4196  cloog-0.18.1.tar.gz
+6c11d292cd01b294f9f84c9a59c230d80e9e4a47e5c6355f046bb36d4f358092  gcc-4.9.4.tar.bz2
+752079520b4690531171d0f4532e40f08600215feefede70b24fabdc6f1ab160  gmp-5.1.3.tar.bz2
+f4b3dbee9712850006e44f0db2103441ab3d13b406f77996d1df19ee89d11fb4  isl-0.12.2.tar.bz2
+ae79f8d41d8a86456b68607e9ca398d00f8b7342d1d83bcf4428178ac45380c7  mpc-0.8.2.tar.gz
+ca498c1c7a74dd37a576f353312d1e68d490978de4395fa28f1cbd46a364e658  mpfr-3.1.5.tar.bz2
new file mode 100755
--- /dev/null
+++ b/build/unix/build-hfsplus/build-hfsplus.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -x
+
+hfplus_version=540.1.linux3
+md5sum=0435afc389b919027b69616ad1b05709
+filename=diskdev_cmds-${hfplus_version}.tar.gz
+make_flags="-j$(getconf _NPROCESSORS_ONLN)"
+
+root_dir="$1"
+if [ -z "$root_dir" -o ! -d "$root_dir" ]; then
+  root_dir=$(mktemp -d)
+fi
+cd $root_dir
+
+if test -z $TMPDIR; then
+  TMPDIR=/tmp/
+fi
+
+# Install clang first
+yum install -y clang
+
+# Set an md5 check file to validate input
+echo "${md5sum} *${TMPDIR}/${filename}" > $TMPDIR/hfsplus.MD5
+
+# Most-upstream is https://opensource.apple.com/source/diskdev_cmds/
+
+# Download the source of the specified version of binutils
+wget -c -P $TMPDIR http://pkgs.fedoraproject.org/repo/pkgs/hfsplus-tools/${filename}/${md5sum}/${filename} || exit 1
+md5sum -c $TMPDIR/hfsplus.MD5 || exit 1
+mkdir hfsplus-source
+tar xzf $TMPDIR/${filename} -C hfsplus-source --strip-components=1
+
+# Build
+cd hfsplus-source
+make $make_flags || exit 1
+cd ..
+
+mkdir hfsplus-tools
+cp hfsplus-source/newfs_hfs.tproj/newfs_hfs hfsplus-tools/newfs_hfs
+## XXX fsck_hfs is unused, but is small and built from the package.
+cp hfsplus-source/fsck_hfs.tproj/fsck_hfs hfsplus-tools/fsck_hfs
+
+# Make a package of the built utils
+cd $root_dir
+tar caf $root_dir/hfsplus-tools.tar.xz hfsplus-tools
--- a/config/external/ffi/moz.build
+++ b/config/external/ffi/moz.build
@@ -80,16 +80,19 @@ else:
             ASFLAGS += ['-no-integrated-as']
     elif CONFIG['FFI_TARGET'] == 'AARCH64':
         ffi_srcs = ('sysv.S', 'ffi.c')
     elif CONFIG['FFI_TARGET'] == 'X86':
         ffi_srcs = ('ffi.c', 'sysv.S', 'win32.S')
     elif CONFIG['FFI_TARGET'] == 'X86_64':
         ffi_srcs = ('ffi64.c', 'unix64.S', 'ffi.c', 'sysv.S')
     elif CONFIG['FFI_TARGET'] == 'X86_WIN32':
+        # MinGW Build for 32 bit
+        if CONFIG['CC_TYPE'] == 'gcc':
+            DEFINES['SYMBOL_UNDERSCORE'] = True
         ffi_srcs = ('ffi.c', 'win32.S')
     elif CONFIG['FFI_TARGET'] == 'X86_WIN64':
         ffi_srcs = ('ffi.c', 'win64.S')
         ASFLAGS += ['-m64']
     elif CONFIG['FFI_TARGET'] == 'X86_DARWIN':
         DEFINES['FFI_MMAP_EXEC_WRIT'] = True
         if CONFIG['OS_TEST'] != 'x86_64':
             ffi_srcs = ('ffi.c', 'darwin.S', 'ffi64.c', 'darwin64.S',
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -953,16 +953,17 @@ rustflags_override = RUSTFLAGS='$(rustfl
 endif
 
 CARGO_BUILD = env $(rustflags_override) \
 	CARGO_TARGET_DIR=. \
 	RUSTC=$(RUSTC) \
 	MOZ_DIST=$(ABS_DIST) \
 	LIBCLANG_PATH=$(MOZ_LIBCLANG_PATH) \
 	CLANG_PATH=$(MOZ_CLANG_PATH) \
+	PKG_CONFIG_ALLOW_CROSS=1 \
 	$(CARGO) build $(cargo_build_flags)
 
 ifdef RUST_LIBRARY_FILE
 
 ifdef RUST_LIBRARY_FEATURES
 rust_features_flag := --features "$(RUST_LIBRARY_FEATURES)"
 endif
 
--- a/devtools/client/debugger/new/debugger.js
+++ b/devtools/client/debugger/new/debugger.js
@@ -5615,17 +5615,17 @@ var Debugger =
 	  "transition-timing-function": {
 	    inherited: false,
 	    supports: 256,
 	    values: ["cubic-bezier", "ease", "ease-in", "ease-in-out", "ease-out", "inherit", "initial", "linear", "step-end", "step-start", "steps", "unset", ],
 	  },
 	  "unicode-bidi": {
 	    inherited: false,
 	    supports: 0,
-	    values: ["-moz-isolate", "-moz-isolate-override", "-moz-plaintext", "bidi-override", "embed", "inherit", "initial", "normal", "unset", ],
+	    values: ["isolate", "isolate-override", "plaintext", "bidi-override", "embed", "inherit", "initial", "normal", "unset", ],
 	  },
 	  "-moz-user-focus": {
 	    inherited: true,
 	    supports: 0,
 	    values: ["ignore", "inherit", "initial", "none", "normal", "select-after", "select-all", "select-before", "select-menu", "select-same", "unset", ],
 	  },
 	  "-moz-user-input": {
 	    inherited: true,
--- a/devtools/client/framework/connect/connect.js
+++ b/devtools/client/framework/connect/connect.js
@@ -74,35 +74,43 @@ var submit = Task.async(function* () {
 });
 
 /**
  * Connection is ready. List actors and build buttons.
  */
 var onConnectionReady = Task.async(function* ([aType, aTraits]) {
   clearTimeout(gConnectionTimeout);
 
-  let response = yield gClient.listAddons();
+  let addons = [];
+  try {
+    let response = yield gClient.listAddons();
+    if (!response.error && response.addons.length > 0) {
+      addons = response.addons;
+    }
+  } catch(e) {
+    // listAddons throws if the runtime doesn't support addons
+  }
 
   let parent = document.getElementById("addonActors");
-  if (!response.error && response.addons.length > 0) {
+  if (addons.length > 0) {
     // Add one entry for each add-on.
-    for (let addon of response.addons) {
+    for (let addon of addons) {
       if (!addon.debuggable) {
         continue;
       }
       buildAddonLink(addon, parent);
     }
   }
   else {
     // Hide the section when there are no add-ons
     parent.previousElementSibling.remove();
     parent.remove();
   }
 
-  response = yield gClient.listTabs();
+  let response = yield gClient.listTabs();
 
   parent = document.getElementById("tabActors");
 
   // Add Global Process debugging...
   let globals = Cu.cloneInto(response, {});
   delete globals.tabs;
   delete globals.selected;
   // ...only if there are appropriate actors (a 'from' property will always
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -29,18 +29,16 @@ var { DOMHelpers } = require("resource:/
 const { KeyCodes } = require("devtools/client/shared/keycodes");
 
 const { BrowserLoader } =
   Cu.import("resource://devtools/client/shared/browser-loader.js", {});
 
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
 
-loader.lazyRequireGetter(this, "CommandUtils",
-  "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "getHighlighterUtils",
   "devtools/client/framework/toolbox-highlighter-utils", true);
 loader.lazyRequireGetter(this, "Selection",
   "devtools/client/framework/selection", true);
 loader.lazyRequireGetter(this, "InspectorFront",
   "devtools/shared/fronts/inspector", true);
 loader.lazyRequireGetter(this, "flags",
   "devtools/shared/flags");
@@ -1200,25 +1198,16 @@ Toolbox.prototype = {
     if (this.target.activeTab) {
       this.target.activeTab.reconfigure({
         "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
       });
     }
   },
 
  /**
-  * Get the toolbar spec for toolbox
-  */
-  getToolbarSpec: function () {
-    let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
-
-    return spec;
-  },
-
- /**
   * Return all toolbox buttons (command buttons, plus any others that were
   * added manually).
 
   /**
    * Update the visibility of the buttons.
    */
   updateToolboxButtonsVisibility() {
     this.toolbarButtons.forEach(button => {
--- a/devtools/client/inspector/layout/components/App.js
+++ b/devtools/client/inspector/layout/components/App.js
@@ -23,16 +23,17 @@ const BOXMODEL_L10N = new LocalizationHe
 const App = createClass({
 
   displayName: "App",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
     grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
     highlighterSettings: PropTypes.shape(Types.highlighterSettings).isRequired,
+    showBoxModelProperties: PropTypes.bool.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
     onToggleGridHighlighter: PropTypes.func.isRequired,
     onToggleShowGridLineNumbers: PropTypes.func.isRequired,
     onToggleShowInfiniteLines: PropTypes.func.isRequired,
   },
 
--- a/devtools/client/inspector/layout/components/BoxModel.js
+++ b/devtools/client/inspector/layout/components/BoxModel.js
@@ -4,35 +4,38 @@
 
 "use strict";
 
 const { addons, createClass, createFactory, DOM: dom, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const BoxModelInfo = createFactory(require("./BoxModelInfo"));
 const BoxModelMain = createFactory(require("./BoxModelMain"));
+const BoxModelProperties = createFactory(require("./BoxModelProperties"));
 
 const Types = require("../types");
 
 module.exports = createClass({
 
   displayName: "BoxModel",
 
   propTypes: {
     boxModel: PropTypes.shape(Types.boxModel).isRequired,
+    showBoxModelProperties: PropTypes.bool.isRequired,
     onHideBoxModelHighlighter: PropTypes.func.isRequired,
     onShowBoxModelEditor: PropTypes.func.isRequired,
     onShowBoxModelHighlighter: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
       boxModel,
+      showBoxModelProperties,
       onHideBoxModelHighlighter,
       onShowBoxModelEditor,
       onShowBoxModelHighlighter,
     } = this.props;
 
     return dom.div(
       {
         className: "boxmodel-container",
@@ -40,13 +43,19 @@ module.exports = createClass({
       BoxModelMain({
         boxModel,
         onHideBoxModelHighlighter,
         onShowBoxModelEditor,
         onShowBoxModelHighlighter,
       }),
       BoxModelInfo({
         boxModel,
-      })
+      }),
+      showBoxModelProperties ?
+        BoxModelProperties({
+          boxModel,
+        })
+        :
+        null,
     );
   },
 
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/components/BoxModelProperties.js
@@ -0,0 +1,89 @@
+/* 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 { addons, createClass, createFactory, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const ComputedProperty = createFactory(require("./ComputedProperty"));
+
+const Types = require("../types");
+
+const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
+const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
+
+module.exports = createClass({
+
+  displayName: "BoxModelProperties",
+
+  propTypes: {
+    boxModel: PropTypes.shape(Types.boxModel).isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  getInitialState() {
+    return {
+      isOpen: true,
+    };
+  },
+
+  onToggleExpander() {
+    this.setState({
+      isOpen: !this.state.isOpen,
+    });
+  },
+
+  render() {
+    let { boxModel } = this.props;
+    let { layout } = boxModel;
+
+    let layoutInfo = ["box-sizing", "display", "float",
+                      "line-height", "position", "z-index"];
+
+    const properties = layoutInfo.map(info => ComputedProperty({
+      name: info,
+      key: info,
+      value: layout[info],
+    }));
+
+    return dom.div(
+      {
+        className: "boxmodel-properties",
+      },
+      dom.div(
+        {
+          className: "boxmodel-properties-header",
+          onDoubleClick: this.onToggleExpander,
+        },
+        dom.div(
+          {
+            className: "boxmodel-properties-expander theme-twisty",
+            open: this.state.isOpen,
+            onClick: this.onToggleExpander,
+          },
+        ),
+        dom.span(
+          {
+            className: "boxmodel-properties-label",
+            title: BOXMODEL_L10N.getStr("boxmodel.propertiesLabel"),
+          },
+          BOXMODEL_L10N.getStr("boxmodel.propertiesLabel"),
+        ),
+      ),
+      dom.div(
+        {
+          className: "boxmodel-properties-wrapper",
+          hidden: !this.state.isOpen,
+          tabIndex: 0,
+        },
+        properties,
+      ),
+    );
+  },
+
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/components/ComputedProperty.js
@@ -0,0 +1,67 @@
+/* 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 { addons, createClass, DOM: dom, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+
+  displayName: "ComputedProperty",
+
+  propTypes: {
+    name: PropTypes.string.isRequired,
+    value: PropTypes.string,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  onFocus() {
+    this.container.focus();
+  },
+
+  render() {
+    const { name, value } = this.props;
+
+    return dom.div(
+      {
+        className: "property-view",
+        tabIndex: "0",
+        ref: container => {
+          this.container = container;
+        },
+      },
+      dom.div(
+        {
+          className: "property-name-container",
+        },
+        dom.div(
+          {
+            className: "property-name theme-fg-color5",
+            tabIndex: "",
+            title: name,
+            onClick: this.onFocus,
+          },
+          name
+        )
+      ),
+      dom.div(
+        {
+          className: "property-value-container",
+        },
+        dom.div(
+          {
+            className: "property-value theme-fg-color1",
+            dir: "ltr",
+            tabIndex: "",
+            onClick: this.onFocus,
+          },
+          value
+        )
+      )
+    );
+  },
+
+});
--- a/devtools/client/inspector/layout/components/moz.build
+++ b/devtools/client/inspector/layout/components/moz.build
@@ -7,12 +7,14 @@
 DevToolsModules(
     'Accordion.css',
     'Accordion.js',
     'App.js',
     'BoxModel.js',
     'BoxModelEditable.js',
     'BoxModelInfo.js',
     'BoxModelMain.js',
+    'BoxModelProperties.js',
+    'ComputedProperty.js',
     'Grid.js',
     'GridDisplaySettings.js',
     'GridList.js',
 )
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -73,16 +73,22 @@ LayoutView.prototype = {
 
     this.layoutInspector = yield this.inspector.walker.getLayoutInspector();
     let store = this.store = Store();
 
     this.loadHighlighterSettings();
 
     let app = App({
       /**
+       * Shows the box model properties under the box model if true, otherwise, hidden by
+       * default.
+       */
+      showBoxModelProperties: true,
+
+      /**
        * Hides the box-model highlighter on the currently selected element.
        */
       onHideBoxModelHighlighter: () => {
         let toolbox = this.inspector.toolbox;
         toolbox.highlighterUtils.unhighlight();
       },
 
       /**
--- a/devtools/client/locales/en-US/boxmodel.properties
+++ b/devtools/client/locales/en-US/boxmodel.properties
@@ -30,8 +30,12 @@ boxmodel.padding=padding
 # LOCALIZATION NOTE (boxmodel.content) This refers to the content in the box model and
 # might be displayed as a label or as a tooltip.
 boxmodel.content=content
 
 # LOCALIZATION NOTE: (boxmodel.geometryButton.tooltip) This label is displayed as a
 # tooltip that appears when hovering over the button that allows users to edit the
 # position of an element in the page.
 boxmodel.geometryButton.tooltip=Edit position
+
+# LOCALIZATION NOTE: (boxmodel.propertiesLabel) This label is displayed as the header
+# for showing and collapsing the properties underneath the box model in the layout view
+boxmodel.propertiesLabel=Box Model Properties
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -77,8 +77,60 @@ responsive.noThrottling=No throttling
 # DevicePixelRatio (DPR) dropdown when is enabled.
 responsive.devicePixelRatio=Device Pixel Ratio
 
 # LOCALIZATION NOTE (responsive.autoDPR): tooltip for the DevicePixelRatio
 # (DPR) dropdown when is disabled because a device is selected.
 # The argument (%1$S) is the selected device (e.g. iPhone 6) that set
 # automatically the DPR value.
 responsive.autoDPR=DPR automatically set by %1$S
+
+# LOCALIZATION NOTE (responsive.customDeviceName): Default value in a form to
+# add a custom device based on an arbitrary size (no association to an existing
+# device).
+responsive.customDeviceName=Custom Device
+
+# LOCALIZATION NOTE (responsive.customDeviceNameFromBase): Default value in a
+# form to add a custom device based on the properties of another.  %1$S is the
+# name of the device we're staring from, such as "Apple iPhone 6".
+responsive.customDeviceNameFromBase=%1$S (Custom)
+
+# LOCALIZATION NOTE (responsive.addDevice): Button text that reveals a form to
+# be used for adding custom devices.
+responsive.addDevice=Add Device
+
+# LOCALIZATION NOTE (responsive.deviceAdderName): Label of form field for the
+# name of a new device.  The available width is very low, so you might see
+# overlapping text if the length is much longer than 5 or so characters.
+responsive.deviceAdderName=Name
+
+# LOCALIZATION NOTE (responsive.deviceAdderSize): Label of form field for the
+# size of a new device.  The available width is very low, so you might see
+# overlapping text if the length is much longer than 5 or so characters.
+responsive.deviceAdderSize=Size
+
+# LOCALIZATION NOTE (responsive.deviceAdderPixelRatio): Label of form field for
+# the devicePixelRatio of a new device.  The available width is very low, so you
+# might see overlapping text if the length is much longer than 5 or so
+# characters.
+responsive.deviceAdderPixelRatio=DPR
+
+# LOCALIZATION NOTE (responsive.deviceAdderUserAgent): Label of form field for
+# the user agent of a new device.  The available width is very low, so you might
+# see overlapping text if the length is much longer than 5 or so characters.
+responsive.deviceAdderUserAgent=UA
+
+# LOCALIZATION NOTE (responsive.deviceAdderTouch): Label of form field for the
+# touch input support of a new device.  The available width is very low, so you
+# might see overlapping text if the length is much longer than 5 or so
+# characters.
+responsive.deviceAdderTouch=Touch
+
+# LOCALIZATION NOTE (responsive.deviceAdderSave): Button text that submits a
+# form to add a new device.
+responsive.deviceAdderSave=Save
+
+# LOCALIZATION NOTE (responsive.deviceDetails): Tooltip that appears when
+# hovering on a device in the device modal.  %1$S is the width of the device.
+# %2$S is the height of the device.  %3$S is the devicePixelRatio value of the
+# device.  %4$S is the user agent of the device.  %5$S is a boolean value
+# noting whether touch input is supported.
+responsive.deviceDetails=Size: %1$S x %2$S\nDPR: %3$S\nUA: %4$S\nTouch: %5$S
--- a/devtools/client/netmonitor/shared/components/params-panel.js
+++ b/devtools/client/netmonitor/shared/components/params-panel.js
@@ -58,24 +58,35 @@ function ParamsPanel({
   // Query String section
   if (query) {
     object[PARAMS_QUERY_STRING] =
       parseQueryString(query)
         .reduce((acc, { name, value }) =>
           name ? Object.assign(acc, { [name]: value }) : acc
         , {});
   }
+
   // Form Data section
   if (formDataSections && formDataSections.length > 0) {
     let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
     object[PARAMS_FORM_DATA] =
       parseQueryString(sections)
-        .reduce((acc, { name, value }) =>
-          name ? Object.assign(acc, { [name]: value }) : acc
-        , {});
+        .reduce((map, obj) => {
+          let value = map[obj.name];
+          // Deal with duplicate key case (ex: multiple selection)
+          if (value) {
+            if (typeof value !== "object") {
+              map[obj.name] = [value];
+            }
+            map[obj.name].push(obj.value);
+          } else {
+            map[obj.name] = obj.value;
+          }
+          return map;
+        }, {});
   }
 
   // Request payload section
   if (formDataSections && formDataSections.length === 0 && postData) {
     try {
       json = JSON.parse(postData);
     } catch (error) {
       // Continue regardless of parsing error
--- a/devtools/client/responsive.html/actions/devices.js
+++ b/devtools/client/responsive.html/actions/devices.js
@@ -5,21 +5,23 @@
 "use strict";
 
 const {
   ADD_DEVICE,
   ADD_DEVICE_TYPE,
   LOAD_DEVICE_LIST_START,
   LOAD_DEVICE_LIST_ERROR,
   LOAD_DEVICE_LIST_END,
+  REMOVE_DEVICE,
   UPDATE_DEVICE_DISPLAYED,
-  UPDATE_DEVICE_MODAL_OPEN,
+  UPDATE_DEVICE_MODAL,
 } = require("./index");
+const { removeDeviceAssociation } = require("./viewports");
 
-const { getDevices } = require("devtools/client/shared/devices");
+const { addDevice, getDevices, removeDevice } = require("devtools/client/shared/devices");
 
 const Services = require("Services");
 const DISPLAYED_DEVICES_PREF = "devtools.responsive.html.displayedDeviceList";
 
 /**
  * Returns an object containing the user preference of displayed devices.
  *
  * @return {Object} containing two Sets:
@@ -66,43 +68,75 @@ function updatePreferredDevices(devices)
 
 module.exports = {
 
   // This function is only exported for testing purposes
   _loadPreferredDevices: loadPreferredDevices,
 
   updatePreferredDevices: updatePreferredDevices,
 
+  addCustomDevice(device) {
+    return function* (dispatch) {
+      // Add custom device to device storage
+      yield addDevice(device, "custom");
+      dispatch({
+        type: ADD_DEVICE,
+        device,
+        deviceType: "custom",
+      });
+    };
+  },
+
   addDevice(device, deviceType) {
     return {
       type: ADD_DEVICE,
       device,
       deviceType,
     };
   },
 
   addDeviceType(deviceType) {
     return {
       type: ADD_DEVICE_TYPE,
       deviceType,
     };
   },
 
+  removeCustomDevice(device) {
+    return function* (dispatch, getState) {
+      // Check if the custom device is currently associated with any viewports
+      let { viewports } = getState();
+      for (let viewport of viewports) {
+        if (viewport.device == device.name) {
+          dispatch(removeDeviceAssociation(viewport.id));
+        }
+      }
+
+      // Remove custom device from device storage
+      yield removeDevice(device, "custom");
+      dispatch({
+        type: REMOVE_DEVICE,
+        device,
+        deviceType: "custom",
+      });
+    };
+  },
+
   updateDeviceDisplayed(device, deviceType, displayed) {
     return {
       type: UPDATE_DEVICE_DISPLAYED,
       device,
       deviceType,
       displayed,
     };
   },
 
   loadDevices() {
-    return function* (dispatch, getState) {
-      yield dispatch({ type: LOAD_DEVICE_LIST_START });
+    return function* (dispatch) {
+      dispatch({ type: LOAD_DEVICE_LIST_START });
       let preferredDevices = loadPreferredDevices();
       let devices;
 
       try {
         devices = yield getDevices();
       } catch (e) {
         console.error("Could not load device list: " + e);
         dispatch({ type: LOAD_DEVICE_LIST_ERROR });
@@ -119,20 +153,27 @@ module.exports = {
           let newDevice = Object.assign({}, device, {
             displayed: preferredDevices.added.has(device.name) ||
               (device.featured && !(preferredDevices.removed.has(device.name))),
           });
 
           dispatch(module.exports.addDevice(newDevice, type));
         }
       }
+
+      // Add an empty "custom" type if it doesn't exist in device storage
+      if (!devices.TYPES.find(type => type == "custom")) {
+        dispatch(module.exports.addDeviceType("custom"));
+      }
+
       dispatch({ type: LOAD_DEVICE_LIST_END });
     };
   },
 
-  updateDeviceModalOpen(isOpen) {
+  updateDeviceModal(isOpen, modalOpenedFromViewport = null) {
     return {
-      type: UPDATE_DEVICE_MODAL_OPEN,
+      type: UPDATE_DEVICE_MODAL,
       isOpen,
+      modalOpenedFromViewport,
     };
   },
 
 };
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -48,30 +48,33 @@ createEnum([
   "LOAD_DEVICE_LIST_START",
 
   // Indicates that the device list loading action threw an error
   "LOAD_DEVICE_LIST_ERROR",
 
   // Indicates that the device list has been loaded successfully
   "LOAD_DEVICE_LIST_END",
 
+  // Remove a device.
+  "REMOVE_DEVICE",
+
   // Remove the viewport's device assocation.
-  "REMOVE_DEVICE",
+  "REMOVE_DEVICE_ASSOCIATION",
 
   // Resize the viewport.
   "RESIZE_VIEWPORT",
 
   // Rotate the viewport.
   "ROTATE_VIEWPORT",
 
   // Take a screenshot of the viewport.
   "TAKE_SCREENSHOT_START",
 
   // Indicates when the screenshot action ends.
   "TAKE_SCREENSHOT_END",
 
   // Update the device display state in the device selector.
   "UPDATE_DEVICE_DISPLAYED",
 
-  // Update the device modal open state.
-  "UPDATE_DEVICE_MODAL_OPEN",
+  // Update the device modal state.
+  "UPDATE_DEVICE_MODAL",
 
 ], module.exports);
--- a/devtools/client/responsive.html/actions/viewports.js
+++ b/devtools/client/responsive.html/actions/viewports.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
   CHANGE_PIXEL_RATIO,
-  REMOVE_DEVICE,
+  REMOVE_DEVICE_ASSOCIATION,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT
 } = require("./index");
 
 module.exports = {
 
   /**
    * Add an additional viewport to display the document.
@@ -22,21 +22,22 @@ module.exports = {
     return {
       type: ADD_VIEWPORT,
     };
   },
 
   /**
    * Change the viewport device.
    */
-  changeDevice(id, device) {
+  changeDevice(id, device, deviceType) {
     return {
       type: CHANGE_DEVICE,
       id,
       device,
+      deviceType,
     };
   },
 
   /**
    * Change the viewport pixel ratio.
    */
   changePixelRatio(id, pixelRatio = 0) {
     return {
@@ -44,19 +45,19 @@ module.exports = {
       id,
       pixelRatio,
     };
   },
 
   /**
    * Remove the viewport's device assocation.
    */
-  removeDevice(id) {
+  removeDeviceAssociation(id) {
     return {
-      type: REMOVE_DEVICE,
+      type: REMOVE_DEVICE_ASSOCIATION,
       id,
     };
   },
 
   /**
    * Resize the viewport.
    */
   resizeViewport(id, width, height) {
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -6,27 +6,29 @@
 
 "use strict";
 
 const { createClass, createFactory, PropTypes, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
+  addCustomDevice,
+  removeCustomDevice,
   updateDeviceDisplayed,
-  updateDeviceModalOpen,
+  updateDeviceModal,
   updatePreferredDevices,
 } = require("./actions/devices");
 const { changeNetworkThrottling } = require("./actions/network-throttling");
 const { takeScreenshot } = require("./actions/screenshot");
 const { changeTouchSimulation } = require("./actions/touch-simulation");
 const {
   changeDevice,
   changePixelRatio,
-  removeDevice,
+  removeDeviceAssociation,
   resizeViewport,
   rotateViewport,
 } = require("./actions/viewports");
 const DeviceModal = createFactory(require("./components/device-modal"));
 const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 const Viewports = createFactory(require("./components/viewports"));
 const Types = require("./types");
 
@@ -39,26 +41,30 @@ let App = createClass({
     displayPixelRatio: Types.pixelRatio.value.isRequired,
     location: Types.location.isRequired,
     networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
   },
 
+  onAddCustomDevice(device) {
+    this.props.dispatch(addCustomDevice(device));
+  },
+
   onBrowserMounted() {
     window.postMessage({ type: "browser-mounted" }, "*");
   },
 
-  onChangeDevice(id, device) {
+  onChangeDevice(id, device, deviceType) {
     window.postMessage({
       type: "change-device",
       device,
     }, "*");
-    this.props.dispatch(changeDevice(id, device.name));
+    this.props.dispatch(changeDevice(id, device.name, deviceType));
     this.props.dispatch(changeTouchSimulation(device.touch));
     this.props.dispatch(changePixelRatio(id, device.pixelRatio));
   },
 
   onChangeNetworkThrottling(enabled, profile) {
     window.postMessage({
       type: "change-network-throtting",
       enabled,
@@ -94,22 +100,26 @@ let App = createClass({
   onDeviceListUpdate(devices) {
     updatePreferredDevices(devices);
   },
 
   onExit() {
     window.postMessage({ type: "exit" }, "*");
   },
 
-  onRemoveDevice(id) {
+  onRemoveCustomDevice(device) {
+    this.props.dispatch(removeCustomDevice(device));
+  },
+
+  onRemoveDeviceAssociation(id) {
     // TODO: Bug 1332754: Move messaging and logic into the action creator.
     window.postMessage({
-      type: "remove-device",
+      type: "remove-device-association",
     }, "*");
-    this.props.dispatch(removeDevice(id));
+    this.props.dispatch(removeDeviceAssociation(id));
     this.props.dispatch(changeTouchSimulation(false));
     this.props.dispatch(changePixelRatio(id, 0));
   },
 
   onResizeViewport(id, width, height) {
     this.props.dispatch(resizeViewport(id, width, height));
   },
 
@@ -120,56 +130,63 @@ let App = createClass({
   onScreenshot() {
     this.props.dispatch(takeScreenshot());
   },
 
   onUpdateDeviceDisplayed(device, deviceType, displayed) {
     this.props.dispatch(updateDeviceDisplayed(device, deviceType, displayed));
   },
 
-  onUpdateDeviceModalOpen(isOpen) {
-    this.props.dispatch(updateDeviceModalOpen(isOpen));
+  onUpdateDeviceModal(isOpen, modalOpenedFromViewport) {
+    this.props.dispatch(updateDeviceModal(isOpen, modalOpenedFromViewport));
   },
 
   render() {
     let {
       devices,
       displayPixelRatio,
       location,
       networkThrottling,
       screenshot,
       touchSimulation,
       viewports,
     } = this.props;
 
     let {
+      onAddCustomDevice,
       onBrowserMounted,
       onChangeDevice,
       onChangeNetworkThrottling,
       onChangePixelRatio,
       onChangeTouchSimulation,
       onContentResize,
       onDeviceListUpdate,
       onExit,
-      onRemoveDevice,
+      onRemoveCustomDevice,
+      onRemoveDeviceAssociation,
       onResizeViewport,
       onRotateViewport,
       onScreenshot,
       onUpdateDeviceDisplayed,
-      onUpdateDeviceModalOpen,
+      onUpdateDeviceModal,
     } = this;
 
     let selectedDevice = "";
     let selectedPixelRatio = { value: 0 };
 
     if (viewports.length) {
       selectedDevice = viewports[0].device;
       selectedPixelRatio = viewports[0].pixelRatio;
     }
 
+    let deviceAdderViewportTemplate = {};
+    if (devices.modalOpenedFromViewport !== null) {
+      deviceAdderViewportTemplate = viewports[devices.modalOpenedFromViewport];
+    }
+
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
         devices,
         displayPixelRatio,
         networkThrottling,
@@ -186,25 +203,28 @@ let App = createClass({
       Viewports({
         devices,
         location,
         screenshot,
         viewports,
         onBrowserMounted,
         onChangeDevice,
         onContentResize,
-        onRemoveDevice,
+        onRemoveDeviceAssociation,
         onRotateViewport,
         onResizeViewport,
-        onUpdateDeviceModalOpen,
+        onUpdateDeviceModal,
       }),
       DeviceModal({
+        deviceAdderViewportTemplate,
         devices,
+        onAddCustomDevice,
         onDeviceListUpdate,
+        onRemoveCustomDevice,
         onUpdateDeviceDisplayed,
-        onUpdateDeviceModalOpen,
+        onUpdateDeviceModal,
       })
     );
   },
 
 });
 
 module.exports = connect(state => state)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/device-adder.js
@@ -0,0 +1,252 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { DOM: dom, createClass, createFactory, PropTypes, addons } =
+  require("devtools/client/shared/vendor/react");
+
+const { getFormatStr, getStr } = require("../utils/l10n");
+const Types = require("../types");
+const ViewportDimension = createFactory(require("./viewport-dimension"));
+
+module.exports = createClass({
+  displayName: "DeviceAdder",
+
+  propTypes: {
+    devices: PropTypes.shape(Types.devices).isRequired,
+    viewportTemplate: PropTypes.shape(Types.viewport).isRequired,
+    onAddCustomDevice: PropTypes.func.isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  getInitialState() {
+    return {};
+  },
+
+  componentWillReceiveProps(nextProps) {
+    let {
+      width,
+      height,
+    } = nextProps.viewportTemplate;
+
+    this.setState({
+      width,
+      height,
+    });
+  },
+
+  onChangeSize(width, height) {
+    this.setState({
+      width,
+      height,
+    });
+  },
+
+  onDeviceAdderShow() {
+    this.setState({
+      deviceAdderDisplayed: true,
+    });
+  },
+
+  onDeviceAdderSave() {
+    let {
+      devices,
+      onAddCustomDevice,
+    } = this.props;
+
+    if (!this.pixelRatioInput.checkValidity()) {
+      return;
+    }
+    if (devices.custom.find(device => device.name == this.nameInput.value)) {
+      this.nameInput.setCustomValidity("Device name already in use");
+      return;
+    }
+
+    this.setState({
+      deviceAdderDisplayed: false,
+    });
+    onAddCustomDevice({
+      name: this.nameInput.value,
+      width: this.state.width,
+      height: this.state.height,
+      pixelRatio: parseFloat(this.pixelRatioInput.value),
+      userAgent: this.userAgentInput.value,
+      touch: this.touchInput.checked,
+    });
+  },
+
+  render() {
+    let {
+      devices,
+      viewportTemplate,
+    } = this.props;
+
+    let {
+      deviceAdderDisplayed,
+      height,
+      width,
+    } = this.state;
+
+    if (!deviceAdderDisplayed) {
+      return dom.div(
+        {
+          id: "device-adder"
+        },
+        dom.button(
+          {
+            id: "device-adder-show",
+            onClick: this.onDeviceAdderShow,
+          },
+          getStr("responsive.addDevice")
+        )
+      );
+    }
+
+    // If a device is currently selected, fold its attributes into a single object for use
+    // as the starting values of the form.  If no device is selected, use the values for
+    // the current window.
+    let deviceName;
+    let normalizedViewport = Object.assign({}, viewportTemplate);
+    if (viewportTemplate.device) {
+      let device = devices[viewportTemplate.deviceType].find(d => {
+        return d.name == viewportTemplate.device;
+      });
+      deviceName = getFormatStr("responsive.customDeviceNameFromBase", device.name);
+      Object.assign(normalizedViewport, {
+        pixelRatio: device.pixelRatio,
+        userAgent: device.userAgent,
+        touch: device.touch,
+      });
+    } else {
+      deviceName = getStr("responsive.customDeviceName");
+      Object.assign(normalizedViewport, {
+        pixelRatio: window.devicePixelRatio,
+        userAgent: navigator.userAgent,
+        touch: false,
+      });
+    }
+
+    return dom.div(
+      {
+        id: "device-adder"
+      },
+      dom.div(
+        {
+          id: "device-adder-content",
+        },
+        dom.div(
+          {
+            id: "device-adder-column-1",
+          },
+          dom.label(
+            {
+              id: "device-adder-name",
+            },
+            dom.span(
+              {
+                className: "device-adder-label",
+              },
+              getStr("responsive.deviceAdderName")
+            ),
+            dom.input({
+              defaultValue: deviceName,
+              ref: input => {
+                this.nameInput = input;
+              },
+            })
+          ),
+          dom.label(
+            {
+              id: "device-adder-size",
+            },
+            dom.span(
+              {
+                className: "device-adder-label"
+              },
+              getStr("responsive.deviceAdderSize")
+            ),
+            ViewportDimension({
+              viewport: {
+                width,
+                height,
+              },
+              onChangeSize: this.onChangeSize,
+              onRemoveDeviceAssociation: () => {},
+            })
+          ),
+          dom.label(
+            {
+              id: "device-adder-pixel-ratio",
+            },
+            dom.span(
+              {
+                className: "device-adder-label"
+              },
+              getStr("responsive.deviceAdderPixelRatio")
+            ),
+            dom.input({
+              type: "number",
+              step: "any",
+              defaultValue: normalizedViewport.pixelRatio,
+              ref: input => {
+                this.pixelRatioInput = input;
+              },
+            })
+          )
+        ),
+        dom.div(
+          {
+            id: "device-adder-column-2",
+          },
+          dom.label(
+            {
+              id: "device-adder-user-agent",
+            },
+            dom.span(
+              {
+                className: "device-adder-label"
+              },
+              getStr("responsive.deviceAdderUserAgent")
+            ),
+            dom.input({
+              defaultValue: normalizedViewport.userAgent,
+              ref: input => {
+                this.userAgentInput = input;
+              },
+            })
+          ),
+          dom.label(
+            {
+              id: "device-adder-touch",
+            },
+            dom.span(
+              {
+                className: "device-adder-label"
+              },
+              getStr("responsive.deviceAdderTouch")
+            ),
+            dom.input({
+              defaultChecked: normalizedViewport.touch,
+              type: "checkbox",
+              ref: input => {
+                this.touchInput = input;
+              },
+            })
+          )
+        ),
+      ),
+      dom.button(
+        {
+          id: "device-adder-save",
+          onClick: this.onDeviceAdderSave,
+        },
+        getStr("responsive.deviceAdderSave")
+      )
+    );
+  },
+});
--- a/devtools/client/responsive.html/components/device-modal.js
+++ b/devtools/client/responsive.html/components/device-modal.js
@@ -1,29 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 
 "use strict";
 
-const { DOM: dom, createClass, PropTypes, addons } =
+const { DOM: dom, createClass, createFactory, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
-const { getStr } = require("../utils/l10n");
+
+const { getStr, getFormatStr } = require("../utils/l10n");
 const Types = require("../types");
+const DeviceAdder = createFactory(require("./device-adder"));
 
 module.exports = createClass({
   displayName: "DeviceModal",
 
   propTypes: {
+    deviceAdderViewportTemplate: PropTypes.shape(Types.viewport).isRequired,
     devices: PropTypes.shape(Types.devices).isRequired,
+    onAddCustomDevice: PropTypes.func.isRequired,
     onDeviceListUpdate: PropTypes.func.isRequired,
+    onRemoveCustomDevice: PropTypes.func.isRequired,
     onUpdateDeviceDisplayed: PropTypes.func.isRequired,
-    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+    onUpdateDeviceModal: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   getInitialState() {
     return {};
   },
 
@@ -58,17 +63,17 @@ module.exports = createClass({
     });
   },
 
   onDeviceModalSubmit() {
     let {
       devices,
       onDeviceListUpdate,
       onUpdateDeviceDisplayed,
-      onUpdateDeviceModalOpen,
+      onUpdateDeviceModal,
     } = this.props;
 
     let preferredDevices = {
       "added": new Set(),
       "removed": new Set(),
     };
 
     for (let type of devices.types) {
@@ -83,36 +88,39 @@ module.exports = createClass({
 
         if (this.state[device.name] != device.displayed) {
           onUpdateDeviceDisplayed(device, type, this.state[device.name]);
         }
       }
     }
 
     onDeviceListUpdate(preferredDevices);
-    onUpdateDeviceModalOpen(false);
+    onUpdateDeviceModal(false);
   },
 
   onKeyDown(event) {
     if (!this.props.devices.isModalOpen) {
       return;
     }
     // Escape keycode
     if (event.keyCode === 27) {
       let {
-        onUpdateDeviceModalOpen
+        onUpdateDeviceModal
       } = this.props;
-      onUpdateDeviceModalOpen(false);
+      onUpdateDeviceModal(false);
     }
   },
 
   render() {
     let {
+      deviceAdderViewportTemplate,
       devices,
-      onUpdateDeviceModalOpen,
+      onAddCustomDevice,
+      onRemoveCustomDevice,
+      onUpdateDeviceModal,
     } = this.props;
 
     const sortedDevices = {};
     for (let type of devices.types) {
       sortedDevices[type] = Object.assign([], devices[type])
         .sort((a, b) => a.name.localeCompare(b.name));
     }
 
@@ -123,17 +131,17 @@ module.exports = createClass({
       },
       dom.div(
         {
           className: "device-modal container",
         },
         dom.button({
           id: "device-close-button",
           className: "toolbar-button devtools-button",
-          onClick: () => onUpdateDeviceModalOpen(false),
+          onClick: () => onUpdateDeviceModal(false),
         }),
         dom.div(
           {
             className: "device-modal-content",
           },
           devices.types.map(type => {
             return dom.div(
               {
@@ -142,43 +150,68 @@ module.exports = createClass({
               },
               dom.header(
                 {
                   className: "device-header",
                 },
                 type
               ),
               sortedDevices[type].map(device => {
+                let details = getFormatStr(
+                  "responsive.deviceDetails", device.width, device.height,
+                  device.pixelRatio, device.userAgent, device.touch
+                );
+
+                let removeDeviceButton;
+                if (type == "custom") {
+                  removeDeviceButton = dom.button({
+                    className: "device-remove-button toolbar-button devtools-button",
+                    onClick: () => onRemoveCustomDevice(device),
+                  });
+                }
+
                 return dom.label(
                   {
                     className: "device-label",
                     key: device.name,
+                    title: details,
                   },
                   dom.input({
                     className: "device-input-checkbox",
                     type: "checkbox",
                     value: device.name,
                     checked: this.state[device.name],
                     onChange: this.onDeviceCheckboxChange,
                   }),
-                  device.name
+                  dom.span(
+                    {
+                      className: "device-name",
+                    },
+                    device.name
+                  ),
+                  removeDeviceButton
                 );
               })
             );
           })
         ),
+        DeviceAdder({
+          devices,
+          viewportTemplate: deviceAdderViewportTemplate,
+          onAddCustomDevice,
+        }),
         dom.button(
           {
             id: "device-submit-button",
             onClick: this.onDeviceModalSubmit,
           },
           getStr("responsive.done")
         )
       ),
       dom.div(
         {
           className: "modal-overlay",
-          onClick: () => onUpdateDeviceModalOpen(false),
+          onClick: () => onUpdateDeviceModal(false),
         }
       )
     );
   },
 });
--- a/devtools/client/responsive.html/components/device-selector.js
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -12,40 +12,42 @@ const Types = require("../types");
 const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
 
 module.exports = createClass({
   displayName: "DeviceSelector",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     selectedDevice: PropTypes.string.isRequired,
+    viewportId: PropTypes.number.isRequired,
     onChangeDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
-    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+    onUpdateDeviceModal: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   onSelectChange({ target }) {
     let {
       devices,
+      viewportId,
       onChangeDevice,
       onResizeViewport,
-      onUpdateDeviceModalOpen,
+      onUpdateDeviceModal,
     } = this.props;
 
     if (target.value === OPEN_DEVICE_MODAL_VALUE) {
-      onUpdateDeviceModalOpen(true);
+      onUpdateDeviceModal(true, viewportId);
       return;
     }
     for (let type of devices.types) {
       for (let device of devices[type]) {
         if (device.name === target.value) {
           onResizeViewport(device.width, device.height);
-          onChangeDevice(device);
+          onChangeDevice(device, type);
           return;
         }
       }
     }
   },
 
   render() {
     let {
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,16 +1,17 @@
 # -*- 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/.
 
 DevToolsModules(
     'browser.js',
+    'device-adder.js',
     'device-modal.js',
     'device-selector.js',
     'dpr-selector.js',
     'global-toolbar.js',
     'network-throttling-selector.js',
     'resizable-viewport.js',
     'viewport-dimension.js',
     'viewport-toolbar.js',
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -25,20 +25,20 @@ module.exports = createClass({
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     swapAfterMount: PropTypes.bool.isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
-    onRemoveDevice: PropTypes.func.isRequired,
+    onRemoveDeviceAssociation: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
-    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+    onUpdateDeviceModal: PropTypes.func.isRequired,
   },
 
   getInitialState() {
     return {
       isResizing: false,
       lastClientX: 0,
       lastClientY: 0,
       ignoreX: false,
@@ -109,17 +109,17 @@ module.exports = createClass({
     this.props.onResizeViewport(width, height);
     // Change the device selector back to an unselected device
     // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
     if (this.props.viewport.device) {
       // In bug 1329843 and others, we may eventually stop this approach of removing the
       // the properties of the device on resize.  However, at the moment, there is no
       // way to edit dPR when a device is selected, and there is no UI at all for editing
       // UA, so it's important to keep doing this for now.
-      this.props.onRemoveDevice();
+      this.props.onRemoveDeviceAssociation();
     }
 
     this.setState({
       lastClientX,
       lastClientY
     });
   },
 
@@ -130,17 +130,17 @@ module.exports = createClass({
       screenshot,
       swapAfterMount,
       viewport,
       onBrowserMounted,
       onChangeDevice,
       onContentResize,
       onResizeViewport,
       onRotateViewport,
-      onUpdateDeviceModalOpen,
+      onUpdateDeviceModal,
     } = this.props;
 
     let resizeHandleClass = "viewport-resize-handle";
     if (screenshot.isCapturing) {
       resizeHandleClass += " hidden";
     }
 
     let contentClass = "viewport-content";
@@ -149,21 +149,21 @@ module.exports = createClass({
     }
 
     return dom.div(
       {
         className: "resizable-viewport",
       },
       ViewportToolbar({
         devices,
-        selectedDevice: viewport.device,
+        viewport,
         onChangeDevice,
         onResizeViewport,
         onRotateViewport,
-        onUpdateDeviceModalOpen,
+        onUpdateDeviceModal,
       }),
       dom.div(
         {
           className: contentClass,
           style: {
             width: viewport.width + "px",
             height: viewport.height + "px",
           },
--- a/devtools/client/responsive.html/components/viewport-dimension.js
+++ b/devtools/client/responsive.html/components/viewport-dimension.js
@@ -10,18 +10,18 @@ const { DOM: dom, createClass, PropTypes
 const Constants = require("../constants");
 const Types = require("../types");
 
 module.exports = createClass({
   displayName: "ViewportDimension",
 
   propTypes: {
     viewport: PropTypes.shape(Types.viewport).isRequired,
-    onRemoveDevice: PropTypes.func.isRequired,
-    onResizeViewport: PropTypes.func.isRequired,
+    onChangeSize: PropTypes.func.isRequired,
+    onRemoveDeviceAssociation: PropTypes.func.isRequired,
   },
 
   getInitialState() {
     let { width, height } = this.props.viewport;
 
     return {
       width,
       height,
@@ -111,20 +111,20 @@ module.exports = createClass({
       });
 
       return;
     }
 
     // Change the device selector back to an unselected device
     // TODO: Bug 1332754: Logic like this probably belongs in the action creator.
     if (this.props.viewport.device) {
-      this.props.onRemoveDevice();
+      this.props.onRemoveDeviceAssociation();
     }
-    this.props.onResizeViewport(parseInt(this.state.width, 10),
-                                parseInt(this.state.height, 10));
+    this.props.onChangeSize(parseInt(this.state.width, 10),
+                            parseInt(this.state.height, 10));
   },
 
   render() {
     let editableClass = "viewport-dimension-editable";
     let inputClass = "viewport-dimension-input";
 
     if (this.state.isEditing) {
       editableClass += " editing";
--- a/devtools/client/responsive.html/components/viewport-toolbar.js
+++ b/devtools/client/responsive.html/components/viewport-toolbar.js
@@ -11,45 +11,46 @@ const { getStr } = require("../utils/l10
 const Types = require("../types");
 const DeviceSelector = createFactory(require("./device-selector"));
 
 module.exports = createClass({
   displayName: "ViewportToolbar",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
-    selectedDevice: PropTypes.string.isRequired,
+    viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
-    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+    onUpdateDeviceModal: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
       devices,
-      selectedDevice,
+      viewport,
       onChangeDevice,
       onResizeViewport,
       onRotateViewport,
-      onUpdateDeviceModalOpen,
+      onUpdateDeviceModal,
     } = this.props;
 
     return dom.div(
       {
         className: "viewport-toolbar container",
       },
       DeviceSelector({
         devices,
-        selectedDevice,
+        selectedDevice: viewport.device,
+        viewportId: viewport.id,
         onChangeDevice,
         onResizeViewport,
-        onUpdateDeviceModalOpen,
+        onUpdateDeviceModal,
       }),
       dom.button({
         className: "viewport-rotate-button toolbar-button devtools-button",
         onClick: onRotateViewport,
         title: getStr("responsive.rotate"),
       })
     );
   },
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -19,38 +19,38 @@ module.exports = createClass({
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     swapAfterMount: PropTypes.bool.isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
-    onRemoveDevice: PropTypes.func.isRequired,
+    onRemoveDeviceAssociation: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
-    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+    onUpdateDeviceModal: PropTypes.func.isRequired,
   },
 
-  onChangeDevice(device) {
+  onChangeDevice(device, deviceType) {
     let {
       viewport,
       onChangeDevice,
     } = this.props;
 
-    onChangeDevice(viewport.id, device);
+    onChangeDevice(viewport.id, device, deviceType);
   },
 
-  onRemoveDevice() {
+  onRemoveDeviceAssociation() {
     let {
       viewport,
-      onRemoveDevice,
+      onRemoveDeviceAssociation,
     } = this.props;
 
-    onRemoveDevice(viewport.id);
+    onRemoveDeviceAssociation(viewport.id);
   },
 
   onResizeViewport(width, height) {
     let {
       viewport,
       onResizeViewport,
     } = this.props;
 
@@ -70,45 +70,45 @@ module.exports = createClass({
     let {
       devices,
       location,
       screenshot,
       swapAfterMount,
       viewport,
       onBrowserMounted,
       onContentResize,
-      onUpdateDeviceModalOpen,
+      onUpdateDeviceModal,
     } = this.props;
 
     let {
       onChangeDevice,
-      onRemoveDevice,
+      onRemoveDeviceAssociation,
       onRotateViewport,
       onResizeViewport,
     } = this;
 
     return dom.div(
       {
         className: "viewport",
       },
       ViewportDimension({
         viewport,
-        onRemoveDevice,
-        onResizeViewport,
+        onChangeSize: onResizeViewport,
+        onRemoveDeviceAssociation,
       }),
       ResizableViewport({
         devices,
         location,
         screenshot,
         swapAfterMount,
         viewport,
         onBrowserMounted,
         onChangeDevice,
         onContentResize,
-        onRemoveDevice,
+        onRemoveDeviceAssociation,
         onResizeViewport,
         onRotateViewport,
-        onUpdateDeviceModalOpen,
+        onUpdateDeviceModal,
       })
     );
   },
 
 });
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -17,35 +17,35 @@ module.exports = createClass({
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
-    onRemoveDevice: PropTypes.func.isRequired,
+    onRemoveDeviceAssociation: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
-    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+    onUpdateDeviceModal: PropTypes.func.isRequired,
   },
 
   render() {
     let {
       devices,
       location,
       screenshot,
       viewports,
       onBrowserMounted,
       onChangeDevice,
       onContentResize,
-      onRemoveDevice,
+      onRemoveDeviceAssociation,
       onResizeViewport,
       onRotateViewport,
-      onUpdateDeviceModalOpen,
+      onUpdateDeviceModal,
     } = this.props;
 
     return dom.div(
       {
         id: "viewports",
       },
       viewports.map((viewport, i) => {
         return Viewport({
@@ -53,18 +53,18 @@ module.exports = createClass({
           devices,
           location,
           screenshot,
           swapAfterMount: i == 0,
           viewport,
           onBrowserMounted,
           onChangeDevice,
           onContentResize,
-          onRemoveDevice,
+          onRemoveDeviceAssociation,
           onResizeViewport,
           onRotateViewport,
-          onUpdateDeviceModalOpen,
+          onUpdateDeviceModal,
         });
       })
     );
   },
 
 });
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -33,17 +33,18 @@
     url("./images/select-arrow.svg#dark-selected");
 }
 
 * {
   box-sizing: border-box;
 }
 
 #root,
-html, body {
+html,
+body {
   height: 100%;
   margin: 0;
 }
 
 #app {
   /* Center the viewports container */
   display: flex;
   align-items: center;
@@ -361,16 +362,17 @@ select > option.divider {
 .viewport-dimension-input {
   color: var(--theme-body-color-inactive);
   transition: all 0.25s ease;
 }
 
 .viewport-dimension-editable.editing,
 .viewport-dimension-input.editing {
   color: var(--viewport-active-color);
+  outline: none;
 }
 
 .viewport-dimension-editable.editing {
   border-bottom: 1px solid var(--theme-selection-background);
 }
 
 .viewport-dimension-editable.editing.invalid {
   border-bottom: 1px solid #d92215;
@@ -419,17 +421,17 @@ select > option.divider {
   display: none;
   position: absolute;
   margin: auto;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
   width: 642px;
-  height: 612px;
+  height: 650px;
   z-index: 1;
 }
 
 /* Handles the opening/closing of the modal */
 #device-modal-wrapper.opened .device-modal {
   animation: fade-in-and-up 0.3s ease;
   animation-fill-mode: forwards;
   display: block;
@@ -452,19 +454,19 @@ select > option.divider {
   opacity: 0.5;
 }
 
 .device-modal-content {
   display: flex;
   flex-direction: column;
   flex-wrap: wrap;
   overflow: auto;
-  height: 550px;
+  height: 515px;
   width: 600px;
-  margin: 20px;
+  margin: 20px 20px 0;
 }
 
 #device-close-button,
 #device-close-button::before {
   position: absolute;
   top: 5px;
   right: 2px;
   width: 12px;
@@ -489,33 +491,123 @@ select > option.divider {
   padding: 0 0 3px 23px;
 }
 
 .device-label {
   font-size: 11px;
   padding-bottom: 3px;
   display: flex;
   align-items: center;
+  /* Largest size without horizontal scrollbars */
+  max-width: 181px;
 }
 
 .device-input-checkbox {
   margin-right: 5px;
 }
 
+.device-name {
+  flex: 1;
+}
+
+.device-remove-button,
+.device-remove-button::before {
+  width: 12px;
+  height: 12px;
+}
+
+.device-remove-button::before {
+  background-image: url("./images/close.svg");
+  margin: -6px 0 0 -6px;
+}
+
 #device-submit-button {
   background-color: var(--theme-tab-toolbar-background);
   border-width: 1px 0 0 0;
   border-top-width: 1px;
   border-top-style: solid;
   border-top-color: var(--theme-splitter-color);
   color: var(--theme-body-color);
   width: 100%;
   height: 20px;
+  position: absolute;
+  bottom: 0;
 }
 
 #device-submit-button:hover {
   background-color: var(--toolbar-tab-hover);
 }
 
 #device-submit-button:hover:active {
   background-color: var(--submit-button-active-background-color);
   color: var(--submit-button-active-color);
 }
+
+/**
+ * Device Adder
+ */
+
+#device-adder {
+  display: flex;
+  flex-direction: column;
+  margin: 0 20px;
+}
+
+#device-adder-content {
+  display: flex;
+}
+
+#device-adder-column-1 {
+  flex: 1;
+  margin-right: 10px;
+}
+
+#device-adder-column-2 {
+  flex: 2;
+}
+
+#device-adder button {
+  background-color: var(--theme-tab-toolbar-background);
+  border: 1px solid var(--theme-splitter-color);
+  border-radius: 2px;
+  color: var(--theme-body-color);
+  margin: 0 auto;
+}
+
+#device-adder label {
+  display: flex;
+  margin-bottom: 5px;
+  align-items: center;
+}
+
+#device-adder label > input,
+#device-adder label > .viewport-dimension {
+  flex: 1;
+  margin: 0;
+}
+
+#device-adder input {
+  background: transparent;
+  border: 1px solid transparent;
+  text-align: center;
+  color: var(--theme-body-color-inactive);
+  transition: all 0.25s ease;
+}
+
+#device-adder input:focus {
+  color: var(--viewport-active-color);
+}
+
+#device-adder label > input:focus,
+#device-adder label > .viewport-dimension:focus  {
+  border-bottom: 1px solid var(--theme-selection-background);
+  outline: none;
+}
+
+.device-adder-label {
+  display: inline-block;
+  margin-right: 5px;
+  min-width: 35px;
+}
+
+#device-adder #device-adder-save {
+  margin-top: 5px;
+}
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -462,18 +462,18 @@ ResponsiveUI.prototype = {
         this.onChangeTouchSimulation(event);
         break;
       case "content-resize":
         this.onContentResize(event);
         break;
       case "exit":
         this.onExit();
         break;
-      case "remove-device":
-        this.onRemoveDevice(event);
+      case "remove-device-association":
+        this.onRemoveDeviceAssociation(event);
         break;
     }
   },
 
   onChangeDevice: Task.async(function* (event) {
     let { userAgent, pixelRatio, touch } = event.data.device;
     yield this.updateUserAgent(userAgent);
     yield this.updateDPPX(pixelRatio);
@@ -507,17 +507,17 @@ ResponsiveUI.prototype = {
     });
   },
 
   onExit() {
     let { browserWindow, tab } = this;
     ResponsiveUIManager.closeIfNeeded(browserWindow, tab);
   },
 
-  onRemoveDevice: Task.async(function* (event) {
+  onRemoveDeviceAssociation: Task.async(function* (event) {
     yield this.updateUserAgent();
     yield this.updateDPPX();
     yield this.updateTouchSimulation();
     // Used by tests
     this.emit("device-removed");
   }),
 
   updateDPPX: Task.async(function* (dppx) {
--- a/devtools/client/responsive.html/reducers/devices.js
+++ b/devtools/client/responsive.html/reducers/devices.js
@@ -5,25 +5,27 @@
 "use strict";
 
 const {
   ADD_DEVICE,
   ADD_DEVICE_TYPE,
   LOAD_DEVICE_LIST_START,
   LOAD_DEVICE_LIST_ERROR,
   LOAD_DEVICE_LIST_END,
+  REMOVE_DEVICE,
   UPDATE_DEVICE_DISPLAYED,
-  UPDATE_DEVICE_MODAL_OPEN,
+  UPDATE_DEVICE_MODAL,
 } = require("../actions/index");
 
 const Types = require("../types");
 
 const INITIAL_DEVICES = {
   types: [],
   isModalOpen: false,
+  modalOpenedFromViewport: null,
   listState: Types.deviceListState.INITIALIZED,
 };
 
 let reducers = {
 
   [ADD_DEVICE](devices, { device, deviceType }) {
     return Object.assign({}, devices, {
       [deviceType]: [...devices[deviceType], device],
@@ -64,19 +66,33 @@ let reducers = {
   },
 
   [LOAD_DEVICE_LIST_END](devices, action) {
     return Object.assign({}, devices, {
       listState: Types.deviceListState.LOADED,
     });
   },
 
-  [UPDATE_DEVICE_MODAL_OPEN](devices, { isOpen }) {
+  [REMOVE_DEVICE](devices, { device, deviceType }) {
+    let index = devices[deviceType].indexOf(device);
+    if (index < 0) {
+      return devices;
+    }
+
+    let list = [...devices[deviceType]];
+    list.splice(index, 1);
+    return Object.assign({}, devices, {
+      [deviceType]: list
+    });
+  },
+
+  [UPDATE_DEVICE_MODAL](devices, { isOpen, modalOpenedFromViewport }) {
     return Object.assign({}, devices, {
       isModalOpen: isOpen,
+      modalOpenedFromViewport,
     });
   },
 
 };
 
 module.exports = function (devices = INITIAL_DEVICES, action) {
   let reducer = reducers[action.type];
   if (!reducer) {
--- a/devtools/client/responsive.html/reducers/viewports.js
+++ b/devtools/client/responsive.html/reducers/viewports.js
@@ -3,27 +3,28 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
   CHANGE_PIXEL_RATIO,
-  REMOVE_DEVICE,
+  REMOVE_DEVICE_ASSOCIATION,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT,
 } = require("../actions/index");
 
 let nextViewportId = 0;
 
 const INITIAL_VIEWPORTS = [];
 const INITIAL_VIEWPORT = {
   id: nextViewportId++,
   device: "",
+  deviceType: "",
   width: 320,
   height: 480,
   pixelRatio: {
     value: 0,
   },
 };
 
 let reducers = {
@@ -31,24 +32,25 @@ let reducers = {
   [ADD_VIEWPORT](viewports) {
     // For the moment, there can be at most one viewport.
     if (viewports.length === 1) {
       return viewports;
     }
     return [...viewports, Object.assign({}, INITIAL_VIEWPORT)];
   },
 
-  [CHANGE_DEVICE](viewports, { id, device }) {
+  [CHANGE_DEVICE](viewports, { id, device, deviceType }) {
     return viewports.map(viewport => {
       if (viewport.id !== id) {
         return viewport;
       }
 
       return Object.assign({}, viewport, {
         device,
+        deviceType,
       });
     });
   },
 
   [CHANGE_PIXEL_RATIO](viewports, { id, pixelRatio }) {
     return viewports.map(viewport => {
       if (viewport.id !== id) {
         return viewport;
@@ -57,24 +59,25 @@ let reducers = {
       return Object.assign({}, viewport, {
         pixelRatio: {
           value: pixelRatio
         },
       });
     });
   },
 
-  [REMOVE_DEVICE](viewports, { id }) {
+  [REMOVE_DEVICE_ASSOCIATION](viewports, { id }) {
     return viewports.map(viewport => {
       if (viewport.id !== id) {
         return viewport;
       }
 
       return Object.assign({}, viewport, {
         device: "",
+        deviceType: "",
       });
     });
   },
 
   [RESIZE_VIEWPORT](viewports, { id, width, height }) {
     return viewports.map(viewport => {
       if (viewport.id !== id) {
         return viewport;
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -12,16 +12,17 @@ support-files =
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/framework/test/shared-redux-head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_device_change.js]
+[browser_device_custom.js]
 [browser_device_modal_error.js]
 [browser_device_modal_exit.js]
 [browser_device_modal_submit.js]
 [browser_device_width.js]
 [browser_dpr_change.js]
 [browser_exit_button.js]
 [browser_frame_script_active.js]
 [browser_menu_item_01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_custom.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test adding and removing custom devices via the modal.
+
+const device = {
+  name: "Test Device",
+  width: 400,
+  height: 570,
+  pixelRatio: 1.5,
+  userAgent: "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+  touch: true,
+  firefoxOS: false,
+  os: "android",
+};
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+const Types = require("devtools/client/responsive.html/types");
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+  let { toolWindow } = ui;
+  let { store, document } = toolWindow;
+  let React = toolWindow.require("devtools/client/shared/vendor/react");
+  let { Simulate } = React.addons.TestUtils;
+
+  // Wait until the viewport has been added and the device list has been loaded
+  yield waitUntilState(store, state => state.viewports.length == 1
+    && state.devices.listState == Types.deviceListState.LOADED);
+
+  let deviceSelector = document.querySelector(".viewport-device-selector");
+  let submitButton = document.querySelector("#device-submit-button");
+
+  openDeviceModal(ui);
+
+  info("Reveal device adder form, check that defaults match the viewport");
+  let adderShow = document.querySelector("#device-adder-show");
+  Simulate.click(adderShow);
+  testDeviceAdder(ui, {
+    name: "Custom Device",
+    width: 320,
+    height: 480,
+    pixelRatio: window.devicePixelRatio,
+    userAgent: navigator.userAgent,
+    touch: false,
+  });
+
+  info("Fill out device adder form and save");
+  setDeviceAdder(ui, device);
+  let adderSave = document.querySelector("#device-adder-save");
+  let saved = waitUntilState(store, state => state.devices.custom.length == 1);
+  Simulate.click(adderSave);
+  yield saved;
+
+  info("Enable device in modal");
+  let deviceCb = [...document.querySelectorAll(".device-input-checkbox")].find(cb => {
+    return cb.value == device.name;
+  });
+  ok(deviceCb, "Custom device checkbox added to modal");
+  deviceCb.click();
+  Simulate.click(submitButton);
+
+  info("Look for custom device in device selector");
+  let selectorOption = [...deviceSelector.options].find(opt => opt.value == device.name);
+  ok(selectorOption, "Custom device option added to device selector");
+});
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+  let { toolWindow } = ui;
+  let { store, document } = toolWindow;
+  let React = toolWindow.require("devtools/client/shared/vendor/react");
+  let { Simulate } = React.addons.TestUtils;
+
+  // Wait until the viewport has been added and the device list has been loaded
+  yield waitUntilState(store, state => state.viewports.length == 1
+    && state.devices.listState == Types.deviceListState.LOADED);
+
+  let deviceSelector = document.querySelector(".viewport-device-selector");
+  let submitButton = document.querySelector("#device-submit-button");
+
+  info("Select existing device from the selector");
+  yield selectDevice(ui, "Test Device");
+
+  openDeviceModal(ui);
+
+  info("Reveal device adder form, check that defaults are based on selected device");
+  let adderShow = document.querySelector("#device-adder-show");
+  Simulate.click(adderShow);
+  testDeviceAdder(ui, Object.assign({}, device, {
+    name: "Test Device (Custom)",
+  }));
+
+  info("Remove previously added custom device");
+  let deviceRemoveButton = document.querySelector(".device-remove-button");
+  let removed = waitUntilState(store, state => state.devices.custom.length == 0);
+  Simulate.click(deviceRemoveButton);
+  yield removed;
+  Simulate.click(submitButton);
+
+  info("Ensure custom device was removed from device selector");
+  yield waitUntilState(store, state => state.viewports[0].device == "");
+  is(deviceSelector.value, "", "Device selector reset to no device");
+  let selectorOption = [...deviceSelector.options].find(opt => opt.value == device.name);
+  ok(!selectorOption, "Custom device option removed from device selector");
+});
+
+function testDeviceAdder(ui, expected) {
+  let { document } = ui.toolWindow;
+
+  let nameInput = document.querySelector("#device-adder-name input");
+  let [ widthInput, heightInput ] = document.querySelectorAll("#device-adder-size input");
+  let pixelRatioInput = document.querySelector("#device-adder-pixel-ratio input");
+  let userAgentInput = document.querySelector("#device-adder-user-agent input");
+  let touchInput = document.querySelector("#device-adder-touch input");
+
+  is(nameInput.value, expected.name, "Device name matches");
+  is(parseInt(widthInput.value, 10), expected.width, "Width matches");
+  is(parseInt(heightInput.value, 10), expected.height, "Height matches");
+  is(parseFloat(pixelRatioInput.value), expected.pixelRatio,
+     "devicePixelRatio matches");
+  is(userAgentInput.value, expected.userAgent, "User agent matches");
+  is(touchInput.checked, expected.touch, "Touch matches");
+}
+
+function setDeviceAdder(ui, value) {
+  let { toolWindow } = ui;
+  let { document } = ui.toolWindow;
+  let React = toolWindow.require("devtools/client/shared/vendor/react");
+  let { Simulate } = React.addons.TestUtils;
+
+  let nameInput = document.querySelector("#device-adder-name input");
+  let [ widthInput, heightInput ] = document.querySelectorAll("#device-adder-size input");
+  let pixelRatioInput = document.querySelector("#device-adder-pixel-ratio input");
+  let userAgentInput = document.querySelector("#device-adder-user-agent input");
+  let touchInput = document.querySelector("#device-adder-touch input");
+
+  nameInput.value = value.name;
+  Simulate.change(nameInput);
+  widthInput.value = value.width;
+  Simulate.change(widthInput);
+  Simulate.blur(widthInput);
+  heightInput.value = value.height;
+  Simulate.change(heightInput);
+  Simulate.blur(heightInput);
+  pixelRatioInput.value = value.pixelRatio;
+  Simulate.change(pixelRatioInput);
+  userAgentInput.value = value.userAgent;
+  Simulate.change(userAgentInput);
+  touchInput.checked = value.touch;
+  Simulate.change(touchInput);
+}
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -55,16 +55,17 @@ Services.prefs.setCharPref("devtools.dev
 Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
 
 registerCleanupFunction(() => {
   flags.testing = false;
   Services.prefs.clearUserPref("devtools.devices.url");
   Services.prefs.clearUserPref("devtools.responsive.html.enabled");
   Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
   asyncStorage.removeItem("devtools.devices.url_cache");
+  asyncStorage.removeItem("devtools.devices.local");
 });
 
 // This depends on the "devtools.responsive.html.enabled" pref
 const { ResponsiveUIManager } = require("resource://devtools/client/responsivedesign/responsivedesign.jsm");
 
 /**
  * Open responsive design mode for the given tab.
  */
@@ -237,40 +238,30 @@ function openDeviceModal({ toolWindow })
   info("Opening device modal through device selector.");
   select.value = OPEN_DEVICE_MODAL_VALUE;
   Simulate.change(select);
   ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
     "The device modal is displayed.");
 }
 
 function changeSelectValue({ toolWindow }, selector, value) {
+  let { document } = toolWindow;
+  let React = toolWindow.require("devtools/client/shared/vendor/react");
+  let { Simulate } = React.addons.TestUtils;
+
   info(`Selecting ${value} in ${selector}.`);
 
-  return new Promise(resolve => {
-    let select = toolWindow.document.querySelector(selector);
-    isnot(select, null, `selector "${selector}" should match an existing element.`);
-
-    let option = [...select.options].find(o => o.value === String(value));
-    isnot(option, undefined, `value "${value}" should match an existing option.`);
+  let select = document.querySelector(selector);
+  isnot(select, null, `selector "${selector}" should match an existing element.`);
 
-    let event = new toolWindow.UIEvent("change", {
-      view: toolWindow,
-      bubbles: true,
-      cancelable: true
-    });
+  let option = [...select.options].find(o => o.value === String(value));
+  isnot(option, undefined, `value "${value}" should match an existing option.`);
 
-    select.addEventListener("change", () => {
-      is(select.value, value,
-        `Select's option with value "${value}" should be selected.`);
-      resolve();
-    }, { once: true });
-
-    select.value = value;
-    select.dispatchEvent(event);
-  });
+  select.value = value;
+  Simulate.change(select);
 }
 
 const selectDevice = (ui, value) => Promise.all([
   once(ui, "device-changed"),
   changeSelectValue(ui, ".viewport-device-selector", value)
 ]);
 
 const selectDPR = (ui, value) =>
--- a/devtools/client/responsive.html/test/unit/test_change_device.js
+++ b/devtools/client/responsive.html/test/unit/test_change_device.js
@@ -29,14 +29,14 @@ add_task(function* () {
     "firefoxOS": true,
     "os": "fxos"
   }, "phones"));
   dispatch(addViewport());
 
   let viewport = getState().viewports[0];
   equal(viewport.device, "", "Default device is unselected");
 
-  dispatch(changeDevice(0, "Firefox OS Flame"));
+  dispatch(changeDevice(0, "Firefox OS Flame", "phones"));
 
   viewport = getState().viewports[0];
   equal(viewport.device, "Firefox OS Flame",
     "Changed to Firefox OS Flame device");
 });
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -84,16 +84,19 @@ exports.devices = {
   consoles: PropTypes.arrayOf(PropTypes.shape(device)),
 
   // An array of watch devices
   watches: PropTypes.arrayOf(PropTypes.shape(device)),
 
   // Whether or not the device modal is open
   isModalOpen: PropTypes.bool,
 
+  // Viewport id that triggered the modal to open
+  modalOpenedFromViewport: PropTypes.number,
+
   // Device list state, possible values are exported above in an enum
   listState: PropTypes.oneOf(Object.keys(exports.deviceListState)),
 
 };
 
 /* VIEWPORT */
 
 /**
@@ -135,16 +138,19 @@ exports.touchSimulation = {
 exports.viewport = {
 
   // The id of the viewport
   id: PropTypes.number,
 
   // The currently selected device applied to the viewport
   device: PropTypes.string,
 
+  // The currently selected device type applied to the viewport
+  deviceType: PropTypes.string,
+
   // The width of the viewport
   width: PropTypes.number,
 
   // The height of the viewport
   height: PropTypes.number,
 
   // The devicePixelRatio of the viewport
   pixelRatio: PropTypes.shape(pixelRatio),
--- a/devtools/client/shared/components/reps/object.js
+++ b/devtools/client/shared/components/reps/object.js
@@ -21,30 +21,31 @@ define(function (require, exports, modul
   /**
    * Renders an object. An object is represented by a list of its
    * properties enclosed in curly brackets.
    */
   const Obj = React.createClass({
     displayName: "Obj",
 
     propTypes: {
-      object: React.PropTypes.object,
+      object: React.PropTypes.object.isRequired,
       // @TODO Change this to Object.values once it's supported in Node's version of V8
       mode: React.PropTypes.oneOf(Object.keys(MODE).map(key => MODE[key])),
       objectLink: React.PropTypes.func,
+      title: React.PropTypes.string,
     },
 
     getTitle: function (object) {
-      let className = object && object.class ? object.class : "Object";
+      let title = this.props.title || object.class || "Object";
       if (this.props.objectLink) {
         return this.props.objectLink({
           object: object
-        }, className);
+        }, title);
       }
-      return className;
+      return title;
     },
 
     safePropIterator: function (object, max) {
       max = (typeof max === "undefined") ? 3 : max;
       try {
         return this.propIterator(object, max);
       } catch (err) {
         console.error(err);
--- a/devtools/client/shared/components/tree/tree-view.css
+++ b/devtools/client/shared/components/tree/tree-view.css
@@ -53,17 +53,17 @@
   transition: all 0.2s ease-in-out;
 }
 
 .treeTable .treeLabel {
   cursor: default;
   overflow: hidden;
   padding-inline-start: 4px;
   white-space: nowrap;
-  unicode-bidi: -moz-plaintext;
+  unicode-bidi: plaintext;
 }
 
 /* No paddding if there is actually no label */
 .treeTable .treeLabel:empty {
   padding-inline-start: 0;
 }
 
 .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
--- a/devtools/client/shared/developer-toolbar.js
+++ b/devtools/client/shared/developer-toolbar.js
@@ -74,109 +74,16 @@ var CommandUtils = {
    * Destroy the remote side of the requisition as well as the local side
    */
   destroyRequisition: function (requisition, target) {
     requisition.destroy();
     gcliInit.releaseSystem(target);
   },
 
   /**
-   * Read a toolbarSpec from preferences
-   * @param pref The name of the preference to read
-   */
-  getCommandbarSpec: function (pref) {
-    let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
-    return JSON.parse(value);
-  },
-
-  /**
-   * Create a list of props for React components that manage the state of the buttons.
-   *
-   * @param {Array} toolbarSpec - An array of strings each of which is a GCLI command.
-   * @param {Object} target
-   * @param {Object} document - Used to listen to unload event of the window.
-   * @param {Requisition} requisition
-   * @param {Function} createButtonState - A function that provides a common interface
-   *                                       to create a button for the toolbox.
-   *
-   * @return {Array} List of ToolboxButton objects..
-   *
-   * Warning: this method uses the unload event of the window that owns the
-   * buttons that are of type checkbox. this means that we don't properly
-   * unregister event handlers until the window is destroyed.
-   */
-  createCommandButtons: function (toolbarSpec, target, document, requisition,
-                                  createButtonState) {
-    return util.promiseEach(toolbarSpec, typed => {
-      // Ask GCLI to parse the typed string (doesn't execute it)
-      return requisition.update(typed).then(() => {
-        // Ignore invalid commands
-        let command = requisition.commandAssignment.value;
-        if (command == null) {
-          throw new Error("No command '" + typed + "'");
-        }
-        if (!command.buttonId) {
-          throw new Error("Attempting to add a button to the toolbar, and the command " +
-                          "did not have an id.");
-        }
-        // Create the ToolboxButton.
-        let button = createButtonState({
-          id: command.buttonId,
-          className: command.buttonClass,
-          description: command.tooltipText || command.description,
-          onClick: requisition.updateExec.bind(requisition, typed)
-        });
-
-        // Allow the command button to be toggleable.
-        if (command.state) {
-          /**
-           * The onChange event should be called with an event object that
-           * contains a target property which specifies which target the event
-           * applies to. For legacy reasons the event object can also contain
-           * a tab property.
-           */
-          const onChange = (eventName, ev) => {
-            if (ev.target == target || ev.tab == target.tab) {
-              let updateChecked = (checked) => {
-                // This will emit a ToolboxButton update event.
-                button.isChecked = checked;
-              };
-
-              // isChecked would normally be synchronous. An annoying quirk
-              // of the 'csscoverage toggle' command forces us to accept a
-              // promise here, but doing Promise.resolve(reply).then(...) here
-              // makes this async for everyone, which breaks some tests so we
-              // treat non-promise replies separately to keep then synchronous.
-              let reply = command.state.isChecked(target);
-              if (typeof reply.then == "function") {
-                reply.then(updateChecked, console.error);
-              } else {
-                updateChecked(reply);
-              }
-            }
-          };
-
-          command.state.onChange(target, onChange);
-          onChange("", { target: target });
-
-          document.defaultView.addEventListener("unload", function (event) {
-            if (command.state.offChange) {
-              command.state.offChange(target, onChange);
-            }
-          }, { once: true });
-        }
-
-        requisition.clear();
-
-        return button;
-      });
-    });
-  },
-
-  /**
    * A helper function to create the environment object that is passed to
    * GCLI commands.
    * @param targetContainer An object containing a 'target' property which
    * reflects the current debug target
    */
   createEnvironment: function (container, targetProperty = "target") {
     if (!container[targetProperty].toString ||
         !/TabTarget/.test(container[targetProperty].toString())) {
--- a/devtools/client/shared/devices.js
+++ b/devtools/client/shared/devices.js
@@ -1,88 +1,113 @@
 /* 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 { Task } = require("devtools/shared/task");
 const { getJSON } = require("devtools/client/shared/getjson");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/device.properties");
+
+loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
 
 const DEVICES_URL = "devtools.devices.url";
-const { LocalizationHelper } = require("devtools/shared/l10n");
-const L10N = new LocalizationHelper("devtools/client/locales/device.properties");
+const LOCAL_DEVICES = "devtools.devices.local";
 
 /* This is a catalog of common web-enabled devices and their properties,
  * intended for (mobile) device emulation.
  *
  * The properties of a device are:
  * - name: brand and model(s).
  * - width: viewport width.
  * - height: viewport height.
  * - pixelRatio: ratio from viewport to physical screen pixels.
  * - userAgent: UA string of the device's browser.
  * - touch: whether it has a touch screen.
- * - firefoxOS: whether Firefox OS is supported.
+ * - os: default OS, such as "ios", "fxos", "android".
  *
  * The device types are:
  *   ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
  *
+ * To propose new devices for the shared catalog, check out the repo at
+ * https://github.com/mozilla/simulated-devices and file a pull request.
+ *
  * You can easily add more devices to this catalog from your own code (e.g. an
  * addon) like so:
  *
  *   var myPhone = { name: "My Phone", ... };
  *   require("devtools/client/shared/devices").addDevice(myPhone, "phones");
  */
 
 // Local devices catalog that addons can add to.
-let localDevices = {};
+let localDevices;
+let localDevicesLoaded = false;
+
+// Load local devices from storage.
+let loadLocalDevices = Task.async(function* () {
+  if (localDevicesLoaded) {
+    return;
+  }
+  let devicesJSON = yield asyncStorage.getItem(LOCAL_DEVICES);
+  if (!devicesJSON) {
+    devicesJSON = "{}";
+  }
+  localDevices = JSON.parse(devicesJSON);
+  localDevicesLoaded = true;
+});
 
 // Add a device to the local catalog.
-function addDevice(device, type = "phones") {
+let addDevice = Task.async(function* (device, type = "phones") {
+  yield loadLocalDevices();
   let list = localDevices[type];
   if (!list) {
     list = localDevices[type] = [];
   }
   list.push(device);
-}
+  yield asyncStorage.setItem(LOCAL_DEVICES, JSON.stringify(localDevices));
+});
 exports.addDevice = addDevice;
 
 // Remove a device from the local catalog.
 // returns `true` if the device is removed, `false` otherwise.
-function removeDevice(device, type = "phones") {
+let removeDevice = Task.async(function* (device, type = "phones") {
+  yield loadLocalDevices();
   let list = localDevices[type];
   if (!list) {
     return false;
   }
 
   let index = list.findIndex(item => device);
 
   if (index === -1) {
     return false;
   }
 
   list.splice(index, 1);
+  yield asyncStorage.setItem(LOCAL_DEVICES, JSON.stringify(localDevices));
 
   return true;
-}
+});
 exports.removeDevice = removeDevice;
 
 // Get the complete devices catalog.
-function getDevices() {
+let getDevices = Task.async(function* () {
   // Fetch common devices from Mozilla's CDN.
-  return getJSON(DEVICES_URL).then(devices => {
-    for (let type in localDevices) {
-      if (!devices[type]) {
-        devices.TYPES.push(type);
-        devices[type] = [];
-      }
-      devices[type] = localDevices[type].concat(devices[type]);
+  let devices = yield getJSON(DEVICES_URL);
+  yield loadLocalDevices();
+  for (let type in localDevices) {
+    if (!devices[type]) {
+      devices.TYPES.push(type);
+      devices[type] = [];
     }
-    return devices;
-  });
-}
+    devices[type] = localDevices[type].concat(devices[type]);
+  }
+  return devices;
+});
 exports.getDevices = getDevices;
 
 // Get the localized string for a device type.
 function getDeviceString(deviceType) {
   return L10N.getStr("device." + deviceType);
 }
 exports.getDeviceString = getDeviceString;
--- a/devtools/client/shared/widgets/ColorWidget.js
+++ b/devtools/client/shared/widgets/ColorWidget.js
@@ -5,16 +5,18 @@
 /**
  * This file is a new working copy of Spectrum.js for the purposes of refreshing the color
  * widget. It is hidden behind a pref("devtools.inspector.colorWidget.enabled").
  */
 
 "use strict";
 
 const EventEmitter = require("devtools/shared/event-emitter");
+const {colorUtils} = require("devtools/shared/css/color");
+
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 /**
  * ColorWidget creates a color picker widget in any container you give it.
  *
  * Simple usage example:
  *
  * const {ColorWidget} = require("devtools/client/shared/widgets/ColorWidget");
@@ -58,18 +60,45 @@ function ColorWidget(parentEl, rgb) {
         </div>
       </div>
     </div>
     <div class="colorwidget-alpha colorwidget-checker colorwidget-box">
       <div class="colorwidget-alpha-inner">
         <div class="colorwidget-alpha-handle colorwidget-slider-control"></div>
       </div>
     </div>
+    <div class="colorwidget-value">
+      <select class="colorwidget-select">
+        <option value="hex">Hex</option>
+        <option value="rgba">RGBA</option>
+        <option value="hsla">HSLA</option>
+      </select>
+      <div class="colorwidget-hex">
+        <input class="colorwidget-hex-input"/>
+      </div>
+      <div class="colorwidget-rgba colorwidget-hidden">
+        <input class="colorwidget-rgba-r" data-id="r" />
+        <input class="colorwidget-rgba-g" data-id="g" />
+        <input class="colorwidget-rgba-b" data-id="b" />
+        <input class="colorwidget-rgba-a" data-id="a" />
+      </div>
+      <div class="colorwidget-hsla colorwidget-hidden">
+        <input class="colorwidget-hsla-h" data-id="h" />
+        <input class="colorwidget-hsla-s" data-id="s" />
+        <input class="colorwidget-hsla-l" data-id="l" />
+        <input class="colorwidget-hsla-a" data-id="a" />
+      </div>
+    </div>
   `;
 
+  this.onSelectValueChange = this.onSelectValueChange.bind(this);
+  this.onHexInputChange = this.onHexInputChange.bind(this);
+  this.onRgbaInputChange = this.onRgbaInputChange.bind(this);
+  this.onHslaInputChange = this.onHslaInputChange.bind(this);
+
   this.onElementClick = this.onElementClick.bind(this);
   this.element.addEventListener("click", this.onElementClick);
 
   this.parentEl.appendChild(this.element);
 
   this.slider = this.element.querySelector(".colorwidget-hue");
   this.slideHelper = this.element.querySelector(".colorwidget-slider");
   ColorWidget.draggable(this.slider, this.onSliderMove.bind(this));
@@ -78,16 +107,41 @@ function ColorWidget(parentEl, rgb) {
   this.dragHelper = this.element.querySelector(".colorwidget-dragger");
   ColorWidget.draggable(this.dragger, this.onDraggerMove.bind(this));
 
   this.alphaSlider = this.element.querySelector(".colorwidget-alpha");
   this.alphaSliderInner = this.element.querySelector(".colorwidget-alpha-inner");
   this.alphaSliderHelper = this.element.querySelector(".colorwidget-alpha-handle");
   ColorWidget.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this));
 
+  this.colorSelect = this.element.querySelector(".colorwidget-select");
+  this.colorSelect.addEventListener("change", this.onSelectValueChange);
+
+  this.hexValue = this.element.querySelector(".colorwidget-hex");
+  this.hexValueInput = this.element.querySelector(".colorwidget-hex-input");
+  this.hexValueInput.addEventListener("input", this.onHexInputChange);
+
+  this.rgbaValue = this.element.querySelector(".colorwidget-rgba");
+  this.rgbaValueInputs = {
+    r: this.element.querySelector(".colorwidget-rgba-r"),
+    g: this.element.querySelector(".colorwidget-rgba-g"),
+    b: this.element.querySelector(".colorwidget-rgba-b"),
+    a: this.element.querySelector(".colorwidget-rgba-a"),
+  };
+  this.rgbaValue.addEventListener("input", this.onRgbaInputChange);
+
+  this.hslaValue = this.element.querySelector(".colorwidget-hsla");
+  this.hslaValueInputs = {
+    h: this.element.querySelector(".colorwidget-hsla-h"),
+    s: this.element.querySelector(".colorwidget-hsla-s"),
+    l: this.element.querySelector(".colorwidget-hsla-l"),
+    a: this.element.querySelector(".colorwidget-hsla-a"),
+  };
+  this.hslaValue.addEventListener("input", this.onHslaInputChange);
+
   if (rgb) {
     this.rgb = rgb;
     this.updateUI();
   }
 }
 
 module.exports.ColorWidget = ColorWidget;
 
@@ -132,16 +186,20 @@ ColorWidget.rgbToHsv = function (r, g, b
       case g: h = (b - r) / d + 2; break;
       case b: h = (r - g) / d + 4; break;
     }
     h /= 6;
   }
   return [h, s, v, a];
 };
 
+ColorWidget.hslToCssString = function (h, s, l, a) {
+  return `hsla(${h}, ${s}%, ${l}%, ${a})`;
+};
+
 ColorWidget.draggable = function (element, onmove, onstart, onstop) {
   onmove = onmove || function () {};
   onstart = onstart || function () {};
   onstop = onstop || function () {};
 
   let doc = element.ownerDocument;
   let dragging = false;
   let offset = {};
@@ -205,16 +263,19 @@ ColorWidget.draggable = function (elemen
   }
 
   element.addEventListener("mousedown", start);
 };
 
 ColorWidget.prototype = {
   set rgb(color) {
     this.hsv = ColorWidget.rgbToHsv(color[0], color[1], color[2], color[3]);
+
+    let { h, s, l } = new colorUtils.CssColor(this.rgbCssString)._getHSLATuple();
+    this.hsl = [h, s, l, color[3]];
   },
 
   get rgb() {
     let rgb = ColorWidget.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2],
       this.hsv[3]);
     return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]),
             Math.round(rgb[3] * 100) / 100];
   },
@@ -245,29 +306,148 @@ ColorWidget.prototype = {
   },
 
   onElementClick: function (e) {
     e.stopPropagation();
   },
 
   onSliderMove: function (dragX, dragY) {
     this.hsv[0] = (dragY / this.slideHeight);
+    this.hsl[0] = (dragY / this.slideHeight) * 360;
     this.updateUI();
     this.onChange();
   },
 
   onDraggerMove: function (dragX, dragY) {
     this.hsv[1] = dragX / this.dragWidth;
     this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
+
+    this.hsl[2] = ((2 - this.hsv[1]) * this.hsv[2] / 2);
+    if (this.hsl[2] && this.hsl[2] < 1) {
+      this.hsl[1] = this.hsv[1] * this.hsv[2] /
+          (this.hsl[2] < 0.5 ? this.hsl[2] * 2 : 2 - this.hsl[2] * 2);
+      this.hsl[1] = this.hsl[1] * 100;
+    }
+    this.hsl[2] = this.hsl[2] * 100;
+
     this.updateUI();
     this.onChange();
   },
 
   onAlphaSliderMove: function (dragX, dragY) {
     this.hsv[3] = dragX / this.alphaSliderWidth;
+    this.hsl[3] = dragX / this.alphaSliderWidth;
+    this.updateUI();
+    this.onChange();
+  },
+
+  onSelectValueChange: function (event) {
+    const selection = event.target.value;
+    this.colorSelect.classList.remove("colorwidget-select-spacing");
+    this.hexValue.classList.add("colorwidget-hidden");
+    this.rgbaValue.classList.add("colorwidget-hidden");
+    this.hslaValue.classList.add("colorwidget-hidden");
+
+    switch (selection) {
+      case "hex":
+        this.hexValue.classList.remove("colorwidget-hidden");
+        break;
+      case "rgba":
+        this.colorSelect.classList.add("colorwidget-select-spacing");
+        this.rgbaValue.classList.remove("colorwidget-hidden");
+        break;
+      case "hsla":
+        this.colorSelect.classList.add("colorwidget-select-spacing");
+        this.hslaValue.classList.remove("colorwidget-hidden");
+        break;
+    }
+  },
+
+  onHexInputChange: function (event) {
+    const hex = event.target.value;
+    const color = new colorUtils.CssColor(hex, true);
+    if (!color.rgba) {
+      return;
+    }
+
+    const { r, g, b, a } = color._getRGBATuple();
+    this.rgb = [r, g, b, a];
+    this.updateUI();
+    this.onChange();
+  },
+
+  onRgbaInputChange: function (event) {
+    const field = event.target.dataset.id;
+    const value = event.target.value.toString();
+    if (!value || isNaN(value) || value.endsWith(".")) {
+      return;
+    }
+
+    let rgb = this.rgb;
+
+    switch (field) {
+      case "r":
+        rgb[0] = value;
+        break;
+      case "g":
+        rgb[1] = value;
+        break;
+      case "b":
+        rgb[2] = value;
+        break;
+      case "a":
+        rgb[3] = Math.min(value, 1);
+        break;
+    }
+
+    this.rgb = rgb;
+
+    this.updateUI();
+    this.onChange();
+  },
+
+  onHslaInputChange: function (event) {
+    const field = event.target.dataset.id;
+    let value = event.target.value.toString();
+    if ((field === "s" || field === "l") && !value.endsWith("%")) {
+      return;
+    }
+
+    if (value.endsWith("%")) {
+      value = value.substring(0, value.length - 1);
+    }
+
+    if (!value || isNaN(value) || value.endsWith(".")) {
+      return;
+    }
+
+    const hsl = this.hsl;
+
+    switch (field) {
+      case "h":
+        hsl[0] = value;
+        break;
+      case "s":
+        hsl[1] = value;
+        break;
+      case "l":
+        hsl[2] = value;
+        break;
+      case "a":
+        hsl[3] = Math.min(value, 1);
+        break;
+    }
+
+    const cssString = ColorWidget.hslToCssString(hsl[0], hsl[1], hsl[2], hsl[3]);
+    const { r, g, b, a } = new colorUtils.CssColor(cssString)._getRGBATuple();
+
+    this.rgb = [r, g, b, a];
+
+    this.hsl = hsl;
+
     this.updateUI();
     this.onChange();
   },
 
   onChange: function () {
     this.emit("changed", this.rgb, this.rgbCssString);
   },
 
@@ -302,16 +482,34 @@ ColorWidget.prototype = {
     // Placing the hue slider
     let slideY = (h * this.slideHeight) - this.slideHelperHeight / 2;
     this.slideHelper.style.top = slideY + "px";
 
     // Placing the alpha slider
     let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) -
       (this.alphaSliderHelperWidth / 2);
     this.alphaSliderHelper.style.left = alphaSliderX + "px";
+
+    const color = new colorUtils.CssColor(this.rgbCssString);
+
+    // Updating the hex field
+    this.hexValueInput.value = color.hex;
+
+    // Updating the RGBA fields
+    const rgb = this.rgb;
+    this.rgbaValueInputs.r.value = rgb[0];
+    this.rgbaValueInputs.g.value = rgb[1];
+    this.rgbaValueInputs.b.value = rgb[2];
+    this.rgbaValueInputs.a.value = parseFloat(rgb[3].toFixed(1));
+
+    // Updating the HSLA fields
+    this.hslaValueInputs.h.value = this.hsl[0];
+    this.hslaValueInputs.s.value = this.hsl[1] + "%";
+    this.hslaValueInputs.l.value = this.hsl[2] + "%";
+    this.hslaValueInputs.a.value = parseFloat(this.hsl[3].toFixed(1));
   },
 
   updateUI: function () {
     this.updateHelperLocations();
 
     let rgb = this.rgb;
     let rgbNoSatVal = this.rgbNoSatVal;
 
--- a/devtools/client/shared/widgets/color-widget.css
+++ b/devtools/client/shared/widgets/color-widget.css
@@ -148,8 +148,64 @@ http://www.briangrinstead.com/blog/keep-
 
 .colorwidget-slider {
   position: absolute;
   top: 0;
   height: 5px;
   left: -3px;
   right: -3px;
 }
+
+/**
+ * Color Widget Editor
+ */
+
+.colorwidget-value {
+  position: relative;
+  margin-top: 8px;
+}
+
+/**
+ * Color Widget Select
+ */
+
+.colorwidget-select {
+  width: 100%;
+}
+
+.colorwidget-select-spacing {
+  letter-spacing: 40px;
+}
+
+.colorwidget-select-spacing option {
+  letter-spacing: initial;
+}
+
+/**
+ * Color Widget Inputs
+ */
+
+.colorwidget-hidden {
+  display: none;
+}
+
+.colorwidget-hex,
+.colorwidget-rgba,
+.colorwidget-hsla {
+  width: 200px;
+  font-size: 0;
+}
+
+.colorwidget-hex-input {
+  width: 192px;
+}
+
+.colorwidget-rgba-r,
+.colorwidget-rgba-g,
+.colorwidget-rgba-b,
+.colorwidget-rgba-a,
+.colorwidget-hsla-h,
+.colorwidget-hsla-s,
+.colorwidget-hsla-l,
+.colorwidget-hsla-a {
+  width: 42px;
+  margin: 0;
+}
\ No newline at end of file
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -69,23 +69,23 @@ SwatchColorPickerTooltip.prototype = Her
     eyedropper.id = "eyedropper-button";
     eyedropper.className = "devtools-button";
     /* pointerEvents for eyedropper has to be set auto to display tooltip when
      * eyedropper is disabled in non-HTML documents.
      */
     eyedropper.style.pointerEvents = "auto";
     container.appendChild(eyedropper);
 
-    this.tooltip.setContent(container, { width: 218, height: 224 });
-
     let spectrum;
     if (NEW_COLOR_WIDGET) {
+      this.tooltip.setContent(container, { width: 218, height: 271 });
       const {ColorWidget} = require("devtools/client/shared/widgets/ColorWidget");
       spectrum = new ColorWidget(spectrumNode, color);
     } else {
+      this.tooltip.setContent(container, { width: 218, height: 224 });
       spectrum = new Spectrum(spectrumNode, color);
     }
 
     // Wait for the tooltip to be shown before calling spectrum.show
     // as it expect to be visible in order to compute DOM element sizes.
     this.tooltip.once("shown", () => {
       spectrum.show();
     });
--- a/devtools/client/themes/boxmodel.css
+++ b/devtools/client/themes/boxmodel.css
@@ -244,8 +244,19 @@
 .boxmodel-element-size {
   flex: 1;
 }
 
 .boxmodel-position-group {
   display: flex;
   align-items: center;
 }
+
+/* Box Model Properties: contains a list of relevant box model properties */
+
+.boxmodel-properties-header {
+  display: flex;
+  padding: 2px 0;
+}
+
+.boxmodel-properties-wrapper {
+  padding: 0 9px;
+}
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -780,17 +780,19 @@ var PageStyleActor = protocol.ActorClass
       "padding-bottom",
       "padding-left",
       "border-top-width",
       "border-right-width",
       "border-bottom-width",
       "border-left-width",
       "z-index",
       "box-sizing",
-      "display"
+      "display",
+      "float",
+      "line-height"
     ]) {
       layout[prop] = style.getPropertyValue(prop);
     }
 
     if (options.autoMargins) {
       layout.autoMargins = this.processMargins(this.cssLogic);
     }
 
--- a/devtools/shared/css/color.js
+++ b/devtools/shared/css/color.js
@@ -403,16 +403,33 @@ CssColor.prototype = {
   _getRGBATuple: function () {
     let tuple = colorToRGBA(this.authored, this.cssColor4);
 
     tuple.a = parseFloat(tuple.a.toFixed(1));
 
     return tuple;
   },
 
+  /**
+   * Returns a HSLA 4-Tuple representation of a color or transparent as
+   * appropriate.
+   */
+  _getHSLATuple: function () {
+    let {r, g, b, a} = colorToRGBA(this.authored, this.cssColor4);
+
+    let [h, s, l] = rgbToHsl([r, g, b]);
+
+    return {
+      h,
+      s,
+      l,
+      a: parseFloat(a.toFixed(1))
+    };
+  },
+
   _hsl: function (maybeAlpha) {
     if (this.lowerCased.startsWith("hsl(") && maybeAlpha === undefined) {
       // We can use it as-is.
       return this.authored;
     }
 
     let {r, g, b} = this._getRGBATuple();
     let [h, s, l] = rgbToHsl([r, g, b]);
--- a/devtools/shared/css/generated/properties-db.js
+++ b/devtools/shared/css/generated/properties-db.js
@@ -9073,19 +9073,16 @@ exports.CSS_PROPERTIES = {
   },
   "unicode-bidi": {
     "isInherited": false,
     "subproperties": [
       "unicode-bidi"
     ],
     "supports": [],
     "values": [
-      "-moz-isolate",
-      "-moz-isolate-override",
-      "-moz-plaintext",
       "bidi-override",
       "embed",
       "inherit",
       "initial",
       "isolate",
       "isolate-override",
       "normal",
       "plaintext",
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -7942,18 +7942,20 @@ nsDocShell::EndPageLoad(nsIWebProgress* 
       if (!(mLoadType & LOAD_CMD_HISTORY)) {
         aStatus = NS_ERROR_OFFLINE;
       }
       DisplayLoadError(aStatus, url, nullptr, aChannel);
     }
   } else if (url && NS_SUCCEEDED(aStatus)) {
     // If we have a host
     nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
-    mozilla::net::PredictorLearnRedirect(url, aChannel,
-                                         loadInfo->GetOriginAttributes());
+    if (loadInfo) {
+      mozilla::net::PredictorLearnRedirect(url, aChannel,
+                                           loadInfo->GetOriginAttributes());
+    }
   }
 
   return NS_OK;
 }
 
 //*****************************************************************************
 // nsDocShell: Content Viewer Management
 //*****************************************************************************
@@ -12600,17 +12602,17 @@ nsDocShell::LoadHistoryEntry(nsISHEntry*
   } else {
     srcdoc = NullString();
   }
 
   // If there is no valid triggeringPrincipal, we deny the load
   MOZ_ASSERT(triggeringPrincipal,
              "need a valid triggeringPrincipal to load from history");
   if (!triggeringPrincipal) {
-    return NS_ERROR_FAILURE;
+    triggeringPrincipal = nsContentUtils::GetSystemPrincipal();
   }
 
   // Passing nullptr as aSourceDocShell gives the same behaviour as before
   // aSourceDocShell was introduced. According to spec we should be passing
   // the source browsing context that was used when the history entry was
   // first created. bug 947716 has been created to address this issue.
   rv = InternalLoad(uri,
                     originalURI,
--- a/docshell/test/unit/test_nsDefaultURIFixup_info.js
+++ b/docshell/test/unit/test_nsDefaultURIFixup_info.js
@@ -421,34 +421,28 @@ var testcases = [ {
     protocolChange: true,
   }, {
     input: "a?.com",
     fixedURI: "http://a/?.com",
     alternateURI: "http://www.a.com/?.com",
     protocolChange: true,
   }, {
     input: "?'.com",
-    fixedURI: "http:///?%27.com",
-    alternateURI: "http://www..com/?%27.com",
     keywordLookup: true,
     protocolChange: true,
   }, {
     input: "' ?.com",
     keywordLookup: true,
     protocolChange: true
   }, {
     input: "?mozilla",
-    fixedURI: "http:///?mozilla",
-    alternateURI: "http://www..com/?mozilla",
     keywordLookup: true,
     protocolChange: true,
   }, {
     input: "??mozilla",
-    fixedURI: "http:///??mozilla",
-    alternateURI: "http://www..com/??mozilla",
     keywordLookup: true,
     protocolChange: true,
   }, {
     input: "mozilla/",
     fixedURI: "http://mozilla/",
     alternateURI: "http://www.mozilla.com/",
     protocolChange: true,
   }, {
--- a/dom/animation/AnimationUtils.cpp
+++ b/dom/animation/AnimationUtils.cpp
@@ -8,16 +8,18 @@
 
 #include "nsDebug.h"
 #include "nsIAtom.h"
 #include "nsIContent.h"
 #include "nsIDocument.h"
 #include "nsGlobalWindow.h"
 #include "nsString.h"
 #include "xpcpublic.h" // For xpc::NativeGlobal
+#include "mozilla/EffectSet.h"
+#include "mozilla/dom/KeyframeEffectReadOnly.h"
 #include "mozilla/Preferences.h"
 
 namespace mozilla {
 
 /* static */ void
 AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage,
                                          const nsIContent* aContent)
 {
@@ -78,9 +80,22 @@ AnimationUtils::IsCoreAPIEnabled()
 }
 
 /* static */ bool
 AnimationUtils::IsCoreAPIEnabledForCaller(dom::CallerType aCallerType)
 {
   return IsCoreAPIEnabled() || aCallerType == dom::CallerType::System;
 }
 
+/* static */ bool
+AnimationUtils::EffectSetContainsAnimatedScale(EffectSet& aEffects,
+                                               const nsIFrame* aFrame)
+{
+  for (const dom::KeyframeEffectReadOnly* effect : aEffects) {
+    if (effect->ContainsAnimatedScale(aFrame)) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
 } // namespace mozilla
--- a/dom/animation/AnimationUtils.h
+++ b/dom/animation/AnimationUtils.h
@@ -9,21 +9,23 @@
 
 #include "mozilla/TimeStamp.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/Nullable.h"
 #include "nsStringFwd.h"
 
 class nsIContent;
 class nsIDocument;
+class nsIFrame;
 struct JSContext;
 
 namespace mozilla {
 
 class ComputedTimingFunction;
+class EffectSet;
 
 class AnimationUtils
 {
 public:
   static dom::Nullable<double>
   TimeDurationToDouble(const dom::Nullable<TimeDuration>& aTime)
   {
     dom::Nullable<double> result;
@@ -68,13 +70,20 @@ public:
    */
   static bool IsCoreAPIEnabled();
 
   /**
    * Returns true if the preference to enable the core Web Animations API is
    * true or the caller is chrome.
    */
   static bool IsCoreAPIEnabledForCaller(dom::CallerType aCallerType);
+
+  /**
+   * Returns true if the given EffectSet contains a current effect that animates
+   * scale. |aFrame| is used for calculation of scale values.
+   */
+  static bool EffectSetContainsAnimatedScale(EffectSet& aEffects,
+                                             const nsIFrame* aFrame);
 };
 
 } // namespace mozilla
 
 #endif
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -537,18 +537,18 @@ KeyframeEffectReadOnly::ComposeStyle(
 
     // Bug 1333311 - We use two branches for Gecko and Stylo. However, it's
     // better to remove the duplicated code.
     if (isServoBackend) {
       // Servo backend
 
       // Bug 1329878 - Stylo: Implement accumulate and addition on Servo
       // AnimationValue.
-      RawServoAnimationValue* servoFromValue = segment->mServoFromValue;
-      RawServoAnimationValue* servoToValue = segment->mServoToValue;
+      RawServoAnimationValue* servoFromValue = segment->mFromValue.mServo;
+      RawServoAnimationValue* servoToValue = segment->mToValue.mServo;
 
       // For unsupported or non-animatable animation types, we get nullptrs.
       if (!servoFromValue || !servoToValue) {
         NS_ERROR("Compose style for unsupported or non-animatable property, "
                  "so get invalid RawServoAnimationValues");
         continue;
       }
 
@@ -594,34 +594,34 @@ KeyframeEffectReadOnly::ComposeStyle(
 
       if (!aStyleRule.mGecko) {
         // Allocate the style rule now that we know we have animation data.
         aStyleRule.mGecko = new AnimValuesStyleRule();
       }
 
       StyleAnimationValue fromValue =
         CompositeValue(prop.mProperty, aStyleRule.mGecko,
-                       segment->mFromValue,
+                       segment->mFromValue.mGecko,
                        segment->mFromComposite);
       StyleAnimationValue toValue =
         CompositeValue(prop.mProperty, aStyleRule.mGecko,
-                       segment->mToValue,
+                       segment->mToValue.mGecko,
                        segment->mToComposite);
 
       // Iteration composition for accumulate
       if (mEffectOptions.mIterationComposite ==
           IterationCompositeOperation::Accumulate &&
           computedTiming.mCurrentIteration > 0) {
         const AnimationPropertySegment& lastSegment =
           prop.mSegments.LastElement();
         // FIXME: Bug 1293492: Add a utility function to calculate both of
         // below StyleAnimationValues.
-        StyleAnimationValue lastValue = lastSegment.mToValue.IsNull()
+        StyleAnimationValue lastValue = lastSegment.mToValue.mGecko.IsNull()
           ? GetUnderlyingStyle(prop.mProperty, aStyleRule.mGecko)
-          : lastSegment.mToValue;
+          : lastSegment.mToValue.mGecko;
         fromValue =
           StyleAnimationValue::Accumulate(prop.mProperty,
                                           lastValue,
                                           Move(fromValue),
                                           computedTiming.mCurrentIteration);
         toValue =
           StyleAnimationValue::Accumulate(prop.mProperty,
                                           lastValue,
@@ -1010,20 +1010,20 @@ KeyframeEffectReadOnly::GetTargetStyleCo
 void
 DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties)
 {
   for (auto& p : aAnimationProperties) {
     printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get());
     for (auto& s : p.mSegments) {
       nsString fromValue, toValue;
       Unused << StyleAnimationValue::UncomputeValue(p.mProperty,
-                                                    s.mFromValue,
+                                                    s.mFromValue.mGecko,
                                                     fromValue);
       Unused << StyleAnimationValue::UncomputeValue(p.mProperty,
-                                                    s.mToValue,
+                                                    s.mToValue.mGecko,
                                                     toValue);
       printf("  %f..%f: %s..%s\n", s.mFromKey, s.mToKey,
              NS_ConvertUTF16toUTF8(fromValue).get(),
              NS_ConvertUTF16toUTF8(toValue).get());
     }
   }
 }
 #endif
@@ -1130,37 +1130,38 @@ KeyframeEffectReadOnly::GetProperties(
     for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length();
          segmentIdx < segmentLen;
          segmentIdx++)
     {
       const AnimationPropertySegment& segment = property.mSegments[segmentIdx];
 
       binding_detail::FastAnimationPropertyValueDetails fromValue;
       CreatePropertyValue(property.mProperty, segment.mFromKey,
-                          segment.mTimingFunction, segment.mFromValue,
+                          segment.mTimingFunction, segment.mFromValue.mGecko,
                           segment.mFromComposite, fromValue);
       // We don't apply timing functions for zero-length segments, so
       // don't return one here.
       if (segment.mFromKey == segment.mToKey) {
         fromValue.mEasing.Reset();
       }
       // The following won't fail since we have already allocated the capacity
       // above.
       propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
 
       // Normally we can ignore the to-value for this segment since it is
       // identical to the from-value from the next segment. However, we need
       // to add it if either:
       // a) this is the last segment, or
       // b) the next segment's from-value differs.
       if (segmentIdx == segmentLen - 1 ||
-          property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) {
+          property.mSegments[segmentIdx + 1].mFromValue.mGecko !=
+            segment.mToValue.mGecko) {
         binding_detail::FastAnimationPropertyValueDetails toValue;
         CreatePropertyValue(property.mProperty, segment.mToKey,
-                            Nothing(), segment.mToValue,
+                            Nothing(), segment.mToValue.mGecko,
                             segment.mToComposite, toValue);
         // It doesn't really make sense to have a timing function on the
         // last property value or before a sudden jump so we just drop the
         // easing property altogether.
         toValue.mEasing.Reset();
         propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
       }
     }
@@ -1616,21 +1617,23 @@ KeyframeEffectReadOnly::CalculateCumulat
       // until we compose it.
       if (segment.mFromComposite != CompositeOperation::Replace ||
           segment.mToComposite != CompositeOperation::Replace) {
         mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
         return;
       }
       RefPtr<nsStyleContext> fromContext =
         CreateStyleContextForAnimationValue(property.mProperty,
-                                            segment.mFromValue, aStyleContext);
+                                            segment.mFromValue.mGecko,
+                                            aStyleContext);
 
       RefPtr<nsStyleContext> toContext =
         CreateStyleContextForAnimationValue(property.mProperty,
-                                            segment.mToValue, aStyleContext);
+                                            segment.mToValue.mGecko,
+                                            aStyleContext);
 
       uint32_t equalStructs = 0;
       uint32_t samePointerStructs = 0;
       nsChangeHint changeHint =
         fromContext->CalcStyleDifference(toContext,
                                          nsChangeHint(0),
                                          &equalStructs,
                                          &samePointerStructs);
@@ -1755,10 +1758,59 @@ KeyframeEffectReadOnly::NeedsBaseStyle(n
     }
   }
   MOZ_ASSERT_UNREACHABLE(
     "Expected a property that can be run on the compositor");
 
   return false;
 }
 
+bool
+KeyframeEffectReadOnly::ContainsAnimatedScale(const nsIFrame* aFrame) const
+{
+  if (!IsCurrent()) {
+    return false;
+  }
+
+  for (const AnimationProperty& prop : mProperties) {
+    if (prop.mProperty != eCSSProperty_transform) {
+      continue;
+    }
+
+    if (NeedsBaseStyle(prop.mProperty)) {
+      StyleAnimationValue baseStyle =
+        EffectCompositor::GetBaseStyle(prop.mProperty, aFrame);
+      MOZ_ASSERT(!baseStyle.IsNull(), "The base value should be set");
+      if (baseStyle.IsNull()) {
+        // If we failed to get the base style, we consider it has scale value
+        // here for the safety.
+        return true;
+      }
+      gfxSize size = baseStyle.GetScaleValue(aFrame);
+      if (size != gfxSize(1.0f, 1.0f)) {
+        return true;
+      }
+    }
+
+    // This is actually overestimate because there are some cases that combining
+    // the base value and from/to value produces 1:1 scale. But it doesn't
+    // really matter.
+    for (const AnimationPropertySegment& segment : prop.mSegments) {
+      if (!segment.mFromValue.IsNull()) {
+        gfxSize from = segment.mFromValue.GetScaleValue(aFrame);
+        if (from != gfxSize(1.0f, 1.0f)) {
+          return true;
+        }
+      }
+      if (!segment.mToValue.IsNull()) {
+        gfxSize to = segment.mToValue.GetScaleValue(aFrame);
+        if (to != gfxSize(1.0f, 1.0f)) {
+          return true;
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/animation/KeyframeEffectReadOnly.h
+++ b/dom/animation/KeyframeEffectReadOnly.h
@@ -55,19 +55,17 @@ enum class CompositeOperation : uint8_t;
 struct AnimationPropertyDetails;
 }
 
 struct AnimationPropertySegment
 {
   float mFromKey, mToKey;
   // NOTE: In the case that no keyframe for 0 or 1 offset is specified
   // the unit of mFromValue or mToValue is eUnit_Null.
-  StyleAnimationValue mFromValue, mToValue;
-  // FIXME add a deep == impl for RawServoAnimationValue
-  RefPtr<RawServoAnimationValue> mServoFromValue, mServoToValue;
+  AnimationValue mFromValue, mToValue;
 
   Maybe<ComputedTimingFunction> mTimingFunction;
   dom::CompositeOperation mFromComposite = dom::CompositeOperation::Replace;
   dom::CompositeOperation mToComposite = dom::CompositeOperation::Replace;
 
   bool operator==(const AnimationPropertySegment& aOther) const
   {
     return mFromKey == aOther.mFromKey &&
@@ -285,16 +283,20 @@ public:
   // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
   // in detail which change hint can be ignored.
   bool CanIgnoreIfNotVisible() const;
 
   // Returns true if the effect is run on the compositor for |aProperty| and
   // needs a base style to composite with.
   bool NeedsBaseStyle(nsCSSPropertyID aProperty) const;
 
+  // Returns true if the effect is current state and has scale animation.
+  // |aFrame| is used for calculation of scale values.
+  bool ContainsAnimatedScale(const nsIFrame* aFrame) const;
+
 protected:
   KeyframeEffectReadOnly(nsIDocument* aDocument,
                          const Maybe<OwningAnimationTarget>& aTarget,
                          AnimationEffectTimingReadOnly* aTiming,
                          const KeyframeEffectParams& aOptions);
 
   ~KeyframeEffectReadOnly() override = default;
 
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -266,18 +266,17 @@ struct AdditionalProperty
  * whose value is a StyleAnimationValue.
  *
  * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
  * to gather data for each individual segment.
  */
 struct KeyframeValueEntry
 {
   nsCSSPropertyID mProperty;
-  StyleAnimationValue mValue;
-  RefPtr<RawServoAnimationValue> mServoValue;
+  AnimationValue mValue;
 
   float mOffset;
   Maybe<ComputedTimingFunction> mTimingFunction;
   dom::CompositeOperation mComposite;
 
   struct PropertyOffsetComparator
   {
     static bool Equals(const KeyframeValueEntry& aLhs,
@@ -699,17 +698,16 @@ KeyframeUtils::GetAnimationPropertiesFro
     const Keyframe& frame = aKeyframes[i];
     for (auto& value : aComputedValues[i]) {
       MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
                  "Invalid computed offset");
       KeyframeValueEntry* entry = entries.AppendElement();
       entry->mOffset = frame.mComputedOffset;
       entry->mProperty = value.mProperty;
       entry->mValue = value.mValue;
-      entry->mServoValue = value.mServoValue;
       entry->mTimingFunction = frame.mTimingFunction;
       entry->mComposite =
         frame.mComposite ? frame.mComposite.value() : aEffectComposite;
     }
   }
 
   nsTArray<AnimationProperty> result;
   BuildSegmentsFromValueEntries(entries, result);
@@ -1399,18 +1397,16 @@ BuildSegmentsFromValueEntries(nsTArray<K
 
     // Now generate the segment.
     AnimationPropertySegment* segment =
       animationProperty->mSegments.AppendElement();
     segment->mFromKey        = aEntries[i].mOffset;
     segment->mToKey          = aEntries[j].mOffset;
     segment->mFromValue      = aEntries[i].mValue;
     segment->mToValue        = aEntries[j].mValue;
-    segment->mServoFromValue = aEntries[i].mServoValue;
-    segment->mServoToValue   = aEntries[j].mServoValue;
     segment->mTimingFunction = aEntries[i].mTimingFunction;
     segment->mFromComposite  = aEntries[i].mComposite;
     segment->mToComposite    = aEntries[j].mComposite;
 
     i = j;
   }
 }
 
@@ -1781,32 +1777,32 @@ GetCumulativeDistances(const nsTArray<Co
         for (size_t propIdx = 0; propIdx < pacedPropertyCount; ++propIdx) {
           nsCSSPropertyID prop = prevPacedValues[propIdx].mProperty;
           MOZ_ASSERT(pacedValues[propIdx].mProperty == prop,
                      "Property mismatch");
 
           double componentDistance = 0.0;
           if (StyleAnimationValue::ComputeDistance(
                 prop,
-                prevPacedValues[propIdx].mValue,
-                pacedValues[propIdx].mValue,
+                prevPacedValues[propIdx].mValue.mGecko,
+                pacedValues[propIdx].mValue.mGecko,
                 aStyleContext,
                 componentDistance)) {
             dist += componentDistance * componentDistance;
           }
         }
         dist = sqrt(dist);
       } else {
         // If the property is longhand, we just use the 1st value.
         // If ComputeDistance() fails, |dist| will remain zero so there will be
         // no distance between the previous paced value and this value.
         Unused <<
           StyleAnimationValue::ComputeDistance(aPacedProperty,
-                                               prevPacedValues[0].mValue,
-                                               pacedValues[0].mValue,
+                                               prevPacedValues[0].mValue.mGecko,
+                                               pacedValues[0].mValue.mGecko,
                                                aStyleContext,
                                                dist);
       }
       cumulativeDistances[i] = cumulativeDistances[preIdx] + dist;
     }
     prevPacedValues.SwapElements(pacedValues);
     preIdx = i;
   }
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -39,16 +39,17 @@
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/Nullable.h"
 #include "Units.h"
 #include "DOMIntersectionObserver.h"
 
 class nsIFrame;
 class nsIDOMMozNamedAttrMap;
+class nsIMozBrowserFrame;
 class nsIURI;
 class nsIScrollableFrame;
 class nsAttrValueOrString;
 class nsContentList;
 class nsDOMTokenList;
 struct nsRect;
 class nsFocusManager;
 class nsGlobalWindow;
@@ -335,16 +336,26 @@ public:
   virtual bool IsLabelable() const;
 
   /**
    * Returns if the element is interactive content as per HTML specification.
    */
   virtual bool IsInteractiveHTMLContent(bool aIgnoreTabindex) const;
 
   /**
+   * Returns |this| as an nsIMozBrowserFrame* if the element is a frame or
+   * iframe element.
+   *
+   * We have this method, rather than using QI, so that we can use it during
+   * the servo traversal, where we can't QI DOM nodes because of non-thread-safe
+   * refcounts.
+   */
+  virtual nsIMozBrowserFrame* GetAsMozBrowserFrame() { return nullptr; }
+
+  /**
    * Is the attribute named stored in the mapped attributes?
    *
    * // XXXbz we use this method in HasAttributeDependentStyle, so svg
    *    returns true here even though it stores nothing in the mapped
    *    attributes.
    */
   NS_IMETHOD_(bool) IsAttributeMapped(const nsIAtom* aAttribute) const;
 
--- a/dom/base/Location.cpp
+++ b/dom/base/Location.cpp
@@ -28,16 +28,17 @@
 #include "nsITextToSubURI.h"
 #include "nsJSUtils.h"
 #include "nsContentUtils.h"
 #include "nsGlobalWindow.h"
 #include "mozilla/Likely.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsNullPrincipal.h"
 #include "ScriptSettings.h"
+#include "mozilla/Unused.h"
 #include "mozilla/dom/LocationBinding.h"
 
 namespace mozilla {
 namespace dom {
 
 static nsresult
 GetDocumentCharacterSetForURI(const nsAString& aHref, nsACString& aCharset)
 {
@@ -694,31 +695,59 @@ NS_IMETHODIMP
 Location::SetProtocol(const nsAString& aProtocol)
 {
   nsCOMPtr<nsIURI> uri;
   nsresult rv = GetWritableURI(getter_AddRefs(uri));
   if (NS_WARN_IF(NS_FAILED(rv) || !uri)) {
     return rv;
   }
 
-  rv = uri->SetScheme(NS_ConvertUTF16toUTF8(aProtocol));
+  nsAString::const_iterator start, end;
+  aProtocol.BeginReading(start);
+  aProtocol.EndReading(end);
+  nsAString::const_iterator iter(start);
+  Unused << FindCharInReadable(':', iter, end);
+
+  rv = uri->SetScheme(NS_ConvertUTF16toUTF8(Substring(start, iter)));
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
+    // Oh, I wish nsStandardURL returned NS_ERROR_MALFORMED_URI for _all_ the
+    // malformed cases, not just some of them!
+    return NS_ERROR_DOM_SYNTAX_ERR;
   }
   nsAutoCString newSpec;
   rv = uri->GetSpec(newSpec);
   if (NS_FAILED(rv)) {
     return rv;
   }
   // We may want a new URI class for the new URI, so recreate it:
   rv = NS_NewURI(getter_AddRefs(uri), newSpec);
   if (NS_FAILED(rv)) {
+    if (rv == NS_ERROR_MALFORMED_URI) {
+      rv = NS_ERROR_DOM_SYNTAX_ERR;
+    }
     return rv;
   }
 
+  bool isHttp;
+  rv = uri->SchemeIs("http", &isHttp);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool isHttps;
+  rv = uri->SchemeIs("https", &isHttps);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!isHttp && !isHttps) {
+    // No-op, per spec.
+    return NS_OK;
+  }
+
   return SetURI(uri);
 }
 
 NS_IMETHODIMP
 Location::GetSearch(nsAString& aSearch)
 {
   aSearch.SetLength(0);
 
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1224,17 +1224,17 @@ BeaconStreamListener::OnDataAvailable(ns
                                       uint32_t count)
 {
   MOZ_ASSERT(false);
   return NS_OK;
 }
 
 bool
 Navigator::SendBeacon(const nsAString& aUrl,
-                      const Nullable<ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams>& aData,
+                      const Nullable<fetch::BodyInit>& aData,
                       ErrorResult& aRv)
 {
   if (aData.IsNull()) {
     return SendBeaconInternal(aUrl, nullptr, eBeaconTypeOther, aRv);
   }
 
   if (aData.Value().IsArrayBuffer()) {
     BodyExtractor<const ArrayBuffer> body(&aData.Value().GetAsArrayBuffer());
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -4,16 +4,17 @@
  * 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 mozilla_dom_Navigator_h
 #define mozilla_dom_Navigator_h
 
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Fetch.h"
 #include "mozilla/dom/Nullable.h"
 #include "mozilla/ErrorResult.h"
 #include "nsIDOMNavigator.h"
 #include "nsIMozNavigatorNetwork.h"
 #include "nsWrapperCache.h"
 #include "nsHashKeys.h"
 #include "nsInterfaceHashtable.h"
 #include "nsString.h"
@@ -221,17 +222,17 @@ public:
 #endif // MOZ_TIME_MANAGER
 #ifdef MOZ_AUDIO_CHANNEL_MANAGER
   system::AudioChannelManager* GetMozAudioChannelManager(ErrorResult& aRv);
 #endif // MOZ_AUDIO_CHANNEL_MANAGER
 
   Presentation* GetPresentation(ErrorResult& aRv);
 
   bool SendBeacon(const nsAString& aUrl,
-                  const Nullable<ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams>& aData,
+                  const Nullable<fetch::BodyInit>& aData,
                   ErrorResult& aRv);
 
   void MozGetUserMedia(const MediaStreamConstraints& aConstraints,
                        NavigatorUserMediaSuccessCallback& aOnSuccess,
                        NavigatorUserMediaErrorCallback& aOnError,
                        CallerType aCallerType,
                        ErrorResult& aRv);
   void MozGetUserMediaDevices(const MediaStreamConstraints& aConstraints,
--- a/dom/base/UseCounters.conf
+++ b/dom/base/UseCounters.conf
@@ -56,8 +56,19 @@ attribute OfflineResourceList.status
 attribute OfflineResourceList.onchecking
 attribute OfflineResourceList.onerror
 attribute OfflineResourceList.onnoupdate
 attribute OfflineResourceList.ondownloading
 attribute OfflineResourceList.onprogress
 attribute OfflineResourceList.onupdateready
 attribute OfflineResourceList.oncached
 attribute OfflineResourceList.onobsolete
+
+// DataTransfer API (gecko-only methods)
+method DataTransfer.addElement
+attribute DataTransfer.mozItemCount
+attribute DataTransfer.mozCursor
+method DataTransfer.mozTypesAt
+method DataTransfer.mozClearDataAt
+method DataTransfer.mozSetDataAt
+method DataTransfer.mozGetDataAt
+attribute DataTransfer.mozUserCancelled
+attribute DataTransfer.mozSourceNode
--- a/dom/base/nsAttrValue.cpp
+++ b/dom/base/nsAttrValue.cpp
@@ -13,16 +13,17 @@
 #include "mozilla/HashFunctions.h"
 
 #include "nsAttrValue.h"
 #include "nsAttrValueInlines.h"
 #include "nsIAtom.h"
 #include "nsUnicharUtils.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/ServoBindingTypes.h"
+#include "mozilla/ServoStyleSet.h"
 #include "mozilla/DeclarationBlockInlines.h"
 #include "nsContentUtils.h"
 #include "nsReadableUtils.h"
 #include "prprf.h"
 #include "nsHTMLCSSStyleSheet.h"
 #include "nsCSSParser.h"
 #include "nsStyledElement.h"
 #include "nsIURI.h"
@@ -642,17 +643,25 @@ nsAttrValue::ToString(nsAString& aResult
     }
     case eCSSDeclaration:
     {
       aResult.Truncate();
       MiscContainer *container = GetMiscContainer();
       if (DeclarationBlock* decl = container->mValue.mCSSDeclaration) {
         decl->ToString(aResult);
       }
-      const_cast<nsAttrValue*>(this)->SetMiscAtomOrString(&aResult);
+
+      // We can reach this during parallel style traversal. If that happens,
+      // don't cache the string. The TLS overhead should't hurt us here, since
+      // main thread consumers will subsequently use the cache, and
+      // off-main-thread consumers only reach this in the rare case of selector
+      // matching on the "style" attribute.
+      if (!ServoStyleSet::IsInServoTraversal()) {
+        const_cast<nsAttrValue*>(this)->SetMiscAtomOrString(&aResult);
+      }
 
       break;
     }
     case eDoubleValue:
     {
       aResult.Truncate();
       aResult.AppendFloat(GetDoubleValue());
       break;
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -9826,16 +9826,19 @@ nsContentUtils::AttemptLargeAllocationLo
   NS_ENSURE_SUCCESS(rv, false);
   NS_ENSURE_TRUE(uri, false);
 
   nsCOMPtr<nsIURI> referrer;
   rv = aChannel->GetReferrer(getter_AddRefs(referrer));
   NS_ENSURE_SUCCESS(rv, false);
 
   nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
+  if (!loadInfo) {
+    return false;
+  }
   nsCOMPtr<nsIPrincipal> triggeringPrincipal = loadInfo->TriggeringPrincipal();
 
   // Get the channel's load flags, and use them to generate nsIWebNavigation
   // load flags. We want to make sure to propagate the refresh and cache busting
   // flags.
   nsLoadFlags channelLoadFlags;
   aChannel->GetLoadFlags(&channelLoadFlags);
 
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -4032,16 +4032,20 @@ nsIDocument::GetRootElement() const
 {
   return (mCachedRootElement && mCachedRootElement->GetParentNode() == this) ?
          mCachedRootElement : GetRootElementInternal();
 }
 
 Element*
 nsDocument::GetRootElementInternal() const
 {
+  // We invoke GetRootElement() immediately before the servo traversal, so we
+  // should always have a cache hit from Servo.
+  MOZ_ASSERT(NS_IsMainThread());
+
   // Loop backwards because any non-elements, such as doctypes and PIs
   // are likely to appear before the root element.
   uint32_t i;
   for (i = mChildren.ChildCount(); i > 0; --i) {
     nsIContent* child = mChildren.ChildAt(i - 1);
     if (child->IsElement()) {
       const_cast<nsDocument*>(this)->mCachedRootElement = child->AsElement();
       return child->AsElement();
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -713,19 +713,18 @@ nsGlobalWindow::RemoveIdleCallback(mozil
 
 nsresult
 nsGlobalWindow::RunIdleRequest(IdleRequest* aRequest,
                                DOMHighResTimeStamp aDeadline,
                                bool aDidTimeout)
 {
   AssertIsOnMainThread();
   RefPtr<IdleRequest> request(aRequest);
-  nsresult result = request->IdleRun(AsInner(), aDeadline, aDidTimeout);
   RemoveIdleCallback(request);
-  return result;
+  return request->IdleRun(AsInner(), aDeadline, aDidTimeout);
 }
 
 nsresult
 nsGlobalWindow::ExecuteIdleRequest(TimeStamp aDeadline)
 {
   AssertIsOnMainThread();
   RefPtr<IdleRequest> request = mIdleRequestCallbacks.getFirst();
 
--- a/dom/base/nsMappedAttributeElement.cpp
+++ b/dom/base/nsMappedAttributeElement.cpp
@@ -32,11 +32,11 @@ nsMappedAttributeElement::SetMappedAttri
 nsMapRuleToAttributesFunc
 nsMappedAttributeElement::GetAttributeMappingFunction() const
 {
   return &MapNoAttributesInto;
 }
 
 void
 nsMappedAttributeElement::MapNoAttributesInto(const nsMappedAttributes* aAttributes,
-                                              nsRuleData* aData)
+                                              mozilla::GenericSpecifiedValues* aGenericData)
 {
 }
--- a/dom/base/nsMappedAttributeElement.h
+++ b/dom/base/nsMappedAttributeElement.h
@@ -8,40 +8,41 @@
  * nsMappedAttributeElement is the base for elements supporting style mapped
  * attributes via nsMappedAttributes (HTML and MathML).
  */
 
 #ifndef NS_MAPPEDATTRIBUTEELEMENT_H_
 #define NS_MAPPEDATTRIBUTEELEMENT_H_
 
 #include "mozilla/Attributes.h"
+#include "mozilla/GenericSpecifiedValues.h"
 #include "nsStyledElement.h"
 
 class nsMappedAttributes;
 struct nsRuleData;
 
 typedef void (*nsMapRuleToAttributesFunc)(const nsMappedAttributes* aAttributes, 
-                                          nsRuleData* aData);
+                                          mozilla::GenericSpecifiedValues* aData);
 
 typedef nsStyledElement nsMappedAttributeElementBase;
 
 class nsMappedAttributeElement : public nsMappedAttributeElementBase
 {
 
 protected:
 
   explicit nsMappedAttributeElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)
     : nsMappedAttributeElementBase(aNodeInfo)
   {}
 
 public:
   virtual nsMapRuleToAttributesFunc GetAttributeMappingFunction() const;
 
   static void MapNoAttributesInto(const nsMappedAttributes* aAttributes, 
-                                  nsRuleData* aRuleData);
+                                  mozilla::GenericSpecifiedValues* aGenericData);
 
   NS_IMETHOD WalkContentStyleRules(nsRuleWalker* aRuleWalker) override;
   virtual bool SetMappedAttribute(nsIDocument* aDocument,
                                     nsIAtom* aName,
                                     nsAttrValue& aValue,
                                     nsresult* aRetval) override;
 };
 
--- a/dom/base/nsMappedAttributes.cpp
+++ b/dom/base/nsMappedAttributes.cpp
@@ -6,18 +6,19 @@
 
 /*
  * A unique per-element set of attributes that is used as an
  * nsIStyleRule; used to implement presentational attributes.
  */
 
 #include "nsMappedAttributes.h"
 #include "nsHTMLStyleSheet.h"
+#include "nsRuleData.h"
 #include "nsRuleWalker.h"
-#include "nsRuleData.h"
+#include "mozilla/GenericSpecifiedValues.h"
 #include "mozilla/HashFunctions.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/ServoDeclarationBlock.h"
 
 using namespace mozilla;
 
 nsMappedAttributes::nsMappedAttributes(nsHTMLStyleSheet* aSheet,
                                        nsMapRuleToAttributesFunc aMapRuleFunc)
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -2590,16 +2590,17 @@ nsObjectLoadingContent::OpenChannel()
                      group, // aLoadGroup
                      shim,  // aCallbacks
                      nsIChannel::LOAD_CALL_CONTENT_SNIFFERS |
                      nsIChannel::LOAD_CLASSIFY_URI |
                      nsIChannel::LOAD_BYPASS_SERVICE_WORKER);
   NS_ENSURE_SUCCESS(rv, rv);
   if (inherit) {
     nsCOMPtr<nsILoadInfo> loadinfo = chan->GetLoadInfo();
+    NS_ENSURE_STATE(loadinfo);
     loadinfo->SetPrincipalToInherit(thisContent->NodePrincipal());
   }
 
   // Referrer
   nsCOMPtr<nsIHttpChannel> httpChan(do_QueryInterface(chan));
   if (httpChan) {
     httpChan->SetReferrerWithPolicy(doc->GetDocumentURI(),
                                     doc->GetReferrerPolicy());
--- a/dom/base/nsScriptLoader.cpp
+++ b/dom/base/nsScriptLoader.cpp
@@ -651,16 +651,29 @@ nsScriptLoader::CheckContentPolicy(nsIDo
     }
     return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT;
   }
 
   return NS_OK;
 }
 
 bool
+nsScriptLoader::ModuleScriptsEnabled()
+{
+  static bool sEnabledForContent = false;
+  static bool sCachedPref = false;
+  if (!sCachedPref) {
+    sCachedPref = true;
+    Preferences::AddBoolVarCache(&sEnabledForContent, "dom.moduleScripts.enabled", false);
+  }
+
+  return nsContentUtils::IsChromeDoc(mDocument) || sEnabledForContent;
+}
+
+bool
 nsScriptLoader::ModuleMapContainsModule(nsModuleLoadRequest *aRequest) const
 {
   // Returns whether we have fetched, or are currently fetching, a module script
   // for the request's URL.
   return mFetchingModules.Contains(aRequest->mURI) ||
          mFetchedModules.Contains(aRequest->mURI);
 }
 
@@ -1224,25 +1237,37 @@ nsScriptLoader::StartLoad(nsScriptLoadRe
 
   nsCOMPtr<nsILoadGroup> loadGroup = mDocument->GetDocumentLoadGroup();
   nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->MasterDocument()->GetWindow();
   NS_ENSURE_TRUE(window, NS_ERROR_NULL_POINTER);
   nsIDocShell *docshell = window->GetDocShell();
   nsCOMPtr<nsIInterfaceRequestor> prompter(do_QueryInterface(docshell));
 
   nsSecurityFlags securityFlags;
-  // TODO: the spec currently gives module scripts different CORS behaviour to
-  // classic scripts.
-  securityFlags = aRequest->mCORSMode == CORS_NONE
-    ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
-    : nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS;
-  if (aRequest->mCORSMode == CORS_ANONYMOUS) {
-    securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
-  } else if (aRequest->mCORSMode == CORS_USE_CREDENTIALS) {
-    securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+  if (aRequest->IsModuleRequest()) {
+    // According to the spec, module scripts have different behaviour to classic
+    // scripts and always use CORS.
+    securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS;
+    if (aRequest->mCORSMode == CORS_NONE) {
+      securityFlags |= nsILoadInfo::SEC_COOKIES_OMIT;
+    } else if (aRequest->mCORSMode == CORS_ANONYMOUS) {
+      securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
+    } else {
+      MOZ_ASSERT(aRequest->mCORSMode == CORS_USE_CREDENTIALS);
+      securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+    }
+  } else {
+    securityFlags = aRequest->mCORSMode == CORS_NONE
+      ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
+      : nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS;
+    if (aRequest->mCORSMode == CORS_ANONYMOUS) {
+      securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
+    } else if (aRequest->mCORSMode == CORS_USE_CREDENTIALS) {
+      securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+    }
   }
   securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME;
 
   nsCOMPtr<nsIChannel> channel;
   nsresult rv = NS_NewChannel(getter_AddRefs(channel),
                               aRequest->mURI,
                               context,
                               securityFlags,
@@ -1444,18 +1469,17 @@ nsScriptLoader::ProcessScriptElement(nsI
 
   // Check the type attribute to determine language and version.
   // If type exists, it trumps the deprecated 'language='
   nsAutoString type;
   bool hasType = aElement->GetScriptType(type);
 
   nsScriptKind scriptKind = nsScriptKind::Classic;
   if (!type.IsEmpty()) {
-    // Support type="module" only for chrome documents.
-    if (nsContentUtils::IsChromeDoc(mDocument) && type.LowerCaseEqualsASCII("module")) {
+    if (ModuleScriptsEnabled() && type.LowerCaseEqualsASCII("module")) {
       scriptKind = nsScriptKind::Module;
     } else {
       NS_ENSURE_TRUE(ParseTypeAttribute(type, &version), false);
     }
   } else if (!hasType) {
     // no 'type=' element
     // "language" is a deprecated attribute of HTML, so we check it only for
     // HTML script elements.
@@ -2495,17 +2519,17 @@ nsScriptLoader::OnStreamComplete(nsIIncr
       mReporter->FlushConsoleReports(mDocument);
     }
     if (NS_FAILED(rv)) {
       rv = NS_ERROR_SRI_CORRUPT;
     }
   } else {
     nsCOMPtr<nsILoadInfo> loadInfo = channel->GetLoadInfo();
 
-    if (loadInfo->GetEnforceSRI()) {
+    if (loadInfo && loadInfo->GetEnforceSRI()) {
       MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug,
               ("nsScriptLoader::OnStreamComplete, required SRI not found"));
       nsCOMPtr<nsIContentSecurityPolicy> csp;
       loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp));
       nsAutoCString violationURISpec;
       mDocument->GetDocumentURI()->GetAsciiSpec(violationURISpec);
       uint32_t lineNo = aRequest->mElement ? aRequest->mElement->GetScriptLineNumber() : 0;
       csp->LogViolationDetails(
@@ -2784,17 +2808,17 @@ nsScriptLoader::PreloadURI(nsIURI *aURI,
 {
   NS_ENSURE_TRUE_VOID(mDocument);
   // Check to see if scripts has been turned off.
   if (!mEnabled || !mDocument->IsScriptEnabled()) {
     return;
   }
 
   // TODO: Preload module scripts.
-  if (nsContentUtils::IsChromeDoc(mDocument) && aType.LowerCaseEqualsASCII("module")) {
+  if (ModuleScriptsEnabled() && aType.LowerCaseEqualsASCII("module")) {
     return;
   }
 
   SRIMetadata sriMetadata;
   if (!aIntegrity.IsEmpty()) {
     MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug,
             ("nsScriptLoader::PreloadURI, integrity=%s",
              NS_ConvertUTF16toUTF8(aIntegrity).get()));
--- a/dom/base/nsScriptLoader.h
+++ b/dom/base/nsScriptLoader.h
@@ -580,16 +580,18 @@ private:
   void AddDeferRequest(nsScriptLoadRequest* aRequest);
   bool MaybeRemovedDeferRequests();
 
   void MaybeMoveToLoadedList(nsScriptLoadRequest* aRequest);
 
   JS::SourceBufferHolder GetScriptSource(nsScriptLoadRequest* aRequest,
                                          nsAutoString& inlineData);
 
+  bool ModuleScriptsEnabled();
+
   void SetModuleFetchStarted(nsModuleLoadRequest *aRequest);
   void SetModuleFetchFinishedAndResumeWaitingRequests(nsModuleLoadRequest *aRequest,
                                                       nsresult aResult);
 
   bool IsFetchingModule(nsModuleLoadRequest *aRequest) const;
 
   bool ModuleMapContainsModule(nsModuleLoadRequest *aRequest) const;
   RefPtr<mozilla::GenericPromise> WaitForModuleFetch(nsModuleLoadRequest *aRequest);
--- a/dom/base/test/browser.ini
+++ b/dom/base/test/browser.ini
@@ -2,18 +2,16 @@
 support-files =
   empty.html
   file_bug1011748_redirect.sjs
   file_bug1011748_OK.sjs
   file_messagemanager_unload.html
   file_use_counter_outer.html
   file_use_counter_svg_getElementById.svg
   file_use_counter_svg_currentScale.svg
-  file_use_counter_svg_background.html
-  file_use_counter_svg_list_style_image.html
   file_use_counter_svg_fill_pattern_definition.svg
   file_use_counter_svg_fill_pattern.svg
   file_use_counter_svg_fill_pattern_internal.svg
   file_use_counter_svg_fill_pattern_data.svg
 
 [browser_bug593387.js]
 [browser_bug902350.js]
 tags = mcb
--- a/dom/base/test/browser_use_counters.js
+++ b/dom/base/test/browser_use_counters.js
@@ -66,22 +66,16 @@ add_task(function* () {
 
   // Check that use counters are incremented by directly loading SVGs
   // that reference patterns defined in the same file or in data: URLs.
   yield check_use_counter_direct("file_use_counter_svg_fill_pattern_internal.svg",
                                  "PROPERTY_FILLOPACITY");
   // data: URLs don't correctly propagate to their referring document yet.
   //yield check_use_counter_direct("file_use_counter_svg_fill_pattern_data.svg",
   //                               "PROPERTY_FILL_OPACITY");
-
-  // Check that use counters are incremented by SVGs loaded as CSS images in
-  // pages loaded in iframes.  Again, SVG images in CSS aren't permitted to
-  // execute script, so we need to use properties here.
-  yield check_use_counter_iframe("file_use_counter_svg_list_style_image.html",
-                                 "PROPERTY_FILL");
 });
 
 add_task(function* () {
   let Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
   Telemetry.canRecordExtended = gOldParentCanRecord;
 
   yield ContentTask.spawn(gBrowser.selectedBrowser, { oldCanRecord: gOldContentCanRecord }, function* (arg) {
     Cu.import("resource://gre/modules/PromiseUtils.jsm");
--- a/dom/base/test/bug403852_fileOpener.js
+++ b/dom/base/test/bug403852_fileOpener.js
@@ -3,15 +3,19 @@ Cu.importGlobalProperties(["File"]);
 
 var testFile = Cc["@mozilla.org/file/directory_service;1"]
                  .getService(Ci.nsIDirectoryService)
                  .QueryInterface(Ci.nsIProperties)
                  .get("ProfD", Ci.nsIFile);
 testFile.append("prefs.js");
 
 addMessageListener("file.open", function () {
-  sendAsyncMessage("file.opened", {
-    file: File.createFromNsIFile(testFile),
-    mtime: testFile.lastModifiedTime,
-    fileWithDate: File.createFromNsIFile(testFile, { lastModified: 123 }),
-    fileDate: 123,
+  File.createFromNsIFile(testFile).then(function(file) {
+    File.createFromNsIFile(testFile, { lastModified: 123 }).then(function(fileWithDate) {
+      sendAsyncMessage("file.opened", {
+        file,
+        mtime: testFile.lastModifiedTime,
+        fileWithDate,
+        fileDate: 123,
+      });
+    });
   });
 });
--- a/dom/base/test/bug578096LoadChromeScript.js
+++ b/dom/base/test/bug578096LoadChromeScript.js
@@ -2,15 +2,17 @@ var file;
 Components.utils.importGlobalProperties(["File"]);
 
 addMessageListener("file.create", function (message) {
   file = Components.classes["@mozilla.org/file/directory_service;1"]
              .getService(Components.interfaces.nsIProperties)
              .get("TmpD", Components.interfaces.nsIFile);
   file.append("foo.txt");
   file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0o600);
-  sendAsyncMessage("file.created", File.createFromNsIFile(file));
+  File.createFromNsIFile(file).then(function(domFile) {
+    sendAsyncMessage("file.created", domFile);
+  });
 });
 
 addMessageListener("file.remove", function (message) {
   file.remove(false);
   sendAsyncMessage("file.removed", {});
 });
--- a/dom/base/test/chrome/test_bug914381.html
+++ b/dom/base/test/chrome/test_bug914381.html
@@ -30,19 +30,28 @@ function createFileWithData(fileData) {
   outStream.write(fileData, fileData.length);
   outStream.close();
 
   return testFile;
 }
 
 /** Test for Bug 914381. File's created in JS using an nsIFile should allow mozGetFullPathInternal calls to succeed **/
 var file = createFileWithData("Test bug 914381");
-var f = File.createFromNsIFile(file);
-is(f.mozFullPathInternal, undefined, "mozFullPathInternal is undefined from js");
-is(f.mozFullPath, file.path, "mozFullPath returns path if created with nsIFile");
+File.createFromNsIFile(file).then(f => {
+  is(f.mozFullPathInternal, undefined, "mozFullPathInternal is undefined from js");
+  is(f.mozFullPath, file.path, "mozFullPath returns path if created with nsIFile");
+})
+.then(() => {
+  return File.createFromFileName(file.path);
+})
+.then(f => {
+  is(f.mozFullPathInternal, undefined, "mozFullPathInternal is undefined from js");
+  is(f.mozFullPath, "", "mozFullPath returns blank if created with a string");
+})
+.then(() => {
+  SimpleTest.finish();
+});
 
-f = File.createFromFileName(file.path);
-is(f.mozFullPathInternal, undefined, "mozFullPathInternal is undefined from js");
-is(f.mozFullPath, "", "mozFullPath returns blank if created with a string");
+SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 </body>
 </html>
--- a/dom/base/test/chrome/test_fileconstructor.xul
+++ b/dom/base/test/chrome/test_fileconstructor.xul
@@ -37,36 +37,46 @@ var file = Components.classes["@mozilla.
 // man I wish this were simpler ...
 file.append("chrome");
 file.append("dom");
 file.append("base");
 file.append("test");
 file.append("chrome");
 file.append("fileconstructor_file.png");
 
-doTest(File.createFromFileName(file.path));
-doTest(File.createFromNsIFile(file));
-function doTest(domfile) {
+File.createFromFileName(file.path).then(function(domFile) {
+  ok(domfile instanceof File, "File() should return a File");
+  is(domfile.type, "image/png", "File should be a PNG");
+  is(domfile.size, 95, "File has size 95 (and more importantly we can read it)");
+})
+.then(() => {
+  return File.createFromNsIFile(file);
+})
+.then(function(domFile) {
   ok(domfile instanceof File, "File() should return a File");
   is(domfile.type, "image/png", "File should be a PNG");
   is(domfile.size, 95, "File has size 95 (and more importantly we can read it)");
-}
-
-try {
-  var nonexistentfile = File.createFromFileName("i/sure/hope/this/does/not/exist/anywhere.txt");
+})
+.then(function() {
+  return File.createFromFileName("i/sure/hope/this/does/not/exist/anywhere.txt");
+})
+.then(function() {
   ok(false, "This should never be reached!");
-} catch (e) {
+}, function() {
   ok(true, "Attempt to construct a non-existent file should fail.");
-}
-
-try {
+}).then(function() {
   var dir = Components.classes["@mozilla.org/file/directory_service;1"]
                       .getService(Components.interfaces.nsIProperties)
                       .get("CurW