Merge mozilla-central to beta. a=merge, l10n=me on a CLOSED TREE
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sat, 03 Mar 2018 00:01:28 +0200
changeset 461297 7475508d19db133051371930650b6623d095d3fc
parent 460800 a1098c825a9c5e07f03ec4ad7a760c868bf20d75 (current diff)
parent 461296 3d0d0e0832be9ce681e4858c1389c3c2cf2a3829 (diff)
child 461298 53f9d1e4e584a2414366b34f7dd397b38d4163a4
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone60.0
Merge mozilla-central to beta. a=merge, l10n=me on a CLOSED TREE
browser/components/newtab/NewTabPrefsProvider.jsm
browser/components/newtab/NewTabRemoteResources.jsm
browser/components/newtab/NewTabSearchProvider.jsm
browser/components/newtab/NewTabWebChannel.jsm
browser/components/newtab/tests/xpcshell/test_NewTabPrefsProvider.js
browser/components/newtab/tests/xpcshell/test_NewTabSearchProvider.js
browser/docs/DirectoryLinksProvider.rst
browser/modules/DirectoryLinksProvider.jsm
browser/modules/test/unit/test_DirectoryLinksProvider.js
devtools/client/inspector/fonts/test/browser_fontinspector_expand-details.js
devtools/client/netmonitor/src/utils/create-store.js
dom/canvas/WebGLContextNotSupported.cpp
dom/events/test/test_bug742376.html
dom/interfaces/canvas/moz.build
dom/interfaces/canvas/nsIDOMWebGLRenderingContext.idl
dom/security/test/contentverifier/browser.ini
dom/security/test/contentverifier/browser_verify_content_about_newtab.js
dom/security/test/contentverifier/browser_verify_content_about_newtab2.js
dom/security/test/contentverifier/file_about_newtab.html
dom/security/test/contentverifier/file_about_newtab_bad.html
dom/security/test/contentverifier/file_about_newtab_bad_csp.html
dom/security/test/contentverifier/file_about_newtab_bad_csp_signature
dom/security/test/contentverifier/file_about_newtab_bad_signature
dom/security/test/contentverifier/file_about_newtab_broken_signature
dom/security/test/contentverifier/file_about_newtab_good_signature
dom/security/test/contentverifier/file_about_newtab_sri.html
dom/security/test/contentverifier/file_about_newtab_sri_signature
dom/security/test/contentverifier/file_contentserver.sjs
dom/security/test/contentverifier/goodChain.pem
dom/security/test/contentverifier/head.js
dom/security/test/contentverifier/script.js
dom/security/test/contentverifier/signature.der
dom/security/test/contentverifier/sk.pem
dom/security/test/contentverifier/style.css
gfx/webrender/res/brush_mask_corner.glsl
gfx/webrender/src/frame.rs
taskcluster/ci/push-apk-breakpoint/kind.yml
taskcluster/taskgraph/transforms/push_apk_breakpoint.py
taskcluster/taskgraph/transforms/task.py
taskcluster/taskgraph/util/push_apk.py
testing/talos/talos/directoryLinks.json
third_party/rust/euclid-0.16.0/.cargo-checksum.json
third_party/rust/euclid-0.16.0/.travis.yml
third_party/rust/euclid-0.16.0/COPYRIGHT
third_party/rust/euclid-0.16.0/Cargo.toml
third_party/rust/euclid-0.16.0/LICENSE-APACHE
third_party/rust/euclid-0.16.0/LICENSE-MIT
third_party/rust/euclid-0.16.0/README.md
third_party/rust/euclid-0.16.0/src/approxeq.rs
third_party/rust/euclid-0.16.0/src/length.rs
third_party/rust/euclid-0.16.0/src/lib.rs
third_party/rust/euclid-0.16.0/src/macros.rs
third_party/rust/euclid-0.16.0/src/num.rs
third_party/rust/euclid-0.16.0/src/point.rs
third_party/rust/euclid-0.16.0/src/rect.rs
third_party/rust/euclid-0.16.0/src/rotation.rs
third_party/rust/euclid-0.16.0/src/scale.rs
third_party/rust/euclid-0.16.0/src/side_offsets.rs
third_party/rust/euclid-0.16.0/src/size.rs
third_party/rust/euclid-0.16.0/src/transform2d.rs
third_party/rust/euclid-0.16.0/src/transform3d.rs
third_party/rust/euclid-0.16.0/src/trig.rs
third_party/rust/euclid-0.16.0/src/vector.rs
third_party/rust/itertools/src/adaptors/multipeek.rs
third_party/rust/itertools/src/lib.rs
third_party/rust/itertools/tests/tests.rs
widget/GfxInfoWebGL.cpp
widget/GfxInfoWebGL.h
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -39,17 +39,16 @@ module.exports = {
     "env": {
       "mozilla/browser-window": true
     }
   },{
     // XXX Bug 1421969. These files/directories are still being fixed,
     // so turn off mozilla/use-services for them for now.
     "files": [
       "extensions/pref/**",
-      "mobile/android/**",
     ],
     "rules": {
       "mozilla/use-services": "off",
     }
   }, {
     // XXX Bug 1434446. These directories have jsm files still being fixed, so
     // turn off global no-unused-vars checking for them.
     "files": [
--- a/accessible/atk/nsMaiInterfaceComponent.cpp
+++ b/accessible/atk/nsMaiInterfaceComponent.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "InterfaceInitFuncs.h"
 
+#include "Accessible-inl.h"
 #include "AccessibleWrap.h"
 #include "nsAccUtils.h"
 #include "nsCoreUtils.h"
 #include "nsMai.h"
 #include "mozilla/Likely.h"
 #include "mozilla/a11y/ProxyAccessible.h"
 
 using namespace mozilla::a11y;
--- a/accessible/base/XULMap.h
+++ b/accessible/base/XULMap.h
@@ -1,43 +1,49 @@
 /* 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/. */
 
+XULMAP_TYPE(browser, OuterDocAccessible)
+XULMAP_TYPE(button, XULButtonAccessible)
 XULMAP_TYPE(checkbox, XULCheckboxAccessible)
 XULMAP_TYPE(dropMarker, XULDropmarkerAccessible)
+XULMAP_TYPE(editor, OuterDocAccessible)
 XULMAP_TYPE(findbar, XULToolbarAccessible)
 XULMAP_TYPE(groupbox, XULGroupboxAccessible)
+XULMAP_TYPE(iframe, OuterDocAccessible)
 XULMAP_TYPE(listbox, XULListboxAccessibleWrap)
 XULMAP_TYPE(listhead, XULColumAccessible)
 XULMAP_TYPE(listheader, XULColumnItemAccessible)
 XULMAP_TYPE(listitem, XULListitemAccessible)
 XULMAP_TYPE(menu, XULMenuitemAccessibleWrap)
 XULMAP_TYPE(menubar, XULMenubarAccessible)
 XULMAP_TYPE(menucaption, XULMenuitemAccessibleWrap)
 XULMAP_TYPE(menuitem, XULMenuitemAccessibleWrap)
 XULMAP_TYPE(menulist, XULComboboxAccessible)
 XULMAP_TYPE(menuseparator, XULMenuSeparatorAccessible)
 XULMAP_TYPE(notification, XULAlertAccessible)
 XULMAP_TYPE(progressmeter, XULProgressMeterAccessible)
 XULMAP_TYPE(radio, XULRadioButtonAccessible)
 XULMAP_TYPE(radiogroup, XULRadioGroupAccessible)
 XULMAP_TYPE(richlistbox, XULListboxAccessibleWrap)
 XULMAP_TYPE(richlistitem, XULListitemAccessible)
+XULMAP_TYPE(scale, XULSliderAccessible)
 XULMAP_TYPE(statusbar, XULStatusBarAccessible)
 XULMAP_TYPE(tab, XULTabAccessible)
 XULMAP_TYPE(tabpanels, XULTabpanelsAccessible)
 XULMAP_TYPE(tabs, XULTabsAccessible)
 XULMAP_TYPE(toolbarseparator, XULToolbarSeparatorAccessible)
 XULMAP_TYPE(toolbarspacer, XULToolbarSeparatorAccessible)
 XULMAP_TYPE(toolbarspring, XULToolbarSeparatorAccessible)
 XULMAP_TYPE(treecol, XULColumnItemAccessible)
 XULMAP_TYPE(treecolpicker, XULButtonAccessible)
 XULMAP_TYPE(treecols, XULTreeColumAccessible)
 XULMAP_TYPE(toolbar, XULToolbarAccessible)
+XULMAP_TYPE(toolbarbutton, XULToolbarButtonAccessible)
 XULMAP_TYPE(tooltip, XULTooltipAccessible)
 
 XULMAP(
   image,
   [](nsIContent* aContent, Accessible* aContext) -> Accessible* {
     if (aContent->IsElement() &&
         aContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::onclick)) {
       return new XULToolbarButtonAccessible(aContent, aContext->Document());
@@ -69,16 +75,54 @@ XULMAP(
       }
     }
 
     return nullptr;
   }
 )
 
 XULMAP(
+  menupopup,
+  [](nsIContent* aContent, Accessible* aContext) {
+    return CreateMenupopupAccessible(aContent, aContext);
+  }
+)
+
+XULMAP(
+  popup,
+  [](nsIContent* aContent, Accessible* aContext) {
+    return CreateMenupopupAccessible(aContent, aContext);
+  }
+)
+
+XULMAP(
+  textbox,
+  [](nsIContent* aContent, Accessible* aContext) -> Accessible* {
+    if (aContent->IsElement() &&
+        aContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                                           nsGkAtoms::autocomplete, eIgnoreCase)) {
+      return new XULComboboxAccessible(aContent, aContext->Document());
+    }
+
+    return new EnumRoleAccessible<roles::SECTION>(aContent, aContext->Document());
+  }
+)
+
+XULMAP(
+  thumb,
+  [](nsIContent* aContent, Accessible* aContext) -> Accessible* {
+    if (aContent->IsElement() &&
+        aContent->AsElement()->ClassList()->Contains(NS_LITERAL_STRING("scale-thumb"))) {
+      return new XULThumbAccessible(aContent, aContext->Document());
+    }
+    return nullptr;
+  }
+)
+
+XULMAP(
   tree,
   [](nsIContent* aContent, Accessible* aContext) -> Accessible* {
     nsIContent* child = nsTreeUtils::GetDescendantChild(aContent,
                                                         nsGkAtoms::treechildren);
     if (!child)
       return nullptr;
 
     nsTreeBodyFrame* treeFrame = do_QueryFrame(child->GetPrimaryFrame());
--- a/accessible/base/nsAccessibilityService.cpp
+++ b/accessible/base/nsAccessibilityService.cpp
@@ -18,16 +18,17 @@
 #include "HTMLListAccessible.h"
 #include "HTMLSelectAccessible.h"
 #include "HTMLTableAccessibleWrap.h"
 #include "HyperTextAccessibleWrap.h"
 #include "RootAccessible.h"
 #include "nsAccUtils.h"
 #include "nsArrayUtils.h"
 #include "nsAttrName.h"
+#include "nsDOMTokenList.h"
 #include "nsEventShell.h"
 #include "nsIURI.h"
 #include "nsTextFormatter.h"
 #include "OuterDocAccessible.h"
 #include "Role.h"
 #ifdef MOZ_ACCESSIBILITY_ATK
 #include "RootAccessibleWrap.h"
 #endif
@@ -138,16 +139,38 @@ MustBeAccessible(nsIContent* aContent, D
   // for it.
   nsAutoString id;
   if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty())
     return aDocument->IsDependentID(id);
 
   return false;
 }
 
+/**
+ * Used by XULMap.h to map both menupopup and popup elements
+ */
+#ifdef MOZ_XUL
+Accessible*
+CreateMenupopupAccessible(nsIContent* aContent, Accessible* aContext)
+{
+#ifdef MOZ_ACCESSIBILITY_ATK
+    // ATK considers this node to be redundant when within menubars, and it makes menu
+    // navigation with assistive technologies more difficult
+    // XXX In the future we will should this for consistency across the nsIAccessible
+    // implementations on each platform for a consistent scripting environment, but
+    // then strip out redundant accessibles in the AccessibleWrap class for each platform.
+    nsIContent *parent = aContent->GetParent();
+    if (parent && parent->IsXULElement(nsGkAtoms::menu))
+      return nullptr;
+#endif
+
+    return new XULMenupopupAccessible(aContent, aContext->Document());
+}
+#endif
+
 ////////////////////////////////////////////////////////////////////////////////
 // Accessible constructors
 
 static Accessible*
 New_HTMLLink(nsIContent* aContent, Accessible* aContext)
 {
   // Only some roles truly enjoy life as HTMLLinkAccessibles, for details
   // see closed bug 494807.
@@ -1444,83 +1467,43 @@ nsAccessibilityService::Shutdown()
 }
 
 already_AddRefed<Accessible>
 nsAccessibilityService::CreateAccessibleByType(nsIContent* aContent,
                                                DocAccessible* aDoc)
 {
   nsAutoString role;
   nsCoreUtils::XBLBindingRole(aContent, role);
-  if (role.IsEmpty() || role.EqualsLiteral("none"))
+  if (role.IsEmpty())
     return nullptr;
 
-  if (role.EqualsLiteral("outerdoc")) {
-    RefPtr<Accessible> accessible = new OuterDocAccessible(aContent, aDoc);
-    return accessible.forget();
-  }
-
   RefPtr<Accessible> accessible;
 #ifdef MOZ_XUL
   // XUL controls
-  if (role.EqualsLiteral("xul:button")) {
-    accessible = new XULButtonAccessible(aContent, aDoc);
-
-  } else if (role.EqualsLiteral("xul:colorpicker")) {
+  if (role.EqualsLiteral("xul:colorpicker")) {
     accessible = new XULColorPickerAccessible(aContent, aDoc);
 
   } else if (role.EqualsLiteral("xul:colorpickertile")) {
     accessible = new XULColorPickerTileAccessible(aContent, aDoc);
 
-  } else if (role.EqualsLiteral("xul:combobox")) {
-    accessible = new XULComboboxAccessible(aContent, aDoc);
-
   } else if (role.EqualsLiteral("xul:link")) {
     accessible = new XULLinkAccessible(aContent, aDoc);
 
-  } else if (role.EqualsLiteral("xul:menupopup")) {
-#ifdef MOZ_ACCESSIBILITY_ATK
-    // ATK considers this node to be redundant when within menubars, and it makes menu
-    // navigation with assistive technologies more difficult
-    // XXX In the future we will should this for consistency across the nsIAccessible
-    // implementations on each platform for a consistent scripting environment, but
-    // then strip out redundant accessibles in the AccessibleWrap class for each platform.
-    nsIContent *parent = aContent->GetParent();
-    if (parent && parent->IsXULElement(nsGkAtoms::menu))
-      return nullptr;
-#endif
-
-    accessible = new XULMenupopupAccessible(aContent, aDoc);
-
-  } else if(role.EqualsLiteral("xul:pane")) {
-    accessible = new EnumRoleAccessible<roles::PANE>(aContent, aDoc);
-
   } else if (role.EqualsLiteral("xul:panel")) {
     if (aContent->IsElement() &&
         aContent->AsElement()->AttrValueIs(kNameSpaceID_None,
                                            nsGkAtoms::noautofocus,
                                            nsGkAtoms::_true, eCaseMatters))
       accessible = new XULAlertAccessible(aContent, aDoc);
     else
       accessible = new EnumRoleAccessible<roles::PANE>(aContent, aDoc);
 
-  } else if (role.EqualsLiteral("xul:scale")) {
-    accessible = new XULSliderAccessible(aContent, aDoc);
-
   } else if (role.EqualsLiteral("xul:text")) {
     accessible = new XULLabelAccessible(aContent, aDoc);
 
-  } else if (role.EqualsLiteral("xul:textbox")) {
-    accessible = new EnumRoleAccessible<roles::SECTION>(aContent, aDoc);
-
-  } else if (role.EqualsLiteral("xul:thumb")) {
-    accessible = new XULThumbAccessible(aContent, aDoc);
-
-  } else if (role.EqualsLiteral("xul:toolbarbutton")) {
-    accessible = new XULToolbarButtonAccessible(aContent, aDoc);
-
   }
 #endif // MOZ_XUL
 
   return accessible.forget();
 }
 
 already_AddRefed<Accessible>
 nsAccessibilityService::CreateAccessibleByFrameType(nsIFrame* aFrame,
--- a/accessible/generic/Accessible-inl.h
+++ b/accessible/generic/Accessible-inl.h
@@ -99,16 +99,25 @@ Accessible::HasNumericValue() const
     return false;
 
   if (roleMapEntry->valueRule == eHasValueMinMaxIfFocusable)
     return InteractiveState() & states::FOCUSABLE;
 
   return true;
 }
 
+inline bool
+Accessible::IsDefunct() const
+{
+  MOZ_ASSERT(mStateFlags & eIsDefunct || IsApplication() || IsDoc() ||
+             mStateFlags & eSharedNode || mContent,
+             "No content");
+  return mStateFlags & eIsDefunct;
+}
+
 inline void
 Accessible::ScrollTo(uint32_t aHow) const
 {
   if (mContent)
     nsCoreUtils::ScrollTo(mDoc->PresShell(), mContent, aHow);
 }
 
 inline bool
--- a/accessible/generic/Accessible.h
+++ b/accessible/generic/Accessible.h
@@ -856,17 +856,17 @@ public:
   /**
    * Return the localized string for the given key.
    */
   static void TranslateString(const nsString& aKey, nsAString& aStringOut);
 
   /**
    * Return true if the accessible is defunct.
    */
-  bool IsDefunct() const { return mStateFlags & eIsDefunct; }
+  bool IsDefunct() const;
 
   /**
    * Return false if the accessible is no longer in the document.
    */
   bool IsInDocument() const { return !(mStateFlags & eIsNotInDocument); }
 
   /**
    * Return true if the accessible should be contained by document node map.
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -697,20 +697,20 @@ DocAccessible::OnPivotChanged(nsIAccessi
 ////////////////////////////////////////////////////////////////////////////////
 // nsIDocumentObserver
 
 NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
 NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
 NS_IMPL_NSIDOCUMENTOBSERVER_STYLE_STUB(DocAccessible)
 
 void
-DocAccessible::AttributeWillChange(nsIDocument* aDocument,
-                                   dom::Element* aElement,
+DocAccessible::AttributeWillChange(dom::Element* aElement,
                                    int32_t aNameSpaceID,
-                                   nsAtom* aAttribute, int32_t aModType,
+                                   nsAtom* aAttribute,
+                                   int32_t aModType,
                                    const nsAttrValue* aNewValue)
 {
   Accessible* accessible = GetAccessible(aElement);
   if (!accessible) {
     if (aElement != mContent)
       return;
 
     accessible = this;
@@ -743,25 +743,23 @@ DocAccessible::AttributeWillChange(nsIDo
   }
 
   if (aAttribute == nsGkAtoms::aria_disabled ||
       aAttribute == nsGkAtoms::disabled)
     mStateBitWasOn = accessible->Unavailable();
 }
 
 void
-DocAccessible::NativeAnonymousChildListChange(nsIDocument* aDocument,
-                                              nsIContent* aContent,
+DocAccessible::NativeAnonymousChildListChange(nsIContent* aContent,
                                               bool aIsRemove)
 {
 }
 
 void
-DocAccessible::AttributeChanged(nsIDocument* aDocument,
-                                dom::Element* aElement,
+DocAccessible::AttributeChanged(dom::Element* aElement,
                                 int32_t aNameSpaceID, nsAtom* aAttribute,
                                 int32_t aModType,
                                 const nsAttrValue* aOldValue)
 {
   NS_ASSERTION(!IsDefunct(),
                "Attribute changed called on defunct document accessible!");
 
   // Proceed even if the element is not accessible because element may become
@@ -1081,19 +1079,17 @@ DocAccessible::ARIAActiveDescendantChang
 #endif
         }
       }
     }
   }
 }
 
 void
-DocAccessible::ContentAppended(nsIDocument* aDocument,
-                               nsIContent* aContainer,
-                               nsIContent* aFirstNewContent)
+DocAccessible::ContentAppended(nsIContent* aFirstNewContent)
 {
 }
 
 void
 DocAccessible::ContentStateChanged(nsIDocument* aDocument,
                                    nsIContent* aContent,
                                    EventStates aStateMask)
 {
@@ -1134,45 +1130,40 @@ DocAccessible::ContentStateChanged(nsIDo
 
 void
 DocAccessible::DocumentStatesChanged(nsIDocument* aDocument,
                                      EventStates aStateMask)
 {
 }
 
 void
-DocAccessible::CharacterDataWillChange(nsIDocument* aDocument,
-                                       nsIContent* aContent,
+DocAccessible::CharacterDataWillChange(nsIContent* aContent,
                                        const CharacterDataChangeInfo&)
 {
 }
 
 void
-DocAccessible::CharacterDataChanged(nsIDocument* aDocument,
-                                    nsIContent* aContent,
+DocAccessible::CharacterDataChanged(nsIContent* aContent,
                                     const CharacterDataChangeInfo&)
 {
 }
 
 void
-DocAccessible::ContentInserted(nsIDocument* aDocument, nsIContent* aContainer,
-                               nsIContent* aChild)
+DocAccessible::ContentInserted(nsIContent* aChild)
 {
 }
 
 void
-DocAccessible::ContentRemoved(nsIDocument* aDocument,
-                              nsIContent* aContainerNode,
-                              nsIContent* aChildNode,
+DocAccessible::ContentRemoved(nsIContent* aChildNode,
                               nsIContent* aPreviousSiblingNode)
 {
 #ifdef A11Y_LOG
   if (logging::IsEnabled(logging::eTree)) {
     logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
-    logging::Node("container node", aContainerNode);
+    logging::Node("container node", aChildNode->GetParent());
     logging::Node("content node", aChildNode);
     logging::MsgEnd();
   }
 #endif
   // This one and content removal notification from layout may result in
   // double processing of same subtrees. If it pops up in profiling, then
   // consider reusing a document node cache to reject these notifications early.
   ContentRemoved(aChildNode);
--- a/accessible/windows/msaa/XULListboxAccessibleWrap.cpp
+++ b/accessible/windows/msaa/XULListboxAccessibleWrap.cpp
@@ -1,15 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "XULListboxAccessibleWrap.h"
 
+#include "Accessible-inl.h"
+
 using namespace mozilla::a11y;
 
 ////////////////////////////////////////////////////////////////////////////////
 // XULListboxAccessibleWrap
 ////////////////////////////////////////////////////////////////////////////////
 
 NS_IMPL_ISUPPORTS_INHERITED0(XULListboxAccessibleWrap,
                              XULListboxAccessible)
--- a/accessible/windows/sdn/sdnDocAccessible.cpp
+++ b/accessible/windows/sdn/sdnDocAccessible.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "sdnDocAccessible.h"
 
+#include "Accessible-inl.h"
 #include "ISimpleDOM.h"
 
 #include "nsNameSpaceManager.h"
 
 using namespace mozilla;
 using namespace mozilla::a11y;
 
 ////////////////////////////////////////////////////////////////////////////////
--- a/accessible/windows/uia/uiaRawElmProvider.cpp
+++ b/accessible/windows/uia/uiaRawElmProvider.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "uiaRawElmProvider.h"
 
+#include "Accessible-inl.h"
 #include "AccessibleWrap.h"
 #include "ARIAMap.h"
 #include "nsIPersistentProperties2.h"
 
 using namespace mozilla;
 using namespace mozilla::a11y;
 
 ////////////////////////////////////////////////////////////////////////////////
--- a/accessible/xul/XULFormControlAccessible.cpp
+++ b/accessible/xul/XULFormControlAccessible.cpp
@@ -164,30 +164,26 @@ XULButtonAccessible::ContainerWidget() c
 
 bool
 XULButtonAccessible::IsAcceptableChild(nsIContent* aEl) const
 {
   // In general XUL button has not accessible children. Nevertheless menu
   // buttons can have button (@type="menu-button") and popup accessibles
   // (@type="menu-button", @type="menu" or columnpicker.
 
-  // XXX: no children until the button is menu button. Probably it's not
-  // totally correct but in general AT wants to have leaf buttons.
-  nsAutoString role;
-  nsCoreUtils::XBLBindingRole(aEl, role);
-
-  // Get an accessible for menupopup or panel elements.
-  if (role.EqualsLiteral("xul:menupopup")) {
+  // Get an accessible for menupopup or popup elements.
+  if (aEl->IsXULElement(nsGkAtoms::menupopup) ||
+      aEl->IsXULElement(nsGkAtoms::popup)) {
     return true;
   }
 
-  // Button type="menu-button" contains a real button. Get an accessible
+  // Button and toolbarbutton are real buttons. Get an accessible
   // for it. Ignore dropmarker button which is placed as a last child.
-  if ((!role.EqualsLiteral("xul:button") &&
-       !role.EqualsLiteral("xul:toolbarbutton")) ||
+  if ((!aEl->IsXULElement(nsGkAtoms::button) &&
+       !aEl->IsXULElement(nsGkAtoms::toolbarbutton)) ||
       aEl->IsXULElement(nsGkAtoms::dropMarker)) {
     return false;
   }
 
   return mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
                                             nsGkAtoms::menuButton, eCaseMatters);
 }
 
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<blocklist lastupdate="1519429178266" xmlns="http://www.mozilla.org/2006/addons-blocklist">
+<blocklist lastupdate="1519826648080" xmlns="http://www.mozilla.org/2006/addons-blocklist">
   <emItems>
     <emItem blockID="i334" id="{0F827075-B026-42F3-885D-98981EE7B1AE}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="i1211" id="flvto@hotger.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
@@ -2206,16 +2206,20 @@
     <emItem blockID="adfd98ef-cebc-406b-b1e0-61bd4c71e4b1" id="{f3c31b34-862c-4bc8-a98f-910cc6314a86}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="2447476f-043b-4d0b-9d3c-8e859c97d950" id="{44e4b2cf-77ba-4f76-aca7-f3fcbc2dda2f} ">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="48b14881-5f6b-4e48-afc5-3d9a7fae26a3" id="/{0c9970a2-6874-483b-a486-2296cfe251c2}|{01c9a4a4-06dd-426b-9500-2ea6fe841b88}|{1c981c7c-30e0-4ed2-955d-6b370e0a9d19}|{2aa275f8-fabc-4766-95b2-ecfc73db310b}|{2cac0be1-10a2-4a0d-b8c5-787837ea5955}|{2eb66f6c-94b3-44f5-9de2-22371236ec99}|{2f8aade6-8717-4277-b8b1-55172d364903}|{3c27c34f-8775-491a-a1c9-fcb15beb26d3}|{3f4dea3e-dbfc-428f-a88b-36908c459e20}|{3f4191fa-8f16-47d2-9414-36bfc9e0c2bf}|{4c140bc5-c2ad-41c3-a407-749473530904}|{05a21129-af2a-464c-809f-f2df4addf209}|{5da81d3d-5db1-432a-affc-4a2fe9a70749}|{5f4e63e4-351f-4a21-a8e5-e50dc72b5566}|{7c1df23b-1fd8-42b9-8752-71fff2b979de}|{7d5e24a1-7bef-4d09-a952-b9519ec00d20}|{7d932012-b4dd-42cc-8a78-b15ca82d0e61}|{7f8bc48d-1c7c-41a0-8534-54adc079338f}|{8a61507d-dc2f-4507-a9b7-7e33b8cbc31b}|{09c8fa16-4eec-4f78-b19d-9b24b1b57e1e}|{9ce2a636-0e49-4b8e-ad17-d0c156c963b0}|{11df9391-dba5-4fe2-bd48-37a9182b796d}|{23c65153-c21e-430a-a2dc-0793410a870d}|{36a4269e-4eef-4538-baea-9dafbf6a8e2f}|{37f8e483-c782-40ed-82e9-36f101b9e41f}|{63df223d-51cf-4f76-aad8-bbc94c895ed2}|{72c1ca96-c05d-46a7-bce1-c507ec3db4ea}|{76ce213c-8e57-4a14-b60a-67a5519bd7a7}|{79db6c96-d65a-4a64-a892-3d26bd02d2d9}|{81ac42f3-3d17-4cff-85af-8b7f89c8826b}|{83d38ac3-121b-4f28-bf9c-1220bd3c643b}|{86d98522-5d42-41d5-83c2-fc57f260a3d9}|{0111c475-01e6-42ea-a9b4-27bed9eb6092}|{214cb48a-ce31-4e48-82cf-a55061f1b766}|{216e0bcc-8a23-4069-8b63-d9528b437258}|{226b0fe6-f80f-48f1-9d8d-0b7a1a04e537}|{302ef84b-2feb-460e-85ca-f5397a77aa6a}|{408a506b-2336-4671-a490-83a1094b4097}|{419be4e9-c981-478e-baa0-937cf1eea1e8}|{0432b92a-bfcf-41b9-b5f0-df9629feece1}|{449e185a-dd91-4f7b-a23a-bbf6c1ca9435}|{591d1b73-5eae-47f4-a41f-8081d58d49bf}|{869b5825-e344-4375-839b-085d3c09ab9f}|{919fed43-3961-48d9-b0ef-893054f4f6f1}|{01166e60-d740-440c-b640-6bf964504b3c}|{2134e327-8060-441c-ba68-b167b82ff5bc}|{02328ee7-a82b-4983-a5f7-d0fc353698f0}|{6072a2a8-f1bc-4c9c-b836-7ac53e3f51e4}|{28044ca8-8e90-435e-bc63-a757af2fb6be}|{28092fa3-9c52-4a41-996d-c43e249c5f08}|{31680d42-c80d-4f8a-86d3-cd4930620369}|{92111c8d-0850-4606-904a-783d273a2059}|{446122cd-cd92-4d0c-9426-4ee0d28f6dca}|{829827cd-03be-4fed-af96-dd5997806fb4}|{4479446e-40f3-48af-ab85-7e3bb4468227}|{9263519f-ca57-4178-b743-2553a40a4bf1}|{71639610-9cc3-47e0-86ed-d5b99eaa41d5}|{84406197-6d37-437c-8d82-ae624b857355}|{93017064-dfd4-425e-a700-353f332ede37}|{a0ab16af-3384-4dbe-8722-476ce3947873}|{a0c54bd8-7817-4a40-b657-6dc7d59bd961}|{a2de96bc-e77f-4805-92c0-95c9a2023c6a}|{a3fbc8be-dac2-4971-b76a-908464cfa0e0}|{a42e5d48-6175-49e3-9e40-0188cde9c5c6}|{a893296e-5f54-43f9-a849-f12dcdee2c98}|{ac296b47-7c03-486f-a1d6-c48b24419749}|{b26bf964-7aa6-44f4-a2a9-d55af4b4eec0}|{be981b5e-1d9d-40dc-bd4f-47a7a027611c}|{be37931c-af60-4337-8708-63889f36445d}|{bfd92dfd-b293-4828-90c1-66af2ac688e6}|{c5cf4d08-0a33-4aa3-a40d-d4911bcc1da7}|{c488a8f5-ea3d-408d-809e-44e82c06ad9d}|{c661c2dc-00f9-4dc1-a9f6-bb2b7e1a4f8d}|{cd28aa38-d2f1-45a3-96c3-6cfd4702ef51}|{cd89045b-2e06-46bb-9e34-48e8799e5ef2}|{cf9d96ff-5997-439a-b32b-98214c621eee}|{d14acee6-f32b-4aa3-a802-6616003fc6a8}|{d97223b8-44e5-46c7-8ab5-e1d8986daf44}|{ddae89bd-6793-45d8-8ec9-7f4fb7212378}|{de3b1909-d4da-45e9-8da5-7d36a30e2fc6}|{df09f268-3c92-49db-8c31-6a25a6643896}|{e5bc3951-c837-4c98-9643-3c113fc8cf5e}|{e9ccb1f2-a8ba-4346-b43b-0d5582bce414}|{e341ed12-a703-47fe-b8dd-5948c38070e4}|{e2139287-2b0d-4f54-b3b1-c9a06c597223}|{ed352072-ddf0-4cb4-9cb6-d8aa3741c2de}|{f0b809eb-be22-432f-b26f-b1cadd1755b9}|{f1bce8e4-9936-495b-bf48-52850c7250ab}|{f01c3add-dc6d-4f35-a498-6b4279aa2ffa}|{f9e1ad25-5961-4cc5-8d66-5496c438a125}|{f4262989-6de0-4604-918f-663b85fad605}|{fc0d55bd-3c50-4139-9409-7df7c1114a9d}/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
   </emItems>
   <pluginItems>
     <pluginItem blockID="p332">
       <match exp="libflashplayer\.so" name="filename"/>
       <match exp="^Shockwave Flash 11.(0|1) r[0-9]{1,3}$" name="description"/>
       <infoURL>https://get.adobe.com/flashplayer/</infoURL>
       <versionRange severity="0" vulnerabilitystatus="1">
         <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -453,17 +453,25 @@ pref("browser.link.open_newwindow.restri
 #ifdef XP_MACOSX
 pref("browser.link.open_newwindow.disabled_in_fullscreen", true);
 #else
 pref("browser.link.open_newwindow.disabled_in_fullscreen", false);
 #endif
 
 // Tabbed browser
 pref("browser.tabs.closeWindowWithLastTab", true);
+// Open related links to a tab, e.g., link in current tab, at next to the
+// current tab if |insertRelatedAfterCurrent| is true.  Otherwise, always
+// append new tab to the end.
 pref("browser.tabs.insertRelatedAfterCurrent", true);
+// Open all links, e.g., bookmarks, history items at next to current tab
+// if |insertAfterCurrent| is true.  Otherwise, append new tab to the end
+// for non-related links. Note that if this is set to true, it will trump
+// the value of browser.tabs.insertRelatedAfterCurrent.
+pref("browser.tabs.insertAfterCurrent", false);
 pref("browser.tabs.warnOnClose", true);
 pref("browser.tabs.warnOnCloseOtherTabs", true);
 pref("browser.tabs.warnOnOpen", true);
 pref("browser.tabs.maxOpenBeforeWarn", 15);
 pref("browser.tabs.loadInBackground", true);
 pref("browser.tabs.opentabfor.middleclick", true);
 pref("browser.tabs.loadDivertedInBackground", false);
 pref("browser.tabs.loadBookmarksInBackground", false);
@@ -1042,21 +1050,17 @@ pref("dom.ipc.plugins.sandbox-level.flas
 #endif
 
 #if defined(MOZ_CONTENT_SANDBOX)
 // This controls the strength of the Windows content process sandbox for testing
 // purposes. This will require a restart.
 // On windows these levels are:
 // See - security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
 // SetSecurityLevelForContentProcess() for what the different settings mean.
-#if defined(NIGHTLY_BUILD)
 pref("security.sandbox.content.level", 5);
-#else
-pref("security.sandbox.content.level", 4);
-#endif
 
 // This controls the depth of stack trace that is logged when Windows sandbox
 // logging is turned on.  This is only currently available for the content
 // process because the only other sandbox (for GMP) has too strict a policy to
 // allow stack tracing.  This does not require a restart to take effect.
 pref("security.sandbox.windows.log.stackTraceDepth", 0);
 #endif
 
@@ -1278,19 +1282,16 @@ pref("browser.newtabpage.compact", false
 pref("browser.newtabpage.thumbnailPlaceholder", false);
 
 // number of rows of newtab grid
 pref("browser.newtabpage.rows", 3);
 
 // number of columns of newtab grid
 pref("browser.newtabpage.columns", 5);
 
-// directory tiles download URL
-pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/v3/links/fetch/%LOCALE%/%CHANNEL%");
-
 // Activity Stream prefs that control to which page to redirect
 pref("browser.newtabpage.activity-stream.prerender", true);
 #ifndef RELEASE_OR_BETA
 #ifdef MOZILLA_OFFICIAL
 pref("browser.newtabpage.activity-stream.debug", false);
 #else
 pref("browser.newtabpage.activity-stream.debug", true);
 #endif
@@ -1654,27 +1655,19 @@ pref("browser.migrate.chrome.history.max
 
 // Enable browser frames for use on desktop.  Only exposed to chrome callers.
 pref("dom.mozBrowserFramesEnabled", true);
 
 pref("extensions.pocket.enabled", true);
 
 pref("signon.schemeUpgrades", true);
 
-// "Simplify Page" feature in Print Preview. This feature is disabled by default
-// in toolkit.
-//
-// This feature is only enabled on Nightly for Linux until bug 1306295 is fixed.
-#ifdef UNIX_BUT_NOT_MAC
-#if defined(NIGHTLY_BUILD)
+// Enable the "Simplify Page" feature in Print Preview. This feature
+// is disabled by default in toolkit.
 pref("print.use_simplify_page", true);
-#endif
-#else
-pref("print.use_simplify_page", true);
-#endif
 
 // Space separated list of URLS that are allowed to send objects (instead of
 // only strings) through webchannels. This list is duplicated in mobile/android/app/mobile.js
 pref("webchannel.allowObject.urlWhitelist", "https://content.cdn.mozilla.net https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
 
 // Whether or not the browser should scan for unsubmitted
 // crash reports, and then show a notification for submitting
 // those reports.
--- a/browser/base/content/browser-feeds.js
+++ b/browser/base/content/browser-feeds.js
@@ -376,17 +376,17 @@ var FeedHandler = {
 
     // For the benefit of applications that might know how to deal with more
     // URLs than just feeds, send feed: URLs in the following format:
     //
     // http urls: replace scheme with feed, e.g.
     // http://foo.com/index.rdf -> feed://foo.com/index.rdf
     // other urls: prepend feed: scheme, e.g.
     // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf
-    let feedURI = NetUtil.newURI(aSpec);
+    let feedURI = Services.io.newURI(aSpec);
     if (feedURI.schemeIs("http")) {
       feedURI = feedURI.mutate()
                        .setScheme("feed")
                        .finalize();
       aSpec = feedURI.spec;
     } else {
       aSpec = "feed:" + aSpec;
     }
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -207,16 +207,17 @@
                     <menuitem id="menu_bookmarksSidebar"
                               key="viewBookmarksSidebarKb"
                               observes="viewBookmarksSidebar"/>
                     <menuitem id="menu_historySidebar"
                               key="key_gotoHistory"
                               observes="viewHistorySidebar"
                               label="&historyButton.label;"/>
                     <menuitem id="menu_tabsSidebar"
+                              class="sync-ui-item"
                               observes="viewTabsSidebar"
                               label="&syncedTabs.sidebar.label;"/>
                   </menupopup>
                 </menu>
                 <menuseparator/>
                 <menu id="viewFullZoomMenu" label="&fullZoom.label;"
                       accesskey="&fullZoom.accesskey;"
                       onpopupshowing="FullZoom.updateMenu();">
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -476,17 +476,17 @@ var PlacesCommandHook = {
       let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
       PlacesUIUtils.showBookmarkDialog({ action: "edit", node }, window.top);
       return;
     }
 
     let parentGuid = parentId == PlacesUtils.bookmarksMenuFolderId ?
                        PlacesUtils.bookmarks.menuGuid :
                        await PlacesUtils.promiseItemGuid(parentId);
-    let defaultInsertionPoint = new InsertionPoint({ parentId, parentGuid });
+    let defaultInsertionPoint = new PlacesInsertionPoint({ parentId, parentGuid });
     PlacesUIUtils.showBookmarkDialog({ action: "add",
                                        type: "bookmark",
                                        uri: makeURI(url),
                                        title,
                                        description,
                                        defaultInsertionPoint,
                                        hiddenRows: [ "description",
                                                      "location",
@@ -552,17 +552,17 @@ var PlacesCommandHook = {
    * @param     url
    *            The nsIURI of the page the feed was attached to
    * @title     title
    *            The title of the feed. Optional.
    * @subtitle  subtitle
    *            A short description of the feed. Optional.
    */
   async addLiveBookmark(url, feedTitle, feedSubtitle) {
-    let toolbarIP = new InsertionPoint({
+    let toolbarIP = new PlacesInsertionPoint({
       parentId: PlacesUtils.toolbarFolderId,
       parentGuid: PlacesUtils.bookmarks.toolbarGuid
     });
 
     let feedURI = makeURI(url);
     let title = feedTitle || gBrowser.contentTitle;
     let description = feedSubtitle;
     if (!description) {
@@ -988,34 +988,34 @@ var PlacesMenuDNDHandler = {
   },
 
   /**
    * Called when the user drags over the <menu> element.
    * @param   event
    *          The DragOver event.
    */
   onDragOver: function PMDH_onDragOver(event) {
-    let ip = new InsertionPoint({
+    let ip = new PlacesInsertionPoint({
       parentId: PlacesUtils.bookmarksMenuFolderId,
       parentGuid: PlacesUtils.bookmarks.menuGuid
     });
     if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer))
       event.preventDefault();
 
     event.stopPropagation();
   },
 
   /**
    * Called when the user drops on the <menu> element.
    * @param   event
    *          The Drop event.
    */
   onDrop: function PMDH_onDrop(event) {
     // Put the item at the end of bookmark menu.
-    let ip = new InsertionPoint({
+    let ip = new PlacesInsertionPoint({
       parentId: PlacesUtils.bookmarksMenuFolderId,
       parentGuid: PlacesUtils.bookmarks.menuGuid
     });
     PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
     PlacesControllerDragHelper.currentDropTarget = null;
     event.stopPropagation();
   }
 };
--- a/browser/base/content/browser-sidebar.js
+++ b/browser/base/content/browser-sidebar.js
@@ -355,47 +355,60 @@ var SidebarUI = {
 
     if (this.isOpen && commandID == this.currentID) {
       this.hide(triggerNode);
       return Promise.resolve();
     }
     return this.show(commandID, triggerNode);
   },
 
+  _loadSidebarExtension(sidebarBroadcaster) {
+    let extensionId = sidebarBroadcaster.getAttribute("extensionId");
+    if (extensionId) {
+      let extensionUrl = sidebarBroadcaster.getAttribute("panel");
+      let browserStyle = sidebarBroadcaster.getAttribute("browserStyle");
+      SidebarUI.browser.contentWindow.loadPanel(extensionId, extensionUrl, browserStyle);
+    }
+  },
+
   /**
    * Show the sidebar, using the parameters from the specified broadcaster.
    * @see SidebarUI note.
    *
    * This wraps the internal method, including a ping to telemetry.
    *
    * @param {string}  commandID     ID of the xul:broadcaster element to use.
    * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
    *                                showing of the sidebar.
    */
   show(commandID, triggerNode) {
-    return this._show(commandID).then(() => {
+    return this._show(commandID).then((sidebarBroadcaster) => {
+      this._loadSidebarExtension(sidebarBroadcaster);
+
       if (triggerNode) {
         updateToggleControlLabel(triggerNode);
       }
 
       this._fireFocusedEvent();
       BrowserUITelemetry.countSidebarEvent(commandID, "show");
     });
   },
 
   /**
    * Show the sidebar, without firing the focused event or logging telemetry.
    * This is intended to be used when the sidebar is opened automatically
    * when a window opens (not triggered by user interaction).
    *
    * @param {string} commandID ID of the xul:broadcaster element to use.
    */
-   showInitially(commandID) {
-     return this._show(commandID);
-   },
+  showInitially(commandID) {
+    return this._show(commandID).then((sidebarBroadcaster) => {
+      this._loadSidebarExtension(sidebarBroadcaster);
+    });
+  },
 
   /**
    * Implementation for show. Also used internally for sidebars that are shown
    * when a window is opened and we don't want to ping telemetry.
    *
    * @param {string} commandID ID of the xul:broadcaster element to use.
    */
   _show(commandID) {
@@ -441,24 +454,24 @@ var SidebarUI = {
       let url = sidebarBroadcaster.getAttribute("sidebarurl");
       this.browser.setAttribute("src", url); // kick off async load
 
       if (this.browser.contentDocument.location.href != url) {
         this.browser.addEventListener("load", event => {
           // We're handling the 'load' event before it bubbles up to the usual
           // (non-capturing) event handlers. Let it bubble up before resolving.
           setTimeout(() => {
-            resolve();
+            resolve(sidebarBroadcaster);
 
             // Now that the currentId is updated, fire a show event.
             this._fireShowEvent();
           }, 0);
         }, {capture: true, once: true});
       } else {
-        resolve();
+        resolve(sidebarBroadcaster);
 
         // Now that the currentId is updated, fire a show event.
         this._fireShowEvent();
       }
 
       let selBrowser = gBrowser.selectedBrowser;
       selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
         {commandID, isOpen: true}
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -29,16 +29,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   E10SUtils: "resource://gre/modules/E10SUtils.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
   LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
   LightweightThemeConsumer: "resource://gre/modules/LightweightThemeConsumer.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   Log: "resource://gre/modules/Log.jsm",
   LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
+  NetUtil: "resource://gre/modules/NetUtil.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
   OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm",
   PageActions: "resource:///modules/PageActions.jsm",
   PageThumbs: "resource://gre/modules/PageThumbs.jsm",
   PanelMultiView: "resource:///modules/PanelMultiView.jsm",
   PanelView: "resource:///modules/PanelMultiView.jsm",
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
@@ -1365,16 +1366,23 @@ var gBrowserInit = {
         Cu.reportError(e);
       }
     }
 
     // Wait until chrome is painted before executing code not critical to making the window visible
     this._boundDelayedStartup = this._delayedStartup.bind(this);
     window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
 
+    if (!PrivateBrowsingUtils.enabled) {
+      document.getElementById("Tools:PrivateBrowsing").hidden = true;
+      // Setting disabled doesn't disable the shortcut, so we just remove
+      // the keybinding.
+      document.getElementById("key_privatebrowsing").remove();
+    }
+
     this._loadHandled = true;
   },
 
   _cancelDelayedStartup() {
     window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
     this._boundDelayedStartup = null;
   },
 
@@ -1966,16 +1974,19 @@ if (AppConstants.platform == "macosx") {
         } catch (e) {
         }
       }
     }
 
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       document.getElementById("macDockMenuNewWindow").hidden = true;
     }
+    if (!PrivateBrowsingUtils.enabled) {
+      document.getElementById("macDockMenuNewPrivateWindow").hidden = true;
+    }
 
     this._delayedStartupTimeoutId = setTimeout(this.nonBrowserWindowDelayedStartup.bind(this), 0);
   };
 
   gBrowserInit.nonBrowserWindowDelayedStartup = function() {
     this._delayedStartupTimeoutId = null;
 
     // initialise the offline listener
@@ -4134,17 +4145,17 @@ function OpenBrowserWindow(options) {
   TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj);
 
   var handler = Cc["@mozilla.org/browser/clh;1"]
                   .getService(Ci.nsIBrowserHandler);
   var defaultArgs = handler.defaultArgs;
   var wintype = document.documentElement.getAttribute("windowtype");
 
   var extraFeatures = "";
-  if (options && options.private) {
+  if (options && options.private && PrivateBrowsingUtils.enabled) {
     extraFeatures = ",private";
     if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
       // Force the new window to load about:privatebrowsing instead of the default home page
       defaultArgs = "about:privatebrowsing";
     }
   } else {
     extraFeatures = ",non-private";
   }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -293,17 +293,17 @@
                      class="subviewbutton subviewbutton-iconic"
                      key="key_gotoHistory"
                      observes="viewHistorySidebar"
                      oncommand="SidebarUI.show('viewHistorySidebar');">
         <observes element="viewHistorySidebar" attribute="checked"/>
       </toolbarbutton>
       <toolbarbutton id="sidebar-switcher-tabs"
                      label="&syncedTabs.sidebar.label;"
-                     class="subviewbutton subviewbutton-iconic"
+                     class="subviewbutton subviewbutton-iconic sync-ui-item"
                      observes="viewTabsSidebar"
                      oncommand="SidebarUI.show('viewTabsSidebar');">
         <observes element="viewTabsSidebar" attribute="checked"/>
       </toolbarbutton>
       <toolbarseparator/>
       <!-- Extension toolbarbuttons go here. -->
       <toolbarseparator id="sidebar-extensions-separator"/>
       <toolbarbutton id="sidebar-reverse-position"
--- a/browser/base/content/macBrowserOverlay.xul
+++ b/browser/base/content/macBrowserOverlay.xul
@@ -53,13 +53,14 @@
 
 <!-- Dock menu -->
 <popupset>
   <menupopup id="menu_mac_dockmenu">
     <!-- The command cannot be cmd_newNavigator because we need to activate
          the application. -->
     <menuitem label="&newNavigatorCmd.label;" oncommand="OpenBrowserWindowFromDockMenu();"
               id="macDockMenuNewWindow" />
-    <menuitem label="&newPrivateWindow.label;" oncommand="OpenBrowserWindowFromDockMenu({private: true});" />
+    <menuitem label="&newPrivateWindow.label;" oncommand="OpenBrowserWindowFromDockMenu({private: true});"
+              id="macDockMenuNewPrivateWindow" />
   </menupopup>
 </popupset>
 
 </overlay>
--- a/browser/base/content/newtab/newTab.js
+++ b/browser/base/content/newtab/newTab.js
@@ -3,17 +3,16 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/PageThumbs.jsm");
 ChromeUtils.import("resource://gre/modules/BackgroundPageThumbs.jsm");
-ChromeUtils.import("resource:///modules/DirectoryLinksProvider.jsm");
 ChromeUtils.import("resource://gre/modules/NewTabUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "Rect",
   "resource://gre/modules/Geometry.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 var {
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -130,17 +130,17 @@ Site.prototype = {
     return str;
   },
 
   /**
    * Renders the site's data (fills the HTML fragment).
    */
   _render: function Site_render() {
     // setup display variables
-    let enhanced = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link);
+    let enhanced = gAllPages.enhanced;
     let url = this.url;
     let title = enhanced && enhanced.title ? enhanced.title :
                 this.link.type == "history" ? this.link.baseDomain :
                 this.title;
     let tooltip = (this.title == url ? this.title : this.title + "\n" + url);
 
     let link = this._querySelector(".newtab-link");
     link.setAttribute("title", tooltip);
@@ -173,18 +173,17 @@ Site.prototype = {
     }
   },
 
   /**
    * Refreshes the thumbnail for the site.
    */
   refreshThumbnail: function Site_refreshThumbnail() {
     // Only enhance tiles if that feature is turned on
-    let link = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link) ||
-               this.link;
+    let link = gAllPages.enhanced || this.link;
 
     let thumbnail = this._querySelector(".newtab-thumbnail.thumbnail");
     if (link.bgColor) {
       thumbnail.style.backgroundColor = link.bgColor;
     }
     let uri = link.imageURI || PageThumbs.getThumbnailURL(this.url);
     thumbnail.src = uri;
 
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -11,16 +11,17 @@ ChromeUtils.import("resource://gre/modul
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
   LoginHelper: "resource://gre/modules/LoginHelper.jsm",
   LoginManagerContextMenu: "resource://gre/modules/LoginManagerContextMenu.jsm",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   DevToolsShim: "chrome://devtools-shim/content/DevToolsShim.jsm",
+  NetUtil: "resource://gre/modules/NetUtil.jsm",
 });
 
 var gContextMenuContentData = null;
 
 function setContextMenuContentData(data) {
   gContextMenuContentData = data;
 }
 
@@ -353,17 +354,17 @@ nsContextMenu.prototype = {
          gBrowserBundle.formatStringFromName("userContextOpenLink.label",
                                              [label], 1));
     }
 
     var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
     var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
     var showContainers = Services.prefs.getBoolPref("privacy.userContext.enabled");
     this.showItem("context-openlink", shouldShow && !isWindowPrivate);
-    this.showItem("context-openlinkprivate", shouldShow);
+    this.showItem("context-openlinkprivate", shouldShow && PrivateBrowsingUtils.enabled);
     this.showItem("context-openlinkintab", shouldShow && !inContainer);
     this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
     this.showItem("context-openlinkinusercontext-menu", shouldShow && !isWindowPrivate && showContainers);
     this.showItem("context-openlinkincurrent", this.onPlainTextLink);
     this.showItem("context-sep-open", shouldShow);
   },
 
   initNavigationItems: function CM_initNavigationItems() {
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -196,17 +196,17 @@ class TabBrowser {
     // set up the shared autoscroll popup
     this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
     this._autoScrollPopup.id = "autoscroller";
     this.appendChild(this._autoScrollPopup);
     this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
     this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
 
     // Hook up the event listeners to the first browser
-    var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false);
+    var tabListener = new TabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false);
     const nsIWebProgress = Ci.nsIWebProgress;
     const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
       .createInstance(nsIWebProgress);
     filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
     this._tabListeners.set(this.mCurrentTab, tabListener);
     this._tabFilters.set(this.mCurrentTab, filter);
     this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL);
 
@@ -759,447 +759,16 @@ class TabBrowser {
       ).URI;
       return resolvedURI.schemeIs("jar") || resolvedURI.schemeIs("file");
     } catch (ex) {
       // aURI might be invalid.
       return false;
     }
   }
 
-  /**
-   * A web progress listener object definition for a given tab.
-   */
-  mTabProgressListener(aTab, aBrowser, aStartsBlank, aWasPreloadedBrowser, aOrigStateFlags) {
-    let stateFlags = aOrigStateFlags || 0;
-    // Initialize mStateFlags to non-zero e.g. when creating a progress
-    // listener for preloaded browsers as there was no progress listener
-    // around when the content started loading. If the content didn't
-    // quite finish loading yet, mStateFlags will very soon be overridden
-    // with the correct value and end up at STATE_STOP again.
-    if (aWasPreloadedBrowser) {
-      stateFlags = Ci.nsIWebProgressListener.STATE_STOP |
-        Ci.nsIWebProgressListener.STATE_IS_REQUEST;
-    }
-
-    return ({
-      mTabBrowser: this,
-      mTab: aTab,
-      mBrowser: aBrowser,
-      mBlank: aStartsBlank,
-
-      // cache flags for correct status UI update after tab switching
-      mStateFlags: stateFlags,
-      mStatus: 0,
-      mMessage: "",
-      mTotalProgress: 0,
-
-      // count of open requests (should always be 0 or 1)
-      mRequestCount: 0,
-
-      destroy() {
-        delete this.mTab;
-        delete this.mBrowser;
-        delete this.mTabBrowser;
-      },
-
-      _callProgressListeners() {
-        Array.unshift(arguments, this.mBrowser);
-        return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments);
-      },
-
-      _shouldShowProgress(aRequest) {
-        if (this.mBlank)
-          return false;
-
-        // Don't show progress indicators in tabs for about: URIs
-        // pointing to local resources.
-        if ((aRequest instanceof Ci.nsIChannel) &&
-            this.mTabBrowser._isLocalAboutURI(aRequest.originalURI, aRequest.URI)) {
-          return false;
-        }
-
-        return true;
-      },
-
-      _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
-        if (!this.mBlank || !aWebProgress.isTopLevel) {
-          return false;
-        }
-
-        // If the state has STATE_STOP, and no requests were in flight, then this
-        // must be the initial "stop" for the initial about:blank document.
-        const nsIWebProgressListener = Ci.nsIWebProgressListener;
-        if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
-            this.mRequestCount == 0 &&
-            !aLocation) {
-          return true;
-        }
-
-        let location = aLocation ? aLocation.spec : "";
-        return location == "about:blank";
-      },
-
-      onProgressChange(aWebProgress, aRequest,
-        aCurSelfProgress, aMaxSelfProgress,
-        aCurTotalProgress, aMaxTotalProgress) {
-        this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0;
-
-        if (!this._shouldShowProgress(aRequest))
-          return;
-
-        if (this.mTotalProgress && this.mTab.hasAttribute("busy"))
-          this.mTab.setAttribute("progress", "true");
-
-        this._callProgressListeners("onProgressChange",
-                                    [aWebProgress, aRequest,
-                                     aCurSelfProgress, aMaxSelfProgress,
-                                     aCurTotalProgress, aMaxTotalProgress]);
-      },
-
-      onProgressChange64(aWebProgress, aRequest,
-        aCurSelfProgress, aMaxSelfProgress,
-        aCurTotalProgress, aMaxTotalProgress) {
-        return this.onProgressChange(aWebProgress, aRequest,
-          aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
-          aMaxTotalProgress);
-      },
-
-      /* eslint-disable complexity */
-      onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
-        if (!aRequest)
-          return;
-
-        const nsIWebProgressListener = Ci.nsIWebProgressListener;
-        const nsIChannel = Ci.nsIChannel;
-        let location, originalLocation;
-        try {
-          aRequest.QueryInterface(nsIChannel);
-          location = aRequest.URI;
-          originalLocation = aRequest.originalURI;
-        } catch (ex) {}
-
-        let ignoreBlank = this._isForInitialAboutBlank(aWebProgress, aStateFlags,
-          location);
-
-        // If we were ignoring some messages about the initial about:blank, and we
-        // got the STATE_STOP for it, we'll want to pay attention to those messages
-        // from here forward. Similarly, if we conclude that this state change
-        // is one that we shouldn't be ignoring, then stop ignoring.
-        if ((ignoreBlank &&
-            aStateFlags & nsIWebProgressListener.STATE_STOP &&
-            aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) ||
-            !ignoreBlank && this.mBlank) {
-          this.mBlank = false;
-        }
-
-        if (aStateFlags & nsIWebProgressListener.STATE_START) {
-          this.mRequestCount++;
-        } else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
-          const NS_ERROR_UNKNOWN_HOST = 2152398878;
-          if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
-            // to prevent bug 235825: wait for the request handled
-            // by the automatic keyword resolver
-            return;
-          }
-          // since we (try to) only handle STATE_STOP of the last request,
-          // the count of open requests should now be 0
-          this.mRequestCount = 0;
-        }
-
-        if (aStateFlags & nsIWebProgressListener.STATE_START &&
-            aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
-          if (aWebProgress.isTopLevel) {
-            // Need to use originalLocation rather than location because things
-            // like about:home and about:privatebrowsing arrive with nsIRequest
-            // pointing to their resolved jar: or file: URIs.
-            if (!(originalLocation && gInitialPages.includes(originalLocation.spec) &&
-                originalLocation != "about:blank" &&
-                this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec &&
-                this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) {
-              // Indicating that we started a load will allow the location
-              // bar to be cleared when the load finishes.
-              // In order to not overwrite user-typed content, we avoid it
-              // (see if condition above) in a very specific case:
-              // If the load is of an 'initial' page (e.g. about:privatebrowsing,
-              // about:newtab, etc.), was not explicitly typed in the location
-              // bar by the user, is not about:blank (because about:blank can be
-              // loaded by websites under their principal), and the current
-              // page in the browser is about:blank (indicating it is a newly
-              // created or re-created browser, e.g. because it just switched
-              // remoteness or is a new tab/window).
-              this.mBrowser.urlbarChangeTracker.startedLoad();
-            }
-            delete this.mBrowser.initialPageLoadedFromURLBar;
-            // If the browser is loading it must not be crashed anymore
-            this.mTab.removeAttribute("crashed");
-          }
-
-          if (this._shouldShowProgress(aRequest)) {
-            if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING) &&
-                aWebProgress && aWebProgress.isTopLevel) {
-              this.mTab.setAttribute("busy", "true");
-              this.mTab._notselectedsinceload = !this.mTab.selected;
-              SchedulePressure.startMonitoring(window, {
-                highPressureFn() {
-                  // Only switch back to the SVG loading indicator after getting
-                  // three consecutive low pressure callbacks. Used to prevent
-                  // switching quickly between the SVG and APNG loading indicators.
-                  gBrowser.tabContainer._schedulePressureCount = gBrowser.schedulePressureDefaultCount;
-                  gBrowser.tabContainer.setAttribute("schedulepressure", "true");
-                },
-                lowPressureFn() {
-                  if (!gBrowser.tabContainer._schedulePressureCount ||
-                    --gBrowser.tabContainer._schedulePressureCount <= 0) {
-                    gBrowser.tabContainer.removeAttribute("schedulepressure");
-                  }
-
-                  // If tabs are closed while they are loading we need to
-                  // stop monitoring schedule pressure. We don't stop monitoring
-                  // during high pressure times because we want to eventually
-                  // return to the SVG tab loading animations.
-                  let continueMonitoring = true;
-                  if (!document.querySelector(".tabbrowser-tab[busy]")) {
-                    SchedulePressure.stopMonitoring(window);
-                    continueMonitoring = false;
-                  }
-                  return { continueMonitoring };
-                },
-              });
-              this.mTabBrowser.syncThrobberAnimations(this.mTab);
-            }
-
-            if (this.mTab.selected) {
-              this.mTabBrowser.mIsBusy = true;
-            }
-          }
-        } else if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
-                   aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
-
-          if (this.mTab.hasAttribute("busy")) {
-            this.mTab.removeAttribute("busy");
-            if (!document.querySelector(".tabbrowser-tab[busy]")) {
-              SchedulePressure.stopMonitoring(window);
-              this.mTabBrowser.tabContainer.removeAttribute("schedulepressure");
-            }
-
-            // Only animate the "burst" indicating the page has loaded if
-            // the top-level page is the one that finished loading.
-            if (aWebProgress.isTopLevel && !aWebProgress.isLoadingDocument &&
-                Components.isSuccessCode(aStatus) &&
-                !this.mTabBrowser.tabAnimationsInProgress &&
-                Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled")) {
-              if (this.mTab._notselectedsinceload) {
-                this.mTab.setAttribute("notselectedsinceload", "true");
-              } else {
-                this.mTab.removeAttribute("notselectedsinceload");
-              }
-
-              this.mTab.setAttribute("bursting", "true");
-            }
-
-            this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]);
-            if (!this.mTab.selected)
-              this.mTab.setAttribute("unread", "true");
-          }
-          this.mTab.removeAttribute("progress");
-
-          if (aWebProgress.isTopLevel) {
-            let isSuccessful = Components.isSuccessCode(aStatus);
-            if (!isSuccessful && !isTabEmpty(this.mTab)) {
-              // Restore the current document's location in case the
-              // request was stopped (possibly from a content script)
-              // before the location changed.
-
-              this.mBrowser.userTypedValue = null;
-
-              let inLoadURI = this.mBrowser.inLoadURI;
-              if (this.mTab.selected && gURLBar && !inLoadURI) {
-                URLBarSetURI();
-              }
-            } else if (isSuccessful) {
-              this.mBrowser.urlbarChangeTracker.finishedLoad();
-            }
-
-            // Ignore initial about:blank to prevent flickering.
-            if (!this.mBrowser.mIconURL && !ignoreBlank) {
-              // Don't switch to the default icon on about:home or about:newtab,
-              // since these pages get their favicon set in browser code to
-              // improve perceived performance.
-              let isNewTab = originalLocation &&
-                (originalLocation.spec == "about:newtab" ||
-                  originalLocation.spec == "about:privatebrowsing" ||
-                  originalLocation.spec == "about:home");
-              if (!isNewTab) {
-                this.mTabBrowser.useDefaultIcon(this.mTab);
-              }
-            }
-          }
-
-          // For keyword URIs clear the user typed value since they will be changed into real URIs
-          if (location.scheme == "keyword")
-            this.mBrowser.userTypedValue = null;
-
-          if (this.mTab.selected)
-            this.mTabBrowser.mIsBusy = false;
-        }
-
-        if (ignoreBlank) {
-          this._callProgressListeners("onUpdateCurrentBrowser",
-                                      [aStateFlags, aStatus, "", 0],
-                                      true, false);
-        } else {
-          this._callProgressListeners("onStateChange",
-                                      [aWebProgress, aRequest, aStateFlags, aStatus],
-                                      true, false);
-        }
-
-        this._callProgressListeners("onStateChange",
-                                    [aWebProgress, aRequest, aStateFlags, aStatus],
-                                    false);
-
-        if (aStateFlags & (nsIWebProgressListener.STATE_START |
-            nsIWebProgressListener.STATE_STOP)) {
-          // reset cached temporary values at beginning and end
-          this.mMessage = "";
-          this.mTotalProgress = 0;
-        }
-        this.mStateFlags = aStateFlags;
-        this.mStatus = aStatus;
-      },
-      /* eslint-enable complexity */
-
-      onLocationChange(aWebProgress, aRequest, aLocation,
-        aFlags) {
-        // OnLocationChange is called for both the top-level content
-        // and the subframes.
-        let topLevel = aWebProgress.isTopLevel;
-
-        if (topLevel) {
-          let isSameDocument = !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
-          // We need to clear the typed value
-          // if the document failed to load, to make sure the urlbar reflects the
-          // failed URI (particularly for SSL errors). However, don't clear the value
-          // if the error page's URI is about:blank, because that causes complete
-          // loss of urlbar contents for invalid URI errors (see bug 867957).
-          // Another reason to clear the userTypedValue is if this was an anchor
-          // navigation initiated by the user.
-          if (this.mBrowser.didStartLoadSinceLastUserTyping() ||
-              ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
-                aLocation.spec != "about:blank") ||
-              (isSameDocument && this.mBrowser.inLoadURI)) {
-            this.mBrowser.userTypedValue = null;
-          }
-
-          // If the tab has been set to "busy" outside the stateChange
-          // handler below (e.g. by sessionStore.navigateAndRestore), and
-          // the load results in an error page, it's possible that there
-          // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
-          // attribute being removed. In this case we should remove the
-          // attribute here.
-          if ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
-              this.mTab.hasAttribute("busy")) {
-            this.mTab.removeAttribute("busy");
-            this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]);
-          }
-
-          // If the browser was playing audio, we should remove the playing state.
-          if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) {
-            clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
-            this.mTab._soundPlayingAttrRemovalTimer = 0;
-            this.mTab.removeAttribute("soundplaying");
-            this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
-          }
-
-          // If the browser was previously muted, we should restore the muted state.
-          if (this.mTab.hasAttribute("muted")) {
-            this.mTab.linkedBrowser.mute();
-          }
-
-          if (this.mTabBrowser.isFindBarInitialized(this.mTab)) {
-            let findBar = this.mTabBrowser.getFindBar(this.mTab);
-
-            // Close the Find toolbar if we're in old-style TAF mode
-            if (findBar.findMode != findBar.FIND_NORMAL) {
-              findBar.close();
-            }
-          }
-
-          this.mTabBrowser.setTabTitle(this.mTab);
-
-          // Don't clear the favicon if this tab is in the pending
-          // state, as SessionStore will have set the icon for us even
-          // though we're pointed at an about:blank. Also don't clear it
-          // if onLocationChange was triggered by a pushState or a
-          // replaceState (bug 550565) or a hash change (bug 408415).
-          if (!this.mTab.hasAttribute("pending") &&
-              aWebProgress.isLoadingDocument &&
-              !isSameDocument) {
-            this.mBrowser.mIconURL = null;
-          }
-
-          let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
-          if (this.mBrowser.registeredOpenURI) {
-            this.mTabBrowser._unifiedComplete
-              .unregisterOpenPage(this.mBrowser.registeredOpenURI,
-                userContextId);
-            delete this.mBrowser.registeredOpenURI;
-          }
-          // Tabs in private windows aren't registered as "Open" so
-          // that they don't appear as switch-to-tab candidates.
-          if (!isBlankPageURL(aLocation.spec) &&
-              (!PrivateBrowsingUtils.isWindowPrivate(window) ||
-                PrivateBrowsingUtils.permanentPrivateBrowsing)) {
-            this.mTabBrowser._unifiedComplete
-              .registerOpenPage(aLocation, userContextId);
-            this.mBrowser.registeredOpenURI = aLocation;
-          }
-        }
-
-        if (!this.mBlank) {
-          this._callProgressListeners("onLocationChange",
-                                      [aWebProgress, aRequest, aLocation, aFlags]);
-        }
-
-        if (topLevel) {
-          this.mBrowser.lastURI = aLocation;
-          this.mBrowser.lastLocationChange = Date.now();
-        }
-      },
-
-      onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
-        if (this.mBlank)
-          return;
-
-        this._callProgressListeners("onStatusChange",
-                                    [aWebProgress, aRequest, aStatus, aMessage]);
-
-        this.mMessage = aMessage;
-      },
-
-      onSecurityChange(aWebProgress, aRequest, aState) {
-        this._callProgressListeners("onSecurityChange",
-                                    [aWebProgress, aRequest, aState]);
-      },
-
-      onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
-        return this._callProgressListeners("onRefreshAttempted",
-                                           [aWebProgress, aURI, aDelay, aSameURI]);
-      },
-
-      QueryInterface(aIID) {
-        if (aIID.equals(Ci.nsIWebProgressListener) ||
-          aIID.equals(Ci.nsIWebProgressListener2) ||
-          aIID.equals(Ci.nsISupportsWeakReference) ||
-          aIID.equals(Ci.nsISupports))
-          return this;
-        throw Cr.NS_NOINTERFACE;
-      }
-    });
-  }
-
   storeIcon(aBrowser, aURI, aLoadingPrincipal, aRequestContextID) {
     try {
       if (!(aURI instanceof Ci.nsIURI)) {
         aURI = makeURI(aURI);
       }
       PlacesUIUtils.loadFavicon(aBrowser, aLoadingPrincipal, aURI, aRequestContextID);
     } catch (ex) {
       Cu.reportError(ex);
@@ -2110,17 +1679,17 @@ class TabBrowser {
     // As frameLoaders start out with an active docShell we have to
     // deactivate it if this is not the selected tab's browser or the
     // browser window is minimized.
     aBrowser.docShellIsActive = this.shouldActivateDocShell(aBrowser);
 
     // Create a new tab progress listener for the new browser we just injected,
     // since tab progress listeners have logic for handling the initial about:blank
     // load
-    listener = this.mTabProgressListener(tab, aBrowser, true, false);
+    listener = new TabProgressListener(tab, aBrowser, true, false);
     this._tabListeners.set(tab, listener);
     filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
 
     // Restore the progress listener.
     aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
 
     // Restore the securityUI state.
     let securityUI = aBrowser.securityUI;
@@ -2511,17 +2080,17 @@ class TabBrowser {
       // browser element, which fires off a bunch of notifications. Some
       // of those notifications can cause code to run that inspects our
       // state, so it is important that the tab element is fully
       // initialized by this point.
       this.mPanelContainer.appendChild(notificationbox);
     }
 
     // wire up a progress listener for the new browser object.
-    let tabListener = this.mTabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent);
+    let tabListener = new TabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent);
     const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
       .createInstance(Ci.nsIWebProgress);
     filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
     browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
     this._tabListeners.set(aTab, tabListener);
     this._tabFilters.set(aTab, filter);
 
     browser.droppedLinkHandler = handleDroppedLink;
@@ -2906,30 +2475,32 @@ class TabBrowser {
           charset: aCharset,
           postData: aPostData,
         });
       } catch (ex) {
         Cu.reportError(ex);
       }
     }
 
-    // If we're opening a tab related to the an existing tab, move it
-    // to a position after that tab.
-    if (openerTab &&
-        Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
-
-      let lastRelatedTab = this._lastRelatedTabMap.get(openerTab);
-      let newTabPos = (lastRelatedTab || openerTab)._tPos + 1;
-      if (lastRelatedTab)
-        lastRelatedTab.owner = null;
-      else
-        t.owner = openerTab;
-      this.moveTabTo(t, newTabPos, true);
+    // Move the new tab after another tab if needed.
+    if ((openerTab &&
+         Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) ||
+         Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")) {
+
+    let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab);
+    let newTabPos = (lastRelatedTab || openerTab || this.mCurrentTab)._tPos + 1;
+
+    if (lastRelatedTab)
+      lastRelatedTab.owner = null;
+    else if (openerTab)
+      t.owner = openerTab;
+    this.moveTabTo(t, newTabPos, true);
+    if (openerTab)
       this._lastRelatedTabMap.set(openerTab, t);
-    }
+  }
 
     // This field is updated regardless if we actually animate
     // since it's important that we keep this count correct in all cases.
     this.tabAnimationsInProgress++;
 
     if (animate) {
       requestAnimationFrame(function() {
         // kick the animation off
@@ -3596,17 +3167,17 @@ class TabBrowser {
     let tabListener = otherTabBrowser._tabListeners.get(aOtherTab);
     otherBrowser.webProgress.removeProgressListener(filter);
     filter.removeProgressListener(tabListener);
 
     // Perform the docshell swap through the common mechanism.
     this._swapBrowserDocShells(aOurTab, otherBrowser, aFlags);
 
     // Restore the listeners for the swapped in tab.
-    tabListener = otherTabBrowser.mTabProgressListener(aOtherTab, otherBrowser, false, false);
+    tabListener = new otherTabBrowser.ownerGlobal.TabProgressListener(aOtherTab, otherBrowser, false, false);
     otherTabBrowser._tabListeners.set(aOtherTab, tabListener);
 
     const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
     filter.addProgressListener(tabListener, notifyAll);
     otherBrowser.webProgress.addProgressListener(filter, notifyAll);
   }
 
   _swapBrowserDocShells(aOurTab, aOtherBrowser, aFlags, aStateFlags) {
@@ -3662,18 +3233,17 @@ class TabBrowser {
         let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
         if (otherTab) {
           otherTab.permanentKey = aOtherBrowser.permanentKey;
         }
       }
     }
 
     // Restore the progress listener
-    tabListener = this.mTabProgressListener(aOurTab, ourBrowser, false, false,
-      aStateFlags);
+    tabListener = new TabProgressListener(aOurTab, ourBrowser, false, false, aStateFlags);
     this._tabListeners.set(aOurTab, tabListener);
 
     const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
     filter.addProgressListener(tabListener, notifyAll);
     ourBrowser.webProgress.addProgressListener(filter, notifyAll);
   }
 
   _swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) {
@@ -5730,8 +5300,433 @@ class TabBrowser {
         this._tabAttrModified(tab, ["activemedia-blocked"]);
         let hist = Services.telemetry.getHistogramById("TAB_AUDIO_INDICATOR_USED");
         hist.add(2 /* unblockByVisitingTab */ );
         tab.finishMediaBlockTimer();
       }
     });
   }
 }
+
+/**
+ * A web progress listener object definition for a given tab.
+ */
+class TabProgressListener {
+  constructor(aTab, aBrowser, aStartsBlank, aWasPreloadedBrowser, aOrigStateFlags) {
+    let stateFlags = aOrigStateFlags || 0;
+    // Initialize mStateFlags to non-zero e.g. when creating a progress
+    // listener for preloaded browsers as there was no progress listener
+    // around when the content started loading. If the content didn't
+    // quite finish loading yet, mStateFlags will very soon be overridden
+    // with the correct value and end up at STATE_STOP again.
+    if (aWasPreloadedBrowser) {
+      stateFlags = Ci.nsIWebProgressListener.STATE_STOP |
+        Ci.nsIWebProgressListener.STATE_IS_REQUEST;
+    }
+
+    this.mTab = aTab;
+    this.mBrowser = aBrowser;
+    this.mBlank = aStartsBlank;
+
+    // cache flags for correct status UI update after tab switching
+    this.mStateFlags = stateFlags;
+    this.mStatus = 0;
+    this.mMessage = "";
+    this.mTotalProgress = 0;
+
+    // count of open requests (should always be 0 or 1)
+    this.mRequestCount = 0;
+  }
+
+  destroy() {
+    delete this.mTab;
+    delete this.mBrowser;
+  }
+
+  _callProgressListeners() {
+    Array.unshift(arguments, this.mBrowser);
+    return gBrowser._callProgressListeners.apply(gBrowser, arguments);
+  }
+
+  _shouldShowProgress(aRequest) {
+    if (this.mBlank)
+      return false;
+
+    // Don't show progress indicators in tabs for about: URIs
+    // pointing to local resources.
+    if ((aRequest instanceof Ci.nsIChannel) &&
+        gBrowser._isLocalAboutURI(aRequest.originalURI, aRequest.URI)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  _isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
+    if (!this.mBlank || !aWebProgress.isTopLevel) {
+      return false;
+    }
+
+    // If the state has STATE_STOP, and no requests were in flight, then this
+    // must be the initial "stop" for the initial about:blank document.
+    const nsIWebProgressListener = Ci.nsIWebProgressListener;
+    if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
+        this.mRequestCount == 0 &&
+        !aLocation) {
+      return true;
+    }
+
+    let location = aLocation ? aLocation.spec : "";
+    return location == "about:blank";
+  }
+
+  onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress,
+                   aCurTotalProgress, aMaxTotalProgress) {
+    this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0;
+
+    if (!this._shouldShowProgress(aRequest))
+      return;
+
+    if (this.mTotalProgress && this.mTab.hasAttribute("busy"))
+      this.mTab.setAttribute("progress", "true");
+
+    this._callProgressListeners("onProgressChange",
+                                [aWebProgress, aRequest,
+                                 aCurSelfProgress, aMaxSelfProgress,
+                                 aCurTotalProgress, aMaxTotalProgress]);
+  }
+
+  onProgressChange64(aWebProgress, aRequest, aCurSelfProgress,
+                     aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
+    return this.onProgressChange(aWebProgress, aRequest,
+      aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
+      aMaxTotalProgress);
+  }
+
+  /* eslint-disable complexity */
+  onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+    if (!aRequest)
+      return;
+
+    const nsIWebProgressListener = Ci.nsIWebProgressListener;
+    const nsIChannel = Ci.nsIChannel;
+    let location, originalLocation;
+    try {
+      aRequest.QueryInterface(nsIChannel);
+      location = aRequest.URI;
+      originalLocation = aRequest.originalURI;
+    } catch (ex) {}
+
+    let ignoreBlank = this._isForInitialAboutBlank(aWebProgress, aStateFlags,
+      location);
+
+    // If we were ignoring some messages about the initial about:blank, and we
+    // got the STATE_STOP for it, we'll want to pay attention to those messages
+    // from here forward. Similarly, if we conclude that this state change
+    // is one that we shouldn't be ignoring, then stop ignoring.
+    if ((ignoreBlank &&
+        aStateFlags & nsIWebProgressListener.STATE_STOP &&
+        aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) ||
+        !ignoreBlank && this.mBlank) {
+      this.mBlank = false;
+    }
+
+    if (aStateFlags & nsIWebProgressListener.STATE_START) {
+      this.mRequestCount++;
+    } else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
+      const NS_ERROR_UNKNOWN_HOST = 2152398878;
+      if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
+        // to prevent bug 235825: wait for the request handled
+        // by the automatic keyword resolver
+        return;
+      }
+      // since we (try to) only handle STATE_STOP of the last request,
+      // the count of open requests should now be 0
+      this.mRequestCount = 0;
+    }
+
+    if (aStateFlags & nsIWebProgressListener.STATE_START &&
+        aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
+      if (aWebProgress.isTopLevel) {
+        // Need to use originalLocation rather than location because things
+        // like about:home and about:privatebrowsing arrive with nsIRequest
+        // pointing to their resolved jar: or file: URIs.
+        if (!(originalLocation && gInitialPages.includes(originalLocation.spec) &&
+            originalLocation != "about:blank" &&
+            this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec &&
+            this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) {
+          // Indicating that we started a load will allow the location
+          // bar to be cleared when the load finishes.
+          // In order to not overwrite user-typed content, we avoid it
+          // (see if condition above) in a very specific case:
+          // If the load is of an 'initial' page (e.g. about:privatebrowsing,
+          // about:newtab, etc.), was not explicitly typed in the location
+          // bar by the user, is not about:blank (because about:blank can be
+          // loaded by websites under their principal), and the current
+          // page in the browser is about:blank (indicating it is a newly
+          // created or re-created browser, e.g. because it just switched
+          // remoteness or is a new tab/window).
+          this.mBrowser.urlbarChangeTracker.startedLoad();
+        }
+        delete this.mBrowser.initialPageLoadedFromURLBar;
+        // If the browser is loading it must not be crashed anymore
+        this.mTab.removeAttribute("crashed");
+      }
+
+      if (this._shouldShowProgress(aRequest)) {
+        if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING) &&
+            aWebProgress && aWebProgress.isTopLevel) {
+          this.mTab.setAttribute("busy", "true");
+          this.mTab._notselectedsinceload = !this.mTab.selected;
+          SchedulePressure.startMonitoring(window, {
+            highPressureFn() {
+              // Only switch back to the SVG loading indicator after getting
+              // three consecutive low pressure callbacks. Used to prevent
+              // switching quickly between the SVG and APNG loading indicators.
+              gBrowser.tabContainer._schedulePressureCount = gBrowser.schedulePressureDefaultCount;
+              gBrowser.tabContainer.setAttribute("schedulepressure", "true");
+            },
+            lowPressureFn() {
+              if (!gBrowser.tabContainer._schedulePressureCount ||
+                --gBrowser.tabContainer._schedulePressureCount <= 0) {
+                gBrowser.tabContainer.removeAttribute("schedulepressure");
+              }
+
+              // If tabs are closed while they are loading we need to
+              // stop monitoring schedule pressure. We don't stop monitoring
+              // during high pressure times because we want to eventually
+              // return to the SVG tab loading animations.
+              let continueMonitoring = true;
+              if (!document.querySelector(".tabbrowser-tab[busy]")) {
+                SchedulePressure.stopMonitoring(window);
+                continueMonitoring = false;
+              }
+              return { continueMonitoring };
+            },
+          });
+          gBrowser.syncThrobberAnimations(this.mTab);
+        }
+
+        if (this.mTab.selected) {
+          gBrowser.mIsBusy = true;
+        }
+      }
+    } else if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
+               aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
+
+      if (this.mTab.hasAttribute("busy")) {
+        this.mTab.removeAttribute("busy");
+        if (!document.querySelector(".tabbrowser-tab[busy]")) {
+          SchedulePressure.stopMonitoring(window);
+          gBrowser.tabContainer.removeAttribute("schedulepressure");
+        }
+
+        // Only animate the "burst" indicating the page has loaded if
+        // the top-level page is the one that finished loading.
+        if (aWebProgress.isTopLevel && !aWebProgress.isLoadingDocument &&
+            Components.isSuccessCode(aStatus) &&
+            !gBrowser.tabAnimationsInProgress &&
+            Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled")) {
+          if (this.mTab._notselectedsinceload) {
+            this.mTab.setAttribute("notselectedsinceload", "true");
+          } else {
+            this.mTab.removeAttribute("notselectedsinceload");
+          }
+
+          this.mTab.setAttribute("bursting", "true");
+        }
+
+        gBrowser._tabAttrModified(this.mTab, ["busy"]);
+        if (!this.mTab.selected)
+          this.mTab.setAttribute("unread", "true");
+      }
+      this.mTab.removeAttribute("progress");
+
+      if (aWebProgress.isTopLevel) {
+        let isSuccessful = Components.isSuccessCode(aStatus);
+        if (!isSuccessful && !isTabEmpty(this.mTab)) {
+          // Restore the current document's location in case the
+          // request was stopped (possibly from a content script)
+          // before the location changed.
+
+          this.mBrowser.userTypedValue = null;
+
+          let inLoadURI = this.mBrowser.inLoadURI;
+          if (this.mTab.selected && gURLBar && !inLoadURI) {
+            URLBarSetURI();
+          }
+        } else if (isSuccessful) {
+          this.mBrowser.urlbarChangeTracker.finishedLoad();
+        }
+
+        // Ignore initial about:blank to prevent flickering.
+        if (!this.mBrowser.mIconURL && !ignoreBlank) {
+          // Don't switch to the default icon on about:home or about:newtab,
+          // since these pages get their favicon set in browser code to
+          // improve perceived performance.
+          let isNewTab = originalLocation &&
+            (originalLocation.spec == "about:newtab" ||
+              originalLocation.spec == "about:privatebrowsing" ||
+              originalLocation.spec == "about:home");
+          if (!isNewTab) {
+            gBrowser.useDefaultIcon(this.mTab);
+          }
+        }
+      }
+
+      // For keyword URIs clear the user typed value since they will be changed into real URIs
+      if (location.scheme == "keyword")
+        this.mBrowser.userTypedValue = null;
+
+      if (this.mTab.selected)
+        gBrowser.mIsBusy = false;
+    }
+
+    if (ignoreBlank) {
+      this._callProgressListeners("onUpdateCurrentBrowser",
+                                  [aStateFlags, aStatus, "", 0],
+                                  true, false);
+    } else {
+      this._callProgressListeners("onStateChange",
+                                  [aWebProgress, aRequest, aStateFlags, aStatus],
+                                  true, false);
+    }
+
+    this._callProgressListeners("onStateChange",
+                                [aWebProgress, aRequest, aStateFlags, aStatus],
+                                false);
+
+    if (aStateFlags & (nsIWebProgressListener.STATE_START |
+        nsIWebProgressListener.STATE_STOP)) {
+      // reset cached temporary values at beginning and end
+      this.mMessage = "";
+      this.mTotalProgress = 0;
+    }
+    this.mStateFlags = aStateFlags;
+    this.mStatus = aStatus;
+  }
+  /* eslint-enable complexity */
+
+  onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+    // OnLocationChange is called for both the top-level content
+    // and the subframes.
+    let topLevel = aWebProgress.isTopLevel;
+
+    if (topLevel) {
+      let isSameDocument = !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
+      // We need to clear the typed value
+      // if the document failed to load, to make sure the urlbar reflects the
+      // failed URI (particularly for SSL errors). However, don't clear the value
+      // if the error page's URI is about:blank, because that causes complete
+      // loss of urlbar contents for invalid URI errors (see bug 867957).
+      // Another reason to clear the userTypedValue is if this was an anchor
+      // navigation initiated by the user.
+      if (this.mBrowser.didStartLoadSinceLastUserTyping() ||
+          ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
+            aLocation.spec != "about:blank") ||
+          (isSameDocument && this.mBrowser.inLoadURI)) {
+        this.mBrowser.userTypedValue = null;
+      }
+
+      // If the tab has been set to "busy" outside the stateChange
+      // handler below (e.g. by sessionStore.navigateAndRestore), and
+      // the load results in an error page, it's possible that there
+      // isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
+      // attribute being removed. In this case we should remove the
+      // attribute here.
+      if ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
+          this.mTab.hasAttribute("busy")) {
+        this.mTab.removeAttribute("busy");
+        gBrowser._tabAttrModified(this.mTab, ["busy"]);
+      }
+
+      // If the browser was playing audio, we should remove the playing state.
+      if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) {
+        clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
+        this.mTab._soundPlayingAttrRemovalTimer = 0;
+        this.mTab.removeAttribute("soundplaying");
+        gBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
+      }
+
+      // If the browser was previously muted, we should restore the muted state.
+      if (this.mTab.hasAttribute("muted")) {
+        this.mTab.linkedBrowser.mute();
+      }
+
+      if (gBrowser.isFindBarInitialized(this.mTab)) {
+        let findBar = gBrowser.getFindBar(this.mTab);
+
+        // Close the Find toolbar if we're in old-style TAF mode
+        if (findBar.findMode != findBar.FIND_NORMAL) {
+          findBar.close();
+        }
+      }
+
+      gBrowser.setTabTitle(this.mTab);
+
+      // Don't clear the favicon if this tab is in the pending
+      // state, as SessionStore will have set the icon for us even
+      // though we're pointed at an about:blank. Also don't clear it
+      // if onLocationChange was triggered by a pushState or a
+      // replaceState (bug 550565) or a hash change (bug 408415).
+      if (!this.mTab.hasAttribute("pending") &&
+          aWebProgress.isLoadingDocument &&
+          !isSameDocument) {
+        this.mBrowser.mIconURL = null;
+      }
+
+      let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
+      if (this.mBrowser.registeredOpenURI) {
+        gBrowser._unifiedComplete
+          .unregisterOpenPage(this.mBrowser.registeredOpenURI, userContextId);
+        delete this.mBrowser.registeredOpenURI;
+      }
+      // Tabs in private windows aren't registered as "Open" so
+      // that they don't appear as switch-to-tab candidates.
+      if (!isBlankPageURL(aLocation.spec) &&
+          (!PrivateBrowsingUtils.isWindowPrivate(window) ||
+            PrivateBrowsingUtils.permanentPrivateBrowsing)) {
+        gBrowser._unifiedComplete.registerOpenPage(aLocation, userContextId);
+        this.mBrowser.registeredOpenURI = aLocation;
+      }
+    }
+
+    if (!this.mBlank) {
+      this._callProgressListeners("onLocationChange",
+                                  [aWebProgress, aRequest, aLocation, aFlags]);
+    }
+
+    if (topLevel) {
+      this.mBrowser.lastURI = aLocation;
+      this.mBrowser.lastLocationChange = Date.now();
+    }
+  }
+
+  onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+    if (this.mBlank)
+      return;
+
+    this._callProgressListeners("onStatusChange",
+                                [aWebProgress, aRequest, aStatus, aMessage]);
+
+    this.mMessage = aMessage;
+  }
+
+  onSecurityChange(aWebProgress, aRequest, aState) {
+    this._callProgressListeners("onSecurityChange",
+                                [aWebProgress, aRequest, aState]);
+  }
+
+  onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
+    return this._callProgressListeners("onRefreshAttempted",
+                                       [aWebProgress, aURI, aDelay, aSameURI]);
+  }
+
+  QueryInterface(aIID) {
+    if (aIID.equals(Ci.nsIWebProgressListener) ||
+        aIID.equals(Ci.nsIWebProgressListener2) ||
+        aIID.equals(Ci.nsISupportsWeakReference) ||
+        aIID.equals(Ci.nsISupports))
+      return this;
+    throw Cr.NS_NOINTERFACE;
+  }
+}
+
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -1,28 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
-const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directory.source";
 
 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true);
 
 // Opens and closes a new tab to clear any existing preloaded ones. This is
 // necessary to prevent any left-over activity-stream preloaded new tabs from
 // affecting these tests.
 BrowserOpenTab();
 const initialTab = gBrowser.selectedTab;
 gBrowser.removeTab(initialTab);
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
-  DirectoryLinksProvider: "resource:///modules/DirectoryLinksProvider.jsm",
   PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
 });
 
 var gWindow = window;
 
 // Default to dummy/empty directory links
 var gDirectorySource = 'data:application/json,{"test":1}';
@@ -89,41 +87,22 @@ add_task(async function setupWindowSize(
         }
       });
     });
   });
 });
 
 registerCleanupFunction(function() {
   Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
-  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, gOrigDirectorySource);
-
-  return watchLinksChangeOnce();
 });
 
 function pushPrefs(...aPrefs) {
   return SpecialPowers.pushPrefEnv({"set": aPrefs});
 }
 
-/**
- * Resolves promise when directory links are downloaded and written to disk
- */
-function watchLinksChangeOnce() {
-  return new Promise(resolve => {
-    let observer = {
-      onManyLinksChanged: () => {
-        DirectoryLinksProvider.removeObserver(observer);
-        resolve();
-      }
-    };
-    observer.onDownloadFail = observer.onManyLinksChanged;
-    DirectoryLinksProvider.addObserver(observer);
-  });
-}
-
 add_task(async function setup() {
   registerCleanupFunction(function() {
     return new Promise(resolve => {
       function cleanupAndFinish() {
         PlacesUtils.history.clear().then(() => {
           whenPagesUpdated().then(resolve);
           NewTabUtils.restore();
         });
@@ -134,25 +113,17 @@ add_task(async function setup() {
 
       if (numCallbacks)
         callbacks.splice(0, numCallbacks, cleanupAndFinish);
       else
         cleanupAndFinish();
     });
   });
 
-  let promiseReady = (async function() {
-    await watchLinksChangeOnce();
-    await whenPagesUpdated();
-  })();
-
-  // Save the original directory source (which is set globally for tests)
-  gOrigDirectorySource = Services.prefs.getCharPref(PREF_NEWTAB_DIRECTORYSOURCE);
-  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, gDirectorySource);
-  await promiseReady;
+  await whenPagesUpdated();
 });
 
 /** Perform an action on a cell within the newtab page.
   * @param aIndex index of cell
   * @param aFn function to call in child process or tab.
   * @returns result of calling the function.
   */
 function performOnCell(aIndex, aFn) {
--- a/browser/base/content/test/performance/browser_appmenu_reflows.js
+++ b/browser/base/content/test/performance/browser_appmenu_reflows.js
@@ -39,41 +39,16 @@ const EXPECTED_APPMENU_OPEN_REFLOWS = [
       "_calculateMaxHeight@resource:///modules/PanelMultiView.jsm",
       "handleEvent@resource:///modules/PanelMultiView.jsm",
     ],
 
     maxCount: 6, // This number should only ever go down - never up.
   },
 ];
 
-const EXPECTED_APPMENU_SUBVIEW_REFLOWS = [
-  /**
-   * The synced tabs view has labels that are multiline. Because of bugs in
-   * XUL layout relating to multiline text in scrollable containers, we need
-   * to manually read their height in order to ensure container heights are
-   * correct. Unfortunately this requires 2 sync reflows.
-   *
-   * If we add more views where this is necessary, we may need to duplicate
-   * these expected reflows further. Bug 1392340 is on file to remove the
-   * reflows completely when opening subviews.
-   */
-  {
-    stack: [
-      "descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
-      "_transitionViews@resource:///modules/PanelMultiView.jsm",
-    ],
-
-    maxCount: 4, // This number should only ever go down - never up.
-  },
-
-  /**
-   * Please don't add anything new!
-   */
-];
-
 add_task(async function() {
   await ensureNoPreloadedBrowser();
 
   // First, open the appmenu.
   await withReflowObserver(async function() {
     let popupShown =
       BrowserTestUtils.waitForEvent(PanelUI.panel, "popupshown");
     await PanelUI.show();
@@ -91,36 +66,40 @@ add_task(async function() {
     async function openSubViewsRecursively(currentView) {
       let navButtons = Array.from(currentView.querySelectorAll(".subviewbutton-nav"));
       if (!navButtons) {
         return;
       }
 
       for (let button of navButtons) {
         info("Click " + button.id);
+        let promiseViewShown = BrowserTestUtils.waitForEvent(PanelUI.panel,
+                                                             "ViewShown");
         button.click();
-        await BrowserTestUtils.waitForEvent(PanelUI.panel, "ViewShown");
+        let viewShownEvent = await promiseViewShown;
 
         // Workaround until bug 1363756 is fixed, then this can be removed.
         let container = PanelUI.multiView.querySelector(".panel-viewcontainer");
         await BrowserTestUtils.waitForCondition(() => {
           return !container.hasAttribute("width");
         });
 
-        info("Shown " + PanelUI.multiView.current.id);
-        await openSubViewsRecursively(PanelUI.multiView.current);
+        info("Shown " + viewShownEvent.originalTarget.id);
+        await openSubViewsRecursively(viewShownEvent.originalTarget);
+        promiseViewShown = BrowserTestUtils.waitForEvent(currentView,
+                                                         "ViewShown");
         PanelUI.multiView.goBack();
-        await BrowserTestUtils.waitForEvent(PanelUI.panel, "ViewShown");
+        await promiseViewShown;
 
         // Workaround until bug 1363756 is fixed, then this can be removed.
         await BrowserTestUtils.waitForCondition(() => {
           return !container.hasAttribute("width");
         });
       }
     }
 
     await openSubViewsRecursively(PanelUI.mainView);
 
     let hidden = BrowserTestUtils.waitForEvent(PanelUI.panel, "popuphidden");
     PanelUI.hide();
     await hidden;
-  }, EXPECTED_APPMENU_SUBVIEW_REFLOWS);
+  }, []);
 });
--- a/browser/base/content/test/performance/browser_startup.js
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -63,17 +63,16 @@ const startupPhases = {
     modules: new Set([
       "chrome://webcompat-reporter/content/WebCompatReporter.jsm",
       "chrome://webcompat/content/data/ua_overrides.jsm",
       "chrome://webcompat/content/lib/ua_overrider.jsm",
       "resource:///modules/AboutNewTab.jsm",
       "resource:///modules/BrowserUITelemetry.jsm",
       "resource:///modules/BrowserUsageTelemetry.jsm",
       "resource:///modules/ContentCrashHandlers.jsm",
-      "resource:///modules/DirectoryLinksProvider.jsm",
       "resource://gre/modules/NewTabUtils.jsm",
       "resource://gre/modules/PageThumbs.jsm",
       "resource://gre/modules/PlacesUtils.jsm",
       "resource://gre/modules/Promise.jsm", // imported by devtools during _delayedStartup
       "resource://gre/modules/Preferences.jsm",
     ]),
     services: new Set([
       "@mozilla.org/browser/search-service;1",
--- a/browser/base/content/test/siteIdentity/browser.ini
+++ b/browser/base/content/test/siteIdentity/browser.ini
@@ -92,9 +92,13 @@ support-files =
   test_no_mcb_on_http_site_font.css
   test_no_mcb_on_http_site_font2.html
   test_no_mcb_on_http_site_font2.css
 [browser_no_mcb_for_loopback.js]
 tags = mcb
 support-files =
   ../general/moz.png
   test_no_mcb_for_loopback.html
+[browser_no_mcb_for_onions.js]
+tags = mcb
+support-files =
+  test_no_mcb_for_onions.html
 [browser_check_identity_state.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/browser_no_mcb_for_onions.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The test loads a HTTPS web page with active content from HTTP .onion URLs
+// and makes sure that the mixed content flags on the docshell are not set.
+//
+// Note that the URLs referenced within the test page intentionally use the
+// unassigned port 8 because we don't want to actually load anything, we just
+// want to check that the URLs are not blocked.
+
+const TEST_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "https://example.com") + "test_no_mcb_for_onions.html";
+
+const PREF_BLOCK_DISPLAY = "security.mixed_content.block_display_content";
+const PREF_BLOCK_ACTIVE = "security.mixed_content.block_active_content";
+const PREF_ONION_WHITELIST = "dom.securecontext.whitelist_onions";
+
+add_task(async function allowOnionMixedContent() {
+  registerCleanupFunction(function() {
+    gBrowser.removeCurrentTab();
+  });
+
+  await SpecialPowers.pushPrefEnv({set: [[PREF_BLOCK_DISPLAY, true]]});
+  await SpecialPowers.pushPrefEnv({set: [[PREF_BLOCK_ACTIVE, true]]});
+  await SpecialPowers.pushPrefEnv({set: [[PREF_ONION_WHITELIST, true]]});
+
+  const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  const browser = gBrowser.getBrowserForTab(tab);
+
+  await ContentTask.spawn(browser, null, function() {
+    is(docShell.hasMixedDisplayContentBlocked, false, "hasMixedDisplayContentBlocked not set");
+    is(docShell.hasMixedActiveContentBlocked, false, "hasMixedActiveContentBlocked not set");
+  });
+
+  await assertMixedContentBlockingState(browser, {
+    activeBlocked: false,
+    activeLoaded: false,
+    passiveLoaded: false,
+ });
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/siteIdentity/test_no_mcb_for_onions.html
@@ -0,0 +1,28 @@
+<!-- See browser_no_mcb_for_onions.js -->
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>Bug 1382359</title>
+  </head>
+
+  <style>
+    @font-face {
+      src: url("http://123456789abcdef.onion:8/test.ttf");
+    }
+  </style>
+
+  <body>
+    <img src="http://123456789abcdef.onion:8/test.png">
+
+    <iframe src="http://123456789abcdef.onion:8/test.html"></iframe>
+  </body>
+
+  <script src="http://123456789abcdef.onion:8/test.js"></script>
+
+  <link href="http://123456789abcdef.onion:8/test.css" rel="stylesheet"></link>
+
+  <script>
+    fetch("http://123456789abcdef.onion:8");
+  </script>
+</html>
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -85,20 +85,16 @@ var whitelist = [
   {file: "resource://gre/defaults/autoconfig/prefcalls.js"},
 
   // modules/libpref/Preferences.cpp
   {file: "resource://gre/greprefs.js"},
 
   // browser/extensions/pdfjs/content/web/viewer.js
   {file: "resource://pdf.js/build/pdf.worker.js"},
 
-  // browser/components/newtab bug 1355166
-  {file: "resource://app/modules/NewTabSearchProvider.jsm"},
-  {file: "resource://app/modules/NewTabWebChannel.jsm"},
-
   // layout/mathml/nsMathMLChar.cpp
   {file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties"},
   {file: "resource://gre/res/fonts/mathfontUnicode.properties"},
 
   // toolkit/components/places/ColorAnalyzer_worker.js
   {file: "resource://gre/modules/ClusterLib.js"},
   {file: "resource://gre/modules/ColorConversion.js"},
 
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -64,16 +64,82 @@ let whitelist = [
   {sourceName: /webide\/skin\/logs\.css$/i,
    intermittent: true,
    errorMessage: /Property contained reference to invalid variable.*background/i,
    isFromDevTools: true},
   {sourceName: /devtools\/skin\/animationinspector\.css$/i,
    intermittent: true,
    errorMessage: /Property contained reference to invalid variable.*color/i,
    isFromDevTools: true},
+
+  // These are CSS custom properties that we found a definition of but
+  // no reference to.
+  // Bug 1441837
+  {propName: "--in-content-category-text-active",
+   isFromDevTools: false},
+  // Bug 1441844
+  {propName: "--chrome-nav-bar-separator-color",
+   isFromDevTools: false},
+  // Bug 1441855
+  {propName: "--chrome-nav-buttons-background",
+   isFromDevTools: false},
+  // Bug 1441855
+  {propName: "--chrome-nav-buttons-hover-background",
+   isFromDevTools: false},
+  // Bug 1441857
+  {propName: "--muteButton-width",
+   isFromDevTools: false},
+  // Bug 1441857
+  {propName: "--closedCaptionButton-width",
+   isFromDevTools: false},
+  // Bug 1441857
+  {propName: "--fullscreenButton-width",
+   isFromDevTools: false},
+  // Bug 1441857
+  {propName: "--durationSpan-width",
+   isFromDevTools: false},
+  // Bug 1441857
+  {propName: "--durationSpan-width-long",
+   isFromDevTools: false},
+  // Bug 1441857
+  {propName: "--positionDurationBox-width",
+   isFromDevTools: false},
+  // Bug 1441857
+  {propName: "--positionDurationBox-width-long",
+   isFromDevTools: false},
+  // Bug 1441860
+  {propName: "--rule-flex-toggle-color",
+   isFromDevTools: true},
+  // Bug 1441929
+  {propName: "--theme-search-overlays-semitransparent",
+   isFromDevTools: true},
+  // Bug 1441878
+  {propName: "--theme-codemirror-gutter-background",
+   isFromDevTools: true},
+  // Bug 1441879
+  {propName: "--arrow-width",
+   isFromDevTools: true},
+  // Bug 1442300
+  {propName: "--in-content-category-background",
+   isFromDevTools: false},
+  // Bug 1442314
+  {propName: "--separator-border-image",
+   isFromDevTools: true},
+
+  // Used on Linux
+  {propName: "--in-content-box-background-odd",
+   platforms: ["win", "macosx"],
+   isFromDevTools: false},
+
+  // These properties *are* actually referenced. Need to find why
+  // their reference isn't getting counted.
+  {propName: "--bezier-diagonal-color",
+   isFromDevTools: true},
+  {propName: "--bezier-grid-color",
+   isFromDevTools: true},
 ];
 
 if (!Services.prefs.getBoolPref("full-screen-api.unprefix.enabled")) {
   whitelist.push({
     sourceName: /(?:res|gre-resources)\/(ua|html)\.css$/i,
     errorMessage: /Unknown pseudo-class .*\bfullscreen\b/i,
     isFromDevTools: false
   });
@@ -189,34 +255,36 @@ function messageIsCSSError(msg) {
       return true;
     }
     info(`Ignored error for ${sourceName} because of filter.`);
   }
   return false;
 }
 
 let imageURIsToReferencesMap = new Map();
+let customPropsToReferencesMap = new Map();
 
 function processCSSRules(sheet) {
   for (let rule of sheet.cssRules) {
     if (rule instanceof CSSMediaRule) {
       processCSSRules(rule);
       continue;
     }
     if (!(rule instanceof CSSStyleRule))
       continue;
 
     // Extract urls from the css text.
     // Note: CSSStyleRule.cssText always has double quotes around URLs even
     //       when the original CSS file didn't.
     let urls = rule.cssText.match(/url\("[^"]*"\)/g);
-    if (!urls)
+    let props = rule.cssText.match(/(var\()?(--[\w\-]+)/g);
+    if (!urls && !props)
       continue;
 
-    for (let url of urls) {
+    for (let url of (urls || [])) {
       // Remove the url(" prefix and the ") suffix.
       url = url.replace(/url\("(.*)"\)/, "$1");
       if (url.startsWith("data:"))
         continue;
 
       // Make the url absolute and remove the ref.
       let baseURI = Services.io.newURI(rule.parentStyleSheet.href);
       url = Services.io.newURI(url, null, baseURI).specIgnoringRef;
@@ -224,16 +292,26 @@ function processCSSRules(sheet) {
       // Store the image url along with the css file referencing it.
       let baseUrl = baseURI.spec.split("?always-parse-css")[0];
       if (!imageURIsToReferencesMap.has(url)) {
         imageURIsToReferencesMap.set(url, new Set([baseUrl]));
       } else {
         imageURIsToReferencesMap.get(url).add(baseUrl);
       }
     }
+
+    for (let prop of (props || [])) {
+      if (prop.startsWith("var(")) {
+        prop = prop.substring(4);
+        let prevValue = customPropsToReferencesMap.get(prop) || 0;
+        customPropsToReferencesMap.set(prop, prevValue + 1);
+      } else if (!customPropsToReferencesMap.has(prop)) {
+        customPropsToReferencesMap.set(prop, undefined);
+      }
+    }
   }
 }
 
 function chromeFileExists(aURI) {
   let available = 0;
   try {
     let channel = NetUtil.newChannel({uri: aURI, loadUsingSystemPrincipal: true});
     let stream = channel.open();
@@ -349,26 +427,50 @@ add_task(async function checkAllTheCSS()
           }
         }
         if (!ignored)
           ok(false, "missing " + image + " referenced from " + ref);
       }
     }
   }
 
+  // Check if all the properties that are defined are referenced.
+  for (let [prop, refCount] of customPropsToReferencesMap) {
+    if (!refCount) {
+      let ignored = false;
+      for (let item of whitelist) {
+        if (item.propName == prop &&
+            isDevtools == item.isFromDevTools) {
+          item.used = true;
+          if (!item.platforms || item.platforms.includes(AppConstants.platform)) {
+            ignored = true;
+          }
+          break;
+        }
+      }
+      if (!ignored) {
+        ok(false, "custom property `" + prop + "` is not referenced");
+      }
+    }
+  }
+
   let messages = Services.console.getMessageArray();
   // Count errors (the test output will list actual issues for us, as well
   // as the ok(false) in messageIsCSSError.
   let errors = messages.filter(messageIsCSSError);
   is(errors.length, 0, "All the styles (" + allPromises.length + ") loaded without errors.");
 
   // Confirm that all whitelist rules have been used.
   for (let item of whitelist) {
-    if (!item.used && isDevtools == item.isFromDevTools && !item.intermittent) {
+    if (!item.used &&
+        (!item.platforms || item.platforms.includes(AppConstants.platform)) &&
+        isDevtools == item.isFromDevTools &&
+        !item.intermittent) {
       ok(false, "Unused whitelist item. " +
+                (item.propName ? " propName: " + item.propName : "") +
                 (item.sourceName ? " sourceName: " + item.sourceName : "") +
                 (item.errorMessage ? " errorMessage: " + item.errorMessage : ""));
     }
   }
 
   // Confirm that all file whitelist rules have been used.
   for (let item of allowedImageReferences) {
     if (!item.used && isDevtools == item.isFromDevTools &&
@@ -383,9 +485,10 @@ add_task(async function checkAllTheCSS()
   iframe.remove();
   doc.head.innerHTML = "";
   doc = null;
   iframe = null;
   win = null;
   hiddenFrame.destroy();
   hiddenFrame = null;
   imageURIsToReferencesMap = null;
+  customPropsToReferencesMap = null;
 });
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -18,16 +18,18 @@ skip-if = !e10s # Tab spinner is e10s on
 skip-if = os == 'mac'
 [browser_navigatePinnedTab.js]
 [browser_new_file_whitelisted_http_tab.js]
 skip-if = !e10s # Test only relevant for e10s.
 [browser_new_web_tab_in_file_process_pref.js]
 skip-if = !e10s # Pref and test only relevant for e10s.
 [browser_newwindow_tabstrip_overflow.js]
 [browser_opened_file_tab_navigated_to_web.js]
+[browser_new_tab_insert_position.js]
+support-files = file_new_tab_page.html
 [browser_overflowScroll.js]
 [browser_pinnedTabs.js]
 [browser_pinnedTabs_closeByKeyboard.js]
 [browser_positional_attributes.js]
 [browser_preloadedBrowser_zoom.js]
 [browser_reload_deleted_file.js]
 skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
 [browser_tabswitch_updatecommands.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_new_tab_insert_position.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function doTest(aInsertRelatedAfterCurrent, aInsertAfterCurrent) {
+  const kDescription = "(aInsertRelatedAfterCurrent=" + aInsertRelatedAfterCurrent +
+                       ", aInsertAfterCurrent=" + aInsertAfterCurrent + "): ";
+  is(gBrowser.tabs.length, 1, kDescription + "one tab is open initially");
+
+  await SpecialPowers.pushPrefEnv({set: [
+    ["browser.tabs.opentabfor.middleclick", true],
+    ["browser.tabs.loadBookmarksInBackground", false],
+    ["browser.tabs.insertRelatedAfterCurrent", aInsertRelatedAfterCurrent],
+    ["browser.tabs.insertAfterCurrent", aInsertAfterCurrent]
+  ]});
+
+  // Add a few tabs.
+  let tabs = [];
+  function addTab(aURL, aReferrer) {
+    let tab = BrowserTestUtils.addTab(gBrowser, aURL, {referrerURI: aReferrer});
+    tabs.push(tab);
+    return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  }
+
+  await addTab("http://mochi.test:8888/#0");
+  await addTab("http://mochi.test:8888/#1");
+  await addTab("http://mochi.test:8888/#2");
+  await addTab("http://mochi.test:8888/#3");
+
+  // Create a new tab page which has a link to "example.com".
+  let pageURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+  pageURL = `${pageURL}file_new_tab_page.html`;
+  let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL);
+  let newTabURISpec = newTab.linkedBrowser.currentURI.spec;
+  const kNewTabIndex = 1;
+  gBrowser.moveTabTo(newTab, kNewTabIndex);
+
+  let openTabIndex = aInsertRelatedAfterCurrent || aInsertAfterCurrent ?
+    kNewTabIndex + 1 : gBrowser.tabs.length;
+  let openTabDescription = aInsertRelatedAfterCurrent || aInsertAfterCurrent ?
+    "immediately to the right" : "at rightmost";
+
+  // Middle click on the cell should open example.com in a new tab.
+  let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/", true);
+  await BrowserTestUtils.synthesizeMouseAtCenter("#link_to_example_com",
+                                                 {button: 1}, gBrowser.selectedBrowser);
+  let openTab = await newTabPromise;
+  is(openTab.linkedBrowser.currentURI.spec, "http://example.com/",
+     "Middle click should open site to correct url.");
+  is(openTab._tPos, openTabIndex,
+     kDescription + "Middle click should open site in a new tab " + openTabDescription);
+
+  // Remove the new opened tab which loaded example.com.
+  gBrowser.removeTab(gBrowser.tabs[openTabIndex]);
+
+  // Go back to the new tab.
+  gBrowser.selectedTab = newTab;
+  is(gBrowser.selectedBrowser.currentURI.spec, newTabURISpec,
+     kDescription + "New tab URI shouldn't be changed");
+
+  openTabIndex = aInsertAfterCurrent ? kNewTabIndex + 1 : gBrowser.tabs.length;
+  openTabDescription = aInsertAfterCurrent ? "immediately to the right" : "at rightmost";
+
+  // Open about:mozilla in new tab from the URL bar.
+  gURLBar.focus();
+  gURLBar.select();
+  newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:mozilla");
+  EventUtils.sendString("about:mozilla");
+  EventUtils.synthesizeKey("KEY_Alt", { altKey: true, code: "AltLeft", type: "keydown" });
+  EventUtils.synthesizeKey("KEY_Enter", { altKey: true, code: "Enter" });
+  EventUtils.synthesizeKey("KEY_Alt", { altKey: false, code: "AltLeft", type: "keyup" });
+  openTab = await newTabPromise;
+
+  is(newTab.linkedBrowser.currentURI.spec, newTabURISpec,
+     kDescription + "example.com should be loaded in current tab.");
+  is(openTab.linkedBrowser.currentURI.spec, "about:mozilla",
+     kDescription + "about:mozilla should be loaded in the new tab.");
+  is(openTab._tPos, openTabIndex,
+     kDescription + "Alt+Enter in the URL bar should open page in a new tab " + openTabDescription);
+
+  // Remove all tabs opened by this test.
+  while (gBrowser.tabs[1]) {
+    gBrowser.removeTab(gBrowser.tabs[1]);
+  }
+}
+
+add_task(async function() {
+  // Current default settings.
+  await doTest(true, false);
+  // Perhaps, some users would love this settings.
+  await doTest(true, true);
+  // Maybe, unrealistic cases, but we should test these cases too.
+  await doTest(false, true);
+  await doTest(false, false);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/file_new_tab_page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+  </head>
+  <body>
+    <a href="http://example.com/" id="link_to_example_com">go to example.com</a>
+  </body>
+</html>
--- a/browser/base/content/webext-panels.js
+++ b/browser/base/content/webext-panels.js
@@ -5,16 +5,17 @@
 
 // Via webext-panels.xul
 /* import-globals-from browser.js */
 /* import-globals-from nsContextMenu.js */
 
 ChromeUtils.defineModuleGetter(this, "ExtensionParent",
                                "resource://gre/modules/ExtensionParent.jsm");
 
+ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   promiseEvent,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
@@ -83,23 +84,21 @@ var gBrowser = {
   getTabModalPromptBox(browser) {
     if (!browser.tabModalPromptBox) {
       browser.tabModalPromptBox = new TabModalPromptBox(browser);
     }
     return browser.tabModalPromptBox;
   },
 };
 
-function loadWebPanel() {
-  let sidebarURI = new URL(location);
+async function loadPanel(extensionId, extensionUrl, browserStyle) {
+  let policy = WebExtensionPolicy.getByID(extensionId);
   let sidebar = {
-    uri: sidebarURI.searchParams.get("panel"),
-    remote: sidebarURI.searchParams.get("remote"),
-    browserStyle: sidebarURI.searchParams.get("browser-style"),
+    uri: extensionUrl,
+    remote: policy.extension.remote,
+    browserStyle,
   };
   getBrowser(sidebar).then(browser => {
-    browser.loadURI(sidebar.uri);
+    let uri = Services.io.newURI(policy.getURL());
+    let triggeringPrincipal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+    browser.loadURIWithFlags(extensionUrl, {triggeringPrincipal});
   });
 }
-
-function load() {
-  this.loadWebPanel();
-}
--- a/browser/base/content/webext-panels.xul
+++ b/browser/base/content/webext-panels.xul
@@ -13,18 +13,17 @@
 <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
 %browserDTD;
 <!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
 %textcontextDTD;
 ]>
 
 <page id="webextpanels-window"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        onload="load()">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
   <script type="application/javascript" src="chrome://browser/content/webext-panels.js"/>
 
   <broadcasterset id="mainBroadcasterSet">
     <broadcaster id="isFrameImage"/>
   </broadcasterset>
index 657101d6e220315df695f721f6c635d0fa3e2079..6b82c923a6624cf00561f8bfb9bf74d0bf7f47c5
GIT binary patch
literal 14340
zc%1E8eQ;b=6+b80wC`=dUiy{Yv?)6cp<mhTN3z-7q%Fy2lQxvn+NN##LEGJZFUgy3
z_AUEfl7>=jv7&YWM?oABr6MzGTd_FefTK(Y5pe`8nBl_#R7A(|56kGl=!`=>_r3cz
zn{5IugVw=4?ae*+^WA&Tx##`P*#`g^8<GP6egF`_%B7hA4kv(z@<I}Lx`@gtA*D)-
zmtZ)XOlwJivK`6nWCKc6q7wbjP#IYF*J0)jX}$3Nrd8eW`(MGba!19q>4GTEsP3A-
zZrMIb8q$?5N;+Yv!%941DEdBWjy|A`bf>h;=A1IDDkD2mT6$<OLoJo5L^?G@ot5@<
zdaHV~qW2ogL4$F-bS-Cem>qjn8q4bTNpsowO<6<LGWvEUr&BJ9ug;t`dyZ5&ci#L3
zOKO+b2bx>kdQ;lqppr54y)sRHeL^=jWtEH~?_tG_nymJzioBZ@I&upAm3wrWe_J|{
zRMI^M)Qr4i*qXg3lNeOS7FN03)r-WM9mA@wCezBc5mhz@b|&<cl9AO+|27(-926?C
zUX-R=y*<@S1?d?<63n7lSG`P>9u*|CTI;G;34&B+wKY`xMJXXj<yKpx7!o8|lq#&&
zh}a@Zx+poU)-~d_g0w-Drdh2qF)mh?(cB=^wk@{=SKH?2A{4+4^{^JYU>Bqy10!$;
z+zEHVH{c{Z4o}02@G`s(Z^B>TJp2Ozr=f%ku?nkk5!T=}xC~d|O035aHe&>%*otxN
z#4cQqpTI5HgWGTi?#BeCP{uygFpD|VaR_h4PvTL$4UgfSco%*FzlitXm+<RcuaK{9
z{X~7kZw(mf(ZZUSPT~XWS3w1_XJ#i8sRR8vZ73r*WHbFAMxh^h>10KVUARC5FsrPn
z&y0rg=dgE{z*e#DJh0c8$!|DrUI%t5&d?QE)urZ?KJB3A2DP6pzolS&3ly(;HY}l(
zgak?B0Q5r+G#Dbe$j|^;%KKpscqny&N^J`CQTsuvxq-_4FaQRmVHD=VB45E{e8o?h
z2QJ@sc5@WnSF}8skH^ikKF+T9%#DeZnlUtez_UJn(Q$Dx2{S&-S(m6pCHhzsvkxAg
zxAFXll_PI2<GG_?jKp&f&)ax@#LAKPE91F?=K{|qo_l!S#`7aq&W#pwyN>+H$UPL4
zc<$kO+huLJL?!yaBIfH8_Rq#*ehd5KId-GU%dKXcXSZ%X>pBhK1i`h_lpf<bOHY7x
zCFP)BjwXV^K(IO37Y-$rXe1B}H-!R9FxnK7!%AZ$?C(>WTm76aW-JQ&TM6MeB(mAG
z!v1>oL`F_(2O9{@%ifWkYACJ#0R1<6M~g-$1FgPc4v_jdK+0GE>3x9}JWJ_40*-9I
zkqXO}Ip~kfkE??CA9G$J{w>b85kF?1sR!Vx3X3fx-o^O};%9R6C3(1gB&sB)m)FlF
zKEnBV#INVPhxlWhZzKL8+YFwg_1SZt8@Izp-Uj$xMPAC5gb!E}u6Qk}X$MYEH{~n&
z;e+Yh)5bM{*=;tI8Dfq>3C~*PVk|m?*B3(W*}Sg!=-Ej#?UJkmgb1bU!HPUN=(#WM
zpJuMbF59@?E}tnXKf=rQythz(2c`Qdo#y@6rrOU|tDVHUz|vsbcbPNEIUt?rQkIJV
zeQH{1@%f5zcwNxXckdft3wWNZfr-!TA2|YWwS8`}NTj1F(&cTAghJlNShUkiw<YQg
zbcBLkp^nb>uEwyvz64+6`FZ1O<<pw-IYA4r;PY<N$;Tz$$;;%X=hAeKl-#%s<BER3
z(6V`1*)oi8TnAOWj$a`!zd~NRo(=L~f8HXTp+09RjZu1w5_y{Wy+2<hx5#O6Lb;`=
zeB0)SOxfDyh1;#N{j4S`#3}VKYeUH>D>~OI<w<*NdC}Rt!`D?PznOfGg`QGgZkv<s
zMxoq(^4wD@jLXg6=?_E$@qo88<nQ!0Hv8MXk<Ru;Zy*qj$2%fT&0&A1Ew`gEx!jP?
zMd||2@V=2?DiKZv!_kl&Ohx>ua5NhBN5kQiBB%U)jiF>;sxcKIBlpo-(Wch?Fn^*c
z)EsDT^$jr<@{KEwPqy?WZ&pbUd{gPiD_~jE6Z|r<KryqXE`E8LH9f}pVrzPW*E0*c
zhYONf&}Z%b#J|RQW<e$1hgs0Kc|EhBwh}N4x|;KT&$!jZv!uWFJZCLEHs@iy{j4I(
zI>6^~*|M9$8Q;(M4>Ts?u<qP{Vn2D?{I<0vXc*p_%cM2)mB#in0~#g^N+d^g9`S=;
z*b%>%?TDWs%n)V?H*Ou!Mz$IWV@QuBa=Te+bHW%%a=u5?(s@3S+^#Ak`=t3DTE<AI
z86{^9R%L~{?Af7aWNjqIEWW<Gpg|NxLEI;~4jl@I0`(sM;d;-ZK%hBV?+FCL;lqbT
zp{g#hrhEV3=*@?2IehC^xY(w$)Avoi-LyX3!2_IMzBs_?oZ;@iZi92u5adT1;=Cw;
zDxr94=7O4~*R1k~TCR<cv0$fju5<oW!N^^7a2bO|ICElUnN!S%FeP`jd+}7G1gTc&
z9ZIS<4XMWHm^6FNtR+{uofG1v(xZZCw$xtbb{7Uu1;JX>a`#FW$TVZAR*l!`W5G)^
zq-xf<gU%)v-LyijR#n&;Wg$&F<Z4x}cCIz!srg8*`Q)~Cx4S5$D?LMxm#SO~J84xr
zSzEg-vo(zeM9mY(M@2<5EZRVx<9gTw3S?oFUj28$=i#gH5WW1rPY~|sWNd#)aPI~9
z4ZI0|hQGnz;T?DvE}(!5unJx1#--SRKJ;T0TW~e5!5D79>+uHMggtm8?ll9W{g}m@
zP)7rA!B6Au_&Gd|cjNtd3crgF<M;3}d=h_(PvOt-dHglLW-TxuAl*1ock%W2RbD$f
zzjrd_t?BGqcZHO9VtC#g46kmVDm;J55%~#v$;0!`8Y>K6tl754OVgP?31e>3l^Rct
z9iBIfbv4Tb7M`~v@+(%>IHhvZWYH`(_?$IT1u3l+%WrIQIwc2bwbdGlQtLF*U#oS^
zT51J$??iKIXI%|hu*=uQ2jLX_2%aV_d;wm9H{cIsz5hx2SAla#|J=A3Yj6oJ!{xXV
zSD_c1G0Ziw6W3ukUPs#4gWItecj7Kgp+dSifGXxl7l&~K58_e$6lvsV@Uv!s{%*Vn
zzlIOusUjWhF4Do%Q|n;STV>t{gs~fFhAd>e(^`tZWf&M|qaV9)!3xh``Yh@Gz*bT!
z^LORh#@gF2edM4PkIO?wiUs%A{`GR6IjG4Wa%6U(V6~%{;$vMQvVQyXOq3DcOZnpk
zG9pCr{#e23bk1`AH?RU(eG@AXM6LIP8=Dwf2nB-mp2lXv3c}@M1=^aLvQ4Yw4SQ?9
zl`0F2>$riNyKrt*6<L9*>P3rP`=oi}ZY-HfEf-4<k&&D~UfVsG=qKB%CiLC?IaS`8
zWo|})-pG={E}CJBBJUlq>KV;WoX_mDE~h9NbG}V|eY#@wG83Q5Y3cN4O;?%i-&xSQ
zbB}5M4P~%9)2B`OikL9I_eRq$Z_30p!_WqICUUt%reBGT7Q70I-zv4&TJRG#RhBc4
z6X-7i*Ez*efdVT1{&yvR{{x7{0ogfKP36H|BH4yQR)%x3tRJw-PETrR;@KAl7ifKJ
zeJy5)qGjB_Y~k^UmV&M=Q>|_`_@Ro<u<$IsbjpQi>7_n?2-5k>Fle5CHOwU7#U!Ls
zUxP;J)Z0TZ=s2DH>TSgyFJF<bU`11Uyz>CbkzYW2t7Ip@Qf=E#YxjACAeGhHCn6Ah
z>d9ys0u1|k4gK>%2kbKUa}4f=lkh_Vt7Gtcc$dKGLiFGY_B*{}i(|J#cJw(kN7j*Z
z=#GPqqt+DpSNoEQQgYk|_J<y}AC#NyLl0M1<+}A(<U<c~gQ~CM5OU12xuRI$?o5UJ
zOw7f;k`T>N1k+r3SBKml%X=Y%Ene*Iuoll^munJt=ju9_Xo6G|D6*OrD@DuIF~K3L
zY4C|I3&Wc5kySM|i7p$<ny8UgMxvrC4~i|A%Bt6_b-5;UcUW7DRyDc1lSh{lPWc7M
zeeg|q7=8dxm=N=M_!XRkSIO0R3;qayg7fe%c#ps)ph)fx`y#}Hbtc$cfk6&5*|#9q
zk;Ai*T%H}ckDQ$(!OS!c5wsk|n+aT6Uwzz*58!w3B)L4N@lkxDNC%7I<=K+|U-JJ;
z{=Y>f|G(t_m;C>d|9@qO|32(zKmB!1{r!K5N>rkcF`*;AH6DYBze^Vqi=lWZ?Y9TO
f<R1d^zU=4n$b<*{&wK<}^8ZWzf64!6{{O!LjEb+I
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -1797,45 +1797,29 @@ var CustomizableUIInternal = {
       }
       let isInteractive = this._isOnInteractiveElement(aEvent);
       log.debug("maybeAutoHidePanel: interactive ? " + isInteractive);
       if (isInteractive) {
         return;
       }
     }
 
-    // We can't use event.target because we might have passed a panelview
-    // anonymous content boundary as well, and so target points to the
-    // panelmultiview in that case. Unfortunately, this means we get
-    // anonymous child nodes instead of the real ones, so looking for the
-    // 'stoooop, don't close me' attributes is more involved.
+    // We can't use event.target because we might have passed an anonymous
+    // content boundary as well, and so target points to the outer element in
+    // that case. Unfortunately, this means we get anonymous child nodes instead
+    // of the real ones, so looking for the 'stoooop, don't close me' attributes
+    // is more involved.
     let target = aEvent.originalTarget;
-    let closemenu = "auto";
-    let widgetType = "button";
     while (target.parentNode && target.localName != "panel") {
-      closemenu = target.getAttribute("closemenu");
-      widgetType = target.getAttribute("widget-type");
-      if (closemenu == "none" || closemenu == "single" ||
-          widgetType == "view") {
-        break;
+      if (target.getAttribute("closemenu") == "none" ||
+          target.getAttribute("widget-type") == "view") {
+        return;
       }
       target = target.parentNode;
     }
-    if (closemenu == "none" || widgetType == "view") {
-      return;
-    }
-
-    if (closemenu == "single") {
-      let panel = this._getPanelForNode(target);
-      let multiview = panel.querySelector("panelmultiview");
-      if (multiview.showingSubView) {
-        multiview.goBack();
-        return;
-      }
-    }
 
     // If we get here, we can actually hide the popup:
     this.hidePanelForNode(aEvent.target);
   },
 
   getUnusedWidgets(aWindowPalette) {
     let window = aWindowPalette.ownerGlobal;
     let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
@@ -3895,16 +3879,96 @@ var CustomizableUI = {
   /**
    * Create an instance of a spring, spacer or separator.
    * @param aId       the type of special widget (spring, spacer or separator)
    * @param aDocument the document in which to create it.
    */
   createSpecialWidget(aId, aDocument) {
     return CustomizableUIInternal.createSpecialWidget(aId, aDocument);
   },
+
+  /**
+   * Fills a submenu with menu items.
+   * @param aMenuItems the menu items to display.
+   * @param aSubview   the subview to fill.
+   */
+  fillSubviewFromMenuItems(aMenuItems, aSubview) {
+    let attrs = ["oncommand", "onclick", "label", "key", "disabled",
+                 "command", "observes", "hidden", "class", "origin",
+                 "image", "checked", "style"];
+
+    let doc = aSubview.ownerDocument;
+    let fragment = doc.createDocumentFragment();
+    for (let menuChild of aMenuItems) {
+      if (menuChild.hidden)
+        continue;
+
+      let subviewItem;
+      if (menuChild.localName == "menuseparator") {
+        // Don't insert duplicate or leading separators. This can happen if there are
+        // menus (which we don't copy) above the separator.
+        if (!fragment.lastChild || fragment.lastChild.localName == "menuseparator") {
+          continue;
+        }
+        subviewItem = doc.createElementNS(kNSXUL, "menuseparator");
+      } else if (menuChild.localName == "menuitem") {
+        subviewItem = doc.createElementNS(kNSXUL, "toolbarbutton");
+        CustomizableUI.addShortcut(menuChild, subviewItem);
+
+        let item = menuChild;
+        if (!item.hasAttribute("onclick")) {
+          subviewItem.addEventListener("click", event => {
+            let newEvent = new doc.defaultView.MouseEvent(event.type, event);
+            item.dispatchEvent(newEvent);
+          });
+        }
+
+        if (!item.hasAttribute("oncommand")) {
+          subviewItem.addEventListener("command", event => {
+            let newEvent = doc.createEvent("XULCommandEvent");
+            newEvent.initCommandEvent(
+              event.type, event.bubbles, event.cancelable, event.view,
+              event.detail, event.ctrlKey, event.altKey, event.shiftKey,
+              event.metaKey, event.sourceEvent, 0);
+            item.dispatchEvent(newEvent);
+          });
+        }
+      } else {
+        continue;
+      }
+      for (let attr of attrs) {
+        let attrVal = menuChild.getAttribute(attr);
+        if (attrVal)
+          subviewItem.setAttribute(attr, attrVal);
+      }
+      // We do this after so the .subviewbutton class doesn't get overriden.
+      if (menuChild.localName == "menuitem") {
+        subviewItem.classList.add("subviewbutton");
+      }
+      fragment.appendChild(subviewItem);
+    }
+    aSubview.appendChild(fragment);
+  },
+
+  /**
+   * A helper function for clearing subviews.
+   * @param aSubview the subview to clear.
+   */
+  clearSubview(aSubview) {
+    let parent = aSubview.parentNode;
+    // We'll take the container out of the document before cleaning it out
+    // to avoid reflowing each time we remove something.
+    parent.removeChild(aSubview);
+
+    while (aSubview.firstChild) {
+      aSubview.firstChild.remove();
+    }
+
+    parent.appendChild(aSubview);
+  },
 };
 Object.freeze(this.CustomizableUI);
 Object.freeze(this.CustomizableUI.windows);
 
 /**
  * All external consumers of widgets are really interacting with these wrappers
  * which provide a common interface.
  */
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -70,87 +70,16 @@ function setAttributes(aNode, aAttrs) {
         }
         value = CustomizableUI.getLocalizedProperty({id: aAttrs.id}, stringId, additionalArgs);
       }
       aNode.setAttribute(name, value);
     }
   }
 }
 
-function fillSubviewFromMenuItems(aMenuItems, aSubview) {
-  let attrs = ["oncommand", "onclick", "label", "key", "disabled",
-               "command", "observes", "hidden", "class", "origin",
-               "image", "checked", "style"];
-
-  let doc = aSubview.ownerDocument;
-  let fragment = doc.createDocumentFragment();
-  for (let menuChild of aMenuItems) {
-    if (menuChild.hidden)
-      continue;
-
-    let subviewItem;
-    if (menuChild.localName == "menuseparator") {
-      // Don't insert duplicate or leading separators. This can happen if there are
-      // menus (which we don't copy) above the separator.
-      if (!fragment.lastChild || fragment.lastChild.localName == "menuseparator") {
-        continue;
-      }
-      subviewItem = doc.createElementNS(kNSXUL, "menuseparator");
-    } else if (menuChild.localName == "menuitem") {
-      subviewItem = doc.createElementNS(kNSXUL, "toolbarbutton");
-      CustomizableUI.addShortcut(menuChild, subviewItem);
-
-      let item = menuChild;
-      if (!item.hasAttribute("onclick")) {
-        subviewItem.addEventListener("click", event => {
-          let newEvent = new doc.defaultView.MouseEvent(event.type, event);
-          item.dispatchEvent(newEvent);
-        });
-      }
-
-      if (!item.hasAttribute("oncommand")) {
-        subviewItem.addEventListener("command", event => {
-          let newEvent = doc.createEvent("XULCommandEvent");
-          newEvent.initCommandEvent(
-            event.type, event.bubbles, event.cancelable, event.view,
-            event.detail, event.ctrlKey, event.altKey, event.shiftKey,
-            event.metaKey, event.sourceEvent, 0);
-          item.dispatchEvent(newEvent);
-        });
-      }
-    } else {
-      continue;
-    }
-    for (let attr of attrs) {
-      let attrVal = menuChild.getAttribute(attr);
-      if (attrVal)
-        subviewItem.setAttribute(attr, attrVal);
-    }
-    // We do this after so the .subviewbutton class doesn't get overriden.
-    if (menuChild.localName == "menuitem") {
-      subviewItem.classList.add("subviewbutton");
-    }
-    fragment.appendChild(subviewItem);
-  }
-  aSubview.appendChild(fragment);
-}
-
-function clearSubview(aSubview) {
-  let parent = aSubview.parentNode;
-  // We'll take the container out of the document before cleaning it out
-  // to avoid reflowing each time we remove something.
-  parent.removeChild(aSubview);
-
-  while (aSubview.firstChild) {
-    aSubview.firstChild.remove();
-  }
-
-  parent.appendChild(aSubview);
-}
-
 const CustomizableWidgets = [
   {
     id: "history-panelmenu",
     type: "view",
     viewId: "PanelUI-history",
     shortcutId: "key_gotoHistory",
     tooltiptext: "history-panelmenu.tooltiptext2",
     recentlyClosedTabsPanel: "appMenu-library-recentlyClosedTabs",
@@ -234,23 +163,16 @@ const CustomizableWidgets = [
         } else {
           element.classList.add("subviewbutton-iconic", "bookmark-item");
         }
       }
       panelview.appendChild(body);
       panelview.appendChild(footer);
     }
   }, {
-    id: "privatebrowsing-button",
-    shortcutId: "key_privatebrowsing",
-    onCommand(e) {
-      let win = e.target.ownerGlobal;
-      win.OpenBrowserWindow({private: true});
-    }
-  }, {
     id: "save-page-button",
     shortcutId: "key_savePage",
     tooltiptext: "save-page-button.tooltiptext3",
     onCommand(aEvent) {
       let win = aEvent.target.ownerGlobal;
       win.saveBrowser(win.gBrowser.selectedBrowser);
     }
   }, {
@@ -943,8 +865,19 @@ if (Services.prefs.getBoolPref("privacy.
       forgetButton.addEventListener("command", this);
     },
     onViewHiding(aEvent) {
       let forgetButton = aEvent.target.querySelector("#PanelUI-panic-view-button");
       forgetButton.removeEventListener("command", this);
     },
   });
 }
+
+if (PrivateBrowsingUtils.enabled) {
+  CustomizableWidgets.push({
+    id: "privatebrowsing-button",
+    shortcutId: "key_privatebrowsing",
+    onCommand(e) {
+      let win = e.target.ownerGlobal;
+      win.OpenBrowserWindow({private: true});
+    }
+  });
+}
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -57,22 +57,24 @@
  *
  * -- Active or inactive
  *
  *    This indicates whether the view is fully scrolled into the visible area
  *    and ready to receive mouse and keyboard events. An active view is always
  *    visible, but a visible view may be inactive. For example, during a scroll
  *    transition, both views will be inactive.
  *
- *    When a view becomes active, the ViewShown event is fired synchronously.
- *    For the main view of the panel, this happens during the "popupshown"
- *    event, which means that other "popupshown" handlers may be called before
- *    the view is active. However, the main view can already receive mouse and
- *    keyboard events at this point, only because this allows regression tests
- *    to use the "popupshown" event to simulate interaction.
+ *    When a view becomes active, the ViewShown event is fired synchronously,
+ *    and the showSubView and goBack methods can be called for navigation.
+ *
+ *    For the main view of the panel, the ViewShown event is dispatched during
+ *    the "popupshown" event, which means that other "popupshown" handlers may
+ *    be called before the view is active. Thus, code that needs to perform
+ *    further navigation automatically should either use the ViewShown event or
+ *    wait for an event loop tick, like BrowserTestUtils.waitForEvent does.
  *
  * -- Navigating with the keyboard
  *
  *    An open view may keep state related to keyboard navigation, even if it is
  *    invisible. When a view is closed, keyboard navigation state is cleared.
  *
  * This diagram shows how <panelview> nodes move during navigation:
  *
@@ -88,20 +90,16 @@
  *     └───┴───┴───┴───┘            └───┘
  *       │ ┌───┬───┬───┬───┐        ┌───┐
  *       │ │{A}│(C)│ D │ B │        │ E │              Go back
  *       │ └───┴───┴───┴───┘        └───┘
  *       │   │   │
  *       │   │   └── Currently visible view
  *       │   │   │
  *       └───┴───┴── Open views
- *
- * If the <panelmultiview> element is "ephemeral", imported subviews will be
- * moved out again to the element specified by the viewCacheId attribute, so
- * that the panel element can be removed safely.
  */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "PanelMultiView",
   "PanelView",
 ];
@@ -123,17 +121,16 @@ XPCOMUtils.defineLazyGetter(this, "gBund
  * registered blockers does not return.
  */
 const BLOCKERS_TIMEOUT_MS = 10000;
 
 const TRANSITION_PHASES = Object.freeze({
   START: 1,
   PREPARE: 2,
   TRANSITION: 3,
-  END: 4
 });
 
 let gNodeToObjectMap = new WeakMap();
 let gWindowsWithUnloadHandler = new WeakSet();
 let gMultiLineElementsMap = new WeakMap();
 
 /**
  * Allows associating an object to a node lazily using a weak map.
@@ -302,24 +299,30 @@ var PanelMultiView = class extends this.
       panelNode.hidePopup();
     }
   }
 
   /**
    * Removes the specified <panel> from the document, ensuring that any
    * <panelmultiview> node it contains is destroyed properly.
    *
+   * If the viewCacheId attribute is present on the <panelmultiview> element,
+   * imported subviews will be moved out again to the element it specifies, so
+   * that the panel element can be removed safely.
+   *
    * If the panel does not contain a <panelmultiview>, it is removed directly.
    * This allows consumers like page actions to accept different panel types.
    */
   static removePopup(panelNode) {
     try {
       let panelMultiViewNode = panelNode.querySelector("panelmultiview");
       if (panelMultiViewNode) {
-        this.forNode(panelMultiViewNode).disconnect();
+        let panelMultiView = this.forNode(panelMultiViewNode);
+        panelMultiView._moveOutKids();
+        panelMultiView.disconnect();
       }
     } finally {
       // Make sure to remove the panel element even if disconnecting fails.
       panelNode.remove();
     }
   }
 
   /**
@@ -340,67 +343,30 @@ var PanelMultiView = class extends this.
 
     gWindowsWithUnloadHandler.add(window);
   }
 
   get _panel() {
     return this.node.parentNode;
   }
 
-  get _mainViewId() {
-    return this.node.getAttribute("mainViewId");
-  }
-  get _mainView() {
-    return this.document.getElementById(this._mainViewId);
-  }
-
-  get _transitioning() {
-    return this.__transitioning;
-  }
   set _transitioning(val) {
-    this.__transitioning = val;
     if (val) {
       this.node.setAttribute("transitioning", "true");
     } else {
       this.node.removeAttribute("transitioning");
     }
   }
 
-  /**
-   * @return {Boolean} |true| when the 'ephemeral' attribute is set, which means
-   *                   that this instance should be ready to be thrown away at
-   *                   any time.
-   */
-  get _ephemeral() {
-    return this.node.hasAttribute("ephemeral");
-  }
-
   get _screenManager() {
     if (this.__screenManager)
       return this.__screenManager;
     return this.__screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
                                     .getService(Ci.nsIScreenManager);
   }
-  /**
-   * @return {panelview} the currently visible subview OR the subview that is
-   *                     about to be shown whilst a 'ViewShowing' event is being
-   *                     dispatched.
-   */
-  get current() {
-    return this.node && this._currentSubView;
-  }
-  get _currentSubView() {
-    // Peek the top of the stack, but fall back to the main view if the list of
-    // opened views is currently empty.
-    let panelView = this.openViews[this.openViews.length - 1];
-    return (panelView && panelView.node) || this._mainView;
-  }
-  get showingSubView() {
-    return this.openViews.length > 1;
-  }
 
   constructor(node) {
     super(node);
     this._openPopupPromise = Promise.resolve(false);
     this._openPopupCancelCallback = () => {};
   }
 
   connect() {
@@ -423,22 +389,16 @@ var PanelMultiView = class extends this.
       this.document.createElement("box");
     offscreenViewStack.classList.add("panel-viewstack");
     offscreenViewContainer.append(offscreenViewStack);
 
     this.node.prepend(offscreenViewContainer);
     this.node.prepend(viewContainer);
 
     this.openViews = [];
-    this.__transitioning = false;
-
-    XPCOMUtils.defineLazyGetter(this, "_panelViewCache", () => {
-      let viewCacheId = this.node.getAttribute("viewCacheId");
-      return viewCacheId ? this.document.getElementById(viewCacheId) : null;
-    });
 
     this._panel.addEventListener("popupshowing", this);
     this._panel.addEventListener("popuppositioned", this);
     this._panel.addEventListener("popuphidden", this);
     this._panel.addEventListener("popupshown", this);
     let cs = this.window.getComputedStyle(this.document.documentElement);
     // Set CSS-determined attributes now to prevent a layout flush when we do
     // it when transitioning between panels.
@@ -447,47 +407,32 @@ var PanelMultiView = class extends this.
     // Proxy these public properties and methods, as used elsewhere by various
     // parts of the browser, to this instance.
     ["goBack", "showSubView"].forEach(method => {
       Object.defineProperty(this.node, method, {
         enumerable: true,
         value: (...args) => this[method](...args)
       });
     });
-    ["current", "showingSubView"].forEach(property => {
-      Object.defineProperty(this.node, property, {
-        enumerable: true,
-        get: () => this[property]
-      });
-    });
   }
 
   disconnect() {
     // Guard against re-entrancy.
     if (!this.node || !this.connected)
       return;
 
-    this._cleanupTransitionPhase();
-    let mainView = this._mainView;
-    if (mainView) {
-      if (this._panelViewCache)
-        this._panelViewCache.appendChild(mainView);
-      mainView.removeAttribute("mainview");
-    }
-
-    this._moveOutKids(this._viewStack);
     this._panel.removeEventListener("mousemove", this);
     this._panel.removeEventListener("popupshowing", this);
     this._panel.removeEventListener("popuppositioned", this);
     this._panel.removeEventListener("popupshown", this);
     this._panel.removeEventListener("popuphidden", this);
     this.window.removeEventListener("keydown", this);
     this.node = this._openPopupPromise = this._openPopupCancelCallback =
       this._viewContainer = this._viewStack = this.__dwu =
-      this._panelViewCache = this._transitionDetails = null;
+      this._transitionDetails = null;
   }
 
   /**
    * Tries to open the panel associated with this PanelMultiView, and displays
    * the main view specified with the "mainViewId" attribute.
    *
    * The hidePopup method can be called while the operation is in progress to
    * prevent the panel from being displayed. View events may also cancel the
@@ -616,32 +561,31 @@ var PanelMultiView = class extends this.
 
     // We close all the views synchronously, so that they are ready to be opened
     // in other PanelMultiView instances. The "popuphidden" handler may also
     // call this function, but the second time openViews will be empty.
     this.closeAllViews();
   }
 
   /**
-   * Remove any child subviews into the panelViewCache, to ensure
-   * they remain usable even if this panelmultiview instance is removed
-   * from the DOM.
-   * @param viewNodeContainer the container from which to remove subviews
+   * Move any child subviews into the element defined by "viewCacheId" to make
+   * sure they will not be removed together with the <panelmultiview> element.
    */
-  _moveOutKids(viewNodeContainer) {
-    if (!this._panelViewCache)
+  _moveOutKids() {
+    let viewCacheId = this.node.getAttribute("viewCacheId");
+    if (!viewCacheId) {
       return;
+    }
 
     // Node.children and Node.childNodes is live to DOM changes like the
     // ones we're about to do, so iterate over a static copy:
-    let subviews = Array.from(viewNodeContainer.childNodes);
+    let subviews = Array.from(this._viewStack.childNodes);
+    let viewCache = this.document.getElementById(viewCacheId);
     for (let subview of subviews) {
-      // XBL lists the 'children' XBL element explicitly. :-(
-      if (subview.nodeName != "children")
-        this._panelViewCache.appendChild(subview);
+      viewCache.appendChild(subview);
     }
   }
 
   /**
    * Slides in the specified view as a subview.
    *
    * @param viewIdOrNode
    *        DOM element or string ID of the <panelview> to display.
@@ -656,42 +600,83 @@ var PanelMultiView = class extends this.
   async _showSubView(viewIdOrNode, anchor) {
     let viewNode = typeof viewIdOrNode == "string" ?
                    this.document.getElementById(viewIdOrNode) : viewIdOrNode;
     if (!viewNode) {
       Cu.reportError(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
       return;
     }
 
+    if (!this.openViews.length) {
+      Cu.reportError(new Error(`Cannot show a subview in a closed panel.`));
+      return;
+    }
+
     let prevPanelView = this.openViews[this.openViews.length - 1];
     let nextPanelView = PanelView.forNode(viewNode);
     if (this.openViews.includes(nextPanelView)) {
       Cu.reportError(new Error(`Subview ${viewNode.id} is already open.`));
       return;
     }
-    if (!(await this._openView(nextPanelView))) {
+
+    // Do not re-enter the process if navigation is already in progress. Since
+    // there is only one active view at any given time, we can do this check
+    // safely, even considering that during the navigation process the actual
+    // view to which prevPanelView refers will change.
+    if (!prevPanelView.active) {
       return;
     }
+    // Marking the view that is about to scrolled out of the visible area as
+    // inactive will prevent re-entrancy and also disable keyboard navigation.
+    // From this point onwards, "await" statements can be used safely.
+    prevPanelView.active = false;
+
+    // Provide visual feedback while navigation is in progress, starting before
+    // the transition starts and ending when the previous view is invisible.
+    if (anchor) {
+      anchor.setAttribute("open", "true");
+    }
+    try {
+      // If the ViewShowing event cancels the operation we have to re-enable
+      // keyboard navigation, but this must be avoided if the panel was closed.
+      if (!(await this._openView(nextPanelView))) {
+        if (prevPanelView.isOpenIn(this)) {
+          // We don't raise a ViewShown event because nothing actually changed.
+          // Technically we should use a different state flag just because there
+          // is code that could check the "active" property to determine whether
+          // to wait for a ViewShown event later, but this only happens in
+          // regression tests and is less likely to be a technique used in
+          // production code, where use of ViewShown is less common.
+          prevPanelView.active = true;
+        }
+        return;
+      }
+
+      prevPanelView.captureKnownSize();
+
+      // The main view of a panel can be a subview in another one. Make sure to
+      // reset all the properties that may be set on a subview.
+      nextPanelView.mainview = false;
+      // The header may change based on how the subview was opened.
+      nextPanelView.headerText = viewNode.getAttribute("title") ||
+                                 (anchor && anchor.getAttribute("label"));
+      // The constrained width of subviews may also vary between panels.
+      nextPanelView.minMaxWidth = prevPanelView.knownWidth;
+
+      if (anchor) {
+        viewNode.classList.add("PanelUI-subView");
+      }
+
+      await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
+    } finally {
+      if (anchor) {
+        anchor.removeAttribute("open");
+      }
+    }
 
-    prevPanelView.captureKnownSize();
-
-    // The main view of a panel can be a subview in another one. Make sure to
-    // reset all the properties that may be set on a subview.
-    nextPanelView.mainview = false;
-    // The header may change based on how the subview was opened.
-    nextPanelView.headerText = viewNode.getAttribute("title") ||
-                               (anchor && anchor.getAttribute("label"));
-    // The constrained width of subviews may also vary between panels.
-    nextPanelView.minMaxWidth = prevPanelView.knownWidth;
-
-    if (anchor) {
-      viewNode.classList.add("PanelUI-subView");
-    }
-
-    await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
     this._activateView(nextPanelView);
   }
 
   /**
    * Navigates backwards by sliding out the most recent subview.
    */
   goBack() {
     this._goBack().catch(Cu.reportError);
@@ -701,33 +686,38 @@ var PanelMultiView = class extends this.
       // This may be called by keyboard navigation or external code when only
       // the main view is open.
       return;
     }
 
     let prevPanelView = this.openViews[this.openViews.length - 1];
     let nextPanelView = this.openViews[this.openViews.length - 2];
 
+    // Like in the showSubView method, do not re-enter navigation while it is
+    // in progress, and make the view inactive immediately. From this point
+    // onwards, "await" statements can be used safely.
+    if (!prevPanelView.active) {
+      return;
+    }
+    prevPanelView.active = false;
+
     prevPanelView.captureKnownSize();
     await this._transitionViews(prevPanelView.node, nextPanelView.node, true);
 
     this._closeLatestView();
 
     this._activateView(nextPanelView);
   }
 
   /**
    * Prepares the main view before showing the panel.
    */
   async _showMainView() {
-    if (!this.node || !this._mainViewId) {
-      return false;
-    }
-
-    let nextPanelView = PanelView.forNode(this._mainView);
+    let nextPanelView = PanelView.forNode(this.document.getElementById(
+      this.node.getAttribute("mainViewId")));
 
     // If the view is already open in another panel, close the panel first.
     let oldPanelMultiViewNode = nextPanelView.node.panelMultiView;
     if (oldPanelMultiViewNode) {
       PanelMultiView.forNode(oldPanelMultiViewNode).hidePopup();
       // Wait for a layout flush after hiding the popup, otherwise the view may
       // not be displayed correctly for some time after the new panel is opened.
       // This is filed as bug 1441015.
@@ -739,27 +729,30 @@ var PanelMultiView = class extends this.
     }
 
     // The main view of a panel can be a subview in another one. Make sure to
     // reset all the properties that may be set on a subview.
     nextPanelView.mainview = true;
     nextPanelView.headerText = "";
     nextPanelView.minMaxWidth = 0;
 
-    await this._cleanupTransitionPhase();
+    // Ensure the view will be visible once the panel is opened.
     nextPanelView.visible = true;
     nextPanelView.descriptionHeightWorkaround();
 
     return true;
   }
 
   /**
    * Opens the specified PanelView and dispatches the ViewShowing event, which
    * can be used to populate the subview or cancel the operation.
    *
+   * This also clears all the attributes and styles that may be left by a
+   * transition that was interrupted.
+   *
    * @resolves With true if the view was opened, false otherwise.
    */
   async _openView(panelView) {
     if (panelView.node.parentNode != this._viewStack) {
       this._viewStack.appendChild(panelView.node);
     }
 
     panelView.node.panelMultiView = this.node;
@@ -778,25 +771,32 @@ var PanelMultiView = class extends this.
     if (canceled) {
       // Handlers for ViewShowing can't know if a different handler requested
       // cancellation, so this will dispatch a ViewHiding event to give a chance
       // to clean up.
       this._closeLatestView();
       return false;
     }
 
+    // Clean up all the attributes and styles related to transitions. We do this
+    // here rather than when the view is closed because we are likely to make
+    // other DOM modifications soon, which isn't the case when closing.
+    let { style } = panelView.node;
+    style.removeProperty("outline");
+    style.removeProperty("width");
+
     return true;
   }
 
   /**
    * Activates the specified view and raises the ViewShown event, unless the
    * view was closed in the meantime.
    */
   _activateView(panelView) {
-    if (panelView.node.panelMultiView == this.node) {
+    if (panelView.isOpenIn(this)) {
       panelView.active = true;
       panelView.dispatchCustomEvent("ViewShown");
     }
   }
 
   /**
    * Closes the most recent PanelView and raises the ViewHiding event.
    *
@@ -804,17 +804,18 @@ var PanelMultiView = class extends this.
    *       to ViewHidden or ViewClosed instead, see bug 1438507.
    */
   _closeLatestView() {
     let panelView = this.openViews.pop();
     panelView.clearNavigation();
     panelView.dispatchCustomEvent("ViewHiding");
     panelView.node.panelMultiView = null;
     // Views become invisible synchronously when they are closed, and they won't
-    // become visible again until they are opened.
+    // become visible again until they are opened. When this is called at the
+    // end of backwards navigation, the view is already invisible.
     panelView.visible = false;
   }
 
   /**
    * Closes all the views that are currently open.
    */
   closeAllViews() {
     // Raise ViewHiding events for open views in reverse order.
@@ -825,51 +826,34 @@ var PanelMultiView = class extends this.
 
   /**
    * Apply a transition to 'slide' from the currently active view to the next
    * one.
    * Sliding the next subview in means that the previous panelview stays where it
    * is and the active panelview slides in from the left in LTR mode, right in
    * RTL mode.
    *
-   * @param {panelview} previousViewNode Node that is currently shown as active,
-   *                                     but is about to be transitioned away.
+   * @param {panelview} previousViewNode Node that is currently displayed, but
+   *                                     is about to be transitioned away. This
+   *                                     must be already inactive at this point.
    * @param {panelview} viewNode         Node that will becode the active view,
    *                                     after the transition has finished.
    * @param {Boolean}   reverse          Whether we're navigation back to a
    *                                     previous view or forward to a next view.
-   * @param {Element}   anchor           the anchor for which we're opening
-   *                                     a new panelview, if any
    */
-  async _transitionViews(previousViewNode, viewNode, reverse, anchor) {
-    // Clean up any previous transition that may be active at this point.
-    await this._cleanupTransitionPhase();
-
-    // There's absolutely no need to show off our epic animation skillz when
-    // the panel's not even open.
-    if (this._panel.state != "open") {
-      return;
-    }
-
+  async _transitionViews(previousViewNode, viewNode, reverse) {
     const { window } = this;
 
     let nextPanelView = PanelView.forNode(viewNode);
     let prevPanelView = PanelView.forNode(previousViewNode);
 
-    if (this._autoResizeWorkaroundTimer)
-      window.clearTimeout(this._autoResizeWorkaroundTimer);
-
     let details = this._transitionDetails = {
       phase: TRANSITION_PHASES.START,
-      previousViewNode, viewNode, reverse, anchor
     };
 
-    if (anchor)
-      anchor.setAttribute("open", "true");
-
     // Set the viewContainer dimensions to make sure only the current view is
     // visible.
     let olderView = reverse ? nextPanelView : prevPanelView;
     this._viewContainer.style.minHeight = olderView.knownHeight + "px";
     this._viewContainer.style.height = prevPanelView.knownHeight + "px";
     this._viewContainer.style.width = prevPanelView.knownWidth + "px";
     // Lock the dimensions of the window that hosts the popup panel.
     let rect = this._panel.popupBoxObject.getOuterScreenRect();
@@ -879,46 +863,49 @@ var PanelMultiView = class extends this.
     let viewRect;
     if (reverse) {
       // Use the cached size when going back to a previous view, but not when
       // reopening a subview, because its contents may have changed.
       viewRect = { width: nextPanelView.knownWidth,
                    height: nextPanelView.knownHeight };
       nextPanelView.visible = true;
     } else if (viewNode.customRectGetter) {
-      // Can't use Object.assign directly with a DOM Rect object because its properties
-      // aren't enumerable.
+      // We use a customRectGetter for WebExtensions panels, because they need
+      // to query the size from an embedded browser. The presence of this
+      // getter also provides an indication that the view node shouldn't be
+      // moved around, otherwise the state of the browser would get disrupted.
       let width = prevPanelView.knownWidth;
       let height = prevPanelView.knownHeight;
       viewRect = Object.assign({height, width}, viewNode.customRectGetter());
       let header = viewNode.firstChild;
       if (header && header.classList.contains("panel-header")) {
         viewRect.height += this._dwu.getBoundsWithoutFlushing(header).height;
       }
       nextPanelView.visible = true;
-      nextPanelView.descriptionHeightWorkaround();
+      await nextPanelView.descriptionHeightWorkaround();
     } else {
-      let oldSibling = viewNode.nextSibling || null;
       this._offscreenViewStack.style.minHeight = olderView.knownHeight + "px";
       this._offscreenViewStack.appendChild(viewNode);
       nextPanelView.visible = true;
 
       // Now that the subview is visible, we can check the height of the
       // description elements it contains.
-      nextPanelView.descriptionHeightWorkaround();
+      await nextPanelView.descriptionHeightWorkaround();
 
       viewRect = await window.promiseDocumentFlushed(() => {
         return this._dwu.getBoundsWithoutFlushing(viewNode);
       });
+      // Bail out if the panel was closed in the meantime.
+      if (!nextPanelView.isOpenIn(this)) {
+        return;
+      }
 
-      try {
-        this._viewStack.insertBefore(viewNode, oldSibling);
-      } catch (ex) {
-        this._viewStack.appendChild(viewNode);
-      }
+      // Place back the view after all the other views that are already open in
+      // order for the transition to work as expected.
+      this._viewStack.appendChild(viewNode);
 
       this._offscreenViewStack.style.removeProperty("min-height");
     }
 
     this._transitioning = true;
     details.phase = TRANSITION_PHASES.PREPARE;
 
     // The 'magic' part: build up the amount of pixels to move right or left.
@@ -940,35 +927,31 @@ var PanelMultiView = class extends this.
       " var(--panelui-subview-transition-duration)";
     this._viewStack.style.willChange = "transform";
     // Use an outline instead of a border so that the size is not affected.
     deepestNode.style.outline = "1px solid var(--panel-separator-color)";
 
     // Now that all the elements are in place for the start of the transition,
     // give the layout code a chance to set the initial values.
     await window.promiseDocumentFlushed(() => {});
+    // Bail out if the panel was closed in the meantime.
+    if (!nextPanelView.isOpenIn(this)) {
+      return;
+    }
 
     // Now set the viewContainer dimensions to that of the new view, which
     // kicks of the height animation.
     this._viewContainer.style.height = viewRect.height + "px";
     this._viewContainer.style.width = viewRect.width + "px";
     this._panel.removeAttribute("width");
     this._panel.removeAttribute("height");
     // We're setting the width property to prevent flickering during the
     // sliding animation with smaller views.
     viewNode.style.width = viewRect.width + "px";
 
-    // For proper bookkeeping, mark the view that is about to scrolled out of
-    // the visible area as inactive, because it won't be possible to simulate
-    // mouse events on it properly. In practice this isn't important, because we
-    // use the separate "transitioning" attribute on the panel to suppress
-    // pointer events. This allows mouse events to be available for the main
-    // view in regression tests that wait for the "popupshown" event.
-    prevPanelView.active = false;
-
     // Kick off the transition!
     details.phase = TRANSITION_PHASES.TRANSITION;
     this._viewStack.style.transform = "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
 
     await new Promise(resolve => {
       details.resolve = resolve;
       this._viewContainer.addEventListener("transitionend", details.listener = ev => {
         // It's quite common that `height` on the view container doesn't need
@@ -984,93 +967,63 @@ var PanelMultiView = class extends this.
         if (ev.target != this._viewStack)
           return;
         this._viewContainer.removeEventListener("transitioncancel", details.cancelListener);
         delete details.cancelListener;
         resolve();
       });
     });
 
-    details.phase = TRANSITION_PHASES.END;
-
-    // Apply the final visibility, unless the view was closed in the meantime.
-    if (nextPanelView.node.panelMultiView == this.node) {
-      prevPanelView.visible = false;
+    // Bail out if the panel was closed during the transition.
+    if (!nextPanelView.isOpenIn(this)) {
+      return;
     }
+    prevPanelView.visible = false;
 
     // This will complete the operation by removing any transition properties.
-    await this._cleanupTransitionPhase(details);
+    nextPanelView.node.style.removeProperty("width");
+    deepestNode.style.removeProperty("outline");
+    this._cleanupTransitionPhase();
 
-    // Focus the correct element, unless the view was closed in the meantime.
-    if (nextPanelView.node.panelMultiView == this.node) {
-      nextPanelView.focusSelectedElement();
-    }
+    nextPanelView.focusSelectedElement();
   }
 
   /**
    * Attempt to clean up the attributes and properties set by `_transitionViews`
    * above. Which attributes and properties depends on the phase the transition
-   * was left from - normally that'd be `TRANSITION_PHASES.END`.
-   *
-   * @param {Object} details Dictionary object containing details of the transition
-   *                         that should be cleaned up after. Defaults to the most
-   *                         recent details.
+   * was left from.
    */
-  async _cleanupTransitionPhase(details = this._transitionDetails) {
-    if (!details || !this.node)
+  _cleanupTransitionPhase() {
+    if (!this._transitionDetails) {
       return;
-
-    let {phase, previousViewNode, viewNode, reverse, resolve, listener, cancelListener, anchor} = details;
-    if (details == this._transitionDetails)
-      this._transitionDetails = null;
+    }
 
-    // Do the things we _always_ need to do whenever the transition ends or is
-    // interrupted.
-    if (anchor)
-      anchor.removeAttribute("open");
+    let {phase, resolve, listener, cancelListener} = this._transitionDetails;
+    this._transitionDetails = null;
 
     if (phase >= TRANSITION_PHASES.START) {
       this._panel.removeAttribute("width");
       this._panel.removeAttribute("height");
-      // Myeah, panel layout auto-resizing is a funky thing. We'll wait
-      // another few milliseconds to remove the width and height 'fixtures',
-      // to be sure we don't flicker annoyingly.
-      // NB: HACK! Bug 1363756 is there to fix this.
-      this._autoResizeWorkaroundTimer = this.window.setTimeout(() => {
-        if (!this._viewContainer)
-          return;
-        this._viewContainer.style.removeProperty("height");
-        this._viewContainer.style.removeProperty("width");
-      }, 500);
+      this._viewContainer.style.removeProperty("height");
+      this._viewContainer.style.removeProperty("width");
     }
     if (phase >= TRANSITION_PHASES.PREPARE) {
       this._transitioning = false;
-      if (reverse)
-        this._viewStack.style.removeProperty("margin-inline-start");
-      let deepestNode = reverse ? previousViewNode : viewNode;
-      deepestNode.style.removeProperty("outline");
+      this._viewStack.style.removeProperty("margin-inline-start");
       this._viewStack.style.removeProperty("transition");
     }
     if (phase >= TRANSITION_PHASES.TRANSITION) {
       this._viewStack.style.removeProperty("transform");
-      viewNode.style.removeProperty("width");
       if (listener)
         this._viewContainer.removeEventListener("transitionend", listener);
       if (cancelListener)
         this._viewContainer.removeEventListener("transitioncancel", cancelListener);
       if (resolve)
         resolve();
     }
-    if (phase >= TRANSITION_PHASES.END) {
-      // We force 'display: none' on the previous view node to make sure that it
-      // doesn't cause an annoying flicker whilst resetting the styles above.
-      previousViewNode.style.display = "none";
-      await this.window.promiseDocumentFlushed(() => {});
-      previousViewNode.style.removeProperty("display");
-    }
   }
 
   _calculateMaxHeight() {
     // While opening the panel, we have to limit the maximum height of any
     // view based on the space that will be available. We cannot just use
     // window.screen.availTop and availHeight because these may return an
     // incorrect value when the window spans multiple screens.
     let anchorBox = this._panel.anchorNode.boxObject;
@@ -1109,20 +1062,21 @@ var PanelMultiView = class extends this.
 
   handleEvent(aEvent) {
     if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) {
       // Shouldn't act on e.g. context menus being shown from within the panel.
       return;
     }
     switch (aEvent.type) {
       case "keydown":
-        if (!this._transitioning) {
-          PanelView.forNode(this._currentSubView)
-                   .keyNavigation(aEvent, this._dir);
-        }
+        // Since we start listening for the "keydown" event when the popup is
+        // already showing and stop listening when the panel is hidden, we
+        // always have at least one view open.
+        let currentView = this.openViews[this.openViews.length - 1];
+        currentView.keyNavigation(aEvent, this._dir);
         break;
       case "mousemove":
         this.openViews.forEach(panelView => panelView.clearNavigation());
         break;
       case "popupshowing": {
         this._viewContainer.setAttribute("panelopen", "true");
         if (!this.node.hasAttribute("disablekeynav")) {
           this.window.addEventListener("keydown", this);
@@ -1143,20 +1097,23 @@ var PanelMultiView = class extends this.
         if (this._panel.state == "showing") {
           let maxHeight = this._calculateMaxHeight();
           this._viewStack.style.maxHeight = maxHeight + "px";
           this._offscreenViewStack.style.maxHeight = maxHeight + "px";
         }
         break;
       }
       case "popupshown":
-        let mainPanelView = PanelView.forNode(this._mainView);
-        // Now that the main view is visible, we can check the height of the
-        // description elements it contains.
-        mainPanelView.descriptionHeightWorkaround();
+        // The main view is always open and visible when the panel is first
+        // shown, so we can check the height of the description elements it
+        // contains and notify consumers using the ViewShown event. In order to
+        // minimize flicker we need to allow synchronous reflows, and we still
+        // make sure the ViewShown event is dispatched synchronously.
+        let mainPanelView = this.openViews[0];
+        mainPanelView.descriptionHeightWorkaround(true).catch(Cu.reportError);
         this._activateView(mainPanelView);
         break;
       case "popuphidden": {
         // WebExtensions consumers can hide the popup from viewshowing, or
         // mid-transition, which disrupts our state:
         this._transitioning = false;
         this._viewContainer.removeAttribute("panelopen");
         this._cleanupTransitionPhase();
@@ -1188,16 +1145,23 @@ var PanelView = class extends this.Assoc
     /**
      * Indicates whether the view is active. When this is false, consumers can
      * wait for the ViewShown event to know when the view becomes active.
      */
     this.active = false;
   }
 
   /**
+   * Indicates whether the view is open in the specified PanelMultiView object.
+   */
+  isOpenIn(panelMultiView) {
+    return this.node.panelMultiView == panelMultiView.node;
+  }
+
+  /**
    * The "mainview" attribute is set before the panel is opened when this view
    * is displayed as the main view, and is removed before the <panelview> is
    * displayed as a subview. The same view element can be displayed as a main
    * view and as a subview at different times.
    */
   set mainview(value) {
     if (value) {
       this.node.setAttribute("mainview", true);
@@ -1300,68 +1264,85 @@ var PanelView = class extends this.Assoc
   /**
    * If the main view or a subview contains wrapping elements, the attribute
    * "descriptionheightworkaround" should be set on the view to force all the
    * wrapping "description", "label" or "toolbarbutton" elements to a fixed
    * height. If the attribute is set and the visibility, contents, or width
    * of any of these elements changes, this function should be called to
    * refresh the calculated heights.
    *
-   * This may trigger a synchronous layout.
+   * @param allowSyncReflows
+   *        If set to true, the function takes a path that allows synchronous
+   *        reflows, but minimizes flickering. This is used for the main view
+   *        because we cannot use the workaround off-screen.
    */
-  descriptionHeightWorkaround() {
+  async descriptionHeightWorkaround(allowSyncReflows = false) {
     if (!this.node.hasAttribute("descriptionheightworkaround")) {
       // This view does not require the workaround.
       return;
     }
 
     // We batch DOM changes together in order to reduce synchronous layouts.
     // First we reset any change we may have made previously. The first time
     // this is called, and in the best case scenario, this has no effect.
     let items = [];
-    // Non-hidden <label> or <description> elements that also aren't empty
-    // and also don't have a value attribute can be multiline (if their
-    // text content is long enough).
-    let isMultiline = ":not(:-moz-any([hidden],[value],:empty))";
-    let selector = [
-      "description" + isMultiline,
-      "label" + isMultiline,
-      "toolbarbutton[wrap]:not([hidden])",
-    ].join(",");
-    for (let element of this.node.querySelectorAll(selector)) {
-      // Ignore items in hidden containers.
-      if (element.closest("[hidden]")) {
-        continue;
+    let collectItems = () => {
+      // Non-hidden <label> or <description> elements that also aren't empty
+      // and also don't have a value attribute can be multiline (if their
+      // text content is long enough).
+      let isMultiline = ":not(:-moz-any([hidden],[value],:empty))";
+      let selector = [
+        "description" + isMultiline,
+        "label" + isMultiline,
+        "toolbarbutton[wrap]:not([hidden])",
+      ].join(",");
+      for (let element of this.node.querySelectorAll(selector)) {
+        // Ignore items in hidden containers.
+        if (element.closest("[hidden]")) {
+          continue;
+        }
+        // Take the label for toolbarbuttons; it only exists on those elements.
+        element = element.labelElement || element;
+
+        let bounds = element.getBoundingClientRect();
+        let previous = gMultiLineElementsMap.get(element);
+        // We don't need to (re-)apply the workaround for invisible elements or
+        // on elements we've seen before and haven't changed in the meantime.
+        if (!bounds.width || !bounds.height ||
+            (previous && element.textContent == previous.textContent &&
+                         bounds.width == previous.bounds.width)) {
+          continue;
+        }
+
+        items.push({ element });
       }
-      // Take the label for toolbarbuttons; it only exists on those elements.
-      element = element.labelElement || element;
-
-      let bounds = element.getBoundingClientRect();
-      let previous = gMultiLineElementsMap.get(element);
-      // We don't need to (re-)apply the workaround for invisible elements or
-      // on elements we've seen before and haven't changed in the meantime.
-      if (!bounds.width || !bounds.height ||
-          (previous && element.textContent == previous.textContent &&
-                       bounds.width == previous.bounds.width)) {
-        continue;
-      }
-
-      items.push({ element });
+    };
+    if (allowSyncReflows) {
+      collectItems();
+    } else {
+      await this.window.promiseDocumentFlushed(collectItems);
     }
 
     // Removing the 'height' property will only cause a layout flush in the next
     // loop below if it was set.
     for (let item of items) {
       item.element.style.removeProperty("height");
     }
 
     // We now read the computed style to store the height of any element that
     // may contain wrapping text.
-    for (let item of items) {
-      item.bounds = item.element.getBoundingClientRect();
+    let measureItems = () => {
+      for (let item of items) {
+        item.bounds = item.element.getBoundingClientRect();
+      }
+    };
+    if (allowSyncReflows) {
+      measureItems();
+    } else {
+      await this.window.promiseDocumentFlushed(measureItems);
     }
 
     // Now we can make all the necessary DOM changes at once.
     for (let { element, bounds } of items) {
       gMultiLineElementsMap.set(element, { bounds, textContent: element.textContent });
       element.style.height = bounds.height + "px";
     }
   }
@@ -1461,21 +1442,28 @@ var PanelView = class extends this.Assoc
    * simulate a click on the currently selected button.
    * The Right or Left key - depending on the text direction - can be used to
    * navigate to the previous view, functioning as a shortcut for the view's
    * back button.
    * Thus, in LTR mode:
    *  - The Right key functions the same as the Enter key, simulating a click
    *  - The Left key triggers a navigation back to the previous view.
    *
+   * Key navigation is only enabled while the view is active, meaning that this
+   * method will return early if it is invoked during a sliding transition.
+   *
    * @param {KeyEvent} event
    * @param {String} dir
    *        Direction for arrow navigation, either "ltr" or "rtl".
    */
   keyNavigation(event, dir) {
+    if (!this.active) {
+      return;
+    }
+
     let buttons = this.buttons;
     if (!buttons || !buttons.length) {
       buttons = this.buttons = this.getNavigableElements();
       // Set the 'tabindex' attribute on the buttons to make sure they're focussable.
       for (let button of buttons) {
         if (!button.classList.contains("subviewbutton-back") &&
             !button.hasAttribute("tabindex")) {
           button.setAttribute("tabindex", 0);
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -405,17 +405,16 @@ const PanelUI = {
       // If the view has a footer, set a convenience class on the panel.
       tempPanel.classList.toggle("cui-widget-panelWithFooter",
                                  viewNode.querySelector(".panel-subview-footer"));
 
       let multiView = document.createElement("panelmultiview");
       multiView.setAttribute("id", "customizationui-widget-multiview");
       multiView.setAttribute("viewCacheId", "appMenu-viewCache");
       multiView.setAttribute("mainViewId", viewNode.id);
-      multiView.setAttribute("ephemeral", true);
       tempPanel.appendChild(multiView);
       viewNode.classList.add("cui-widget-panelview");
 
       let viewShown = false;
       let panelRemover = () => {
         viewNode.classList.remove("cui-widget-panelview");
         if (viewShown) {
           CustomizableUI.removePanelCloseListeners(tempPanel);
--- a/browser/components/customizableui/test/browser_PanelMultiView.js
+++ b/browser/components/customizableui/test/browser_PanelMultiView.js
@@ -56,22 +56,23 @@ function assertLabelVisible(viewIndex, e
 
 /**
  * Opens the specified view as the main view in the specified panel.
  */
 async function openPopup(panelIndex, viewIndex) {
   gPanelMultiViews[panelIndex].setAttribute("mainViewId",
                                             gPanelViews[viewIndex].id);
 
-  let promiseShown = BrowserTestUtils.waitForEvent(gPanels[panelIndex],
-                                                   "popupshown");
+  let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[viewIndex],
+                                                   "ViewShown");
   PanelMultiView.openPopup(gPanels[panelIndex], gPanelAnchors[panelIndex],
                            "bottomcenter topright");
   await promiseShown;
 
+  Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active);
   assertLabelVisible(viewIndex, true);
 }
 
 /**
  * Closes the specified panel.
  */
 async function hidePopup(panelIndex) {
   gPanelMultiViews[panelIndex].setAttribute("mainViewId",
@@ -87,28 +88,30 @@ async function hidePopup(panelIndex) {
  * Opens the specified subview in the specified panel.
  */
 async function showSubView(panelIndex, viewIndex) {
   let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[viewIndex],
                                                    "ViewShown");
   gPanelMultiViews[panelIndex].showSubView(gPanelViews[viewIndex]);
   await promiseShown;
 
+  Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active);
   assertLabelVisible(viewIndex, true);
 }
 
 /**
  * Navigates backwards to the specified view, which is displayed as a result.
  */
 async function goBack(panelIndex, viewIndex) {
   let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[viewIndex],
                                                    "ViewShown");
   gPanelMultiViews[panelIndex].goBack();
   await promiseShown;
 
+  Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active);
   assertLabelVisible(viewIndex, true);
 }
 
 /**
  * Records the specified events on an element into the specified array. An
  * optional callback can be used to respond to events and trigger nested events.
  */
 function recordEvents(element, eventTypes, recordArray,
@@ -274,16 +277,48 @@ add_task(async function test_simple_even
     "panelview-0: ViewShown",
     "panelview-0: ViewHiding",
     "panelmultiview-0: PanelMultiViewHidden",
     "panel-0: popuphidden",
   ]);
 });
 
 /**
+ * Tests that further navigation is suppressed until the new view is shown.
+ */
+add_task(async function test_navigation_suppression() {
+  await openPopup(0, 0);
+
+  // Test re-entering the "showSubView" method.
+  let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[1], "ViewShown");
+  gPanelMultiViews[0].showSubView(gPanelViews[1]);
+  Assert.ok(!PanelView.forNode(gPanelViews[0]).active,
+            "The previous view should become inactive synchronously.");
+
+  // The following call will have no effect.
+  gPanelMultiViews[0].showSubView(gPanelViews[2]);
+  await promiseShown;
+
+  // Test re-entering the "goBack" method.
+  promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[0], "ViewShown");
+  gPanelMultiViews[0].goBack();
+  Assert.ok(!PanelView.forNode(gPanelViews[1]).active,
+            "The previous view should become inactive synchronously.");
+
+  // The following call will have no effect.
+  gPanelMultiViews[0].goBack();
+  await promiseShown;
+
+  // Main view 0 should be displayed.
+  assertLabelVisible(0, true);
+
+  await hidePopup(0);
+});
+
+/**
  * Tests reusing views that are already open in another panel. In this test, the
  * structure of the first panel will change dynamically:
  *
  * - Panel 0
  *   - View 0
  *     - View 1
  * - Panel 1
  *   - View 1
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -167,16 +167,27 @@ var Policies = {
   "DisablePocket": {
     onBeforeAddons(manager, param) {
       if (param) {
         setAndLockPref("extensions.pocket.enabled", false);
       }
     }
   },
 
+  "DisablePrivateBrowsing": {
+    onBeforeAddons(manager, param) {
+      if (param) {
+        manager.disallowFeature("privatebrowsing");
+        manager.disallowFeature("about:privatebrowsing", true);
+        setAndLockPref("browser.privatebrowsing.autostart", false);
+      }
+    }
+  },
+
+
   "DisplayBookmarksToolbar": {
     onBeforeUIStartup(manager, param) {
       if (param) {
         // This policy is meant to change the default behavior, not to force it.
         // If this policy was alreay applied and the user chose to re-hide the
         // bookmarks toolbar, do not show it again.
         runOnce("displayBookmarksToolbar", () => {
           gXulStore.setValue(BROWSER_DOCUMENT_URL, "PersonalToolbar", "collapsed", "false");
@@ -205,16 +216,40 @@ var Policies = {
   },
 
   "FlashPlugin": {
     onBeforeUIStartup(manager, param) {
       addAllowDenyPermissions("plugin:flash", param.Allow, param.Block);
     }
   },
 
+  "Homepage": {
+    onBeforeUIStartup(manager, param) {
+      // |homepages| will be a string containing a pipe-separated ('|') list of
+      // URLs because that is what the "Home page" section of about:preferences
+      // (and therefore what the pref |browser.startup.homepage|) accepts.
+      let homepages = param.URL.spec;
+      if (param.Additional && param.Additional.length > 0) {
+        homepages += "|" + param.Additional.map(url => url.spec).join("|");
+      }
+      if (param.Locked) {
+        setAndLockPref("browser.startup.homepage", homepages);
+        setAndLockPref("browser.startup.page", 1);
+        setAndLockPref("pref.browser.homepage.disable_button.current_page", true);
+        setAndLockPref("pref.browser.homepage.disable_button.bookmark_page", true);
+        setAndLockPref("pref.browser.homepage.disable_button.restore_default", true);
+      } else {
+        runOncePerModification("setHomepage", homepages, () => {
+          Services.prefs.setStringPref("browser.startup.homepage", homepages);
+          Services.prefs.setIntPref("browser.startup.page", 1);
+        });
+      }
+    }
+  },
+
   "InstallAddons": {
     onBeforeUIStartup(manager, param) {
       addAllowDenyPermissions("install", param.Allow, null);
     }
   },
 
   "Popups": {
     onBeforeUIStartup(manager, param) {
@@ -321,11 +356,41 @@ function addAllowDenyPermissions(permiss
  *        The callback to run only once.
  */
 function runOnce(actionName, callback) {
   let prefName = `browser.policies.runonce.${actionName}`;
   if (Services.prefs.getBoolPref(prefName, false)) {
     log.debug(`Not running action ${actionName} again because it has already run.`);
     return;
   }
+  Services.prefs.setBoolPref(prefName, true);
   callback();
-  Services.prefs.setBoolPref(prefName, true);
 }
+
+/**
+ * runOncePerModification
+ *
+ * Helper function similar to runOnce. The difference is that runOnce runs the
+ * callback once when the policy is set, then never again.
+ * runOncePerModification runs the callback once each time the policy value
+ * changes from its previous value.
+ *
+ * @param {string} actionName
+ *        A given name which will be used to track if this callback has run.
+ *        This string will be part of a pref name.
+ * @param {string} policyValue
+ *        The current value of the policy. This will be compared to previous
+ *        values given to this function to determine if the policy value has
+ *        changed. Regardless of the data type of the policy, this must be a
+ *        string.
+ * @param {Function} callback
+ *        The callback to be run when the pref value changes
+ */
+function runOncePerModification(actionName, policyValue, callback) {
+  let prefName = `browser.policies.runOncePerModification.${actionName}`;
+  let oldPolicyValue = Services.prefs.getStringPref(prefName, undefined);
+  if (policyValue === oldPolicyValue) {
+    log.debug(`Not running action ${actionName} again because the policy's value is unchanged`);
+    return;
+  }
+  Services.prefs.setStringPref(prefName, policyValue);
+  callback();
+}
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -148,16 +148,24 @@
     "DisablePocket": {
       "description": "Prevents ability to save webpages to Pocket.",
       "first_available": "60.0",
 
       "type": "boolean",
       "enum": [true]
     },
 
+    "DisablePrivateBrowsing": {
+      "description": "Disables private browsing.",
+      "first_available": "60.0",
+
+      "type": "boolean",
+      "enum": [true]
+    },
+
     "DisplayBookmarksToolbar": {
       "description": "Causes the bookmarks toolbar to be displayed by default.",
       "first_available": "60.0",
 
       "type": "boolean",
       "enum": [true]
     },
 
@@ -194,16 +202,39 @@
           "type": "array",
           "items": {
             "type": "origin"
           }
         }
       }
     },
 
+    "Homepage": {
+      "description": "Set and optionally lock the homepage.",
+      "first_available": "60.0",
+      "enterprise_only": true,
+
+      "type": "object",
+      "properties": {
+        "URL": {
+          "type": "URL"
+        },
+        "Locked": {
+          "type": "boolean"
+        },
+        "Additional": {
+          "type": "array",
+          "items": {
+            "type": "URL"
+          }
+        }
+      },
+      "required": ["URL"]
+    },
+
     "InstallAddons": {
       "description": "Allow or deny popup websites to install webextensions.",
       "first_available": "60.0",
 
       "type": "object",
       "properties": {
         "Allow": {
           "type": "array",
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -20,12 +20,14 @@ support-files =
 [browser_policy_block_about_support.js]
 [browser_policy_block_set_desktop_background.js]
 [browser_policy_bookmarks.js]
 [browser_policy_default_browser_check.js]
 [browser_policy_disable_formhistory.js]
 [browser_policy_disable_fxscreenshots.js]
 [browser_policy_disable_masterpassword.js]
 [browser_policy_disable_pocket.js]
+[browser_policy_disable_privatebrowsing.js]
 [browser_policy_disable_shield.js]
 [browser_policy_display_bookmarks.js]
 [browser_policy_display_menu.js]
 [browser_policy_remember_passwords.js]
+[browser_policy_set_homepage.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_disable_privatebrowsing.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+add_task(async function setup() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "DisablePrivateBrowsing": true
+    }
+  });
+});
+
+add_task(async function test_menu_shown() {
+  is(PrivateBrowsingUtils.enabled, false, "Private browsing should be disabled");
+  let newWin = await BrowserTestUtils.openNewBrowserWindow();
+  let privateBrowsingCommand = newWin.document.getElementById("Tools:PrivateBrowsing");
+  is(privateBrowsingCommand.hidden, true, "The private browsing command should be hidden");
+  await BrowserTestUtils.closeWindow(newWin);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Prefs that will be touched by the test and need to be reset when the test
+// cleans up.
+const TOUCHED_PREFS = {
+  "browser.startup.homepage": "String",
+  "browser.startup.page": "Int",
+  "pref.browser.homepage.disable_button.current_page": "Bool",
+  "pref.browser.homepage.disable_button.bookmark_page": "Bool",
+  "pref.browser.homepage.disable_button.restore_default": "Bool",
+  "browser.policies.runOncePerModification.setHomepage": "String",
+};
+let ORIGINAL_PREF_VALUE = {};
+
+add_task(function read_original_pref_values() {
+  for (let pref in TOUCHED_PREFS) {
+    let prefType = TOUCHED_PREFS[pref];
+    ORIGINAL_PREF_VALUE[pref] =
+      Services.prefs[`get${prefType}Pref`](pref, undefined);
+  }
+});
+registerCleanupFunction(function restore_pref_values() {
+  let defaults = Services.prefs.getDefaultBranch("");
+  for (let pref in TOUCHED_PREFS) {
+    Services.prefs.unlockPref(pref);
+    Services.prefs.clearUserPref(pref);
+    let originalValue = ORIGINAL_PREF_VALUE[pref];
+    let prefType = TOUCHED_PREFS[pref];
+    if (originalValue !== undefined) {
+      defaults[`set${prefType}Pref`](pref, originalValue);
+    }
+  }
+});
+
+add_task(async function homepage_test_simple() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Homepage": {
+        "URL": "http://example1.com/"
+      }
+    }
+  });
+  is(Services.prefs.getStringPref("browser.startup.homepage", ""),
+     "http://example1.com/", "Homepage URL should have been set");
+  is(Services.prefs.getIntPref("browser.startup.page", -1), 1,
+     "Homepage should be used instead of blank page or previous tabs");
+});
+
+add_task(async function homepage_test_repeat_same_policy_value() {
+  // Simulate homepage change after policy applied
+  Services.prefs.setStringPref("browser.startup.homepage",
+                               "http://example2.com/");
+  Services.prefs.setIntPref("browser.startup.page", 3);
+
+  // Policy should have no effect. Homepage has not been locked and policy value
+  // has not changed. We should be respecting the homepage that the user gave.
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Homepage": {
+        "URL": "http://example1.com/"
+      }
+    }
+  });
+  is(Services.prefs.getStringPref("browser.startup.homepage", ""),
+     "http://example2.com/",
+     "Homepage URL should not have been overridden by pre-existing policy value");
+  is(Services.prefs.getIntPref("browser.startup.page", -1), 3,
+     "Start page type should not have been overridden by pre-existing policy value");
+});
+
+add_task(async function homepage_test_different_policy_value() {
+  // This policy is a change from the policy's previous value. This should
+  // override the user's homepage
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Homepage": {
+        "URL": "http://example3.com/"
+      }
+    }
+  });
+  is(Services.prefs.getStringPref("browser.startup.homepage", ""),
+     "http://example3.com/",
+     "Homepage URL should have been overridden by the policy value");
+  is(Services.prefs.getIntPref("browser.startup.page", -1), 1,
+     "Start page type should have been overridden by setting the policy value");
+});
+
+add_task(async function homepage_test_empty_additional() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Homepage": {
+        "URL": "http://example1.com/",
+        "Additional": []
+      }
+    }
+  });
+  is(Services.prefs.getStringPref("browser.startup.homepage", ""),
+     "http://example1.com/", "Homepage URL should have been set properly");
+});
+
+add_task(async function homepage_test_single_additional() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Homepage": {
+        "URL": "http://example1.com/",
+        "Additional": ["http://example2.com/"]
+      }
+    }
+  });
+  is(Services.prefs.getStringPref("browser.startup.homepage", ""),
+     "http://example1.com/|http://example2.com/",
+     "Homepage URL should have been set properly");
+
+});
+
+add_task(async function homepage_test_multiple_additional() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Homepage": {
+        "URL": "http://example1.com/",
+        "Additional": ["http://example2.com/",
+                       "http://example3.com/"]
+      }
+    }
+  });
+  is(Services.prefs.getStringPref("browser.startup.homepage", ""),
+     "http://example1.com/|http://example2.com/|http://example3.com/",
+     "Homepage URL should have been set properly");
+
+});
+
+add_task(async function homepage_test_locked() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Homepage": {
+        "URL": "http://example4.com/",
+        "Additional": ["http://example5.com/",
+                       "http://example6.com/"],
+        "Locked": true
+      }
+    }
+  });
+  is(Services.prefs.getStringPref("browser.startup.homepage", ""),
+     "http://example4.com/|http://example5.com/|http://example6.com/",
+     "Homepage URL should have been set properly");
+  is(Services.prefs.getIntPref("browser.startup.page", -1), 1,
+     "Homepage should be used instead of blank page or previous tabs");
+  is(Services.prefs.prefIsLocked("browser.startup.homepage"), true,
+     "Homepage pref should be locked");
+  is(Services.prefs.prefIsLocked("browser.startup.page"), true,
+     "Start page type pref should be locked");
+
+  // Test that UI is disabled when the Locked property is enabled
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+  await ContentTask.spawn(tab.linkedBrowser, null, async function() {
+    is(content.document.getElementById("browserStartupPage").disabled, true,
+       "When homepage is locked, the startup page choice controls should be locked");
+    is(content.document.getElementById("browserHomePage").disabled, true,
+       "Homepage input should be disabled");
+    is(content.document.getElementById("useCurrent").disabled, true,
+       "\"Use Current Page\" button should be disabled");
+    is(content.document.getElementById("useBookmark").disabled, true,
+       "\"Use Bookmark...\" button should be disabled");
+    is(content.document.getElementById("restoreDefaultHomePage").disabled, true,
+       "\"Restore to Default\" button should be disabled");
+  });
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -311,17 +311,17 @@ class BasePopup {
         allowScriptsToClose: true,
         blockParser: this.blockParser,
         fixedWidth: this.fixedWidth,
         maxWidth: 800,
         maxHeight: 600,
         stylesheets: this.STYLESHEETS,
       });
 
-      browser.loadURI(popupURL);
+      browser.loadURIWithFlags(popupURL, {triggeringPrincipal: this.extension.principal});
     });
   }
 
   unblockParser() {
     this.browserReady.then(browser => {
       this.browser.messageManager.sendAsyncMessage("Extension:UnblockParser");
     });
   }
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -654,16 +654,20 @@ class Tab extends TabBase {
   get pinned() {
     return this.nativeTab.pinned;
   }
 
   get active() {
     return this.nativeTab.selected;
   }
 
+  get highlighted() {
+    return this.nativeTab.selected;
+  }
+
   get selected() {
     return this.nativeTab.selected;
   }
 
   get status() {
     if (this.nativeTab.getAttribute("busy") === "true") {
       return "loading";
     }
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -158,16 +158,17 @@
     "paths": [
       ["sidebarAction"]
     ]
   },
   "tabs": {
     "url": "chrome://browser/content/ext-tabs.js",
     "schema": "chrome://browser/content/schemas/tabs.json",
     "scopes": ["addon_parent"],
+    "events": ["update", "disable"],
     "paths": [
       ["tabs"]
     ]
   },
   "urlOverrides": {
     "url": "chrome://browser/content/ext-url-overrides.js",
     "schema": "chrome://browser/content/schemas/url_overrides.json",
     "scopes": ["addon_parent"],
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -259,17 +259,19 @@ class ParentDevToolsPanel {
 
     extensions.emit("extension-browser-inserted", browser, {
       devtoolsToolboxInfo: {
         toolboxPanelId: this.id,
         inspectedWindowTabId: getTargetTabIdForToolbox(toolbox),
       },
     });
 
-    browser.loadURI(url);
+    browser.loadURIWithFlags(url, {
+      triggeringPrincipal: extension.principal,
+    });
   }
 
   destroyBrowserElement() {
     const {browser, unwatchExtensionProxyContextLoad} = this;
     if (unwatchExtensionProxyContextLoad) {
       this.unwatchExtensionProxyContextLoad = null;
       unwatchExtensionProxyContextLoad();
     }
--- a/browser/components/extensions/ext-devtools.js
+++ b/browser/components/extensions/ext-devtools.js
@@ -171,17 +171,19 @@ class DevToolsPage extends HiddenExtensi
 
     extensions.emit("extension-browser-inserted", this.browser, {
       devtoolsToolboxInfo: {
         inspectedWindowTabId: getTargetTabIdForToolbox(this.toolbox),
         themeName: DevToolsShim.getTheme(),
       },
     });
 
-    this.browser.loadURI(this.url);
+    this.browser.loadURIWithFlags(this.url, {
+      triggeringPrincipal: this.extension.principal,
+    });
 
     await this.waitForTopLevelContext;
   }
 
   close() {
     if (this.closed) {
       throw new Error("Unable to shutdown a closed DevToolsPage instance");
     }
--- a/browser/components/extensions/ext-sidebarAction.js
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -131,42 +131,33 @@ this.sidebarAction = class extends Exten
       this.updateWindow(window);
       let {SidebarUI} = window;
       if (install || SidebarUI.lastOpenedId == this.id) {
         SidebarUI.show(this.id);
       }
     }
   }
 
-  sidebarUrl(panel) {
-    let url = `${sidebarURL}?panel=${encodeURIComponent(panel)}`;
-
-    if (this.extension.remote) {
-      url += "&remote=1";
-    }
-
-    if (this.browserStyle) {
-      url += "&browser-style=1";
-    }
-
-    return url;
-  }
-
   createMenuItem(window, details) {
     let {document, SidebarUI} = window;
 
     // Use of the broadcaster allows browser-sidebar.js to properly manage the
     // checkmarks in the menus.
     let broadcaster = document.createElementNS(XUL_NS, "broadcaster");
     broadcaster.setAttribute("id", this.id);
     broadcaster.setAttribute("autoCheck", "false");
     broadcaster.setAttribute("type", "checkbox");
     broadcaster.setAttribute("group", "sidebar");
     broadcaster.setAttribute("label", details.title);
-    broadcaster.setAttribute("sidebarurl", this.sidebarUrl(details.panel));
+    broadcaster.setAttribute("sidebarurl", sidebarURL);
+    broadcaster.setAttribute("panel", details.panel);
+    if (this.browserStyle) {
+      broadcaster.setAttribute("browserStyle", "true");
+    }
+    broadcaster.setAttribute("extensionId", this.extension.id);
     let id = `ext-key-id-${this.id}`;
     broadcaster.setAttribute("key", id);
 
     // oncommand gets attached to menuitem, so we use the observes attribute to
     // get the command id we pass to SidebarUI.
     broadcaster.setAttribute("oncommand", "SidebarUI.toggle(this.getAttribute('observes'))");
 
     let header = document.getElementById("sidebar-switcher-target");
@@ -222,20 +213,19 @@ this.sidebarAction = class extends Exten
       menu = this.createMenuItem(window, tabData);
     }
 
     // Update the broadcaster first, it will update both menus.
     let broadcaster = document.getElementById(this.id);
     broadcaster.setAttribute("tooltiptext", title);
     broadcaster.setAttribute("label", title);
 
-    let url = this.sidebarUrl(tabData.panel);
-    let urlChanged = url !== broadcaster.getAttribute("sidebarurl");
+    let urlChanged = tabData.panel !== broadcaster.getAttribute("panel");
     if (urlChanged) {
-      broadcaster.setAttribute("sidebarurl", url);
+      broadcaster.setAttribute("panel", tabData.panel);
     }
 
     this.setMenuIcon(menu, tabData);
 
     let button = document.getElementById(this.buttonId);
     this.setMenuIcon(button, tabData);
 
     // Update the sidebar if this extension is the current sidebar.
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -19,16 +19,33 @@ XPCOMUtils.defineLazyGetter(this, "strBu
 });
 
 var {
   ExtensionError,
 } = ExtensionUtils;
 
 const TABHIDE_PREFNAME = "extensions.webextensions.tabhide.enabled";
 
+function showHiddenTabs(id) {
+  let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+  while (windowsEnum.hasMoreElements()) {
+    let win = windowsEnum.getNext();
+    if (win.closed || !win.gBrowser) {
+      continue;
+    }
+
+    for (let tab of win.gBrowser.tabs) {
+      if (tab.hidden && tab.ownerGlobal &&
+          SessionStore.getTabValue(tab, "hiddenBy") === id) {
+        win.gBrowser.showTab(tab);
+      }
+    }
+  }
+}
+
 let tabListener = {
   tabReadyInitialized: false,
   tabReadyPromises: new WeakMap(),
   initializingTabs: new WeakSet(),
 
   initTabReady() {
     if (!this.tabReadyInitialized) {
       windowTracker.addListener("progress", this);
@@ -76,33 +93,24 @@ let tabListener = {
         this.tabReadyPromises.set(nativeTab, deferred);
       }
     }
     return deferred.promise;
   },
 };
 
 this.tabs = class extends ExtensionAPI {
-  onShutdown(reason) {
-    if (!this.extension.hasPermission("tabHide")) {
-      return;
+  static onUpdate(id, manifest) {
+    if (!manifest.permissions || !manifest.permissions.includes("tabHide")) {
+      showHiddenTabs(id);
     }
-    if (reason == "ADDON_DISABLE" ||
-        reason == "ADDON_UNINSTALL") {
-      // Show all hidden tabs if a tab managing extension is uninstalled or
-      // disabled.  If a user has more than one, the extensions will need to
-      // self-manage re-hiding tabs.
-      for (let tab of this.extension.tabManager.query()) {
-        let nativeTab = tabTracker.getTab(tab.id);
-        if (nativeTab.hidden && nativeTab.ownerGlobal &&
-            SessionStore.getTabValue(nativeTab, "hiddenBy") === this.extension.id) {
-          nativeTab.ownerGlobal.gBrowser.showTab(nativeTab);
-        }
-      }
-    }
+  }
+
+  static onDisable(id) {
+    showHiddenTabs(id);
   }
 
   getAPI(context) {
     let {extension} = context;
 
     let {tabManager} = extension;
 
     function getTabOrActive(tabId) {
@@ -502,20 +510,23 @@ this.tabs = class extends ExtensionAPI {
 
           if (updateProperties.url !== null) {
             let url = context.uri.resolve(updateProperties.url);
 
             if (!context.checkLoadURL(url, {dontReportErrors: true})) {
               return Promise.reject({message: `Illegal URL: ${url}`});
             }
 
-            let flags = updateProperties.loadReplace
-              ? Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY
-              : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
-            nativeTab.linkedBrowser.loadURIWithFlags(url, {flags});
+            let options = {
+              flags: updateProperties.loadReplace
+                      ? Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY
+                      : Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+              triggeringPrincipal: context.principal,
+            };
+            nativeTab.linkedBrowser.loadURIWithFlags(url, options);
           }
 
           if (updateProperties.active !== null) {
             if (updateProperties.active) {
               tabbrowser.selectedTab = nativeTab;
             } else {
               // Not sure what to do here? Which tab should we select?
             }
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -123,16 +123,19 @@ this.windows = class extends ExtensionAP
             let tab = tabTracker.getTab(createData.tabId);
 
             // Private browsing tabs can only be moved to private browsing
             // windows.
             let incognito = PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser);
             if (createData.incognito !== null && createData.incognito != incognito) {
               return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
             }
+            if (createData.incognito && !PrivateBrowsingUtils.enabled) {
+              return Promise.reject({message: "`incognito` cannot be used if incognito mode is disabled"});
+            }
             createData.incognito = incognito;
 
             args.appendElement(tab);
           } else if (createData.url !== null) {
             if (Array.isArray(createData.url)) {
               let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
               for (let url of createData.url) {
                 array.appendElement(mkstr(url));
--- a/browser/components/extensions/schemas/devtools.json
+++ b/browser/components/extensions/schemas/devtools.json
@@ -19,13 +19,13 @@
             "devtools"
           ]
         }]
       }
     ]
   },
   {
     "namespace": "devtools",
-    "permissions": ["devtools"],
+    "permissions": ["manifest:devtools_page"],
     "allowedContexts": ["devtools", "devtools_only"],
     "defaultContexts": ["devtools", "devtools_only"]
   }
 ]
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -159,19 +159,19 @@ skip-if = !e10s
 [browser_ext_tabs_executeScript.js]
 [browser_ext_tabs_executeScript_good.js]
 [browser_ext_tabs_executeScript_bad.js]
 [browser_ext_tabs_executeScript_multiple.js]
 [browser_ext_tabs_executeScript_no_create.js]
 [browser_ext_tabs_executeScript_runAt.js]
 [browser_ext_tabs_getCurrent.js]
 [browser_ext_tabs_hide.js]
+[browser_ext_tabs_hide_update.js]
 [browser_ext_tabs_insertCSS.js]
 [browser_ext_tabs_lastAccessed.js]
-skip-if = os == 'win' # Bug 1434590
 [browser_ext_tabs_lazy.js]
 [browser_ext_tabs_removeCSS.js]
 [browser_ext_tabs_move_array.js]
 [browser_ext_tabs_move_window.js]
 [browser_ext_tabs_move_window_multiple.js]
 [browser_ext_tabs_move_window_pinned.js]
 [browser_ext_tabs_onHighlighted.js]
 [browser_ext_tabs_onUpdated.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js
@@ -0,0 +1,143 @@
+"use strict";
+
+ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
+
+AddonTestUtils.initMochitest(this);
+
+const ID = "@test-tabs-addon";
+
+async function updateExtension(ID, options) {
+  let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+  await Promise.all([
+    AddonTestUtils.promiseWebExtensionStartup(ID),
+    AddonManager.installTemporaryAddon(xpi),
+  ]);
+}
+
+async function disableExtension(ID) {
+  let disabledPromise = awaitEvent("shutdown", ID);
+  let addon = await AddonManager.getAddonByID(ID);
+  addon.userDisabled = true;
+  await disabledPromise;
+}
+
+function getExtension() {
+  async function background() {
+    let tabs = await browser.tabs.query({url: "http://example.com/"});
+    let testTab = tabs[0];
+
+    browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+      if ("hidden" in changeInfo) {
+        browser.test.assertEq(tabId, testTab.id, "correct tab was hidden");
+        browser.test.assertTrue(changeInfo.hidden, "tab is hidden");
+        browser.test.sendMessage("changeInfo");
+      }
+    });
+
+    let hidden = await browser.tabs.hide(testTab.id);
+    browser.test.assertEq(hidden[0], testTab.id, "tabs.hide hide the tab");
+    tabs = await browser.tabs.query({hidden: true});
+    browser.test.assertEq(tabs[0].id, testTab.id, "tabs.query result was hidden");
+    browser.test.sendMessage("ready");
+  }
+
+  let extdata = {
+    manifest: {
+      version: "1.0",
+      "applications": {
+        "gecko": {
+          "id": ID,
+        },
+      },
+      permissions: ["tabs", "tabHide"],
+    },
+    background,
+    useAddonManager: "temporary",
+  };
+  return ExtensionTestUtils.loadExtension(extdata);
+}
+
+// Test our update handling.  Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown.  We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_update() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.webextensions.tabhide.enabled", true]],
+  });
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+  await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+
+  const extension = getExtension();
+  await extension.startup();
+
+  // test onUpdated
+  await Promise.all([
+    extension.awaitMessage("ready"),
+    extension.awaitMessage("changeInfo"),
+  ]);
+  Assert.ok(tab.hidden, "Tab is hidden by extension");
+
+  // Test that update doesn't hide tabs when tabHide permission is present.
+  let extdata = {
+    manifest: {
+      version: "2.0",
+      "applications": {
+        "gecko": {
+          "id": ID,
+        },
+      },
+      permissions: ["tabs", "tabHide"],
+    },
+  };
+  await updateExtension(ID, extdata);
+  Assert.ok(tab.hidden, "Tab is hidden hidden after update");
+
+  // Test that update does hide tabs when tabHide permission is removed.
+  extdata.manifest = {
+    version: "3.0",
+    "applications": {
+      "gecko": {
+        "id": ID,
+      },
+    },
+    permissions: ["tabs"],
+  };
+  await updateExtension(ID, extdata);
+  Assert.ok(!tab.hidden, "Tab is not hidden hidden after update");
+
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
+
+
+// Test our update handling.  Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown.  We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_disable() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.webextensions.tabhide.enabled", true]],
+  });
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+  await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+
+  const extension = getExtension();
+  await extension.startup();
+
+  // test onUpdated
+  await Promise.all([
+    extension.awaitMessage("ready"),
+    extension.awaitMessage("changeInfo"),
+  ]);
+  Assert.ok(tab.hidden, "Tab is hidden by extension");
+
+  // Test that disable does hide tabs.
+  await disableExtension(ID);
+  Assert.ok(!tab.hidden, "Tab is not hidden hidden after disable");
+
+  await extension.unload();
+
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/browser/components/extensions/test/browser/browser_ext_tabs_query.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js
@@ -165,16 +165,41 @@ add_task(async function() {
       browser.test.notifyPass("tabs.query");
     },
   });
 
   await extension.startup();
   await extension.awaitFinish("tabs.query");
   await extension.unload();
 
+  // match highlighted
+  extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs"],
+    },
+
+    background: async function() {
+      let tabs1 = await browser.tabs.query({highlighted: false});
+      browser.test.assertEq(3, tabs1.length, "should have three non-highlighted tabs");
+
+      let tabs2 = await browser.tabs.query({highlighted: true});
+      browser.test.assertEq(1, tabs2.length, "should have one highlighted tab");
+
+      for (let tab of [...tabs1, ...tabs2]) {
+        browser.test.assertEq(tab.active, tab.highlighted, "highlighted and active are equal in tab " + tab.index);
+      }
+
+      browser.test.notifyPass("tabs.query");
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("tabs.query");
+  await extension.unload();
+
   // test width and height
   extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["tabs"],
     },
 
     background: function() {
       browser.test.onMessage.addListener(async msg => {
--- a/browser/components/extensions/test/browser/browser_ext_themes_icons.js
+++ b/browser/components/extensions/test/browser/browser_ext_themes_icons.js
@@ -69,25 +69,25 @@ function checkButtons(icons, iconInfo, a
         button[1],
         `The ${button[1]} should not have its icon customized in the ${area}`);
     }
   }
 }
 
 async function runTestWithIcons(icons) {
   const FRAME_COLOR = [71, 105, 91];
-  const BACKGROUND_TAB_TEXT_COLOR = [207, 221, 192, .9];
+  const TAB_BACKGROUND_TEXT_COLOR = [207, 221, 192, .9];
   let manifest = {
     "theme": {
       "images": {
         "theme_frame": "fox.svg",
       },
       "colors": {
         "frame": FRAME_COLOR,
-        "background_tab_text": BACKGROUND_TAB_TEXT_COLOR,
+        "tab_background_text": TAB_BACKGROUND_TEXT_COLOR,
       },
       "icons": {},
     },
   };
   let files = {
     "fox.svg": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
   };
 
deleted file mode 100644
--- a/browser/components/newtab/NewTabPrefsProvider.jsm
+++ /dev/null
@@ -1,126 +0,0 @@
-"use strict";
-
-var EXPORTED_SYMBOLS = ["NewTabPrefsProvider"];
-
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
-  const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {});
-  return EventEmitter;
-});
-
-// Supported prefs and data type
-const gPrefsMap = new Map([
-  ["browser.newtabpage.enabled", "bool"],
-  ["browser.newtabpage.enhanced", "bool"],
-  ["browser.newtabpage.introShown", "bool"],
-  ["browser.newtabpage.updateIntroShown", "bool"],
-  ["browser.newtabpage.pinned", "str"],
-  ["browser.newtabpage.blocked", "str"],
-  ["browser.search.hiddenOneOffs", "str"],
-]);
-
-// prefs that are important for the newtab page
-const gNewtabPagePrefs = new Set([
-  "browser.newtabpage.enabled",
-  "browser.newtabpage.enhanced",
-  "browser.newtabpage.pinned",
-  "browser.newtabpage.blocked",
-  "browser.newtabpage.introShown",
-  "browser.newtabpage.updateIntroShown",
-  "browser.search.hiddenOneOffs",
-]);
-
-let PrefsProvider = function PrefsProvider() {
-  EventEmitter.decorate(this);
-};
-
-PrefsProvider.prototype = {
-
-  observe(subject, topic, data) { // jshint ignore:line
-    if (topic === "nsPref:changed") {
-      if (gPrefsMap.has(data)) {
-        switch (gPrefsMap.get(data)) {
-          case "bool":
-            this.emit(data, Services.prefs.getBoolPref(data, false));
-            break;
-          case "str":
-            this.emit(data, Services.prefs.getStringPref(data, ""));
-            break;
-          case "localized":
-            if (Services.prefs.getPrefType(data) == Ci.nsIPrefBranch.PREF_INVALID) {
-              this.emit(data, "");
-            } else {
-              try {
-                this.emit(data, Services.prefs.getComplexValue(data, Ci.nsIPrefLocalizedString));
-              } catch (e) {
-                this.emit(data, Services.prefs.getStringPref(data));
-              }
-            }
-            break;
-          default:
-            this.emit(data);
-            break;
-        }
-      }
-    } else {
-      Cu.reportError(new Error("NewTabPrefsProvider observing unknown topic"));
-    }
-  },
-
-  /*
-   * Return the preferences that are important to the newtab page
-   */
-  get newtabPagePrefs() {
-    let results = {};
-    for (let pref of gNewtabPagePrefs) {
-      results[pref] = null;
-
-      if (Services.prefs.getPrefType(pref) != Ci.nsIPrefBranch.PREF_INVALID) {
-        switch (gPrefsMap.get(pref)) {
-          case "bool":
-            results[pref] = Services.prefs.getBoolPref(pref);
-            break;
-          case "str":
-            results[pref] = Services.prefs.getStringPref(pref);
-            break;
-          case "localized":
-            try {
-              results[pref] = Services.prefs.getComplexValue(pref, Ci.nsIPrefLocalizedString);
-            } catch (e) {
-              results[pref] = Services.prefs.getStringPref(pref);
-            }
-            break;
-        }
-      }
-    }
-    return results;
-  },
-
-  get prefsMap() {
-    return gPrefsMap;
-  },
-
-  init() {
-    for (let pref of gPrefsMap.keys()) {
-      Services.prefs.addObserver(pref, this);
-    }
-  },
-
-  uninit() {
-    for (let pref of gPrefsMap.keys()) {
-      Services.prefs.removeObserver(pref, this);
-    }
-  }
-};
-
-/**
- * Singleton that serves as the default new tab pref provider for the grid.
- */
-const gPrefs = new PrefsProvider();
-
-let NewTabPrefsProvider = {
-  prefs: gPrefs,
-  newtabPagePrefSet: gNewtabPagePrefs,
-};
deleted file mode 100644
--- a/browser/components/newtab/NewTabRemoteResources.jsm
+++ /dev/null
@@ -1,15 +0,0 @@
-/* exported NewTabRemoteResources */
-
-"use strict";
-
-var EXPORTED_SYMBOLS = ["NewTabRemoteResources"];
-
-const NewTabRemoteResources = {
-  MODE_CHANNEL_MAP: {
-    production: {origin: "https://content.cdn.mozilla.net"},
-    staging: {origin: "https://s3_proxy_tiles.stage.mozaws.net"},
-    test: {origin: "https://example.com"},
-    test2: {origin: "http://mochi.test:8888"},
-    dev: {origin: "http://localhost:8888"}
-  }
-};
deleted file mode 100644
--- a/browser/components/newtab/NewTabSearchProvider.jsm
+++ /dev/null
@@ -1,98 +0,0 @@
-"use strict";
-
-var EXPORTED_SYMBOLS = ["NewTabSearchProvider"];
-
-const CURRENT_ENGINE = "browser-search-engine-modified";
-
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-ChromeUtils.defineModuleGetter(this, "ContentSearch",
-                               "resource:///modules/ContentSearch.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
-  const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {});
-  return EventEmitter;
-});
-
-function SearchProvider() {
-  EventEmitter.decorate(this);
-}
-
-SearchProvider.prototype = {
-
-  observe(subject, topic, data) { // jshint unused:false
-    // all other topics are not relevant to content searches and can be
-    // ignored by NewTabSearchProvider
-    if (data === "engine-current" && topic === CURRENT_ENGINE) {
-      (async () => {
-        try {
-          let state = await ContentSearch.currentStateObj(true);
-          let engine = state.currentEngine;
-          this.emit(CURRENT_ENGINE, engine);
-        } catch (e) {
-          Cu.reportError(e);
-        }
-      })();
-    }
-  },
-
-  init() {
-    try {
-      Services.obs.addObserver(this, CURRENT_ENGINE, true);
-    } catch (e) {
-      Cu.reportError(e);
-    }
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-    Ci.nsISupportsWeakReference
-  ]),
-
-  uninit() {
-    try {
-      Services.obs.removeObserver(this, CURRENT_ENGINE);
-    } catch (e) {
-      Cu.reportError(e);
-    }
-  },
-
-  get searchSuggestionUIStrings() {
-    return ContentSearch.searchSuggestionUIStrings;
-  },
-
-  removeFormHistory({browser}, suggestion) {
-    ContentSearch.removeFormHistoryEntry({target: browser}, suggestion);
-  },
-
-  manageEngines(browser) {
-    const browserWin = browser.ownerGlobal;
-    browserWin.openPreferences("paneSearch", { origin: "contentSearch" });
-  },
-
-  async asyncGetState() {
-    let state = await ContentSearch.currentStateObj(true);
-    return state;
-  },
-
-  async asyncPerformSearch({browser}, searchData) {
-    ContentSearch.performSearch({target: browser}, searchData);
-    await ContentSearch.addFormHistoryEntry({target: browser}, searchData.searchString);
-  },
-
-  async asyncCycleEngine(engineName) {
-    Services.search.currentEngine = Services.search.getEngineByName(engineName);
-    let state = await ContentSearch.currentStateObj(true);
-    let newEngine = state.currentEngine;
-    this.emit(CURRENT_ENGINE, newEngine);
-  },
-
-  async asyncGetSuggestions(engineName, searchString, target) {
-    let suggestions = ContentSearch.getSuggestions(engineName, searchString, target.browser);
-    return suggestions;
-  },
-};
-
-const NewTabSearchProvider = {
-  search: new SearchProvider(),
-};
deleted file mode 100644
--- a/browser/components/newtab/NewTabWebChannel.jsm
+++ /dev/null
@@ -1,286 +0,0 @@
-"use strict";
-
-var EXPORTED_SYMBOLS = ["NewTabWebChannel"];
-
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-ChromeUtils.defineModuleGetter(this, "NewTabPrefsProvider",
-                               "resource:///modules/NewTabPrefsProvider.jsm");
-ChromeUtils.defineModuleGetter(this, "NewTabRemoteResources",
-                               "resource:///modules/NewTabRemoteResources.jsm");
-ChromeUtils.defineModuleGetter(this, "WebChannel",
-                               "resource://gre/modules/WebChannel.jsm");
-XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
-  const {EventEmitter} = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {});
-  return EventEmitter;
-});
-
-const CHAN_ID = "newtab";
-const PREF_ENABLED = "browser.newtabpage.remote";
-const PREF_MODE = "browser.newtabpage.remote.mode";
-
-/**
- * NewTabWebChannel is the conduit for all communication with unprivileged newtab instances.
- *
- * It allows for the ability to broadcast to all newtab browsers.
- * If the browser.newtab.remote pref is false, the object will be in an uninitialized state.
- *
- * Mode choices:
- * 'production': pages from our production CDN
- * 'staging': pages from our staging CDN
- * 'test': intended for tests
- * 'test2': intended for tests
- * 'dev': intended for development
- *
- *  An unknown mode will result in 'production' mode, which is the default
- *
- *  Incoming messages are expected to be JSON-serialized and in the format:
- *
- *  {
- *    type: "REQUEST_SCREENSHOT",
- *    data: {
- *      url: "https://example.com"
- *    }
- *  }
- *
- *  Or:
- *
- *  {
- *    type: "REQUEST_SCREENSHOT",
- *  }
- *
- *  Outgoing messages are expected to be objects serializable by structured cloning, in a similar format:
- *  {
- *    type: "RECEIVE_SCREENSHOT",
- *    data: {
- *      "url": "https://example.com",
- *      "image": "dataURi:....."
- *    }
- *  }
- */
-let NewTabWebChannelImpl = function NewTabWebChannelImpl() {
-  EventEmitter.decorate(this);
-  this._handlePrefChange = this._handlePrefChange.bind(this);
-  this._incomingMessage = this._incomingMessage.bind(this);
-};
-
-NewTabWebChannelImpl.prototype = {
-  _prefs: {},
-  _channel: null,
-
-  // a WeakMap containing browsers as keys and a weak ref to their principal
-  // as value
-  _principals: null,
-
-  // a Set containing weak refs to browsers
-  _browsers: null,
-
-  /*
-   * Returns current channel's ID
-   */
-  get chanId() {
-    return CHAN_ID;
-  },
-
-  /*
-   * Returns the number of browsers currently tracking
-   */
-  get numBrowsers() {
-    return this._getBrowserRefs().length;
-  },
-
-  /*
-   * Returns current channel's origin
-   */
-  get origin() {
-    if (!(this._prefs.mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) {
-      this._prefs.mode = "production";
-    }
-    return NewTabRemoteResources.MODE_CHANNEL_MAP[this._prefs.mode].origin;
-  },
-
-  /*
-   * Unloads all browsers and principals
-   */
-  _unloadAll() {
-    if (this._principals != null) {
-      this._principals = new WeakMap();
-    }
-    this._browsers = new Set();
-    this.emit("targetUnloadAll");
-  },
-
-  /*
-   * Checks if a browser is known
-   *
-   * This will cause an iteration through all known browsers.
-   * That's ok, we don't expect a lot of browsers
-   */
-  _isBrowserKnown(browser) {
-    for (let bRef of this._getBrowserRefs()) {
-      let b = bRef.get();
-      if (b && b.permanentKey === browser.permanentKey) {
-        return true;
-      }
-    }
-
-    return false;
-  },
-
-  /*
-   * Obtains all known browser refs
-   */
-  _getBrowserRefs() {
-    // Some code may try to emit messages after teardown.
-    if (!this._browsers) {
-      return [];
-    }
-    let refs = [];
-    for (let bRef of this._browsers) {
-      /*
-       * even though we hold a weak ref to browser, it seems that browser
-       * objects aren't gc'd immediately after a tab closes. They stick around
-       * in memory, but thankfully they don't have a documentURI in that case
-       */
-      let browser = bRef.get();
-      if (browser && browser.documentURI) {
-        refs.push(bRef);
-      } else {
-        // need to clean up principals because the browser object is not gc'ed
-        // immediately
-        this._principals.delete(browser);
-        this._browsers.delete(bRef);
-        this.emit("targetUnload");
-      }
-    }
-    return refs;
-  },
-
-  /*
-   * Receives a message from content.
-   *
-   * Keeps track of browsers for broadcast, relays messages to listeners.
-   */
-  _incomingMessage(id, message, target) {
-    if (this.chanId !== id) {
-      Cu.reportError(new Error("NewTabWebChannel unexpected message destination"));
-    }
-
-    /*
-     * need to differentiate by browser, because event targets are created each
-     * time a message is sent.
-     */
-    if (!this._isBrowserKnown(target.browser)) {
-      this._browsers.add(Cu.getWeakReference(target.browser));
-      this._principals.set(target.browser, Cu.getWeakReference(target.principal));
-      this.emit("targetAdd");
-    }
-
-    try {
-      let msg = JSON.parse(message);
-      this.emit(msg.type, {data: msg.data, target});
-    } catch (err) {
-      Cu.reportError(err);
-    }
-  },
-
-  /*
-   * Sends a message to all known browsers
-   */
-  broadcast(actionType, message) {
-    for (let bRef of this._getBrowserRefs()) {
-      let browser = bRef.get();
-      try {
-        let principal = this._principals.get(browser).get();
-        if (principal && browser && browser.documentURI) {
-          this._channel.send({type: actionType, data: message}, {browser, principal});
-        }
-      } catch (e) {
-        Cu.reportError(new Error("NewTabWebChannel WeakRef is dead"));
-        this._principals.delete(browser);
-      }
-    }
-  },
-
-  /*
-   * Sends a message to a specific target
-   */
-  send(actionType, message, target) {
-    try {
-      this._channel.send({type: actionType, data: message}, target);
-    } catch (e) {
-      // Web Channel might be dead
-      Cu.reportError(e);
-    }
-  },
-
-  /*
-   * Pref change observer callback
-   */
-  _handlePrefChange(prefName, newState, forceState) { // eslint-disable-line no-unused-vars
-    switch (prefName) {
-      case PREF_ENABLED:
-        if (!this._prefs.enabled && newState) {
-          // changing state from disabled to enabled
-          this.setupState();
-        } else if (this._prefs.enabled && !newState) {
-          // changing state from enabled to disabled
-          this.tearDownState();
-        }
-        break;
-      case PREF_MODE:
-        if (this._prefs.mode !== newState) {
-          // changing modes
-          this.tearDownState();
-          this.setupState();
-        }
-        break;
-    }
-  },
-
-  /*
-   * Sets up the internal state
-   */
-  setupState() {
-    this._prefs.enabled = Services.prefs.getBoolPref(PREF_ENABLED, false);
-
-    let mode = Services.prefs.getStringPref(PREF_MODE, "production");
-    if (!(mode in NewTabRemoteResources.MODE_CHANNEL_MAP)) {
-      mode = "production";
-    }
-    this._prefs.mode = mode;
-    this._principals = new WeakMap();
-    this._browsers = new Set();
-
-    if (this._prefs.enabled) {
-      this._channel = new WebChannel(this.chanId, Services.io.newURI(this.origin));
-      this._channel.listen(this._incomingMessage);
-    }
-  },
-
-  tearDownState() {
-    if (this._channel) {
-      this._channel.stopListening();
-    }
-    this._prefs = {};
-    this._unloadAll();
-    this._channel = null;
-    this._principals = null;
-    this._browsers = null;
-  },
-
-  init() {
-    this.setupState();
-    NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handlePrefChange);
-    NewTabPrefsProvider.prefs.on(PREF_MODE, this._handlePrefChange);
-  },
-
-  uninit() {
-    this.tearDownState();
-    NewTabPrefsProvider.prefs.off(PREF_ENABLED, this._handlePrefChange);
-    NewTabPrefsProvider.prefs.off(PREF_MODE, this._handlePrefChange);
-  }
-};
-
-let NewTabWebChannel = new NewTabWebChannelImpl();
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -8,23 +8,16 @@ with Files("**"):
     BUG_COMPONENT = ("Firefox", "New Tab Page")
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += [
     'tests/xpcshell/xpcshell.ini',
 ]
 
-EXTRA_JS_MODULES += [
-    'NewTabPrefsProvider.jsm',
-    'NewTabRemoteResources.jsm',
-    'NewTabSearchProvider.jsm',
-    'NewTabWebChannel.jsm'
-]
-
 XPIDL_SOURCES += [
     'nsIAboutNewTabService.idl',
 ]
 
 XPIDL_MODULE = 'browser-newtab'
 
 EXTRA_COMPONENTS += [
     'aboutNewTabService.js',
deleted file mode 100644
--- a/browser/components/newtab/tests/xpcshell/test_NewTabPrefsProvider.js
+++ /dev/null
@@ -1,42 +0,0 @@
-"use strict";
-
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/Preferences.jsm");
-
-ChromeUtils.defineModuleGetter(this, "NewTabPrefsProvider",
-    "resource:///modules/NewTabPrefsProvider.jsm");
-
-add_task(async function test_observe() {
-  let prefsMap = NewTabPrefsProvider.prefs.prefsMap;
-  for (let prefName of prefsMap.keys()) {
-    let prefValueType = prefsMap.get(prefName);
-
-    let beforeVal;
-    let afterVal;
-
-    switch (prefValueType) {
-      case "bool":
-        beforeVal = false;
-        afterVal = true;
-        Preferences.set(prefName, beforeVal);
-        break;
-      case "localized":
-      case "str":
-        beforeVal = "";
-        afterVal = "someStr";
-        Preferences.set(prefName, beforeVal);
-        break;
-    }
-    NewTabPrefsProvider.prefs.init();
-    let promise = new Promise(resolve => {
-      NewTabPrefsProvider.prefs.once(prefName, (name, data) => { // jshint ignore:line
-        resolve([name, data]);
-      });
-    });
-    Preferences.set(prefName, afterVal);
-    let [actualName, actualData] = await promise;
-    equal(prefName, actualName, `emitter sent the correct pref: ${prefName}`);
-    equal(afterVal, actualData, `emitter collected correct pref data for ${prefName}`);
-    NewTabPrefsProvider.prefs.uninit();
-  }
-});
deleted file mode 100644
--- a/browser/components/newtab/tests/xpcshell/test_NewTabSearchProvider.js
+++ /dev/null
@@ -1,74 +0,0 @@
-"use strict";
-
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-ChromeUtils.defineModuleGetter(this, "NewTabSearchProvider",
-    "resource:///modules/NewTabSearchProvider.jsm");
-ChromeUtils.defineModuleGetter(this, "ContentSearch",
-                               "resource:///modules/ContentSearch.jsm");
-
-// ensure a profile exists
-do_get_profile();
-
-function hasProp(obj) {
-  return function(aProp) {
-    ok(obj.hasOwnProperty(aProp), `expect to have property ${aProp}`);
-  };
-}
-
-add_task(async function test_search() {
-  ContentSearch.init();
-  let observerPromise = new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
-      if (aData === "init-complete" && aTopic === "browser-search-service") {
-        Services.obs.removeObserver(observer, "browser-search-service");
-        resolve();
-      }
-    }, "browser-search-service");
-  });
-  Services.search.init();
-  await observerPromise;
-  Assert.ok(Services.search.isInitialized);
-
-  // get initial state of search and check it has correct properties
-  let state = await NewTabSearchProvider.search.asyncGetState();
-  let stateProps = hasProp(state);
-  ["engines", "currentEngine"].forEach(stateProps);
-
-  // check that the current engine is correct and has correct properties
-  let {currentEngine} = state;
-  equal(currentEngine.name, Services.search.currentEngine.name, "Current engine has been correctly set");
-  var engineProps = hasProp(currentEngine);
-  ["name", "placeholder", "iconBuffer"].forEach(engineProps);
-
-  // create dummy test engines to test observer
-  Services.search.addEngineWithDetails("TestSearch1", "", "", "", "GET",
-    "http://example.com/?q={searchTerms}");
-  Services.search.addEngineWithDetails("TestSearch2", "", "", "", "GET",
-    "http://example.com/?q={searchTerms}");
-
-  // set one of the dummy test engines to the default engine
-  Services.search.defaultEngine = Services.search.getEngineByName("TestSearch1");
-
-  // test that the event emitter is working by setting a new current engine "TestSearch2"
-  let engineName = "TestSearch2";
-  NewTabSearchProvider.search.init();
-
-  // event emitter will fire when current engine is changed
-  let promise = new Promise(resolve => {
-    NewTabSearchProvider.search.once("browser-search-engine-modified", (name, data) => { // jshint ignore:line
-      resolve([name, data.name]);
-    });
-  });
-
-  // set a new current engine
-  Services.search.currentEngine = Services.search.getEngineByName(engineName);
-  let expectedEngineName = Services.search.currentEngine.name;
-
-  // emitter should fire and return the new engine
-  let [eventName, actualEngineName] = await promise;
-  equal(eventName, "browser-search-engine-modified", `emitter sent the correct event ${eventName}`);
-  equal(expectedEngineName, actualEngineName, `emitter set the correct engine ${expectedEngineName}`);
-  NewTabSearchProvider.search.uninit();
-});
--- a/browser/components/newtab/tests/xpcshell/xpcshell.ini
+++ b/browser/components/newtab/tests/xpcshell/xpcshell.ini
@@ -1,8 +1,6 @@
 [DEFAULT]
 head =
 firefox-appdir = browser
 skip-if = toolkit == 'android'
 
 [test_AboutNewTabService.js]
-[test_NewTabPrefsProvider.js]
-[test_NewTabSearchProvider.js]
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -376,17 +376,17 @@ nsBrowserContentHandler.prototype = {
           let localSchemes = new Set(["chrome", "file", "resource"]);
           if (uri instanceof Ci.nsINestedURI) {
             uri = uri.QueryInterface(Ci.nsINestedURI).innerMostURI;
           }
           return localSchemes.has(uri.scheme);
         };
         if (isLocal(resolvedURI)) {
           // If the URI is local, we are sure it won't wrongly inherit chrome privs
-          var features = "chrome,dialog=no,all" + this.getFeatures(cmdLine);
+          let features = "chrome,dialog=no,all" + this.getFeatures(cmdLine);
           openWindow(null, resolvedURI.spec, "_blank", features);
           cmdLine.preventDefault = true;
         } else {
           dump("*** Preventing load of web URI as chrome\n");
           dump("    If you're trying to load a webpage, do not pass --chrome.\n");
         }
       } catch (e) {
         Cu.reportError(e);
@@ -397,43 +397,55 @@ nsBrowserContentHandler.prototype = {
       cmdLine.preventDefault = true;
     }
     if (cmdLine.handleFlag("silent", false))
       cmdLine.preventDefault = true;
 
     try {
       var privateWindowParam = cmdLine.handleFlagWithParam("private-window", false);
       if (privateWindowParam) {
-        let resolvedURI = resolveURIInternal(cmdLine, privateWindowParam);
-        handURIToExistingBrowser(resolvedURI, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine, true,
+        let forcePrivate = true;
+        let resolvedURI;
+        if (!PrivateBrowsingUtils.enabled) {
+          // Load about:privatebrowsing in a normal tab, which will display an error indicating
+          // access to private browsing has been disabled.
+          forcePrivate = false;
+          resolvedURI = Services.io.newURI("about:privatebrowsing");
+        } else {
+          resolvedURI = resolveURIInternal(cmdLine, privateWindowParam);
+        }
+        handURIToExistingBrowser(resolvedURI, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine, forcePrivate,
                                  Services.scriptSecurityManager.getSystemPrincipal());
-        cmdLine.preventDefault = true;
       }
     } catch (e) {
       if (e.result != Cr.NS_ERROR_INVALID_ARG) {
         throw e;
       }
       // NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param.
       if (cmdLine.handleFlag("private-window", false)) {
+        let features = "chrome,dialog=no,all";
+        if (PrivateBrowsingUtils.enabled) {
+          features += ",private";
+        }
         openWindow(null, this.chromeURL, "_blank",
-          "chrome,dialog=no,private,all" + this.getFeatures(cmdLine),
+          features + this.getFeatures(cmdLine),
           "about:privatebrowsing");
         cmdLine.preventDefault = true;
       }
     }
 
     var searchParam = cmdLine.handleFlagWithParam("search", false);
     if (searchParam) {
       doSearch(searchParam, cmdLine);
       cmdLine.preventDefault = true;
     }
 
     // The global PB Service consumes this flag, so only eat it in per-window
     // PB builds.
-    if (cmdLine.handleFlag("private", false)) {
+    if (cmdLine.handleFlag("private", false) && PrivateBrowsingUtils.enabled) {
       PrivateBrowsingUtils.enterTemporaryAutoStartMode();
     }
 
     var fileParam = cmdLine.handleFlagWithParam("file", false);
     if (fileParam) {
       var file = cmdLine.resolveFile(fileParam);
       var fileURI = Services.io.newFileURI(file);
       openWindow(null, this.chromeURL, "_blank",
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -90,17 +90,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.jsm",
   BrowserErrorReporter: "resource:///modules/BrowserErrorReporter.jsm",
   BrowserUITelemetry: "resource:///modules/BrowserUITelemetry.jsm",
   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
   ContentClick: "resource:///modules/ContentClick.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
   CustomizableUI: "resource:///modules/CustomizableUI.jsm",
   DateTimePickerHelper: "resource://gre/modules/DateTimePickerHelper.jsm",
-  DirectoryLinksProvider: "resource:///modules/DirectoryLinksProvider.jsm",
   ExtensionsUI: "resource:///modules/ExtensionsUI.jsm",
   Feeds: "resource:///modules/Feeds.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
   FxAccounts: "resource://gre/modules/FxAccounts.jsm",
   HybridContentTelemetry: "resource://gre/modules/HybridContentTelemetry.jsm",
   Integration: "resource://gre/modules/Integration.jsm",
@@ -993,19 +992,17 @@ BrowserGlue.prototype = {
     DateTimePickerHelper.init();
     // Check if Sync is configured
     if (Services.prefs.prefHasUserValue("services.sync.username")) {
       WeaveService.init();
     }
 
     PageThumbs.init();
 
-    DirectoryLinksProvider.init();
     NewTabUtils.init();
-    NewTabUtils.links.addProvider(DirectoryLinksProvider);
 
     PageActions.init();
 
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
 
     // Set the default favicon size for UI views that use the page-icon protocol.
     PlacesUtils.favicons.setDefaultIconURIPreferredSize(16 * aWindow.devicePixelRatio);
@@ -1815,17 +1812,17 @@ BrowserGlue.prototype = {
       if (toolbarIsCustomized || getToolbarFolderCount() > NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE) {
         xulStore.setValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed", "false");
       }
     }
   },
 
   // eslint-disable-next-line complexity
   _migrateUI: function BG__migrateUI() {
-    const UI_VERSION = 62;
+    const UI_VERSION = 64;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
 
     let currentUIVersion;
     if (Services.prefs.prefHasUserValue("browser.migration.version")) {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } else {
       // This is a new profile, nothing to migrate.
       Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
@@ -2311,16 +2308,21 @@ BrowserGlue.prototype = {
                       "TabsToolbar", "toolbar-menubar"];
       for (let resourceName of ["mode", "iconsize"]) {
         for (let toolbarId of toolbars) {
           xulStore.removeValue(BROWSER_DOCURL, toolbarId, resourceName);
         }
       }
     }
 
+    if (currentUIVersion < 64) {
+      OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
+                                  "directoryLinks.json"), {ignoreAbsent: true});
+    }
+
     // Update the migration version.
     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
   },
 
   _checkForDefaultBrowser() {
     // Perform default browser checking.
     if (!ShellService) {
       return;
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -252,71 +252,16 @@ var PlacesUIUtils = {
     });
   },
 
   getString: function PUIU_getString(key) {
     return bundle.GetStringFromName(key);
   },
 
   /**
-   * Constructs a Places Transaction for the drop or paste of a blob of data
-   * into a container.
-   *
-   * @param   aData
-   *          The unwrapped data blob of dropped or pasted data.
-   * @param   aNewParentGuid
-   *          GUID of the container the data was dropped or pasted into.
-   * @param   aIndex
-   *          The index within the container the item was dropped or pasted at.
-   * @param   aCopy
-   *          The drag action was copy, so don't move folders or links.
-   *
-   * @return  a Places Transaction that can be transacted for performing the
-   *          move/insert command.
-   */
-  getTransactionForData(aData, aNewParentGuid, aIndex, aCopy) {
-    if (!this.SUPPORTED_FLAVORS.includes(aData.type))
-      throw new Error(`Unsupported '${aData.type}' data type`);
-
-    if ("itemGuid" in aData && "instanceId" in aData &&
-        aData.instanceId == PlacesUtils.instanceId) {
-      if (!this.PLACES_FLAVORS.includes(aData.type))
-        throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
-
-      let info = { guid: aData.itemGuid,
-                   newParentGuid: aNewParentGuid,
-                   newIndex: aIndex };
-      if (aCopy) {
-        info.excludingAnnotation = "Places/SmartBookmark";
-        return PlacesTransactions.Copy(info);
-      }
-      return PlacesTransactions.Move(info);
-    }
-
-    // Since it's cheap and harmless, we allow the paste of separators and
-    // bookmarks from builds that use legacy transactions (i.e. when itemGuid
-    // was not set on PLACES_FLAVORS data). Containers are a different story,
-    // and thus disallowed.
-    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
-      throw new Error("Can't copy a container from a legacy-transactions build");
-
-    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
-      return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid,
-                                               index: aIndex });
-    }
-
-    let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title
-                                                       : aData.uri;
-    return PlacesTransactions.NewBookmark({ url: Services.io.newURI(aData.uri),
-                                            title,
-                                            parentGuid: aNewParentGuid,
-                                            index: aIndex });
-  },
-
-  /**
    * Shows the bookmark dialog corresponding to the specified info.
    *
    * @param aInfo
    *        Describes the item to be edited/added in the dialog.
    *        See documentation at the top of bookmarkProperties.js
    * @param aWindow
    *        Owner window for the new dialog.
    *
@@ -400,16 +345,87 @@ var PlacesUIUtils = {
 
       node = node.parentNode;
     }
 
     return null;
   },
 
   /**
+   * Returns the active PlacesController for a given command.
+   *
+   * @param win The window containing the affected view
+   * @param command The command
+   * @return a PlacesController
+   */
+  getControllerForCommand(win, command) {
+    // A context menu may be built for non-focusable views.  Thus, we first try
+    // to look for a view associated with document.popupNode
+    let popupNode;
+    try {
+      popupNode = win.document.popupNode;
+    } catch (e) {
+      // The document went away (bug 797307).
+      return null;
+    }
+    if (popupNode) {
+      let view = this.getViewForNode(popupNode);
+      if (view && view._contextMenuShown)
+        return view.controllers.getControllerForCommand(command);
+    }
+
+    // When we're not building a context menu, only focusable views
+    // are possible.  Thus, we can safely use the command dispatcher.
+    let controller = win.top.document.commandDispatcher
+                        .getControllerForCommand(command);
+    return controller || null;
+  },
+
+  /**
+   * Update all the Places commands for the given window.
+   *
+   * @param win The window to update.
+   */
+  updateCommands(win) {
+    // Get the controller for one of the places commands.
+    let controller = this.getControllerForCommand(win, "placesCmd_open");
+    for (let command of [
+      "placesCmd_open",
+      "placesCmd_open:window",
+      "placesCmd_open:privatewindow",
+      "placesCmd_open:tab",
+      "placesCmd_new:folder",
+      "placesCmd_new:bookmark",
+      "placesCmd_new:separator",
+      "placesCmd_show:info",
+      "placesCmd_reload",
+      "placesCmd_sortBy:name",
+      "placesCmd_cut",
+      "placesCmd_copy",
+      "placesCmd_paste",
+      "placesCmd_delete",
+    ]) {
+      win.goSetCommandEnabled(command,
+                              controller && controller.isCommandEnabled(command));
+    }
+  },
+
+  /**
+   * Executes the given command on the currently active controller.
+   *
+   * @param win The window containing the affected view
+   * @param command The command to execute
+   */
+  doCommand(win, command) {
+    let controller = this.getControllerForCommand(win, command);
+    if (controller && controller.isCommandEnabled(command))
+      controller.doCommand(command);
+  },
+
+  /**
    * By calling this before visiting an URL, the visit will be associated to a
    * TRANSITION_TYPED transition (if there is no a referrer).
    * This is used when visiting pages from the history menu, history sidebar,
    * url bar, url autocomplete results, and history searches from the places
    * organizer.  If this is not called visits will be marked as
    * TRANSITION_LINK.
    */
   markPageAsTyped: function PUIU_markPageAsTyped(aURL) {
@@ -1171,33 +1187,263 @@ var PlacesUIUtils = {
     try {
       await functionToWrap();
     } finally {
       if (itemsBeingChanged > ITEM_CHANGED_BATCH_NOTIFICATION_THRESHOLD) {
         resultNode.onEndUpdateBatch();
       }
     }
   },
+
+  /**
+   * Constructs a Places Transaction for the drop or paste of a blob of data
+   * into a container.
+   *
+   * @param   aData
+   *          The unwrapped data blob of dropped or pasted data.
+   * @param   aNewParentGuid
+   *          GUID of the container the data was dropped or pasted into.
+   * @param   aIndex
+   *          The index within the container the item was dropped or pasted at.
+   * @param   aCopy
+   *          The drag action was copy, so don't move folders or links.
+   *
+   * @return  a Places Transaction that can be transacted for performing the
+   *          move/insert command.
+   */
+  getTransactionForData(aData, aNewParentGuid, aIndex, aCopy) {
+    if (!this.SUPPORTED_FLAVORS.includes(aData.type))
+      throw new Error(`Unsupported '${aData.type}' data type`);
+
+    if ("itemGuid" in aData && "instanceId" in aData &&
+        aData.instanceId == PlacesUtils.instanceId) {
+      if (!this.PLACES_FLAVORS.includes(aData.type))
+        throw new Error(`itemGuid unexpectedly set on ${aData.type} data`);
+
+      let info = { guid: aData.itemGuid,
+                   newParentGuid: aNewParentGuid,
+                   newIndex: aIndex };
+      if (aCopy) {
+        info.excludingAnnotation = "Places/SmartBookmark";
+        return PlacesTransactions.Copy(info);
+      }
+      return PlacesTransactions.Move(info);
+    }
+
+    // Since it's cheap and harmless, we allow the paste of separators and
+    // bookmarks from builds that use legacy transactions (i.e. when itemGuid
+    // was not set on PLACES_FLAVORS data). Containers are a different story,
+    // and thus disallowed.
+    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
+      throw new Error("Can't copy a container from a legacy-transactions build");
+
+    if (aData.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
+      return PlacesTransactions.NewSeparator({ parentGuid: aNewParentGuid,
+                                               index: aIndex });
+    }
+
+    let title = aData.type != PlacesUtils.TYPE_UNICODE ? aData.title
+                                                       : aData.uri;
+    return PlacesTransactions.NewBookmark({ url: Services.io.newURI(aData.uri),
+                                            title,
+                                            parentGuid: aNewParentGuid,
+                                            index: aIndex });
+  },
+
+  /**
+   * Processes a set of transfer items that have been dropped or pasted.
+   * Batching will be applied where necessary.
+   *
+   * @param {Array} items A list of unwrapped nodes to process.
+   * @param {Object} insertionPoint The requested point for insertion.
+   * @param {Boolean} doCopy Set to true to copy the items, false will move them
+   *                         if possible.
+   * @paramt {Object} view The view that should be used for batching.
+   * @return {Array} Returns an empty array when the insertion point is a tag, else
+   *                 returns an array of copied or moved guids.
+   */
+  async handleTransferItems(items, insertionPoint, doCopy, view) {
+    let transactions;
+    let itemsCount;
+    if (insertionPoint.isTag) {
+      let urls = items.filter(item => "uri" in item).map(item => item.uri);
+      itemsCount = urls.length;
+      transactions = [PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName })];
+    } else {
+      let insertionIndex = await insertionPoint.getIndex();
+      itemsCount = items.length;
+      transactions = await getTransactionsForTransferItems(
+        items, insertionIndex, insertionPoint.guid, doCopy);
+    }
+
+    // Check if we actually have something to add, if we don't it probably wasn't
+    // valid, or it was moving to the same location, so just ignore it.
+    if (!transactions.length) {
+      return [];
+    }
+
+    let guidsToSelect = [];
+    let resultForBatching = getResultForBatching(view);
+
+    // If we're inserting into a tag, we don't get the guid, so we'll just
+    // pass the transactions direct to the batch function.
+    let batchingItem = transactions;
+    if (!insertionPoint.isTag) {
+      // If we're not a tag, then we need to get the ids of the items to select.
+      batchingItem = async () => {
+        for (let transaction of transactions) {
+          let guid = await transaction.transact();
+          if (guid) {
+            guidsToSelect.push(guid);
+          }
+        }
+      };
+    }
+
+    await this.batchUpdatesForNode(resultForBatching, itemsCount, async () => {
+      await PlacesTransactions.batch(batchingItem);
+    });
+
+    return guidsToSelect;
+  },
 };
 
-
-PlacesUIUtils.PLACES_FLAVORS = [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
-                                PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
-                                PlacesUtils.TYPE_X_MOZ_PLACE];
-
-PlacesUIUtils.URI_FLAVORS = [PlacesUtils.TYPE_X_MOZ_URL,
-                             TAB_DROP_TYPE,
-                             PlacesUtils.TYPE_UNICODE],
-
-PlacesUIUtils.SUPPORTED_FLAVORS = [...PlacesUIUtils.PLACES_FLAVORS,
-                                   ...PlacesUIUtils.URI_FLAVORS];
+// These are lazy getters to avoid importing PlacesUtils immediately.
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "PLACES_FLAVORS", () => {
+  return [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
+          PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
+          PlacesUtils.TYPE_X_MOZ_PLACE];
+});
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "URI_FLAVORS", () => {
+  return [PlacesUtils.TYPE_X_MOZ_URL,
+          TAB_DROP_TYPE,
+          PlacesUtils.TYPE_UNICODE];
+});
+XPCOMUtils.defineLazyGetter(PlacesUIUtils, "SUPPORTED_FLAVORS", () => {
+  return [...PlacesUIUtils.PLACES_FLAVORS,
+          ...PlacesUIUtils.URI_FLAVORS];
+});
 
 XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() {
   return Services.prefs.getComplexValue("intl.ellipsis",
                                         Ci.nsIPrefLocalizedString).data;
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInBackground",
                                       PREF_LOAD_BOOKMARKS_IN_BACKGROUND, false);
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "loadBookmarksInTabs",
                                       PREF_LOAD_BOOKMARKS_IN_TABS, false);
 XPCOMUtils.defineLazyPreferenceGetter(PlacesUIUtils, "openInTabClosesMenu",
   "browser.bookmarks.openInTabClosesMenu", false);
+
+/**
+ * Determines if an unwrapped node can be moved.
+ *
+ * @param unwrappedNode
+ *        A node unwrapped by PlacesUtils.unwrapNodes().
+ * @return True if the node can be moved, false otherwise.
+ */
+function canMoveUnwrappedNode(unwrappedNode) {
+  if ((unwrappedNode.concreteGuid && PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) ||
+      unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) {
+    return false;
+  }
+
+  let parentGuid = unwrappedNode.parentGuid;
+  // If there's no parent Guid, this was likely a virtual query that returns
+  // bookmarks, such as a tags query.
+  if (!parentGuid ||
+      parentGuid == PlacesUtils.bookmarks.rootGuid) {
+    return false;
+  }
+  // leftPaneFolderId and allBookmarksFolderId are lazy getters running
+  // at least a synchronous DB query. Therefore we don't want to invoke
+  // them first, especially because isCommandEnabled may be called way
+  // before the left pane folder is even necessary.
+  if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" &&
+      (unwrappedNode.parent == PlacesUIUtils.leftPaneFolderId)) {
+    return false;
+  }
+  return true;
+}
+
+/**
+ * This gets the most appropriate item for using for batching. In the case of multiple
+ * views being related, the method returns the most expensive result to batch.
+ * For example, if it detects the left-hand library pane, then it will look for
+ * and return the reference to the right-hand pane.
+ *
+ * @param {Object} viewOrElement The item to check.
+ * @return {Object} Will return the best result node to batch, or null
+ *                  if one could not be found.
+ */
+function getResultForBatching(viewOrElement) {
+  if (viewOrElement && viewOrElement instanceof Ci.nsIDOMElement &&
+      viewOrElement.id === "placesList") {
+    // Note: fall back to the existing item if we can't find the right-hane pane.
+    viewOrElement = viewOrElement.ownerDocument.getElementById("placeContent") || viewOrElement;
+  }
+
+  if (viewOrElement && viewOrElement.result) {
+    return viewOrElement.result;
+  }
+
+  return null;
+}
+
+/**
+ * Processes a set of transfer items and returns transactions to insert or
+ * move them.
+ *
+ * @param {Array} items A list of unwrapped nodes to get transactions for.
+ * @param {Integer} insertionIndex The requested index for insertion.
+ * @param {String} insertionParentGuid The guid of the parent folder to insert
+ *                                     or move the items to.
+ * @param {Boolean} doCopy Set to true to copy the items, false will move them
+ *                         if possible.
+ * @return {Array} Returns an array of created PlacesTransactions.
+ */
+async function getTransactionsForTransferItems(items, insertionIndex,
+                                               insertionParentGuid, doCopy) {
+  let transactions = [];
+  let index = insertionIndex;
+
+  for (let item of items) {
+    if (index != -1 && item.itemGuid) {
+      // Note: we use the parent from the existing bookmark as the sidebar
+      // gives us an unwrapped.parent that is actually a query and not the real
+      // parent.
+      let existingBookmark = await PlacesUtils.bookmarks.fetch(item.itemGuid);
+
+      // If we're dropping on the same folder, then we may need to adjust
+      // the index to insert at the correct place.
+      if (existingBookmark && insertionParentGuid == existingBookmark.parentGuid) {
+        if (index > existingBookmark.index) {
+          // If we're dragging down, we need to go one lower to insert at
+          // the real point as moving the element changes the index of
+          // everything below by 1.
+          index--;
+        } else if (index == existingBookmark.index) {
+          // This isn't moving so we skip it.
+          continue;
+        }
+      }
+    }
+
+    // If this is not a copy, check for safety that we can move the
+    // source, otherwise report an error and fallback to a copy.
+    if (!doCopy && !canMoveUnwrappedNode(item)) {
+      Cu.reportError("Tried to move an unmovable Places " +
+                     "node, reverting to a copy operation.");
+      doCopy = true;
+    }
+    transactions.push(
+      PlacesUIUtils.getTransactionForData(item,
+                                          insertionParentGuid,
+                                          index,
+                                          doCopy));
+
+    if (index != -1 && item.itemGuid) {
+      index++;
+    }
+  }
+  return transactions;
+}
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -156,17 +156,17 @@ var BookmarkPropertiesPanel = {
 
       if ("title" in dialogInfo)
         this._title = dialogInfo.title;
 
       if ("defaultInsertionPoint" in dialogInfo) {
         this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
       } else {
         this._defaultInsertionPoint =
-          new InsertionPoint({
+          new PlacesInsertionPoint({
             parentId: PlacesUtils.bookmarksMenuFolderId,
             parentGuid: PlacesUtils.bookmarks.menuGuid
           });
       }
 
       switch (dialogInfo.type) {
         case "bookmark":
           this._itemType = BOOKMARK_ITEM;
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -212,20 +212,20 @@ PlacesViewBase.prototype = {
           tagName = container.title;
           // TODO (Bug 1160193): properly support dropping on a tag root.
           if (!tagName)
             return null;
         }
       }
     }
 
-    if (PlacesControllerDragHelper.disallowInsertion(container, this))
+    if (this.controller.disallowInsertion(container))
       return null;
 
-    return new InsertionPoint({
+    return new PlacesInsertionPoint({
       parentId: PlacesUtils.getConcreteItemId(container),
       parentGuid: PlacesUtils.getConcreteItemGuid(container),
       index, orientation, tagName
     });
   },
 
   buildContextMenu: function PVB_buildContextMenu(aPopup) {
     this._contextMenuShown = aPopup;
@@ -1514,86 +1514,86 @@ PlacesToolbar.prototype = {
         // This is a folder.
         // If we are in the middle of it, drop inside it.
         // Otherwise, drop before it, with regards to RTL mode.
         let threshold = eltRect.width * 0.25;
         if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
                        : (aEvent.clientX < eltRect.left + threshold)) {
           // Drop before this folder.
           dropPoint.ip =
-            new InsertionPoint({
+            new PlacesInsertionPoint({
               parentId: PlacesUtils.getConcreteItemId(this._resultNode),
               parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
               index: eltIndex,
               orientation: Ci.nsITreeView.DROP_BEFORE
             });
           dropPoint.beforeIndex = eltIndex;
         } else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
                             : (aEvent.clientX < eltRect.right - threshold)) {
           // Drop inside this folder.
           let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
                         elt._placesNode.title : null;
           dropPoint.ip =
-            new InsertionPoint({
+            new PlacesInsertionPoint({
               parentId: PlacesUtils.getConcreteItemId(elt._placesNode),
               parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode),
               tagName
             });
           dropPoint.beforeIndex = eltIndex;
           dropPoint.folderElt = elt;
         } else {
           // Drop after this folder.
           let beforeIndex =
             (eltIndex == this._rootElt.childNodes.length - 1) ?
             -1 : eltIndex + 1;
 
           dropPoint.ip =
-            new InsertionPoint({
+            new PlacesInsertionPoint({
               parentId: PlacesUtils.getConcreteItemId(this._resultNode),
               parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
               index: beforeIndex,
               orientation: Ci.nsITreeView.DROP_BEFORE
             });
           dropPoint.beforeIndex = beforeIndex;
         }
       } else {
         // This is a non-folder node or a read-only folder.
         // Drop before it with regards to RTL mode.
         let threshold = eltRect.width * 0.5;
         if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
                        : (aEvent.clientX < eltRect.left + threshold)) {
           // Drop before this bookmark.
           dropPoint.ip =
-            new InsertionPoint({
+            new PlacesInsertionPoint({
               parentId: PlacesUtils.getConcreteItemId(this._resultNode),
               parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
               index: eltIndex,
               orientation: Ci.nsITreeView.DROP_BEFORE
             });
           dropPoint.beforeIndex = eltIndex;
         } else {
           // Drop after this bookmark.
           let beforeIndex =
             eltIndex == this._rootElt.childNodes.length - 1 ?
             -1 : eltIndex + 1;
           dropPoint.ip =
-            new InsertionPoint({
+            new PlacesInsertionPoint({
               parentId: PlacesUtils.getConcreteItemId(this._resultNode),
               parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
               index: beforeIndex,
               orientation: Ci.nsITreeView.DROP_BEFORE
             });
           dropPoint.beforeIndex = beforeIndex;
         }
       }
     } else {
       // We are most likely dragging on the empty area of the
       // toolbar, we should drop after the last node.
       dropPoint.ip =
-        new InsertionPoint({
+        new PlacesInsertionPoint({
           parentId: PlacesUtils.getConcreteItemId(this._resultNode),
           parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
           orientation: Ci.nsITreeView.DROP_BEFORE
         });
       dropPoint.beforeIndex = -1;
     }
 
     return dropPoint;
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -1,20 +1,13 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-ChromeUtils.defineModuleGetter(this, "ForgetAboutSite",
-                               "resource://gre/modules/ForgetAboutSite.jsm");
-ChromeUtils.defineModuleGetter(this, "NetUtil",
-                               "resource://gre/modules/NetUtil.jsm");
-ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
-                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
-
 /**
  * Represents an insertion point within a container where we can insert
  * items.
  * @param {object} an object containing the following properties:
  *   - parentId
  *     The identifier of the parent container
  *   - parentGuid
  *     The unique identifier of the parent container
@@ -25,30 +18,30 @@ ChromeUtils.defineModuleGetter(this, "Pr
  *     insertion point to accommodate the orientation should be done by
  *     the person who constructs the IP, not the user. The orientation
  *     is provided for informational purposes only! Defaults to DROP_ON.
  *   - tagName
  *     The tag name if this IP is set to a tag, null otherwise.
  *   - dropNearNode
  *     When defined index will be calculated based on this node
  */
-function InsertionPoint({ parentId, parentGuid,
-                          index = PlacesUtils.bookmarks.DEFAULT_INDEX,
-                          orientation = Ci.nsITreeView.DROP_ON,
-                          tagName = null,
-                          dropNearNode = null }) {
+function PlacesInsertionPoint({ parentId, parentGuid,
+                                index = PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                orientation = Ci.nsITreeView.DROP_ON,
+                                tagName = null,
+                                dropNearNode = null }) {
   this.itemId = parentId;
   this.guid = parentGuid;
   this._index = index;
   this.orientation = orientation;
   this.tagName = tagName;
   this.dropNearNode = dropNearNode;
 }
 
-InsertionPoint.prototype = {
+PlacesInsertionPoint.prototype = {
   set index(val) {
     return this._index = val;
   },
 
   async getIndex() {
     if (this.dropNearNode) {
       // If dropNearNode is set up we must calculate the index of the item near
       // which we will drop.
@@ -225,17 +218,17 @@ PlacesController.prototype = {
       this.remove("Remove Selection").catch(Cu.reportError);
       break;
     case "placesCmd_deleteDataHost":
       var host;
       if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
         var queries = this._view.selectedNode.getQueries();
         host = queries[0].domain;
       } else
-        host = NetUtil.newURI(this._view.selectedNode.uri).host;
+        host = Services.io.newURI(this._view.selectedNode.uri).host;
       ForgetAboutSite.removeDataFromDomain(host)
                      .catch(Cu.reportError);
       break;
     case "cmd_selectAll":
       this.selectAll();
       break;
     case "placesCmd_open":
       PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
@@ -270,17 +263,17 @@ PlacesController.prototype = {
     case "placesCmd_createBookmark":
       let node = this._view.selectedNode;
       PlacesUIUtils.showBookmarkDialog({ action: "add",
                                          type: "bookmark",
                                          hiddenRows: [ "description",
                                                         "keyword",
                                                         "location",
                                                         "loadInSidebar" ],
-                                         uri: NetUtil.newURI(node.uri),
+                                         uri: Services.io.newURI(node.uri),
                                          title: node.title
                                        }, window.top);
       break;
     }
   },
 
   onEvent: function PC_onEvent(eventName) { },
 
@@ -423,17 +416,17 @@ PlacesController.prototype = {
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
           nodeData.folder = true;
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
           nodeData.separator = true;
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
           nodeData.link = true;
-          uri = NetUtil.newURI(node.uri);
+          uri = Services.io.newURI(node.uri);
           if (PlacesUtils.nodeIsBookmark(node)) {
             nodeData.bookmark = true;
             var parentNode = node.parent;
             if (parentNode) {
               if (PlacesUtils.nodeIsTagQuery(parentNode))
                 nodeData.tagChild = true;
               else if (this.hasCachedLivemarkInfo(parentNode))
                 nodeData.livemarkChild = true;
@@ -467,16 +460,20 @@ PlacesController.prototype = {
    * @param   aMenuItem
    *          the context menu item
    * @param   aMetaData
    *          meta data about the selection
    * @return true if the conditions (see buildContextMenu) are satisfied
    *         and the item can be displayed, false otherwise.
    */
   _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
+    if (aMenuItem.hasAttribute("hideifprivatebrowsing") && !PrivateBrowsingUtils.enabled) {
+      return false;
+    }
+
     var selectiontype = aMenuItem.getAttribute("selectiontype");
     if (!selectiontype) {
       selectiontype = "single|multiple";
     }
     var selectionTypes = selectiontype.split("|");
     if (selectionTypes.includes("any")) {
       return true;
     }
@@ -1208,17 +1205,17 @@ PlacesController.prototype = {
       type = type.value;
       items = PlacesUtils.unwrapNodes(data, type);
     } catch (ex) {
       // No supported data exists or nodes unwrap failed, just bail out.
       return;
     }
 
     let doCopy = action == "copy";
-    let itemsToSelect = await handleTransferItems(items, ip, doCopy, this._view);
+    let itemsToSelect = await PlacesUIUtils.handleTransferItems(items, ip, doCopy, this._view);
 
     // Cut/past operations are not repeatable, so clear the clipboard.
     if (action == "cut") {
       this._clearClipboard();
     }
 
     if (itemsToSelect.length > 0)
       this._view.selectItems(itemsToSelect, false);
@@ -1251,17 +1248,51 @@ PlacesController.prototype = {
    * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
    * null otherwise.
    * @param aNode
    *        a places result node.
    * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
    */
   getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode) {
     return this._cachedLivemarkInfoObjects.get(aNode, null);
-  }
+  },
+
+  /**
+   * Checks if we can insert into a container.
+   * @param   container
+   *          The container were we are want to drop
+   */
+  disallowInsertion(container) {
+    NS_ASSERT(container, "empty container");
+    // Allow dropping into Tag containers and editable folders.
+    return !PlacesUtils.nodeIsTagQuery(container) &&
+           (!PlacesUtils.nodeIsFolder(container) ||
+            PlacesUIUtils.isFolderReadOnly(container, this._view));
+  },
+
+  /**
+   * Determines if a node can be moved.
+   *
+   * @param   aNode
+   *          A nsINavHistoryResultNode node.
+   * @return True if the node can be moved, false otherwise.
+   */
+  canMoveNode(node) {
+    // Only bookmark items are movable.
+    if (node.itemId == -1)
+      return false;
+
+    // Once tags and bookmarked are divorced, the tag-query check should be
+    // removed.
+    let parentNode = node.parent;
+    return parentNode != null &&
+           PlacesUtils.nodeIsFolder(parentNode) &&
+           !PlacesUIUtils.isFolderReadOnly(parentNode, this._view) &&
+           !PlacesUtils.nodeIsTagQuery(parentNode);
+  },
 };
 
 /**
  * Handles drag and drop operations for views. Note that this is view agnostic!
  * You should not use PlacesController._view within these methods, since
  * the view that the item(s) have been dropped on was not necessarily active.
  * Drop functions are passed the view that is being dropped on.
  */
@@ -1374,72 +1405,16 @@ var PlacesControllerDragHelper = {
           }
         }
       }
     }
     return true;
   },
 
   /**
-   * Determines if an unwrapped node can be moved.
-   *
-   * @param unwrappedNode
-   *        A node unwrapped by PlacesUtils.unwrapNodes().
-   * @return True if the node can be moved, false otherwise.
-   */
-  canMoveUnwrappedNode(unwrappedNode) {
-    if ((unwrappedNode.concreteGuid && PlacesUtils.isRootItem(unwrappedNode.concreteGuid)) ||
-        unwrappedNode.id <= 0 || PlacesUtils.isRootItem(unwrappedNode.id)) {
-      return false;
-    }
-
-    let parentGuid = unwrappedNode.parentGuid;
-    // If there's no parent Guid, this was likely a virtual query that returns
-    // bookmarks, such as a tags query.
-    if (!parentGuid ||
-        parentGuid == PlacesUtils.bookmarks.rootGuid) {
-      return false;
-    }
-    // leftPaneFolderId and allBookmarksFolderId are lazy getters running
-    // at least a synchronous DB query. Therefore we don't want to invoke
-    // them first, especially because isCommandEnabled may be called way
-    // before the left pane folder is even necessary.
-    if (typeof Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId").get != "function" &&
-        (unwrappedNode.parent == PlacesUIUtils.leftPaneFolderId)) {
-      return false;
-    }
-    return true;
-  },
-
-  /**
-   * Determines if a node can be moved.
-   *
-   * @param   aNode
-   *          A nsINavHistoryResultNode node.
-   * @param   aView
-   *          The view originating the request
-   * @param   [optional] aDOMNode
-   *          A XUL DOM node.
-   * @return True if the node can be moved, false otherwise.
-   */
-  canMoveNode(aNode, aView) {
-    // Only bookmark items are movable.
-    if (aNode.itemId == -1)
-      return false;
-
-    // Once tags and bookmarked are divorced, the tag-query check should be
-    // removed.
-    let parentNode = aNode.parent;
-    return parentNode != null &&
-           PlacesUtils.nodeIsFolder(parentNode) &&
-           !PlacesUIUtils.isFolderReadOnly(parentNode, aView) &&
-           !PlacesUtils.nodeIsTagQuery(parentNode);
-  },
-
-  /**
    * Handles the drop of one or more items onto a view.
    *
    * @param {Object} insertionPoint The insertion point where the items should
    *                                be dropped.
    * @param {Object} dt             The dataTransfer information for the drop.
    * @param {Object} view           The view or the tree element. This allows
    *                                batching to take place.
    */
@@ -1481,226 +1456,15 @@ var PlacesControllerDragHelper = {
           title: data.label,
           type: PlacesUtils.TYPE_X_MOZ_URL
         });
       } else {
         throw new Error("bogus data was passed as a tab");
       }
     }
 
-    await handleTransferItems(nodes, insertionPoint, doCopy, view);
+    await PlacesUIUtils.handleTransferItems(nodes, insertionPoint, doCopy, view);
   },
-
-  /**
-   * Checks if we can insert into a container.
-   * @param   aContainer
-   *          The container were we are want to drop
-   * @param   aView
-   *          The view generating the request
-   */
-  disallowInsertion(aContainer, aView) {
-    NS_ASSERT(aContainer, "empty container");
-    // Allow dropping into Tag containers and editable folders.
-    return !PlacesUtils.nodeIsTagQuery(aContainer) &&
-           (!PlacesUtils.nodeIsFolder(aContainer) ||
-            PlacesUIUtils.isFolderReadOnly(aContainer, aView));
-  }
 };
 
-
 XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
                                    "@mozilla.org/widget/dragservice;1",
                                    "nsIDragService");
-
-function goUpdatePlacesCommands() {
-  // Get the controller for one of the places commands.
-  var placesController = doGetPlacesControllerForCommand("placesCmd_open");
-  function updatePlacesCommand(aCommand) {
-    goSetCommandEnabled(aCommand, placesController &&
-                                  placesController.isCommandEnabled(aCommand));
-  }
-
-  updatePlacesCommand("placesCmd_open");
-  updatePlacesCommand("placesCmd_open:window");
-  updatePlacesCommand("placesCmd_open:privatewindow");
-  updatePlacesCommand("placesCmd_open:tab");
-  updatePlacesCommand("placesCmd_new:folder");
-  updatePlacesCommand("placesCmd_new:bookmark");
-  updatePlacesCommand("placesCmd_new:separator");
-  updatePlacesCommand("placesCmd_show:info");
-  updatePlacesCommand("placesCmd_reload");
-  updatePlacesCommand("placesCmd_sortBy:name");
-  updatePlacesCommand("placesCmd_cut");
-  updatePlacesCommand("placesCmd_copy");
-  updatePlacesCommand("placesCmd_paste");
-  updatePlacesCommand("placesCmd_delete");
-}
-
-function doGetPlacesControllerForCommand(aCommand) {
-  // A context menu may be built for non-focusable views.  Thus, we first try
-  // to look for a view associated with document.popupNode
-  let popupNode;
-  try {
-    popupNode = document.popupNode;
-  } catch (e) {
-    // The document went away (bug 797307).
-    return null;
-  }
-  if (popupNode) {
-    let view = PlacesUIUtils.getViewForNode(popupNode);
-    if (view && view._contextMenuShown)
-      return view.controllers.getControllerForCommand(aCommand);
-  }
-
-  // When we're not building a context menu, only focusable views
-  // are possible.  Thus, we can safely use the command dispatcher.
-  let controller = top.document.commandDispatcher
-                      .getControllerForCommand(aCommand);
-  if (controller)
-    return controller;
-
-  return null;
-}
-
-function goDoPlacesCommand(aCommand) {
-  let controller = doGetPlacesControllerForCommand(aCommand);
-  if (controller && controller.isCommandEnabled(aCommand))
-    controller.doCommand(aCommand);
-}
-
-/**
- * This gets the most appropriate item for using for batching. In the case of multiple
- * views being related, the method returns the most expensive result to batch.
- * For example, if it detects the left-hand library pane, then it will look for
- * and return the reference to the right-hand pane.
- *
- * @param {Object} viewOrElement The item to check.
- * @return {Object} Will return the best result node to batch, or null
- *                  if one could not be found.
- */
-function getResultForBatching(viewOrElement) {
-  if (viewOrElement && viewOrElement instanceof Ci.nsIDOMElement &&
-      viewOrElement.id === "placesList") {
-    // Note: fall back to the existing item if we can't find the right-hane pane.
-    viewOrElement = document.getElementById("placeContent") || viewOrElement;
-  }
-
-  if (viewOrElement && viewOrElement.result) {
-    return viewOrElement.result;
-  }
-
-  return null;
-}
-
-/**
- * Processes a set of transfer items that have been dropped or pasted.
- * Batching will be applied where necessary.
- *
- * @param {Array} items A list of unwrapped nodes to process.
- * @param {Object} insertionPoint The requested point for insertion.
- * @param {Boolean} doCopy Set to true to copy the items, false will move them
- *                         if possible.
- * @return {Array} Returns an empty array when the insertion point is a tag, else
- *                 returns an array of copied or moved guids.
- */
-async function handleTransferItems(items, insertionPoint, doCopy, view) {
-  let transactions;
-  let itemsCount;
-  if (insertionPoint.isTag) {
-    let urls = items.filter(item => "uri" in item).map(item => item.uri);
-    itemsCount = urls.length;
-    transactions = [PlacesTransactions.Tag({ urls, tag: insertionPoint.tagName })];
-  } else {
-    let insertionIndex = await insertionPoint.getIndex();
-    itemsCount = items.length;
-    transactions = await getTransactionsForTransferItems(
-      items, insertionIndex, insertionPoint.guid, doCopy);
-  }
-
-  // Check if we actually have something to add, if we don't it probably wasn't
-  // valid, or it was moving to the same location, so just ignore it.
-  if (!transactions.length) {
-    return [];
-  }
-
-  let guidsToSelect = [];
-  let resultForBatching = getResultForBatching(view);
-
-  // If we're inserting into a tag, we don't get the guid, so we'll just
-  // pass the transactions direct to the batch function.
-  let batchingItem = transactions;
-  if (!insertionPoint.isTag) {
-    // If we're not a tag, then we need to get the ids of the items to select.
-    batchingItem = async () => {
-      for (let transaction of transactions) {
-        let guid = await transaction.transact();
-        if (guid) {
-          guidsToSelect.push(guid);
-        }
-      }
-    };
-  }
-
-  await PlacesUIUtils.batchUpdatesForNode(resultForBatching, itemsCount, async () => {
-    await PlacesTransactions.batch(batchingItem);
-  });
-
-  return guidsToSelect;
-}
-
-/**
- * Processes a set of transfer items and returns transactions to insert or
- * move them.
- *
- * @param {Array} items A list of unwrapped nodes to get transactions for.
- * @param {Integer} insertionIndex The requested index for insertion.
- * @param {String} insertionParentGuid The guid of the parent folder to insert
- *                                     or move the items to.
- * @param {Boolean} doCopy Set to true to copy the items, false will move them
- *                         if possible.
- * @return {Array} Returns an array of created PlacesTransactions.
- */
-async function getTransactionsForTransferItems(items, insertionIndex,
-                                               insertionParentGuid, doCopy) {
-  let transactions = [];
-  let index = insertionIndex;
-
-  for (let item of items) {
-    if (index != -1 && item.itemGuid) {
-      // Note: we use the parent from the existing bookmark as the sidebar
-      // gives us an unwrapped.parent that is actually a query and not the real
-      // parent.
-      let existingBookmark = await PlacesUtils.bookmarks.fetch(item.itemGuid);
-
-      // If we're dropping on the same folder, then we may need to adjust
-      // the index to insert at the correct place.
-      if (existingBookmark && insertionParentGuid == existingBookmark.parentGuid) {
-        if (index > existingBookmark.index) {
-          // If we're dragging down, we need to go one lower to insert at
-          // the real point as moving the element changes the index of
-          // everything below by 1.
-          index--;
-        } else if (index == existingBookmark.index) {
-          // This isn't moving so we skip it.
-          continue;
-        }
-      }
-    }
-
-    // If this is not a copy, check for safety that we can move the
-    // source, otherwise report an error and fallback to a copy.
-    if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(item)) {
-      Cu.reportError("Tried to move an unmovable Places " +
-                     "node, reverting to a copy operation.");
-      doCopy = true;
-    }
-    transactions.push(
-      PlacesUIUtils.getTransactionForData(item,
-                                          insertionParentGuid,
-                                          index,
-                                          doCopy));
-
-    if (index != -1 && item.itemGuid) {
-      index++;
-    }
-  }
-  return transactions;
-}
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -34,17 +34,17 @@ var gEditItemOverlay = {
                            node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
     let isTag = node && PlacesUtils.nodeIsTagQuery(node);
     if (isTag) {
       itemId = PlacesUtils.getConcreteItemId(node);
       // For now we don't have access to the item guid synchronously for tags,
       // so we'll need to fetch it later.
     }
     let isURI = node && PlacesUtils.nodeIsURI(node);
-    let uri = isURI ? NetUtil.newURI(node.uri) : null;
+    let uri = isURI ? Services.io.newURI(node.uri) : null;
     let title = node ? node.title : null;
     let isBookmark = isItem && isURI;
     let bulkTagging = !node;
     let uris = bulkTagging ? aInitInfo.uris : null;
     let visibleRows = new Set();
     let isParentReadOnly = false;
     let postData = aInitInfo.postData;
     let parentId = -1;
@@ -927,17 +927,17 @@ var gEditItemOverlay = {
                .filter(tag => tag.length > 0); // Kill empty tags.
   },
 
   async newFolder() {
     let ip = this._folderTree.insertionPoint;
 
     // default to the bookmarks menu folder
     if (!ip) {
-      ip = new InsertionPoint({
+      ip = new PlacesInsertionPoint({
         parentId: PlacesUtils.bookmarksMenuFolderId,
         parentGuid: PlacesUtils.bookmarks.menuGuid
       });
     }
 
     // XXXmano: add a separate "New Folder" string at some point...
     let title = this._element("newFolderButton").label;
     await PlacesTransactions.NewFolder({ parentGuid: ip.guid, title,
@@ -1067,17 +1067,17 @@ var gEditItemOverlay = {
     }
 
     if (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId) {
       return;
     }
 
     switch (aProperty) {
     case "uri":
-      let newURI = NetUtil.newURI(aValue);
+      let newURI = Services.ui.newURI(aValue);
       if (!newURI.equals(this._paneInfo.uri)) {
         this._paneInfo.uri = newURI;
         if (this._paneInfo.visibleRows.has("locationRow"))
           this._initLocationField();
 
         if (this._paneInfo.visibleRows.has("tagsRow")) {
           delete this._paneInfo._cachedCommonTags;
           this._onTagsChange(aGuid, newURI).catch(Cu.reportError);
--- a/browser/components/places/content/menu.xml
+++ b/browser/components/places/content/menu.xml
@@ -85,17 +85,17 @@
             let eventY = aEvent.layerY + (scrollbox.boxObject.y - this.boxObject.y);
             let scrollboxOffset = scrollbox.scrollBoxObject.y -
                                   (scrollbox.boxObject.y - this.boxObject.y);
             let eltY = elt.boxObject.y - scrollboxOffset;
             let eltHeight = elt.boxObject.height;
 
             if (!elt._placesNode) {
               // If we are dragging over a non places node drop at the end.
-              dropPoint.ip = new InsertionPoint({
+              dropPoint.ip = new PlacesInsertionPoint({
                 parentId: PlacesUtils.getConcreteItemId(resultNode),
                 parentGuid: PlacesUtils.getConcreteItemGuid(resultNode)
               });
               // We can set folderElt if we are dropping over a static menu that
               // has an internal placespopup.
               let isMenu = elt.localName == "menu" ||
                  (elt.localName == "toolbarbutton" &&
                   elt.getAttribute("type") == "menu");
@@ -108,49 +108,49 @@
             let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
                             elt._placesNode.title : null;
             if ((PlacesUtils.nodeIsFolder(elt._placesNode) &&
                  !PlacesUIUtils.isFolderReadOnly(elt._placesNode, this._rootView)) ||
                 PlacesUtils.nodeIsTagQuery(elt._placesNode)) {
               // This is a folder or a tag container.
               if (eventY - eltY < eltHeight * 0.20) {
                 // If mouse is in the top part of the element, drop above folder.
-                dropPoint.ip = new InsertionPoint({
+                dropPoint.ip = new PlacesInsertionPoint({
                   parentId: PlacesUtils.getConcreteItemId(resultNode),
                   parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
                   orientation: Ci.nsITreeView.DROP_BEFORE,
                   tagName,
                   dropNearNode: elt._placesNode
                 });
                 return dropPoint;
               } else if (eventY - eltY < eltHeight * 0.80) {
                 // If mouse is in the middle of the element, drop inside folder.
-                dropPoint.ip = new InsertionPoint({
+                dropPoint.ip = new PlacesInsertionPoint({
                   parentId: PlacesUtils.getConcreteItemId(elt._placesNode),
                   parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode),
                   tagName
                 });
                 dropPoint.folderElt = elt;
                 return dropPoint;
               }
             } else if (eventY - eltY <= eltHeight / 2) {
               // This is a non-folder node or a readonly folder.
               // If the mouse is above the middle, drop above this item.
-              dropPoint.ip = new InsertionPoint({
+              dropPoint.ip = new PlacesInsertionPoint({
                 parentId: PlacesUtils.getConcreteItemId(resultNode),
                 parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
                 orientation: Ci.nsITreeView.DROP_BEFORE,
                 tagName,
                 dropNearNode: elt._placesNode
               });
               return dropPoint;
             }
 
             // Drop below the item.
-            dropPoint.ip = new InsertionPoint({
+            dropPoint.ip = new PlacesInsertionPoint({
               parentId: PlacesUtils.getConcreteItemId(resultNode),
               parentGuid: PlacesUtils.getConcreteItemGuid(resultNode),
               orientation: Ci.nsITreeView.DROP_AFTER,
               tagName,
               dropNearNode: elt._placesNode,
             });
             return dropPoint;
         ]]></body>
@@ -345,17 +345,17 @@
         let elt = event.target;
         if (!elt._placesNode)
           return;
 
         let draggedElt = elt._placesNode;
 
         // Force a copy action if parent node is a query or we are dragging a
         // not-removable node.
-        if (!PlacesControllerDragHelper.canMoveNode(draggedElt, this._rootView))
+        if (!this._rootView.controller.canMoveNode(draggedElt))
           event.dataTransfer.effectAllowed = "copyLink";
 
         // Activate the view and cache the dragged element.
         this._rootView._draggedElt = draggedElt;
         this._rootView.controller.setDataTransfer(event);
         this.setAttribute("dragstart", "true");
         event.stopPropagation();
       ]]></handler>
--- a/browser/components/places/content/placesOverlay.xul
+++ b/browser/components/places/content/placesOverlay.xul
@@ -20,73 +20,77 @@
   <script type="application/javascript"><![CDATA[
     ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
     ChromeUtils.defineModuleGetter(window,
       "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
     ChromeUtils.defineModuleGetter(window,
       "PlacesUIUtils", "resource:///modules/PlacesUIUtils.jsm");
     ChromeUtils.defineModuleGetter(window,
       "PlacesTransactions", "resource://gre/modules/PlacesTransactions.jsm");
+    ChromeUtils.defineModuleGetter(window,
+      "ForgetAboutSite", "resource://gre/modules/ForgetAboutSite.jsm");
+
     XPCOMUtils.defineLazyScriptGetter(window, "PlacesTreeView",
       "chrome://browser/content/places/treeView.js");
+    XPCOMUtils.defineLazyScriptGetter(window,
+      ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
+      "chrome://browser/content/places/controller.js");
   ]]></script>
-  <script type="application/javascript"
-          src="chrome://browser/content/places/controller.js"/>
 
   <!-- Bookmarks and history tooltip -->
   <tooltip id="bhTooltip" noautohide="true"
            onpopupshowing="return window.top.BookmarksEventHandler.fillInBHTooltip(document, event)">
     <vbox id="bhTooltipTextBox" flex="1">
       <label id="bhtTitleText" class="tooltip-label" />
       <label id="bhtUrlText" crop="center" class="tooltip-label uri-element" />
     </vbox>
   </tooltip>
 
   <commandset id="placesCommands"
               commandupdater="true"
               events="focus,sort,places"
-              oncommandupdate="goUpdatePlacesCommands();">
+              oncommandupdate="PlacesUIUtils.updateCommands(window);">
     <command id="placesCmd_open"
-             oncommand="goDoPlacesCommand('placesCmd_open');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open');"/>
     <command id="placesCmd_open:window"
-             oncommand="goDoPlacesCommand('placesCmd_open:window');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open:window');"/>
     <command id="placesCmd_open:privatewindow"
-             oncommand="goDoPlacesCommand('placesCmd_open:privatewindow');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open:privatewindow');"/>
     <command id="placesCmd_open:tab"
-             oncommand="goDoPlacesCommand('placesCmd_open:tab');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_open:tab');"/>
 
     <command id="placesCmd_new:bookmark"
-             oncommand="goDoPlacesCommand('placesCmd_new:bookmark');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_new:bookmark');"/>
     <command id="placesCmd_new:folder"
-             oncommand="goDoPlacesCommand('placesCmd_new:folder');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_new:folder');"/>
     <command id="placesCmd_new:separator"
-             oncommand="goDoPlacesCommand('placesCmd_new:separator');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_new:separator');"/>
     <command id="placesCmd_show:info"
-             oncommand="goDoPlacesCommand('placesCmd_show:info');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_show:info');"/>
     <command id="placesCmd_rename"
-             oncommand="goDoPlacesCommand('placesCmd_show:info');"
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_show:info');"
              observes="placesCmd_show:info"/>
     <command id="placesCmd_reload"
-             oncommand="goDoPlacesCommand('placesCmd_reload');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_reload');"/>
     <command id="placesCmd_sortBy:name"
-             oncommand="goDoPlacesCommand('placesCmd_sortBy:name');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_sortBy:name');"/>
     <command id="placesCmd_deleteDataHost"
-             oncommand="goDoPlacesCommand('placesCmd_deleteDataHost');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_deleteDataHost');"/>
     <command id="placesCmd_createBookmark"
-             oncommand="goDoPlacesCommand('placesCmd_createBookmark');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_createBookmark');"/>
 
     <!-- Special versions of cut/copy/paste/delete which check for an open context menu. -->
     <command id="placesCmd_cut"
-             oncommand="goDoPlacesCommand('placesCmd_cut');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_cut');"/>
     <command id="placesCmd_copy"
-             oncommand="goDoPlacesCommand('placesCmd_copy');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_copy');"/>
     <command id="placesCmd_paste"
-             oncommand="goDoPlacesCommand('placesCmd_paste');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_paste');"/>
     <command id="placesCmd_delete"
-             oncommand="goDoPlacesCommand('placesCmd_delete');"/>
+             oncommand="PlacesUIUtils.doCommand(window, 'placesCmd_delete');"/>
   </commandset>
 
   <menupopup id="placesContext"
              onpopupshowing="this._view = PlacesUIUtils.getViewForNode(document.popupNode);
                              if (!PlacesUIUtils.openInTabClosesMenu) {
                                document.getElementById ('placesContext_open:newtab')
                                .setAttribute('closemenu', 'single');
                              }
--- a/browser/components/places/content/tree.xml
+++ b/browser/components/places/content/tree.xml
@@ -502,17 +502,17 @@
               container = lastSelected.parent;
 
               // See comment in the treeView.js's copy of this method
               if (!container || !container.containerOpen)
                 return null;
 
               // Avoid the potentially expensive call to getChildIndex
               // if we know this container doesn't allow insertion
-              if (PlacesControllerDragHelper.disallowInsertion(container, this))
+              if (this.controller.disallowInsertion(container))
                 return null;
 
               var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
               if (queryOptions.sortingMode !=
                     Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
                 // If we are within a sorted view, insert at the end
                 index = -1;
               } else if (queryOptions.excludeItems ||
@@ -525,28 +525,28 @@
                 dropNearNode = lastSelected;
               } else {
                 var lsi = container.getChildIndex(lastSelected);
                 index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
               }
             }
           }
 
-          if (PlacesControllerDragHelper.disallowInsertion(container, this))
+          if (this.controller.disallowInsertion(container))
             return null;
 
           // TODO (Bug 1160193): properly support dropping on a tag root.
           let tagName = null;
           if (PlacesUtils.nodeIsTagQuery(container)) {
             tagName = container.title;
             if (!tagName)
               return null;
           }
 
-          return new InsertionPoint({
+          return new PlacesInsertionPoint({
             parentId: PlacesUtils.getConcreteItemId(container),
             parentGuid: PlacesUtils.getConcreteItemGuid(container),
             index, orientation, tagName, dropNearNode
           });
         ]]></body>
       </method>
 
       <!-- nsIPlacesView -->
@@ -757,17 +757,17 @@
           if (!node.parent) {
             event.preventDefault();
             event.stopPropagation();
             return;
           }
 
           // If this node is child of a readonly container (e.g. a livemark)
           // or cannot be moved, we must force a copy.
-          if (!PlacesControllerDragHelper.canMoveNode(node, this)) {
+          if (!this.controller.canMoveNode(node)) {
             event.dataTransfer.effectAllowed = "copyLink";
             break;
           }
         }
 
         this._controller.setDataTransfer(event);
         event.stopPropagation();
       ]]></handler>
--- a/browser/components/places/content/treeView.js
+++ b/browser/components/places/content/treeView.js
@@ -1431,17 +1431,17 @@ PlacesTreeView.prototype = {
         // container here.  However, we can simply bail out when this happens,
         // because we would then be back here in less than a millisecond, when
         // the container had been reopened.
         if (!container || !container.containerOpen)
           return null;
 
         // Avoid the potentially expensive call to getChildIndex
         // if we know this container doesn't allow insertion.
-        if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+        if (this._controller.disallowInsertion(container))
           return null;
 
         let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
         if (queryOptions.sortingMode !=
               Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
           // If we are within a sorted view, insert at the end.
           index = -1;
         } else if (queryOptions.excludeItems ||
@@ -1454,28 +1454,28 @@ PlacesTreeView.prototype = {
           dropNearNode = lastSelected;
         } else {
           let lsi = container.getChildIndex(lastSelected);
           index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
         }
       }
     }
 
-    if (PlacesControllerDragHelper.disallowInsertion(container, this._tree.element))
+    if (this._controller.disallowInsertion(container))
       return null;
 
     // TODO (Bug 1160193): properly support dropping on a tag root.
     let tagName = null;
     if (PlacesUtils.nodeIsTagQuery(container)) {
       tagName = container.title;
       if (!tagName)
         return null;
     }
 
-    return new InsertionPoint({
+    return new PlacesInsertionPoint({
       parentId: PlacesUtils.getConcreteItemId(container),
       parentGuid: PlacesUtils.getConcreteItemGuid(container),
       index, orientation, tagName, dropNearNode
     });
   },
 
   drop: function PTV_drop(aRow, aOrientation, aDataTransfer) {
     if (this._controller.disableUserActions) {
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -14,16 +14,18 @@ support-files =
 [browser_0_library_left_pane_migration.js]
 [browser_addBookmarkForFrame.js]
 [browser_bookmark_add_tags.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmark_change_location.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmark_folder_moveability.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
+[browser_bookmark_load_in_sidebar.js]
+skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmark_private_window.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmark_remove_tags.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmarklet_windowOpen.js]
 support-files =
   bookmarklet_windowOpen_dummy.html
 [browser_bookmarks_change_title.js]
--- a/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
+++ b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
@@ -23,32 +23,32 @@ add_task(async function() {
       title: "",
       type: PlacesUtils.bookmarks.TYPE_FOLDER,
     });
     let folderId = await PlacesUtils.promiseItemId(folder.guid);
     tree.selectItems([folder.guid]);
     Assert.equal(tree.selectedNode.bookmarkGuid, folder.guid,
                  "Selected the expected node");
     Assert.equal(tree.selectedNode.type, 6, "node is a folder");
-    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+    Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
               "can move regular folder node");
 
     info("Test a folder shortcut");
     let shortcut = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "bar",
       url: `place:folder=${folderId}`
     });
     tree.selectItems([shortcut.guid]);
     Assert.equal(tree.selectedNode.bookmarkGuid, shortcut.guid,
                  "Selected the expected node");
     Assert.equal(tree.selectedNode.type, 9, "node is a folder shortcut");
     Assert.equal(PlacesUtils.getConcreteItemGuid(tree.selectedNode),
                  folder.guid, "shortcut node guid and concrete guid match");
-    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+    Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
               "can move folder shortcut node");
 
     info("Test a query");
     let bookmark = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "",
       url: "http://foo.com",
     });
@@ -58,53 +58,53 @@ add_task(async function() {
     let query = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "bar",
       url: `place:terms=foo`
     });
     tree.selectItems([query.guid]);
     Assert.equal(tree.selectedNode.bookmarkGuid, query.guid,
                  "Selected the expected node");
-    Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+    Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
               "can move query node");
 
 
     info("Test a tag container");
     PlacesUtils.tagging.tagURI(Services.io.newURI(bookmark.url.href), ["bar"]);
     // Add the tags root query.
     let tagsQuery = await PlacesUtils.bookmarks.insert({
       parentGuid: root.guid,
       title: "",
       url: "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY,
     });
     tree.selectItems([tagsQuery.guid]);
     PlacesUtils.asQuery(tree.selectedNode).containerOpen = true;
     Assert.equal(tree.selectedNode.childCount, 1, "has tags");
     let tagNode = tree.selectedNode.getChild(0);
-    Assert.ok(!PlacesControllerDragHelper.canMoveNode(tagNode, tree),
+    Assert.ok(!tree.controller.canMoveNode(tagNode),
               "should not be able to move tag container node");
     tree.selectedNode.containerOpen = false;
 
     info("Test that special folders and cannot be moved but other shortcuts can.");
     let roots = [
       PlacesUtils.bookmarks.menuGuid,
       PlacesUtils.bookmarks.unfiledGuid,
       PlacesUtils.bookmarks.toolbarGuid,
     ];
 
     for (let guid of roots) {
       tree.selectItems([guid]);
-      Assert.ok(!PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+      Assert.ok(!tree.controller.canMoveNode(tree.selectedNode),
                 "shouldn't be able to move default shortcuts to roots");
       let id = await PlacesUtils.promiseItemId(guid);
       let s = await PlacesUtils.bookmarks.insert({
         parentGuid: root.guid,
         title: "bar",
         url: `place:folder=${id}`,
       });
       tree.selectItems([s.guid]);
       Assert.equal(tree.selectedNode.bookmarkGuid, s.guid,
                    "Selected the expected node");
-      Assert.ok(PlacesControllerDragHelper.canMoveNode(tree.selectedNode, tree),
+      Assert.ok(tree.controller.canMoveNode(tree.selectedNode),
                 "should be able to move user-created shortcuts to roots");
     }
   });
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_load_in_sidebar.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that a bookmark can be loaded inside the Bookmarks sidebar.
+ */
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+
+// Cleanup.
+registerCleanupFunction(async () => {
+  await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_load_in_sidebar() {
+  let bm = await PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+    url: TEST_URL,
+    title: TEST_URL,
+  });
+
+  await withSidebarTree("bookmarks", async function(tree) {
+    tree.selectItems([bm.guid]);
+    await withBookmarksDialog(
+      false,
+      function openPropertiesDialog() {
+        tree.controller.doCommand("placesCmd_show:info");
+      },
+
+      // Check the "Load this bookmark in the sidebar" option.
+      async function test(dialogWin) {
+        let loadInSidebar = dialogWin.document.getElementById("editBMPanel_loadInSidebarCheckbox");
+        let promiseCheckboxChanged = PlacesTestUtils.waitForNotification(
+          "onItemChanged",
+          (id, parentId, checked) => checked === true
+        );
+
+        loadInSidebar.click();
+
+        EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+        await promiseCheckboxChanged;
+      }
+    );
+
+    let sidebar = document.getElementById("sidebar");
+
+    let sidebarLoadedPromise = new Promise(resolve => {
+      sidebar.addEventListener("load", function() {
+        executeSoon(resolve);
+      }, {capture: true, once: true});
+    });
+
+    // Select and open the bookmark in the sidebar.
+    tree.selectItems([bm.guid]);
+    tree.controller.doCommand("placesCmd_open");
+
+    await sidebarLoadedPromise;
+
+    let sidebarTitle = document.getElementById("sidebar-title");
+    let sidebarBrowser = sidebar.contentDocument.getElementById("web-panels-browser");
+
+    await BrowserTestUtils.browserLoaded(sidebarBrowser, false, TEST_URL);
+
+    let h1Elem = sidebarBrowser.contentDocument.getElementsByTagName("h1")[0];
+
+    // Check that the title and the content of the page are loaded successfully.
+    Assert.equal(sidebarTitle.value, TEST_URL, "The sidebar title is successfully loaded.");
+    Assert.equal(h1Elem.textContent, TEST_URL, "The sidebar content is successfully loaded.");
+  });
+});
--- a/browser/components/places/tests/browser/browser_check_correct_controllers.js
+++ b/browser/components/places/tests/browser/browser_check_correct_controllers.js
@@ -29,38 +29,38 @@ add_task(async function test() {
   registerCleanupFunction(() => {
     SidebarUI.hide();
   });
 
   // Focus the tree and check if its controller is returned.
   let tree = sidebar.contentDocument.getElementById("bookmarks-view");
   tree.focus();
 
-  let controller = doGetPlacesControllerForCommand("placesCmd_copy");
+  let controller = PlacesUIUtils.getControllerForCommand(window, "placesCmd_copy");
   let treeController = tree.controllers
                            .getControllerForCommand("placesCmd_copy");
   ok(controller == treeController, "tree controller was returned");
 
   // Open the context menu for a toolbar item, and check if the toolbar's
   // controller is returned.
   let toolbarItems = document.getElementById("PlacesToolbarItems");
   EventUtils.synthesizeMouse(toolbarItems.childNodes[0],
                              4, 4, { type: "contextmenu", button: 2 },
                              window);
-  controller = doGetPlacesControllerForCommand("placesCmd_copy");
+  controller = PlacesUIUtils.getControllerForCommand(window, "placesCmd_copy");
   let toolbarController = document.getElementById("PlacesToolbar")
                                   .controllers
                                   .getControllerForCommand("placesCmd_copy");
   ok(controller == toolbarController, "the toolbar controller was returned");
 
   document.getElementById("placesContext").hidePopup();
 
   // Now that the context menu is closed, try to get the tree controller again.
   tree.focus();
-  controller = doGetPlacesControllerForCommand("placesCmd_copy");
+  controller = PlacesUIUtils.getControllerForCommand(window, "placesCmd_copy");
   ok(controller == treeController, "tree controller was returned");
 
   if (wasCollapsed) {
     await promiseSetToolbarVisibility(toolbar, false);
   }
 });
 
 function promiseLoadedSidebar(cmd) {
--- a/browser/components/places/tests/browser/browser_controller_onDrop.js
+++ b/browser/components/places/tests/browser/browser_controller_onDrop.js
@@ -71,17 +71,17 @@ async function run_drag_test(startBookma
 
   await withSidebarTree("bookmarks", async (tree) => {
     tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
     PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
 
     // Simulating a drag-drop with a tree view turns out to be really difficult
     // as you can't get a node for the source/target. Hence, we fake the
     // insertion point and drag data and call the function direct.
-    let ip = new InsertionPoint({
+    let ip = new PlacesInsertionPoint({
       parentId: await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid),
       parentGuid: newParentGuid,
       index: insertionIndex,
       orientation: Ci.nsITreeView.DROP_ON
     });
 
     let bookmarkWithId = JSON.stringify(Object.assign({
       id: bookmarkIds.get(dragBookmark.guid),
--- a/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js
+++ b/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js
@@ -64,17 +64,17 @@ async function simulateDrop(selectTarget
 
     Assert.equal(guid, sourceBm.guid,
       "Should have the correct guid.");
     Assert.equal(dataObject.title, PlacesUtils.bookmarks.getLocalizedTitle(sourceBm),
       "Should have the correct title.");
 
     Assert.equal(dataTransfer.dropEffect, dropEffect);
 
-    let ip = new InsertionPoint({
+    let ip = new PlacesInsertionPoint({
       parentId: await PlacesUtils.promiseItemId(targetGuid),
       parentGuid: targetGuid,
       index: 0,
       orientation: Ci.nsITreeView.DROP_ON
     });
 
     await PlacesControllerDragHelper.onDrop(ip, dataTransfer);
   });
--- a/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js
+++ b/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js
@@ -51,17 +51,17 @@ async function run_drag_test(startBookma
 
   await withSidebarTree("bookmarks", async (tree) => {
     tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
     PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
 
     // Simulating a drag-drop with a tree view turns out to be really difficult
     // as you can't get a node for the source/target. Hence, we fake the
     // insertion point and drag data and call the function direct.
-    let ip = new InsertionPoint({
+    let ip = new PlacesInsertionPoint({
       isTag: true,
       tagName: TAG_NAME,
       orientation: Ci.nsITreeView.DROP_ON
     });
 
     let bookmarkWithId = JSON.stringify(Object.assign({
       id: bookmarkId,
       itemGuid: dragBookmark.guid,
--- a/browser/components/places/tests/browser/browser_library_commands.js
+++ b/browser/components/places/tests/browser/browser_library_commands.js
@@ -20,121 +20,121 @@ add_task(async function test_date_contai
   info("Ensure date containers under History cannot be cut but can be deleted");
 
   await PlacesTestUtils.addVisits(TEST_URI);
 
   // Select and open the left pane "History" query.
   let PO = library.PlacesOrganizer;
 
   PO.selectLeftPaneBuiltIn("History");
-  isnot(PO._places.selectedNode, null, "We correctly selected History");
+  Assert.notEqual(PO._places.selectedNode, null, "We correctly selected History");
 
   // Check that both delete and cut commands are disabled, cause this is
   // a child of the left pane folder.
-  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is disabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is disabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is disabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is disabled");
   let historyNode = PlacesUtils.asContainer(PO._places.selectedNode);
   historyNode.containerOpen = true;
 
   // Check that we have a child container. It is "Today" container.
-  is(historyNode.childCount, 1, "History node has one child");
+  Assert.equal(historyNode.childCount, 1, "History node has one child");
   let todayNode = historyNode.getChild(0);
   let todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
-  is(todayNode.title, todayNodeExpectedTitle,
-     "History child is the expected container");
+  Assert.equal(todayNode.title, todayNodeExpectedTitle,
+    "History child is the expected container");
 
   // Select "Today" container.
   PO._places.selectNode(todayNode);
-  is(PO._places.selectedNode, todayNode,
-     "We correctly selected Today container");
+  Assert.equal(PO._places.selectedNode, todayNode,
+    "We correctly selected Today container");
   // Check that delete command is enabled but cut command is disabled, cause
   // this is an history item.
-  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is disabled");
-  ok(PO._places.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is enabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is disabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is enabled");
 
   // Execute the delete command and check visit has been removed.
   let promiseURIRemoved = PlacesTestUtils.waitForNotification(
     "onDeleteURI", v => TEST_URI.equals(v), "history");
   PO._places.controller.doCommand("cmd_delete");
   await promiseURIRemoved;
 
   // Test live update of "History" query.
-  is(historyNode.childCount, 0, "History node has no more children");
+  Assert.equal(historyNode.childCount, 0, "History node has no more children");
 
   historyNode.containerOpen = false;
 
-  ok(!(await PlacesUtils.history.hasVisits(TEST_URI)), "Visit has been removed");
+  Assert.ok(!(await PlacesUtils.history.hasVisits(TEST_URI)), "Visit has been removed");
 
   library.close();
 });
 
 add_task(async function test_query_on_toolbar() {
   let library = await promiseLibrary();
   info("Ensure queries can be cut or deleted");
 
   // Select and open the left pane "Bookmarks Toolbar" folder.
   let PO = library.PlacesOrganizer;
 
   PO.selectLeftPaneBuiltIn("BookmarksToolbar");
-  isnot(PO._places.selectedNode, null, "We have a valid selection");
-  is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
-     PlacesUtils.toolbarFolderId,
-     "We have correctly selected bookmarks toolbar node.");
+  Assert.notEqual(PO._places.selectedNode, null, "We have a valid selection");
+  Assert.equal(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+    PlacesUtils.toolbarFolderId,
+    "We have correctly selected bookmarks toolbar node.");
 
   // Check that both cut and delete commands are disabled, cause this is a child
   // of the All Bookmarks special query.
-  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is disabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is disabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is disabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is disabled");
 
   let toolbarNode = PlacesUtils.asContainer(PO._places.selectedNode);
   toolbarNode.containerOpen = true;
 
   // Add an History query to the toolbar.
   let query = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                    url: "place:sort=4",
                                                    title: "special_query",
                                                    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
                                                    index: 0 });
 
   // Get first child and check it is the just inserted query.
-  ok(toolbarNode.childCount > 0, "Toolbar node has children");
+  Assert.ok(toolbarNode.childCount > 0, "Toolbar node has children");
   let queryNode = toolbarNode.getChild(0);
-  is(queryNode.title, "special_query", "Query node is correctly selected");
+  Assert.equal(queryNode.title, "special_query", "Query node is correctly selected");
 
   // Select query node.
   PO._places.selectNode(queryNode);
-  is(PO._places.selectedNode, queryNode, "We correctly selected query node");
+  Assert.equal(PO._places.selectedNode, queryNode, "We correctly selected query node");
 
   // Check that both cut and delete commands are enabled.
-  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(PO._places.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is enabled");
-  ok(PO._places.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is enabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is enabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is enabled");
 
   // Execute the delete command and check bookmark has been removed.
   let promiseItemRemoved = PlacesTestUtils.waitForNotification(
     "onItemRemoved", (...args) => query.guid == args[5]);
   PO._places.controller.doCommand("cmd_delete");
   await promiseItemRemoved;
 
-  is((await PlacesUtils.bookmarks.fetch(query.guid)), null,
+  Assert.equal((await PlacesUtils.bookmarks.fetch(query.guid)), null,
      "Query node bookmark has been correctly removed");
 
   toolbarNode.containerOpen = false;
 
   library.close();
 });
 
 add_task(async function test_search_contents() {
@@ -146,35 +146,35 @@ add_task(async function test_search_cont
 
   let library = await promiseLibrary();
   info("Ensure query contents can be cut or deleted");
 
   // Select and open the left pane "Bookmarks Toolbar" folder.
   let PO = library.PlacesOrganizer;
 
   PO.selectLeftPaneBuiltIn("BookmarksToolbar");
-  isnot(PO._places.selectedNode, null, "We have a valid selection");
-  is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
-     PlacesUtils.toolbarFolderId,
-     "We have correctly selected bookmarks toolbar node.");
+  Assert.notEqual(PO._places.selectedNode, null, "We have a valid selection");
+  Assert.equal(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
+    PlacesUtils.toolbarFolderId,
+    "We have correctly selected bookmarks toolbar node.");
 
   let searchBox = library.document.getElementById("searchFilter");
   searchBox.value = "example";
   library.PlacesSearchBox.search(searchBox.value);
 
   let bookmarkNode = library.ContentTree.view.selectedNode;
-  is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+  Assert.equal(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
 
   // Check that both cut and delete commands are enabled.
-  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is enabled");
-  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is enabled");
+  Assert.ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is enabled");
+  Assert.ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is enabled");
 
   library.close();
 });
 
 add_task(async function test_tags() {
   await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                        url: "http://example.com/",
                                        title: "example page",
@@ -185,51 +185,51 @@ add_task(async function test_tags() {
   let library = await promiseLibrary();
   info("Ensure query contents can be cut or deleted");
 
   // Select and open the left pane "Bookmarks Toolbar" folder.
   let PO = library.PlacesOrganizer;
 
   PO.selectLeftPaneBuiltIn("Tags");
   let tagsNode = PO._places.selectedNode;
-  isnot(tagsNode, null, "We have a valid selection");
+  Assert.notEqual(tagsNode, null, "We have a valid selection");
   let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
-  is(tagsNode.title, tagsTitle,
-     "Tags has been properly selected");
+  Assert.equal(tagsNode.title, tagsTitle,
+    "Tags has been properly selected");
 
   // Check that both cut and delete commands are disabled.
-  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is disabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is disabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is disabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is disabled");
 
   // Now select the tag.
   PlacesUtils.asContainer(tagsNode).containerOpen = true;
   let tag = tagsNode.getChild(0);
   PO._places.selectNode(tag);
-  is(PO._places.selectedNode.title, "test",
-     "The created tag has been properly selected");
+  Assert.equal(PO._places.selectedNode.title, "test",
+    "The created tag has been properly selected");
 
   // Check that cut is disabled but delete is enabled.
-  ok(PO._places.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is disabled");
-  ok(PO._places.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is enabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is disabled");
+  Assert.ok(PO._places.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is enabled");
 
   let bookmarkNode = library.ContentTree.view.selectedNode;
-  is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
+  Assert.equal(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
 
   // Check that both cut and delete commands are enabled.
-  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
-     "Copy command is enabled");
-  ok(!library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
-     "Cut command is disabled");
-  ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
-     "Delete command is enabled");
+  Assert.ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+    "Copy command is enabled");
+  Assert.ok(!library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+    "Cut command is disabled");
+  Assert.ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+    "Delete command is enabled");
 
   tagsNode.containerOpen = false;
 
   library.close();
 });
--- a/browser/components/places/tests/browser/browser_stayopenmenu.js
+++ b/browser/components/places/tests/browser/browser_stayopenmenu.js
@@ -141,17 +141,17 @@ add_task(async function testStayopenBook
   ok(appMenu.open, "Menu should remain open.");
 
   // Test App Menu's Bookmarks Library stayopen clicks: middle-click.
   promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
   EventUtils.synthesizeMouseAtCenter(testMenuitem, {button: 1});
   newTab = await promiseTabOpened;
   ok(true, "Bookmark middle-click opened new tab.");
   await BrowserTestUtils.removeTab(newTab);
-  is(PanelUI.multiView.current.id, "PanelUI-bookmarks", "Should still show the bookmarks subview");
+  ok(PanelView.forNode(BMview).active, "Should still show the bookmarks subview");
   ok(appMenu.open, "Menu should remain open.");
 
   // Close the App Menu
   appMenuPopup.hidePopup();
   ok(!appMenu.open, "The menu should now be closed.");
 
   // Disable the rest of the tests on Mac due to Mac's handling of menus being
   // slightly different to the other platforms.
--- a/browser/components/preferences/connection.js
+++ b/browser/components/preferences/connection.js
@@ -256,16 +256,19 @@ var gConnectionsDialog = {
     let isLocked = API_PROXY_PREFS.some(
       pref => Services.prefs.prefIsLocked(pref));
 
     function setInputsDisabledState(isControlled) {
       let disabled = isLocked || isControlled;
       for (let element of gConnectionsDialog.getProxyControls()) {
         element.disabled = disabled;
       }
+      if (!isControlled) {
+        gConnectionsDialog.proxyTypeChanged();
+      }
     }
 
     if (isLocked) {
       // An extension can't control this setting if any pref is locked.
       hideControllingExtension(PROXY_KEY);
       setInputsDisabledState(false);
     } else {
       handleControllingExtension(PREF_SETTING_TYPE, PROXY_KEY)
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -29,16 +29,18 @@
 <!ENTITY % permissionsDTD SYSTEM "chrome://browser/locale/preferences/permissions.dtd">
 <!ENTITY % passwordManagerDTD SYSTEM "chrome://passwordmgr/locale/passwordManager.dtd">
 <!ENTITY % historyDTD SYSTEM "chrome://mozapps/locale/update/history.dtd">
 <!ENTITY % certManagerDTD SYSTEM "chrome://pippki/locale/certManager.dtd">
 <!ENTITY % deviceManangerDTD SYSTEM "chrome://pippki/locale/deviceManager.dtd">
 <!ENTITY % connectionDTD SYSTEM "chrome://browser/locale/preferences/connection.dtd">
 <!ENTITY % siteDataSettingsDTD SYSTEM
   "chrome://browser/locale/preferences/siteDataSettings.dtd" >
+<!ENTITY % clearSiteDataDTD SYSTEM
+  "chrome://browser/locale/preferences/clearSiteData.dtd" >
 <!ENTITY % privacyDTD SYSTEM "chrome://browser/locale/preferences/privacy.dtd">
 <!ENTITY % searchDTD SYSTEM "chrome://browser/locale/preferences/search.dtd">
 <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
 <!ENTITY % syncDTD SYSTEM "chrome://browser/locale/preferences/sync.dtd">
 <!ENTITY % securityDTD SYSTEM
   "chrome://browser/locale/preferences/security.dtd">
 <!ENTITY % containersDTD SYSTEM
   "chrome://browser/locale/preferences/containers.dtd">
@@ -59,16 +61,17 @@
 %colorsDTD;
 %permissionsDTD;
 %passwordManagerDTD;
 %historyDTD;
 %certManagerDTD;
 %deviceManangerDTD;
 %connectionDTD;
 %siteDataSettingsDTD;
+%clearSiteDataDTD;
 %privacyDTD;
 %searchDTD;
 %syncBrandDTD;
 %syncDTD;
 %securityDTD;
 %containersDTD;
 %sanitizeDTD;
 %mainDTD;
@@ -83,16 +86,17 @@
 
 <page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
       xmlns:html="http://www.w3.org/1999/xhtml"
       disablefastfind="true"
       data-l10n-id="pref-page"
       data-l10n-attrs="title">
 
   <link rel="localization" href="branding/brand.ftl"/>
+  <link rel="localization" href="browser/branding/sync-brand.ftl"/>
   <link rel="localization" href="browser/preferences/preferences.ftl"/>
   <script type="text/javascript" src="chrome://global/content/l10n.js"></script>
 
   <html:link rel="shortcut icon"
               href="chrome://browser/skin/settings.svg"/>
 
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -403,17 +403,17 @@ var gPrivacyPane = {
     appendSearchKeywords("passwordExceptions", [
       bundlePrefs.getString("savedLoginsExceptions_title"),
       bundlePrefs.getString("savedLoginsExceptions_desc3"),
     ]);
     appendSearchKeywords("showPasswords", [
       signonBundle.getString("loginsDescriptionAll2"),
     ]);
     appendSearchKeywords("cookieExceptions", [
-      bundlePrefs.getString("cookiepermissionstext"),
+      bundlePrefs.getString("cookiepermissionstext1"),
     ]);
     appendSearchKeywords("trackingProtectionExceptions", [
       bundlePrefs.getString("trackingprotectionpermissionstitle"),
       bundlePrefs.getString("trackingprotectionpermissionstext2"),
     ]);
     appendSearchKeywords("changeBlockList", [
       bundlePrefs.getString("blockliststitle"),
       bundlePrefs.getString("blockliststext"),
@@ -449,21 +449,26 @@ var gPrivacyPane = {
     appendSearchKeywords("addonExceptions", [
       bundlePrefs.getString("addons_permissions_title2"),
       bundlePrefs.getString("addonspermissionstext"),
     ]);
     appendSearchKeywords("viewSecurityDevicesButton", [
       pkiBundle.getString("enable_fips"),
     ]);
     appendSearchKeywords("siteDataSettings", [
-      bundlePrefs.getString("siteDataSettings2.description"),
+      bundlePrefs.getString("siteDataSettings3.description"),
       bundlePrefs.getString("removeAllCookies.label"),
       bundlePrefs.getString("removeSelectedCookies.label"),
     ]);
 
+    if (!PrivateBrowsingUtils.enabled) {
+      document.getElementById("privateBrowsingAutoStart").hidden = true;
+      document.querySelector("menuitem[value='dontremember']").hidden = true;
+    }
+
     // Notify observers that the UI is now ready
     Services.obs.notifyObservers(window, "privacy-pane-loaded");
   },
 
   // TRACKING PROTECTION MODE
 
   /**
    * Selects the right item of the Tracking Protection radiogroup.
@@ -876,18 +881,18 @@ var gPrivacyPane = {
   showCookieExceptions() {
     var bundlePreferences = document.getElementById("bundlePreferences");
     var params = {
       blockVisible: true,
       sessionVisible: true,
       allowVisible: true,
       prefilledHost: "",
       permissionType: "cookie",
-      windowTitle: bundlePreferences.getString("cookiepermissionstitle"),
-      introText: bundlePreferences.getString("cookiepermissionstext")
+      windowTitle: bundlePreferences.getString("cookiepermissionstitle1"),
+      introText: bundlePreferences.getString("cookiepermissionstext1")
     };
     gSubDialog.open("chrome://browser/content/preferences/permissions.xul",
       null, params);
   },
 
   // CLEAR PRIVATE DATA
 
   /*
@@ -1400,17 +1405,17 @@ var gPrivacyPane = {
   },
 
   updateTotalDataSizeLabel(siteDataUsage) {
     SiteDataManager.getCacheSize().then(function(cacheUsage) {
       let prefStrBundle = document.getElementById("bundlePreferences");
       let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize");
       let totalUsage = siteDataUsage + cacheUsage;
       let size = DownloadUtils.convertByteUnits(totalUsage);
-      totalSiteDataSizeLabel.textContent = prefStrBundle.getFormattedString("totalSiteDataSize1", size);
+      totalSiteDataSizeLabel.textContent = prefStrBundle.getFormattedString("totalSiteDataSize2", size);
     });
   },
 
   clearSiteData() {
     gSubDialog.open("chrome://browser/content/preferences/clearSiteData.xul");
   },
 
   initDataCollection() {
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -70,50 +70,41 @@
     <label id="historyModeLabel"
            control="historyMode"
            accesskey="&historyHeader2.pre.accesskey;">&historyHeader2.pre.label;
     </label>
     <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
     <hbox>
       <menulist id="historyMode">
         <menupopup>
-          <menuitem label="&historyHeader.remember.label;" value="remember" searchkeywords="&rememberDescription.label;"/>
+          <menuitem label="&historyHeader.remember.label;" value="remember" searchkeywords="&rememberDescription1.label;"/>
           <menuitem label="&historyHeader.dontremember.label;" value="dontremember" searchkeywords="&dontrememberDescription.label;"/>
           <menuitem label="&historyHeader.custom.label;" value="custom" searchkeywords="&privateBrowsingPermanent2.label;
                                                                                         &rememberHistory2.label;
                                                                                         &rememberSearchForm.label;
-                                                                                        &acceptCookies3.label;
-                                                                                        &cookieExceptions.label;
-                                                                                        &acceptThirdParty3.pre.label;
-                                                                                        &acceptThirdParty.always.label;
-                                                                                        &acceptThirdParty.visited.label;
-                                                                                        &acceptThirdParty.never.label;
-                                                                                        &keepUntil2.label;
-                                                                                        &expire.label;
-                                                                                        &close.label;
                                                                                         &clearOnClose.label;
                                                                                         &clearOnCloseSettings.label;"/>
         </menupopup>
       </menulist>
     </hbox>
     <label>&historyHeader.post.label;</label>
   </hbox>
   <hbox>
     <deck id="historyPane" flex="1">
       <vbox id="historyRememberPane">
         <hbox align="center" flex="1">
           <vbox flex="1">
-            <description>&rememberDescription.label;</description>
+            <description class="description-with-side-element">&rememberDescription1.label;</description>
           </vbox>
         </hbox>
       </vbox>
       <vbox id="historyDontRememberPane">
         <hbox align="center" flex="1">
           <vbox flex="1">
-            <description>&dontrememberDescription.label;</description>
+            <description class="description-with-side-element">&dontrememberDescription.label;</description>
           </vbox>
         </hbox>
       </vbox>
       <vbox id="historyCustomPane">
         <vbox>
           <checkbox id="privateBrowsingAutoStart"
                     label="&privateBrowsingPermanent2.label;"
                     accesskey="&privateBrowsingPermanent2.accesskey;"
@@ -167,96 +158,105 @@
 </groupbox>
 
 <!-- Site Data -->
 <groupbox id="siteDataGroup" hidden="true" data-category="panePrivacy" data-hidden-from-search="true">
   <caption><label>&siteData1.label;</label></caption>
 
   <hbox data-subcategory="sitedata" align="baseline">
     <vbox flex="1">
+      <description class="description-with-side-element" flex="1">
+        <html:span id="totalSiteDataSize" class="tail-with-learn-more"></html:span>
+        <label id="siteDataLearnMoreLink" class="learnMore text-link">&siteDataLearnMoreLink.label;</label>
+      </description>
       <radiogroup id="acceptCookies"
                   preference="network.cookie.cookieBehavior"
                   onsyncfrompreference="return gPrivacyPane.readAcceptCookies();"
                   onsynctopreference="return gPrivacyPane.writeAcceptCookies();">
-      <description flex="1">
-        <label id="totalSiteDataSize" class="tail-with-learn-more"></label>
-        <label id="siteDataLearnMoreLink" class="learnMore text-link">&siteDataLearnMoreLink.label;</label>
-      </description>
-      <hbox id="cookiesBox">
-        <radio label="&acceptCookies3.label;"
-               value="0"
-               accesskey="&acceptCookies3.accesskey;"
+        <hbox id="cookiesBox">
+          <radio label="&acceptCookies4.label;"
+                 value="0"
+                 accesskey="&acceptCookies4.accesskey;"
+                 flex="1" />
+        </hbox>
+        <hbox id="keepRow"
+              class="indent"
+              align="center">
+          <label id="keepUntil"
+                 control="keepCookiesUntil"
+                 accesskey="&keepUntil2.accesskey;">&keepUntil2.label;</label>
+          <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
+          <hbox>
+            <menulist id="keepCookiesUntil"
+                      preference="network.cookie.lifetimePolicy">
+              <menupopup>
+                <menuitem label="&expire.label;" value="0"/>
+                <menuitem label="&close.label;" value="2"/>
+              </menupopup>
+            </menulist>
+          </hbox>
+        </hbox>
+        <hbox id="acceptThirdPartyRow"
+              class="indent"
+              align="center">
+          <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu"
+                 accesskey="&acceptThirdParty3.pre.accesskey;">&acceptThirdParty3.pre.label;</label>
+          <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
+          <hbox>
+            <menulist id="acceptThirdPartyMenu" preference="network.cookie.cookieBehavior"
+            onsyncfrompreference="return gPrivacyPane.readAcceptThirdPartyCookies();"
+            onsynctopreference="return gPrivacyPane.writeAcceptThirdPartyCookies();">
+              <menupopup>
+                <menuitem label="&acceptThirdParty.always.label;" value="always"/>
+                <menuitem label="&acceptThirdParty.visited.label;" value="visited"/>
+                <menuitem label="&acceptThirdParty.never.label;" value="never"/>
+              </menupopup>
+            </menulist>
+          </hbox>
+        </hbox>
+        <radio label="&blockCookies.label;"
+               value="2"
+               accesskey="&blockCookies.accesskey;"
                flex="1" />
-      </hbox>
-      <hbox id="keepRow"
-            class="indent"
-            align="center">
-        <label id="keepUntil"
-               control="keepCookiesUntil"
-               accesskey="&keepUntil2.accesskey;">&keepUntil2.label;</label>
-        <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
-        <hbox>
-          <menulist id="keepCookiesUntil"
-                    preference="network.cookie.lifetimePolicy">
-            <menupopup>
-              <menuitem label="&expire.label;" value="0"/>
-              <menuitem label="&close.label;" value="2"/>
-            </menupopup>
-          </menulist>
-        </hbox>
+      </radiogroup>
+    </vbox>
+    <vbox>
+      <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
+      <hbox>
+        <button id="clearSiteDataButton"
+            class="accessory-button"
+            icon="clear"
+            searchkeywords="&clearSiteData.label;
+                            &clearCache.label;"
+            label="&clearSiteData1.label;" accesskey="&clearSiteData1.accesskey;"/>
       </hbox>
-      <hbox id="acceptThirdPartyRow"
-            class="indent"
-            align="center">
-        <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu"
-               accesskey="&acceptThirdParty3.pre.accesskey;">&acceptThirdParty3.pre.label;</label>
-        <!-- Please don't remove the wrapping hbox/vbox/box for these elements. It's used to properly compute the search tooltip position. -->
-        <hbox>
-          <menulist id="acceptThirdPartyMenu" preference="network.cookie.cookieBehavior"
-          onsyncfrompreference="return gPrivacyPane.readAcceptThirdPartyCookies();"
-          onsynctopreference="return gPrivacyPane.writeAcceptThirdPartyCookies();">
-            <menupopup>
-              <menuitem label="&acceptThirdParty.always.label;" value="always"/>
-              <menuitem label="&acceptThirdParty.visited.label;" value="visited"/>
-              <menuitem label="&acceptThirdParty.never.label;" value="never"/>
-            </menupopup>
-          </menulist>
-        </hbox>
+      <hbox>
+        <button id="siteDataSettings"
+                class="accessory-button"
+                label="&siteDataSettings.label;"
+                accesskey="&siteDataSettings.accesskey;"
+                searchkeywords="&window1.title;
+                                &hostCol.label;
+                                &cookiesCol.label;
+                                &usageCol.label;"/>
       </hbox>
-      <radio label="&blockCookies.label;"
-             value="2"
-             accesskey="&blockCookies.accesskey;"
-             flex="1" />
-    </radiogroup>
-    </vbox>
-    <vbox align="end">
-      <button id="clearSiteDataButton"
-          class="accessory-button"
-          icon="clear"
-          label="&clearSiteData1.label;" accesskey="&clearSiteData1.accesskey;"/>
-      <button id="siteDataSettings"
-              class="accessory-button"
-              label="&siteDataSettings.label;"
-              accesskey="&siteDataSettings.accesskey;"
-              searchkeywords="&window.title;
-                              &hostCol.label;
-                              &statusCol.label;
-                              &usageCol.label;"/>
-      <button id="cookieExceptions"
-              class="accessory-button"
-              label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;"
-              preference="pref.privacy.disable_button.cookie_exceptions"
-              searchkeywords="&address2.label;
-                              &block.label;
-                              &session.label;
-                              &allow.label;
-                              &removepermission2.label;
-                              &removeallpermissions2.label;
-                              &button.cancel.label;
-                              &button.ok.label;"/>
+      <hbox>
+        <button id="cookieExceptions"
+                class="accessory-button"
+                label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;"
+                preference="pref.privacy.disable_button.cookie_exceptions"
+                searchkeywords="&address2.label;
+                                &block.label;
+                                &session.label;
+                                &allow.label;
+                                &removepermission2.label;
+                                &removeallpermissions2.label;
+                                &button.cancel.label;
+                                &button.ok.label;"/>
+      </hbox>
     </vbox>
   </hbox>
 </groupbox>
 
 <!-- Address Bar -->
 <groupbox id="locationBarGroup"
           data-category="panePrivacy"
           hidden="true">
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -27,16 +27,17 @@ skip-if = !updater
 [browser_search_subdialogs_within_preferences_1.js]
 [browser_search_subdialogs_within_preferences_2.js]
 [browser_search_subdialogs_within_preferences_3.js]
 [browser_search_subdialogs_within_preferences_4.js]
 [browser_search_subdialogs_within_preferences_5.js]
 [browser_search_subdialogs_within_preferences_6.js]
 [browser_search_subdialogs_within_preferences_7.js]
 [browser_search_subdialogs_within_preferences_8.js]
+[browser_search_subdialogs_within_preferences_site_data.js]
 [browser_bug795764_cachedisabled.js]
 [browser_bug1018066_resetScrollPosition.js]
 [browser_bug1020245_openPreferences_to_paneContent.js]
 [browser_bug1184989_prevent_scrolling_when_preferences_flipped.js]
 [browser_engines.js]
 support-files =
   browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul
 [browser_change_app_handler.js]
--- a/browser/components/preferences/in-content/tests/browser_connection.js
+++ b/browser/components/preferences/in-content/tests/browser_connection.js
@@ -52,16 +52,20 @@ function runConnectionTests(win) {
   let networkProxyTypePref = win.Preferences.get("network.proxy.type");
 
   // make sure the networkProxyNone textbox is formatted properly
   is(networkProxyNone.getAttribute("multiline"), "true",
      "networkProxyNone textbox is multiline");
   is(networkProxyNone.getAttribute("rows"), "2",
      "networkProxyNone textbox has two rows");
 
+  // make sure manual proxy controls are disabled when the window is opened
+  let networkProxyHTTP = doc.getElementById("networkProxyHTTP");
+  is(networkProxyHTTP.disabled, true, "networkProxyHTTP textbox is disabled");
+
   // check if sanitizing the given input for the no_proxies_on pref results in
   // expected string
   function testSanitize(input, expected, errorMessage) {
     networkProxyNonePref.value = input;
     win.gConnectionsDialog.sanitizeNoProxiesPref();
     is(networkProxyNonePref.value, expected, errorMessage);
   }
 
--- a/browser/components/preferences/in-content/tests/browser_extension_controlled.js
+++ b/browser/components/preferences/in-content/tests/browser_extension_controlled.js
@@ -668,27 +668,40 @@ add_task(async function testExtensionCon
       if (isControlled) {
         let controlledDesc = controlledSection.querySelector("description");
         // There are two spaces before "set_proxy" because it's " <image /> set_proxy".
         is(controlledDesc.textContent, `An extension,  set_proxy, is controlling how ${brandShortName} connects to the internet.`,
           "The user is notified that an extension is controlling proxy settings.");
       }
       function getProxyControls() {
         let controlGroup = doc.getElementById("networkProxyType");
-        return [
-          ...controlGroup.querySelectorAll(":scope > radio"),
-          ...controlGroup.querySelectorAll("label"),
-          ...controlGroup.querySelectorAll("textbox"),
-          ...controlGroup.querySelectorAll("checkbox"),
-          ...doc.querySelectorAll("#networkProxySOCKSVersion > radio"),
-          ...doc.querySelectorAll("#ConnectionsDialogPane > checkbox"),
-        ];
+        let manualControlContainer = controlGroup.querySelector("grid");
+        return {
+          manualControls: [
+            ...manualControlContainer.querySelectorAll("label"),
+            ...manualControlContainer.querySelectorAll("textbox"),
+            ...manualControlContainer.querySelectorAll("checkbox"),
+            ...doc.querySelectorAll("#networkProxySOCKSVersion > radio")],
+          pacControls: [doc.getElementById("networkProxyAutoconfigURL")],
+          otherControls: [
+            ...controlGroup.querySelectorAll(":scope > radio"),
+            ...doc.querySelectorAll("#ConnectionsDialogPane > checkbox")],
+        };
       }
       let controlState = isControlled ? "disabled" : "enabled";
-      for (let element of getProxyControls()) {
+      let controls = getProxyControls();
+      for (let element of controls.manualControls) {
+        let disabled = isControlled || proxyType !== proxySvc.PROXYCONFIG_MANUAL;
+        is(element.disabled, disabled, `Proxy controls are ${controlState}.`);
+      }
+      for (let element of controls.pacControls) {
+        let disabled = isControlled || proxyType !== proxySvc.PROXYCONFIG_PAC;
+        is(element.disabled, disabled, `Proxy controls are ${controlState}.`);
+      }
+      for (let element of controls.otherControls) {
         is(element.disabled, isControlled, `Proxy controls are ${controlState}.`);
       }
     } else {
       is(doc.getElementById(CONNECTION_SETTINGS_DESC_ID).textContent,
          expectedConnectionSettingsMessage(doc, isControlled),
          "The connection settings description is as expected.");
     }
   }
@@ -745,17 +758,17 @@ add_task(async function testExtensionCon
   let panelDoc = panelObj.panel.document;
 
   verifyState(panelDoc, false);
 
   await closeProxyPanel(panelObj);
 
   verifyState(mainDoc, false);
 
-  // Install an extension that sets Tracking Protection.
+  // Install an extension that controls proxy settings.
   let extension = ExtensionTestUtils.loadExtension({
     useAddonManager: "permanent",
     manifest: {
       name: "set_proxy",
       applications: {gecko: {id: EXTENSION_ID}},
       permissions: ["browserSettings"],
     },
     background,
--- a/browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_7.js
+++ b/browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_7.js
@@ -25,16 +25,8 @@ add_task(async function() {
  * Test for searching for the "Connection Settings" subdialog.
  */
 add_task(async function() {
   await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
   await evaluateSearchResults("Use system proxy settings", "connectionGroup");
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
-/**
- * Test for searching for the "Settings - Site Data" subdialog.
- */
-add_task(async function() {
-  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
-  await evaluateSearchResults("store site data on your computer", "siteDataGroup");
-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
-});
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_site_data.js
@@ -0,0 +1,35 @@
+/*
+* This file contains tests for the Preferences search bar.
+*/
+
+// Enabling Searching functionatily. Will display search bar form this testcase forward.
+add_task(async function() {
+  await SpecialPowers.pushPrefEnv({"set": [["browser.preferences.search", true]]});
+});
+
+/**
+ * Test for searching for the "Settings - Site Data" subdialog.
+ */
+add_task(async function() {
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  await evaluateSearchResults("cookies", ["siteDataGroup", "historyGroup"]);
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function() {
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  await evaluateSearchResults("site data", ["siteDataGroup", "historyGroup"]);
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function() {
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  await evaluateSearchResults("cache", ["siteDataGroup", "historyGroup"]);
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function() {
+  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  await evaluateSearchResults("third-party", "siteDataGroup");
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/components/preferences/in-content/tests/browser_siteData.js
+++ b/browser/components/preferences/in-content/tests/browser_siteData.js
@@ -85,17 +85,17 @@ add_task(async function() {
   let prefStrBundle = doc.getElementById("bundlePreferences");
   let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize");
   is(clearBtn.disabled, false, "Should enable clear button after sites updated");
   is(settingsButton.disabled, false, "Should enable settings button after sites updated");
   await SiteDataManager.getTotalUsage()
                        .then(usage => {
                          actual = totalSiteDataSizeLabel.textContent;
                          expected = prefStrBundle.getFormattedString(
-                           "totalSiteDataSize1", DownloadUtils.convertByteUnits(usage + cacheSize));
+                           "totalSiteDataSize2", DownloadUtils.convertByteUnits(usage + cacheSize));
                           is(actual, expected, "Should show the right total site data size");
                        });
 
   Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
   is(clearBtn.disabled, true, "Should disable clear button while updating sites");
   is(settingsButton.disabled, true, "Should disable settings button while updating sites");
   actual = totalSiteDataSizeLabel.textContent;
   expected = prefStrBundle.getString("loadingSiteDataSize1");
@@ -104,17 +104,17 @@ add_task(async function() {
   Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
   is(clearBtn.disabled, false, "Should enable clear button after sites updated");
   is(settingsButton.disabled, false, "Should enable settings button after sites updated");
   cacheSize = await SiteDataManager.getCacheSize();
   await SiteDataManager.getTotalUsage()
                        .then(usage => {
                          actual = totalSiteDataSizeLabel.textContent;
                          expected = prefStrBundle.getFormattedString(
-                           "totalSiteDataSize1", DownloadUtils.convertByteUnits(usage + cacheSize));
+                           "totalSiteDataSize2", DownloadUtils.convertByteUnits(usage + cacheSize));
                           is(actual, expected, "Should show the right total site data size");
                        });
 
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 // Test clearing service wroker through the settings panel
 add_task(async function() {
--- a/browser/components/preferences/siteDataSettings.js
+++ b/browser/components/preferences/siteDataSettings.js
@@ -88,17 +88,17 @@ let gSiteDataSettings = {
       let sortCol = document.querySelector("treecol[data-isCurrentSortCol=true]");
       this._sortSites(this._sites, sortCol);
       this._buildSitesList(this._sites);
       Services.obs.notifyObservers(null, "sitedata-settings-init");
     });
 
     let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
     let settingsDescription = document.getElementById("settingsDescription");
-    settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings2.description", [brandShortName]);
+    settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings3.description", [brandShortName]);
 
     setEventListener("sitesList", "select", this.onSelect);
     setEventListener("hostCol", "click", this.onClickTreeCol);
     setEventListener("usageCol", "click", this.onClickTreeCol);
     setEventListener("lastAccessedCol", "click", this.onClickTreeCol);
     setEventListener("cookiesCol", "click", this.onClickTreeCol);
     setEventListener("cancel", "command", this.close);
     setEventListener("save", "command", this.saveChanges);
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -1,16 +1,19 @@
 /* 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";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
+                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
 let { getChromeWindow } = ChromeUtils.import("resource:///modules/syncedtabs/util.js", {});
 
 let log = ChromeUtils.import("resource://gre/modules/Log.jsm", {})
             .Log.repository.getLogger("Sync.RemoteTabs");
 
 var EXPORTED_SYMBOLS = [
   "TabListView"
 ];
@@ -515,18 +518,20 @@ TabListView.prototype = {
     let item = this.container.querySelector(".item.selected");
     let showTabOptions = this._isTab(item);
 
     let el = menu.firstChild;
 
     while (el) {
       let show = false;
       if (showTabOptions) {
-        if (el.getAttribute("id") != "syncedTabsOpenAllInTabs" &&
-            el.getAttribute("id") != "syncedTabsManageDevices") {
+        if (el.getAttribute("id") == "syncedTabsOpenSelectedInPrivateWindow") {
+          show = PrivateBrowsingUtils.enabled;
+        } else if (el.getAttribute("id") != "syncedTabsOpenAllInTabs" &&
+                   el.getAttribute("id") != "syncedTabsManageDevices") {
           show = true;
         }
       } else if (el.getAttribute("id") == "syncedTabsOpenAllInTabs") {
         const tabs = item.querySelectorAll(".item-tabs-list > .item.tab");
         show = tabs.length > 0;
       } else if (el.getAttribute("id") == "syncedTabsRefresh") {
         show = true;
       } else if (el.getAttribute("id") == "syncedTabsManageDevices") {
--- a/browser/components/uitour/test/browser_UITour5.js
+++ b/browser/components/uitour/test/browser_UITour5.js
@@ -24,17 +24,18 @@ add_UITour_task(async function test_high
   let ViewShownPromise = new Promise(resolve => {
     appMenu.addEventListener("ViewShown", resolve, { once: true });
   });
   let highlightHiddenPromise = elementHiddenPromise(highlight, "Should hide highlight");
   let libraryBtn = document.getElementById("appMenu-library-button");
   libraryBtn.dispatchEvent(new Event("command"));
   await highlightHiddenPromise;
   await ViewShownPromise;
-  is(PanelUI.multiView.current.id, "appMenu-libraryView", "Should show the library subview");
+  let libView = document.getElementById("appMenu-libraryView");
+  ok(PanelView.forNode(libView).active, "Should show the library subview");
   is(appMenu.state, "open", "Should still open the app menu for the library subview");
 
   // Clean up
   let appMenuHiddenPromise = promisePanelElementHidden(window, appMenu);
   gContentAPI.hideMenu("appMenu");
   await appMenuHiddenPromise;
   is(appMenu.state, "closed", "Should close the app menu");
 });
deleted file mode 100644
--- a/browser/docs/DirectoryLinksProvider.rst
+++ /dev/null
@@ -1,107 +0,0 @@
-=============================================
-Directory Links Architecture and Data Formats
-=============================================
-
-Directory links are enhancements to the new tab experience that combine content
-Firefox already knows about from user browsing with external content. There is 1
-kind of links:
-
-- directory links fill in additional tiles on the new tab page if there would
-  have been empty tiles because the user has a clean profile or cleared history
-
-To power the above features, DirectoryLinksProvider module downloads, at most
-once per 24 hours, the directory source links as JSON with enough data for
-Firefox to determine what should be shown or not.
-
-For the directory source endpoint, the default preference values point
-to Mozilla key-pinned servers with encryption. No cookies are set by the servers
-and Firefox enforces this by making anonymous requests.
-
-- default directory source endpoint:
-  https://tiles.services.mozilla.com/v3/links/fetch/%LOCALE%/%CHANNEL%
-
-
-Preferences
-===========
-
-There is one main preference that controls downloading links.
-
-``browser.newtabpage.directory.source``
----------------------------------------
-
-This endpoint tells Firefox where to download directory source file as a GET
-request. It should return JSON of the appropriate format containing the relevant
-links data. The value can be a data URI, e.g., an empty JSON object effectively
-turns off remote downloading: ``data:text/plain,{}``
-
-The preference value will have %LOCALE% and %CHANNEL% replaced by the
-appropriate values for the build of Firefox, e.g.,
-
-- directory source endpoint:
-  https://tiles.services.mozilla.com/v3/links/fetch/en-US/release
-
-
-Data Flow
-=========
-
-When Firefox starts, it checks for a cached directory source file. If one
-exists, it checks for its timestamp to determine if a new file should be
-downloaded.
-
-If a directory source file needs to be downloaded, a GET request is made then
-cached and unpacked the JSON into the different types of links. Various checks
-filter out invalid links, e.g., those with http-hosted images or those that
-don't fit the allowed suggestions.
-
-When a new tab page is built, DirectoryLinksProvider module provides additional
-link data that is combined with history link data to determine which links can
-be displayed or not.
-
-As the new tab page is rendered, any images for tiles are downloaded if not
-already cached. The default servers hosting the images are Mozilla CDN that
-don't use cookies: https://tiles.cdn.mozilla.net/ and Firefox enforces that the
-images come from mozilla.net or data URIs when using the default directory
-source.
-
-
-Source JSON Format
-==================
-
-Firefox expects links data in a JSON object with a top level "directory" key
-providing an array of tile objects.
-
-Example
--------
-
-Below is an example directory source file::
-
-  {
-      "directory": [
-          {
-              "bgColor": "",
-              "directoryId": 498,
-              "enhancedImageURI": "https://tiles.cdn.mozilla.net/images/d11ba0b3095bb19d8092cd29be9cbb9e197671ea.28088.png",
-              "imageURI": "https://tiles.cdn.mozilla.net/images/1332a68badf11e3f7f69bf7364e79c0a7e2753bc.5316.png",
-              "title": "Mozilla Community",
-              "type": "affiliate",
-              "url": "http://contribute.mozilla.org/"
-          }
-      ]
-  }
-
-Link Object
------------
-
-Each link object has various values that Firefox uses to display a tile:
-
-- ``url`` - string url for the page to be loaded when the tile is clicked. Only
-  https and http URLs are allowed.
-- ``title`` - string that appears below the tile.
-- ``type`` - string relationship of the link to Mozilla. Expected values:
-  affiliate.
-- ``imageURI`` - string url for the tile image to show. Only https and data URIs
-  are allowed.
-- ``enhancedImageURI`` - string url for the image to be shown before the user
-  hovers. Only https and data URIs are allowed.
-- ``bgColor`` - string css color for additional fill background color.
-- ``directoryId`` - id of the tile to be used during ping reporting
--- a/browser/docs/index.rst
+++ b/browser/docs/index.rst
@@ -2,11 +2,10 @@
 Firefox
 =======
 
 This is the nascent documentation of the Firefox front-end code.
 
 .. toctree::
    :maxdepth: 1
 
-   DirectoryLinksProvider
    UITelemetry
    BrowserUsageTelemetry
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -1,18 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-var MAIN_MESSAGE_TYPE = "ActivityStream:Main";
-var CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
-var PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser";
-var UI_CODE = 1;
-var BACKGROUND_PROCESS = 2;
+this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+this.PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser";
+this.UI_CODE = 1;
+this.BACKGROUND_PROCESS = 2;
 
 /**
  * globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process?
  *                       Use this in action creators if you need different logic
  *                       for ui/background processes.
  */
 const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
 // Export for tests
@@ -83,16 +83,18 @@ for (const type of [
   "TELEMETRY_UNDESIRED_EVENT",
   "TELEMETRY_USER_EVENT",
   "TOP_SITES_CANCEL_EDIT",
   "TOP_SITES_EDIT",
   "TOP_SITES_INSERT",
   "TOP_SITES_PIN",
   "TOP_SITES_UNPIN",
   "TOP_SITES_UPDATED",
+  "TOTAL_BOOKMARKS_REQUEST",
+  "TOTAL_BOOKMARKS_RESPONSE",
   "UNINIT",
   "WEBEXT_CLICK",
   "WEBEXT_DISMISS"
 ]) {
   actionTypes[type] = type;
 }
 
 // Helper function for creating routed actions between content and main
@@ -272,33 +274,33 @@ function WebExtEvent(type, data, importC
     throw new Error("WebExtEvent actions should include a property \"source\", the id of the webextension that should receive the event.");
   }
   const action = {type, data};
   return importContext === UI_CODE ? AlsoToMain(action) : action;
 }
 
 this.actionTypes = actionTypes;
 
-var actionCreators = {
+this.actionCreators = {
   BroadcastToContent,
   UserEvent,
   UndesiredEvent,
   PerfEvent,
   ImpressionStats,
   AlsoToOneContent,
   OnlyToOneContent,
   AlsoToMain,
   OnlyToMain,
   AlsoToPreloaded,
   SetPref,
   WebExtEvent
 };
 
 // These are helpers to test for certain kinds of actions
-var actionUtils = {
+this.actionUtils = {
   isSendToMain(action) {
     if (!action.meta) {
       return false;
     }
     return action.meta.to === MAIN_MESSAGE_TYPE && action.meta.from === CONTENT_MESSAGE_TYPE;
   },
   isBroadcastToContent(action) {
     if (!action.meta) {
@@ -333,17 +335,17 @@ var actionUtils = {
       action.meta.to === CONTENT_MESSAGE_TYPE;
   },
   getPortIdOfSender(action) {
     return (action.meta && action.meta.fromTarget) || null;
   },
   _RouteMessage
 };
 
-var EXPORTED_SYMBOLS = [
+const EXPORTED_SYMBOLS = [
   "actionTypes",
   "actionCreators",
   "actionUtils",
   "globalImportContext",
   "UI_CODE",
   "BACKGROUND_PROCESS",
   "MAIN_MESSAGE_TYPE",
   "CONTENT_MESSAGE_TYPE",
--- a/browser/extensions/activity-stream/common/Dedupe.jsm
+++ b/browser/extensions/activity-stream/common/Dedupe.jsm
@@ -1,9 +1,9 @@
-var Dedupe = class Dedupe {
+this.Dedupe = class Dedupe {
   constructor(createKey) {
     this.createKey = createKey || this.defaultCreateKey;
   }
 
   defaultCreateKey(item) {
     return item;
   }
 
@@ -26,9 +26,9 @@ var Dedupe = class Dedupe {
       }
       result.push(valueMap);
       valueMap.forEach((value, key) => globalKeys.add(key));
     }
     return result.map(m => Array.from(m.values()));
   }
 };
 
-var EXPORTED_SYMBOLS = ["Dedupe"];
+const EXPORTED_SYMBOLS = ["Dedupe"];
--- a/browser/extensions/activity-stream/common/PerfService.jsm
+++ b/browser/extensions/activity-stream/common/PerfService.jsm
@@ -116,10 +116,10 @@ function _PerfService(options) {
       throw new Error(`No marks with the name ${name}`);
     }
 
     let mostRecentEntry = entries[entries.length - 1];
     return this._perf.timeOrigin + mostRecentEntry.startTime;
   }
 };
 
-var perfService = new _PerfService();
-var EXPORTED_SYMBOLS = ["_PerfService", "perfService"];
+this.perfService = new _PerfService();
+const EXPORTED_SYMBOLS = ["_PerfService", "perfService"];
--- a/browser/extensions/activity-stream/common/PrerenderData.jsm
+++ b/browser/extensions/activity-stream/common/PrerenderData.jsm
@@ -41,17 +41,17 @@ class _PrerenderData {
       } else if (getPref(prefs) !== this.initialPrefs[prefs]) {
         return false;
       }
     }
     return true;
   }
 }
 
-var PrerenderData = new _PrerenderData({
+this.PrerenderData = new _PrerenderData({
   initialPrefs: {
     "migrationExpired": true,
     "showTopSites": true,
     "showSearch": true,
     "topSitesRows": 1,
     "collapseTopSites": false,
     "section.highlights.collapsed": false,
     "section.topstories.collapsed": false,
@@ -91,9 +91,9 @@ var PrerenderData = new _PrerenderData({
       icon: "highlights",
       order: 2,
       title: {id: "header_highlights"}
     }
   ]
 });
 
 this._PrerenderData = _PrerenderData;
-var EXPORTED_SYMBOLS = ["PrerenderData", "_PrerenderData"];
+const EXPORTED_SYMBOLS = ["PrerenderData", "_PrerenderData"];
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -202,16 +202,29 @@ function Sections(prevState = INITIAL_ST
       }
       return newState;
     case at.SECTION_UPDATE:
       newState = prevState.map(section => {
         if (section && section.id === action.data.id) {
           // If the action is updating rows, we should consider initialized to be true.
           // This can be overridden if initialized is defined in the action.data
           const initialized = action.data.rows ? {initialized: true} : {};
+
+          // Make sure pinned cards stay at their current position when rows are updated.
+          // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards.
+          if (action.data.rows && action.data.rows.length > 0 && section.rows.find(card => card.pinned)) {
+            const rows = Array.from(action.data.rows);
+            section.rows.forEach((card, index) => {
+              if (card.pinned) {
+                rows.splice(index, 0, card);
+              }
+            });
+            return Object.assign({}, section, initialized, Object.assign({}, action.data, {rows}));
+          }
+
           return Object.assign({}, section, initialized, action.data);
         }
         return section;
       });
 
       if (!action.data.dedupeConfigurations) {
         return newState;
       }
@@ -268,16 +281,17 @@ function Sections(prevState = INITIAL_ST
     case at.PLACES_SAVED_TO_POCKET:
       if (!action.data) {
         return prevState;
       }
       return prevState.map(section => Object.assign({}, section, {
         rows: section.rows.map(item => {
           if (item.url === action.data.url) {
             return Object.assign({}, item, {
+              open_url: action.data.open_url,
               pocket_id: action.data.pocket_id,
               title: action.data.title,
               type: "pocket"
             });
           }
           return item;
         })
       }));
@@ -337,11 +351,11 @@ function PreferencesPane(prevState = INI
       return prevState;
   }
 }
 
 this.INITIAL_STATE = INITIAL_STATE;
 this.TOP_SITES_DEFAULT_ROWS = TOP_SITES_DEFAULT_ROWS;
 this.TOP_SITES_MAX_SITES_PER_ROW = TOP_SITES_MAX_SITES_PER_ROW;
 
-var reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections, PreferencesPane};
+this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections, PreferencesPane};
 
-var EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"];
+const EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"];
--- a/browser/extensions/activity-stream/css/activity-stream-linux.css
+++ b/browser/extensions/activity-stream/css/activity-stream-linux.css
@@ -1191,41 +1191,42 @@ main {
       margin-inline-start: 8px;
       margin-top: -1px; }
   .collapsible-section .section-top-bar {
     position: relative; }
     .collapsible-section .section-top-bar .context-menu-button {
       background: url("chrome://browser/skin/page-action.svg") no-repeat right center;
       border: 0;
       cursor: pointer;
-      fill: #D7D7DB;
+      fill: #737373;
       height: 27px;
       offset-inline-end: 0;
       opacity: 0;
       position: absolute;
       top: 0;
       transition-duration: 200ms;
       transition-property: opacity;
       width: 27px; }
       .collapsible-section .section-top-bar .context-menu-button:-moz-any(:active, :focus, :hover) {
-        fill: rgba(12, 12, 13, 0.8); }
+        fill: #0C0C0D; }
     .collapsible-section .section-top-bar .context-menu {
       top: 16px; }
+    @media (max-width: 1458px) {
+      .collapsible-section .section-top-bar .context-menu {
+        margin-inline-end: 5px;
+        margin-inline-start: auto;
+        offset-inline-end: 0;
+        offset-inline-start: auto; } }
   .collapsible-section:hover .section-top-bar .context-menu-button, .collapsible-section.active .section-top-bar .context-menu-button {
     opacity: 1; }
   .collapsible-section.active {
-    background: rgba(237, 237, 240, 0.2); }
+    background: rgba(237, 237, 240, 0.6);
+    border-radius: 4px; }
     .collapsible-section.active .section-top-bar .context-menu-button {
-      fill: rgba(12, 12, 13, 0.8); }
-  @media (max-width: 1458px) {
-    .collapsible-section .context-menu {
-      margin-inline-end: 5px;
-      margin-inline-start: auto;
-      offset-inline-end: 0;
-      offset-inline-start: auto; } }
+      fill: #0C0C0D; }
   .collapsible-section .section-disclaimer {
     color: #4A4A4F;
     font-size: 13px;
     margin-bottom: 16px;
     position: relative; }
     .collapsible-section .section-disclaimer .section-disclaimer-text {
       display: inline-block;
       min-height: 26px;
--- a/browser/extensions/activity-stream/css/activity-stream-linux.css.map
+++ b/browser/extensions/activity-stream/css/activity-stream-linux.css.map
@@ -19,26 +19,26 @@
 		"../content-src/components/Card/_Card.scss",
 		"../content-src/components/ManualMigration/_ManualMigration.scss",
 		"../content-src/components/CollapsibleSection/_CollapsibleSection.scss"
 	],
 	"sourcesContent": [
 		"/* This is the linux variant */ // sass-lint:disable-line no-css-comments\n\n$os-infopanel-arrow-height: 10px;\n$os-infopanel-arrow-offset-end: 6px;\n$os-infopanel-arrow-width: 20px;\n$os-search-focus-shadow-radius: 3px;\n\n@import './activity-stream';\n",
 		"@import './normalize';\n@import './variables';\n@import './icons';\n\nhtml,\nbody,\n#root { // sass-lint:disable-line no-ids\n  height: 100%;\n}\n\nbody {\n  background: $background-primary;\n  color: $text-primary;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;\n  font-size: 16px;\n  overflow-y: scroll;\n}\n\nh1,\nh2 {\n  font-weight: normal;\n}\n\na {\n  color: $link-primary;\n  text-decoration: none;\n\n  &:hover {\n    color: $link-secondary;\n  }\n}\n\n// For screen readers\n.sr-only {\n  border: 0;\n  clip: rect(0, 0, 0, 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n\n.inner-border {\n  border: $border-secondary;\n  border-radius: $border-radius;\n  height: 100%;\n  left: 0;\n  pointer-events: none;\n  position: absolute;\n  top: 0;\n  width: 100%;\n  z-index: 100;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n.show-on-init {\n  opacity: 0;\n  transition: opacity 0.2s ease-in;\n\n  &.on {\n    animation: fadeIn 0.2s;\n    opacity: 1;\n  }\n}\n\n.actions {\n  border-top: $border-secondary;\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: flex-start;\n  margin: 0;\n  padding: 15px 25px 0;\n\n  button {\n    background-color: $input-secondary;\n    border: $border-primary;\n    border-radius: 4px;\n    color: inherit;\n    cursor: pointer;\n    margin-bottom: 15px;\n    padding: 10px 30px;\n    white-space: nowrap;\n\n    &:hover:not(.dismiss) {\n      box-shadow: $shadow-primary;\n      transition: box-shadow 150ms;\n    }\n\n    &.dismiss {\n      border: 0;\n      padding: 0;\n      text-decoration: underline;\n    }\n\n    &.done {\n      background: $input-primary;\n      border: solid 1px $blue-60;\n      color: $white;\n      margin-inline-start: auto;\n    }\n  }\n}\n\n// Make sure snippets show up above other UI elements\n#snippets-container { // sass-lint:disable-line no-ids\n  z-index: 1;\n}\n\n// Components\n@import '../components/Base/Base';\n@import '../components/ErrorBoundary/ErrorBoundary';\n@import '../components/TopSites/TopSites';\n@import '../components/Sections/Sections';\n@import '../components/Topics/Topics';\n@import '../components/Search/Search';\n@import '../components/ContextMenu/ContextMenu';\n@import '../components/PreferencesPane/PreferencesPane';\n@import '../components/ConfirmDialog/ConfirmDialog';\n@import '../components/Card/Card';\n@import '../components/ManualMigration/ManualMigration';\n@import '../components/CollapsibleSection/CollapsibleSection';\n",
 		"html {\n  box-sizing: border-box;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: inherit;\n}\n\n*::-moz-focus-inner {\n  border: 0;\n}\n\nbody {\n  margin: 0;\n}\n\nbutton,\ninput {\n  background-color: inherit;\n  color: inherit;\n  font-family: inherit;\n  font-size: inherit;\n}\n\n[hidden] {\n  display: none !important; // sass-lint:disable-line no-important\n}\n",
-		"// Photon colors from http://design.firefox.com/photon/visuals/color.html\n$blue-50: #0A84FF;\n$blue-60: #0060DF;\n$grey-10: #F9F9FA;\n$grey-20: #EDEDF0;\n$grey-30: #D7D7DB;\n$grey-40: #B1B1B3;\n$grey-50: #737373;\n$grey-60: #4A4A4F;\n$grey-90: #0C0C0D;\n$teal-70: #008EA4;\n$red-60: #D70022;\n\n// Photon opacity from http://design.firefox.com/photon/visuals/color.html#opacity\n$grey-90-10: rgba($grey-90, 0.1);\n$grey-90-20: rgba($grey-90, 0.2);\n$grey-90-30: rgba($grey-90, 0.3);\n$grey-90-40: rgba($grey-90, 0.4);\n$grey-90-50: rgba($grey-90, 0.5);\n$grey-90-60: rgba($grey-90, 0.6);\n$grey-90-70: rgba($grey-90, 0.7);\n$grey-90-80: rgba($grey-90, 0.8);\n$grey-90-90: rgba($grey-90, 0.9);\n\n$grey-30-20: rgba($grey-20, 0.2);\n\n$black: #000;\n$black-5: rgba($black, 0.05);\n$black-10: rgba($black, 0.1);\n$black-15: rgba($black, 0.15);\n$black-20: rgba($black, 0.2);\n$black-25: rgba($black, 0.25);\n$black-30: rgba($black, 0.3);\n\n// Photon transitions from http://design.firefox.com/photon/motion/duration-and-easing.html\n$photon-easing: cubic-bezier(0.07, 0.95, 0, 1);\n\n// Aliases and derived styles based on Photon colors for common usage\n$background-primary: $grey-10;\n$background-secondary: $grey-20;\n$border-primary: 1px solid $grey-40;\n$border-secondary: 1px solid $grey-30;\n$fill-primary: $grey-90-80;\n$fill-secondary: $grey-90-60;\n$fill-tertiary: $grey-30;\n$input-primary: $blue-60;\n$input-secondary: $grey-10;\n$link-primary: $blue-60;\n$link-secondary: $teal-70;\n$shadow-primary: 0 0 0 5px $grey-30;\n$shadow-secondary: 0 1px 4px 0 $grey-90-10;\n$text-primary: $grey-90;\n$text-conditional: $grey-60;\n$text-secondary: $grey-50;\n$input-border: solid 1px $grey-90-20;\n$input-border-active: solid 1px $blue-50;\n$input-error-border: solid 1px $red-60;\n$input-error-boxshadow: 0 0 0 2px rgba($red-60, 0.35);\n$input-focus-boxshadow: 0 0 0 2px rgba($blue-50, 0.35);\n\n$white: #FFF;\n$border-radius: 3px;\n\n$base-gutter: 32px;\n$section-horizontal-padding: 25px;\n$section-vertical-padding: 10px;\n$section-spacing: 40px - $section-vertical-padding * 2;\n$grid-unit: 96px; // 1 top site\n\n$icon-size: 16px;\n$smaller-icon-size: 12px;\n$larger-icon-size: 32px;\n\n$wrapper-default-width: $grid-unit * 2 + $base-gutter * 1 + $section-horizontal-padding * 2; // 2 top sites\n$wrapper-max-width-small: $grid-unit * 3 + $base-gutter * 2 + $section-horizontal-padding * 2; // 3 top sites\n$wrapper-max-width-medium: $grid-unit * 4 + $base-gutter * 3 + $section-horizontal-padding * 2; // 4 top sites\n$wrapper-max-width-large: $grid-unit * 6 + $base-gutter * 5 + $section-horizontal-padding * 2; // 6 top sites\n$wrapper-max-width-widest: $grid-unit * 8 + $base-gutter * 7 + $section-horizontal-padding * 2; // 8 top sites\n// For the breakpoints, we need to add space for the scrollbar to avoid weird\n// layout issues when the scrollbar is visible. 16px is wide enough to cover all\n// OSes and keeps it simpler than a per-OS value.\n$scrollbar-width: 16px;\n$break-point-small: $wrapper-max-width-small + $base-gutter * 2 + $scrollbar-width;\n$break-point-medium: $wrapper-max-width-medium + $base-gutter * 2 + $scrollbar-width;\n$break-point-large: $wrapper-max-width-large + $base-gutter * 2 + $scrollbar-width;\n$break-point-widest: $wrapper-max-width-widest + $base-gutter * 2 + $scrollbar-width;\n\n$section-title-font-size: 13px;\n\n$card-width: $grid-unit * 2 + $base-gutter;\n$card-height: 266px;\n$card-preview-image-height: 122px;\n$card-title-margin: 2px;\n$card-text-line-height: 19px;\n// Larger cards for wider screens:\n$card-width-large: 309px;\n$card-height-large: 370px;\n$card-preview-image-height-large: 155px;\n\n$topic-margin-top: 12px;\n\n$context-menu-button-size: 27px;\n$context-menu-button-boxshadow: 0 2px $grey-90-10;\n$context-menu-border-color: $black-20;\n$context-menu-shadow: 0 5px 10px $black-30, 0 0 0 1px $context-menu-border-color;\n$context-menu-font-size: 14px;\n$context-menu-border-radius: 5px;\n$context-menu-outer-padding: 5px;\n$context-menu-item-padding: 3px 12px;\n\n$error-fallback-font-size: 12px;\n$error-fallback-line-height: 1.5;\n\n$inner-box-shadow: 0 0 0 1px $black-10;\n\n$image-path: '../data/content/assets/';\n\n$snippets-container-height: 120px;\n\n@mixin fade-in {\n  box-shadow: inset $inner-box-shadow, $shadow-primary;\n  transition: box-shadow 150ms;\n}\n\n@mixin fade-in-card {\n  box-shadow: $shadow-primary;\n  transition: box-shadow 150ms;\n}\n\n@mixin context-menu-button {\n  .context-menu-button {\n    background-clip: padding-box;\n    background-color: $white;\n    background-image: url('chrome://browser/skin/page-action.svg');\n    background-position: 55%;\n    border: $border-primary;\n    border-radius: 100%;\n    box-shadow: $context-menu-button-boxshadow;\n    cursor: pointer;\n    fill: $fill-primary;\n    height: $context-menu-button-size;\n    offset-inline-end: -($context-menu-button-size / 2);\n    opacity: 0;\n    position: absolute;\n    top: -($context-menu-button-size / 2);\n    transform: scale(0.25);\n    transition-duration: 200ms;\n    transition-property: transform, opacity;\n    width: $context-menu-button-size;\n\n    &:-moz-any(:active, :focus) {\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n}\n\n@mixin context-menu-button-hover {\n  .context-menu-button {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@mixin context-menu-open-middle {\n  .context-menu {\n    margin-inline-end: auto;\n    margin-inline-start: auto;\n    offset-inline-end: auto;\n    offset-inline-start: -$base-gutter;\n  }\n}\n\n@mixin context-menu-open-left {\n  .context-menu {\n    margin-inline-end: 5px;\n    margin-inline-start: auto;\n    offset-inline-end: 0;\n    offset-inline-start: auto;\n  }\n}\n\n@mixin flip-icon {\n  &:dir(rtl) {\n    transform: scaleX(-1);\n  }\n}\n",
+		"// Photon colors from http://design.firefox.com/photon/visuals/color.html\n$blue-50: #0A84FF;\n$blue-60: #0060DF;\n$grey-10: #F9F9FA;\n$grey-20: #EDEDF0;\n$grey-30: #D7D7DB;\n$grey-40: #B1B1B3;\n$grey-50: #737373;\n$grey-60: #4A4A4F;\n$grey-90: #0C0C0D;\n$teal-70: #008EA4;\n$red-60: #D70022;\n\n// Photon opacity from http://design.firefox.com/photon/visuals/color.html#opacity\n$grey-90-10: rgba($grey-90, 0.1);\n$grey-90-20: rgba($grey-90, 0.2);\n$grey-90-30: rgba($grey-90, 0.3);\n$grey-90-40: rgba($grey-90, 0.4);\n$grey-90-50: rgba($grey-90, 0.5);\n$grey-90-60: rgba($grey-90, 0.6);\n$grey-90-70: rgba($grey-90, 0.7);\n$grey-90-80: rgba($grey-90, 0.8);\n$grey-90-90: rgba($grey-90, 0.9);\n\n$grey-20-60: rgba($grey-20, 0.6);\n\n$black: #000;\n$black-5: rgba($black, 0.05);\n$black-10: rgba($black, 0.1);\n$black-15: rgba($black, 0.15);\n$black-20: rgba($black, 0.2);\n$black-25: rgba($black, 0.25);\n$black-30: rgba($black, 0.3);\n\n// Photon transitions from http://design.firefox.com/photon/motion/duration-and-easing.html\n$photon-easing: cubic-bezier(0.07, 0.95, 0, 1);\n\n// Aliases and derived styles based on Photon colors for common usage\n$background-primary: $grey-10;\n$background-secondary: $grey-20;\n$border-primary: 1px solid $grey-40;\n$border-secondary: 1px solid $grey-30;\n$fill-primary: $grey-90-80;\n$fill-secondary: $grey-90-60;\n$fill-tertiary: $grey-30;\n$input-primary: $blue-60;\n$input-secondary: $grey-10;\n$link-primary: $blue-60;\n$link-secondary: $teal-70;\n$shadow-primary: 0 0 0 5px $grey-30;\n$shadow-secondary: 0 1px 4px 0 $grey-90-10;\n$text-primary: $grey-90;\n$text-conditional: $grey-60;\n$text-secondary: $grey-50;\n$input-border: solid 1px $grey-90-20;\n$input-border-active: solid 1px $blue-50;\n$input-error-border: solid 1px $red-60;\n$input-error-boxshadow: 0 0 0 2px rgba($red-60, 0.35);\n$input-focus-boxshadow: 0 0 0 2px rgba($blue-50, 0.35);\n\n$white: #FFF;\n$border-radius: 3px;\n\n$base-gutter: 32px;\n$section-horizontal-padding: 25px;\n$section-vertical-padding: 10px;\n$section-spacing: 40px - $section-vertical-padding * 2;\n$grid-unit: 96px; // 1 top site\n\n$icon-size: 16px;\n$smaller-icon-size: 12px;\n$larger-icon-size: 32px;\n\n$wrapper-default-width: $grid-unit * 2 + $base-gutter * 1 + $section-horizontal-padding * 2; // 2 top sites\n$wrapper-max-width-small: $grid-unit * 3 + $base-gutter * 2 + $section-horizontal-padding * 2; // 3 top sites\n$wrapper-max-width-medium: $grid-unit * 4 + $base-gutter * 3 + $section-horizontal-padding * 2; // 4 top sites\n$wrapper-max-width-large: $grid-unit * 6 + $base-gutter * 5 + $section-horizontal-padding * 2; // 6 top sites\n$wrapper-max-width-widest: $grid-unit * 8 + $base-gutter * 7 + $section-horizontal-padding * 2; // 8 top sites\n// For the breakpoints, we need to add space for the scrollbar to avoid weird\n// layout issues when the scrollbar is visible. 16px is wide enough to cover all\n// OSes and keeps it simpler than a per-OS value.\n$scrollbar-width: 16px;\n$break-point-small: $wrapper-max-width-small + $base-gutter * 2 + $scrollbar-width;\n$break-point-medium: $wrapper-max-width-medium + $base-gutter * 2 + $scrollbar-width;\n$break-point-large: $wrapper-max-width-large + $base-gutter * 2 + $scrollbar-width;\n$break-point-widest: $wrapper-max-width-widest + $base-gutter * 2 + $scrollbar-width;\n\n$section-title-font-size: 13px;\n\n$card-width: $grid-unit * 2 + $base-gutter;\n$card-height: 266px;\n$card-preview-image-height: 122px;\n$card-title-margin: 2px;\n$card-text-line-height: 19px;\n// Larger cards for wider screens:\n$card-width-large: 309px;\n$card-height-large: 370px;\n$card-preview-image-height-large: 155px;\n\n$topic-margin-top: 12px;\n\n$context-menu-button-size: 27px;\n$context-menu-button-boxshadow: 0 2px $grey-90-10;\n$context-menu-border-color: $black-20;\n$context-menu-shadow: 0 5px 10px $black-30, 0 0 0 1px $context-menu-border-color;\n$context-menu-font-size: 14px;\n$context-menu-border-radius: 5px;\n$context-menu-outer-padding: 5px;\n$context-menu-item-padding: 3px 12px;\n\n$error-fallback-font-size: 12px;\n$error-fallback-line-height: 1.5;\n\n$inner-box-shadow: 0 0 0 1px $black-10;\n\n$image-path: '../data/content/assets/';\n\n$snippets-container-height: 120px;\n\n@mixin fade-in {\n  box-shadow: inset $inner-box-shadow, $shadow-primary;\n  transition: box-shadow 150ms;\n}\n\n@mixin fade-in-card {\n  box-shadow: $shadow-primary;\n  transition: box-shadow 150ms;\n}\n\n@mixin context-menu-button {\n  .context-menu-button {\n    background-clip: padding-box;\n    background-color: $white;\n    background-image: url('chrome://browser/skin/page-action.svg');\n    background-position: 55%;\n    border: $border-primary;\n    border-radius: 100%;\n    box-shadow: $context-menu-button-boxshadow;\n    cursor: pointer;\n    fill: $fill-primary;\n    height: $context-menu-button-size;\n    offset-inline-end: -($context-menu-button-size / 2);\n    opacity: 0;\n    position: absolute;\n    top: -($context-menu-button-size / 2);\n    transform: scale(0.25);\n    transition-duration: 200ms;\n    transition-property: transform, opacity;\n    width: $context-menu-button-size;\n\n    &:-moz-any(:active, :focus) {\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n}\n\n@mixin context-menu-button-hover {\n  .context-menu-button {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@mixin context-menu-open-middle {\n  .context-menu {\n    margin-inline-end: auto;\n    margin-inline-start: auto;\n    offset-inline-end: auto;\n    offset-inline-start: -$base-gutter;\n  }\n}\n\n@mixin context-menu-open-left {\n  .context-menu {\n    margin-inline-end: 5px;\n    margin-inline-start: auto;\n    offset-inline-end: 0;\n    offset-inline-start: auto;\n  }\n}\n\n@mixin flip-icon {\n  &:dir(rtl) {\n    transform: scaleX(-1);\n  }\n}\n",
 		".icon {\n  background-position: center center;\n  background-repeat: no-repeat;\n  background-size: $icon-size;\n  -moz-context-properties: fill;\n  display: inline-block;\n  fill: $fill-primary;\n  height: $icon-size;\n  vertical-align: middle;\n  width: $icon-size;\n\n  &.icon-spacer {\n    margin-inline-end: 8px;\n  }\n\n  &.icon-small-spacer {\n    margin-inline-end: 6px;\n  }\n\n  &.icon-bookmark-added {\n    background-image: url('chrome://browser/skin/bookmark.svg');\n  }\n\n  &.icon-bookmark-hollow {\n    background-image: url('chrome://browser/skin/bookmark-hollow.svg');\n  }\n\n  &.icon-clear-input {\n    fill: $fill-secondary;\n    background-image: url('#{$image-path}glyph-cancel-16.svg');\n  }\n\n  &.icon-delete {\n    background-image: url('#{$image-path}glyph-delete-16.svg');\n  }\n\n  &.icon-modal-delete {\n    background-image: url('#{$image-path}glyph-modal-delete-32.svg');\n    background-size: $larger-icon-size;\n    height: $larger-icon-size;\n    width: $larger-icon-size;\n  }\n\n  &.icon-dismiss {\n    background-image: url('#{$image-path}glyph-dismiss-16.svg');\n  }\n\n  &.icon-info {\n    background-image: url('#{$image-path}glyph-info-16.svg');\n  }\n\n  &.icon-import {\n    background-image: url('#{$image-path}glyph-import-16.svg');\n  }\n\n  &.icon-new-window {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-newWindow-16.svg');\n  }\n\n  &.icon-new-window-private {\n    background-image: url('chrome://browser/skin/privateBrowsing.svg');\n  }\n\n  &.icon-settings {\n    background-image: url('chrome://browser/skin/settings.svg');\n  }\n\n  &.icon-pin {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-pin-16.svg');\n  }\n\n  &.icon-unpin {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-unpin-16.svg');\n  }\n\n  &.icon-edit {\n    background-image: url('#{$image-path}glyph-edit-16.svg');\n  }\n\n  &.icon-pocket {\n    background-image: url('#{$image-path}glyph-pocket-16.svg');\n  }\n\n  &.icon-pocket-small {\n    background-image: url('#{$image-path}glyph-pocket-16.svg');\n    background-size: $smaller-icon-size;\n  }\n\n  &.icon-historyItem { // sass-lint:disable-line class-name-format\n    background-image: url('#{$image-path}glyph-historyItem-16.svg');\n  }\n\n  &.icon-trending {\n    background-image: url('#{$image-path}glyph-trending-16.svg');\n    transform: translateY(2px); // trending bolt is visually top heavy\n  }\n\n  &.icon-now {\n    background-image: url('chrome://browser/skin/history.svg');\n  }\n\n  &.icon-topsites {\n    background-image: url('#{$image-path}glyph-topsites-16.svg');\n  }\n\n  &.icon-pin-small {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-pin-12.svg');\n    background-size: $smaller-icon-size;\n    height: $smaller-icon-size;\n    width: $smaller-icon-size;\n  }\n\n  &.icon-check {\n    background-image: url('chrome://browser/skin/check.svg');\n  }\n\n  &.icon-webextension {\n    background-image: url('#{$image-path}glyph-webextension-16.svg');\n  }\n\n  &.icon-highlights {\n    background-image: url('#{$image-path}glyph-highlights-16.svg');\n  }\n\n  &.icon-arrowhead-down {\n    background-image: url('#{$image-path}glyph-arrowhead-down-16.svg');\n  }\n\n  &.icon-arrowhead-down-small {\n    background-image: url('#{$image-path}glyph-arrowhead-down-12.svg');\n    background-size: $smaller-icon-size;\n    height: $smaller-icon-size;\n    width: $smaller-icon-size;\n  }\n\n  &.icon-arro