Merge m-c to b2g-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 20 Oct 2015 12:36:00 +0200
changeset 303750 cf88cfee3b8bcb8880ae2845a98b61a705a8f107
parent 303749 660daf08abd88aff95b409a860b97df9bbbfc2d9 (current diff)
parent 303657 f7b746b4e91307448cb0746a41f677bfc23908b0 (diff)
child 303751 77cc0d20b7db7d8c4657d1e8cdd3080ec10a0349
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to b2g-inbound
browser/components/extensions/test/browser/browser_ext_browserAction_icon.js
dom/base/test/test_bug438519.html
dom/tests/mochitest/dom-level2-core/test_documenttypeinternalSubset01.html
dom/tests/mochitest/dom-level2-core/test_internalSubset01.html
testing/web-platform/meta/content-security-policy/blink-contrib/shared-worker-connect-src-allowed.sub.html.ini
testing/web-platform/meta/cors/redirect-userinfo.htm.ini
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1210154 - Update the clang toolchain
+Bug 1215696 - Update mp4parse to v0.1.1
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -252,16 +252,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/SimpleServiceDiscovery.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
   "resource://gre/modules/ReaderMode.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent",
   "resource:///modules/ReaderParent.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
+  "resource://gre/modules/LoginManagerParent.jsm");
+
 var gInitialPages = [
   "about:blank",
   "about:newtab",
   "about:home",
   "about:privatebrowsing",
   "about:welcomeback",
   "about:sessionrestore"
 ];
@@ -1189,16 +1192,20 @@ var gBrowserInit = {
         SessionStore.reviveCrashedTab(tab);
         break;
       case "restoreAll":
         SessionStore.reviveAllCrashedTabs();
         break;
       }
     }, false, true);
 
+    gBrowser.addEventListener("InsecureLoginFormsStateChange", function() {
+      gIdentityHandler.refreshForInsecureLoginForms();
+    });
+
     let uriToLoad = this._getUriToLoad();
     if (uriToLoad && uriToLoad != "about:blank") {
       if (uriToLoad instanceof Ci.nsISupportsArray) {
         let count = uriToLoad.Count();
         let specs = [];
         for (let i = 0; i < count; i++) {
           let urisstring = uriToLoad.GetElementAt(i).QueryInterface(Ci.nsISupportsString);
           specs.push(urisstring.data);
@@ -7051,32 +7058,44 @@ var gIdentityHandler = {
     this._sslStatus = gBrowser.securityUI
                               .QueryInterface(Ci.nsISSLStatusProvider)
                               .SSLStatus;
     if (this._sslStatus) {
       this._sslStatus.QueryInterface(Ci.nsISSLStatus);
     }
 
     // Then, update the user interface with the available data.
-    if (this._identityBox) {
-      this.refreshIdentityBlock();
-    }
+    this.refreshIdentityBlock();
     // Handle a location change while the Control Center is focused
     // by closing the popup (bug 1207542)
     if (shouldHidePopup) {
       this._identityPopup.hidePopup();
     }
 
     // NOTE: We do NOT update the identity popup (the control center) when
     // we receive a new security state on the existing page (i.e. from a
     // subframe). If the user opened the popup and looks at the provided
     // information we don't want to suddenly change the panel contents.
   },
 
   /**
+   * This is called asynchronously when requested by the Logins module, after
+   * the insecure login forms state for the page has been updated.
+   */
+  refreshForInsecureLoginForms() {
+    // Check this._uri because we don't want to refresh the user interface if
+    // this is called before the first page load in the window for any reason.
+    if (!this._uri) {
+      Cu.reportError("Unexpected early call to refreshForInsecureLoginForms.");
+      return;
+    }
+    this.refreshIdentityBlock();
+  },
+
+  /**
    * Attempt to provide proper IDN treatment for host names
    */
   getEffectiveHost: function() {
     if (!this._IDNService)
       this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
                          .getService(Ci.nsIIDNService);
     try {
       return this._IDNService.convertToDisplayIDN(this._uri.host, {});
@@ -7102,16 +7121,20 @@ var gIdentityHandler = {
     }
     return "unknownIdentity";
   },
 
   /**
    * Updates the identity block user interface with the data from this object.
    */
   refreshIdentityBlock() {
+    if (!this._identityBox) {
+      return;
+    }
+
     let icon_label = "";
     let tooltip = "";
     let icon_country_label = "";
     let icon_labels_dir = "ltr";
 
     if (this._isSecureInternalUI) {
       this._identityBox.className = "chromeUI";
       let brandBundle = document.getElementById("bundle_brand");
@@ -7170,16 +7193,21 @@ var gIdentityHandler = {
         } else if (this._isMixedActiveContentBlocked) {
           this._identityBox.classList.add("mixedDisplayContentLoadedActiveBlocked");
         } else if (this._isMixedPassiveContentLoaded) {
           this._identityBox.classList.add("mixedDisplayContent");
         } else {
           this._identityBox.classList.add("weakCipher");
         }
       }
+      if (LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser)) {
+        // Insecure login forms can only be present on "unknown identity"
+        // pages, either already insecure or with mixed active content loaded.
+        this._identityBox.classList.add("insecureLoginForms");
+      }
       tooltip = gNavigatorBundle.getString("identity.unknown.tooltip");
     }
 
     // Push the appropriate strings out to the UI
     this._identityBox.tooltipText = tooltip;
     this._identityIconLabel.value = icon_label;
     this._identityIconCountryLabel.value = icon_country_label;
     // Set cropping and direction
@@ -7207,16 +7235,22 @@ var gIdentityHandler = {
     } else if (this._isURILoadedFromFile) {
       connection = "file";
     } else if (this._isEV) {
       connection = "secure-ev";
     } else if (this._isSecure) {
       connection = "secure";
     }
 
+    // Determine if there are insecure login forms.
+    let loginforms = "secure";
+    if (LoginManagerParent.hasInsecureLoginForms(gBrowser.selectedBrowser)) {
+      loginforms = "insecure";
+    }
+
     // Determine the mixed content state.
     let mixedcontent = [];
     if (this._isMixedPassiveContentLoaded) {
       mixedcontent.push("passive-loaded");
     }
     if (this._isMixedActiveContentLoaded) {
       mixedcontent.push("active-loaded");
     } else if (this._isMixedActiveContentBlocked) {
@@ -7244,16 +7278,17 @@ var gIdentityHandler = {
       } else {
         elem.removeAttribute(attr);
       }
     }
 
     for (let id of elementIDs) {
       let element = document.getElementById(id);
       updateAttribute(element, "connection", connection);
+      updateAttribute(element, "loginforms", loginforms);
       updateAttribute(element, "ciphers", ciphers);
       updateAttribute(element, "mixedcontent", mixedcontent);
       updateAttribute(element, "isbroken", this._isBroken);
     }
 
     // Initialize the optional strings to empty values
     let supplemental = "";
     let verifier = "";
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -263,17 +263,17 @@ tags = mcb
 [browser_bug832435.js]
 [browser_bug839103.js]
 [browser_bug880101.js]
 [browser_bug882977.js]
 [browser_bug902156.js]
 tags = mcb
 [browser_bug906190.js]
 tags = mcb
-skip-if = buildapp == "mulet" || e10s # Bug 1093642 - test manipulates content and relies on content focus
+skip-if = buildapp == "mulet" || e10s || os == "linux" # Bug 1093642 - test manipulates content and relies on content focus, Bug 1212520 - Re-enable on Linux
 [browser_mixedContentFromOnunload.js]
 tags = mcb
 [browser_mixedContentFramesOnHttp.js]
 tags = mcb
 [browser_bug970746.js]
 [browser_bug1015721.js]
 skip-if = os == 'win' || e10s # Bug 1159268 - Need a content-process safe version of synthesizeWheel
 [browser_bug1064280_changeUrlInPinnedTab.js]
@@ -317,16 +317,17 @@ support-files = fxa_profile_handler.sjs
 [browser_fxa_web_channel.js]
 [browser_gestureSupport.js]
 skip-if = e10s # Bug 863514 - no gesture support.
 [browser_getshortcutoruri.js]
 [browser_hide_removing.js]
 [browser_homeDrop.js]
 skip-if = buildapp == 'mulet'
 [browser_identity_UI.js]
+[browser_insecureLoginForms.js]
 [browser_keywordBookmarklets.js]
 skip-if = e10s # Bug 1102025 - different principals for the bookmarklet only in e10s mode (unclear if test or 'real' issue)
 [browser_keywordSearch.js]
 [browser_keywordSearch_postData.js]
 [browser_lastAccessedTab.js]
 skip-if = toolkit == "windows" # Disabled on Windows due to frequent failures (bug 969405)
 [browser_locationBarCommand.js]
 skip-if = os == "linux" # Linux: Intermittent failures, bug 917535
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_insecureLoginForms.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Load directly from the browser-chrome support files of login tests.
+const testUrlPath =
+      "://example.com/browser/toolkit/components/passwordmgr/test/browser/";
+
+/**
+ * Waits for the given number of occurrences of InsecureLoginFormsStateChange
+ * on the given browser element.
+ */
+function waitForInsecureLoginFormsStateChange(browser, count) {
+  return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
+                                       false, () => --count == 0);
+}
+
+/**
+ * Checks the insecure login forms logic for the identity block.
+ */
+add_task(function* test_simple() {
+  for (let scheme of ["http", "https"]) {
+    let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html");
+    let browser = tab.linkedBrowser;
+    yield Promise.all([
+      BrowserTestUtils.switchTab(gBrowser, tab),
+      BrowserTestUtils.browserLoaded(browser),
+      // One event is triggered by pageshow and one by DOMFormHasPassword.
+      waitForInsecureLoginFormsStateChange(browser, 2),
+    ]);
+
+    let { gIdentityHandler } = gBrowser.ownerGlobal;
+    gIdentityHandler._identityBox.click();
+    document.getElementById("identity-popup-security-expander").click();
+
+    if (scheme == "http") {
+      let identityBoxImage = gBrowser.ownerGlobal
+            .getComputedStyle(document.getElementById("page-proxy-favicon"), "")
+            .getPropertyValue("list-style-image");
+      let securityViewBG = gBrowser.ownerGlobal
+            .getComputedStyle(document.getElementById("identity-popup-securityView"), "")
+            .getPropertyValue("background-image");
+      let securityContentBG = gBrowser.ownerGlobal
+            .getComputedStyle(document.getElementById("identity-popup-security-content"), "")
+            .getPropertyValue("background-image");
+      is(identityBoxImage,
+         "url(\"chrome://browser/skin/identity-mixed-active-loaded.svg\")",
+         "Using expected icon image in the identity block");
+      is(securityViewBG,
+         "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+         "Using expected icon image in the Control Center main view");
+      is(securityContentBG,
+         "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+         "Using expected icon image in the Control Center subview");
+    }
+
+    // Messages should be visible when the scheme is HTTP, and invisible when
+    // the scheme is HTTPS.
+    is(Array.every(document.querySelectorAll("[when-loginforms=insecure]"),
+                   element => !is_hidden(element)),
+       scheme == "http",
+       "The relevant messages should visible or hidden.");
+
+    gIdentityHandler._identityPopup.hidden = true;
+    gBrowser.removeTab(tab);
+  }
+});
+
+/**
+ * Checks that the insecure login forms logic does not regress mixed content
+ * blocking messages when mixed active content is loaded.
+ */
+add_task(function* test_mixedcontent() {
+  yield new Promise(resolve => SpecialPowers.pushPrefEnv({
+    "set": [["security.mixed_content.block_active_content", false]],
+  }, resolve));
+
+  // Load the page with the subframe in a new tab.
+  let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html");
+  let browser = tab.linkedBrowser;
+  yield Promise.all([
+    BrowserTestUtils.switchTab(gBrowser, tab),
+    BrowserTestUtils.browserLoaded(browser),
+    // Two events are triggered by pageshow and one by DOMFormHasPassword.
+    waitForInsecureLoginFormsStateChange(browser, 3),
+  ]);
+
+  assertMixedContentBlockingState(browser, { activeLoaded: true,
+                                             activeBlocked: false,
+                                             passiveLoaded: false });
+
+  gBrowser.removeTab(tab);
+});
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -885,16 +885,23 @@ function assertMixedContentBlockingState
       // There is a case here with weak ciphers, but no bc tests are handling this yet.
       is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/conn-degraded.svg\")",
         "CC using degraded icon");
       is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/conn-degraded.svg\")",
         "CC using degraded icon");
     }
   }
 
+  if (activeLoaded || activeBlocked || passiveLoaded) {
+    doc.getElementById("identity-popup-security-expander").click();
+    is(Array.filter(doc.querySelectorAll("[observes=identity-popup-mcb-learn-more]"),
+                    element => !is_hidden(element)).length, 1,
+       "The 'Learn more' link should be visible once.");
+  }
+
   gIdentityHandler._identityPopup.hidden = true;
 }
 
 function makeActionURI(action, params) {
   let url = "moz-action:" + action + "," + JSON.stringify(params);
   return NetUtil.newURI(url);
 }
 
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -36,16 +36,17 @@
           <vbox id="identity-popup-security-descriptions">
             <description class="identity-popup-warning-gray"
                          when-mixedcontent="active-blocked">&identity.activeBlocked;</description>
             <description class="identity-popup-warning-yellow"
                          when-mixedcontent="passive-loaded">&identity.passiveLoaded;</description>
             <description when-mixedcontent="active-loaded">&identity.activeLoaded;</description>
             <description class="identity-popup-warning-yellow"
                          when-ciphers="weak">&identity.weakEncryption;</description>
+            <description when-loginforms="insecure">&identity.insecureLoginForms;</description>
           </vbox>
         </vbox>
         <button id="identity-popup-security-expander"
                 class="identity-popup-expander"
                 when-connection="not-secure secure secure-ev"
                 oncommand="gIdentityHandler.toggleSubView('security', this)"/>
       </hbox>
 
@@ -111,17 +112,21 @@
                      when-connection="secure-ev"
                      class="header"/>
         <description id="identity-popup-content-supplemental"
                      when-connection="secure-ev"/>
         <description id="identity-popup-content-verifier"
                      when-connection="secure secure-ev"/>
 
         <!-- Connection is Not Secure -->
-        <description when-connection="not-secure">&identity.description.insecure;</description>
+        <description when-connection="not-secure"
+                     and-when-loginforms="secure">&identity.description.insecure;</description>
+
+        <!-- Insecure login forms -->
+        <description when-loginforms="insecure">&identity.description.insecureLoginForms;</description>
 
         <!-- Weak Cipher -->
         <description when-ciphers="weak">&identity.description.weakCipher;</description>
         <description class="identity-popup-warning-yellow"
                      when-ciphers="weak">&identity.description.weakCipher2;</description>
 
         <!-- Active Mixed Content Blocked -->
         <description class="identity-popup-warning-gray"
@@ -133,18 +138,24 @@
                      when-mixedcontent="passive-loaded">&identity.description.passiveLoaded2; <label observes="identity-popup-mcb-learn-more"/></description>
 
         <!-- Passive Mixed Content Loaded, Active Mixed Content Blocked -->
         <description when-mixedcontent="passive-loaded active-blocked">&identity.description.passiveLoaded;</description>
         <description when-mixedcontent="passive-loaded active-blocked"
                      class="identity-popup-warning-yellow">&identity.description.passiveLoaded3; <label observes="identity-popup-mcb-learn-more"/></description>
 
         <!-- Active Mixed Content Blocking Disabled -->
-        <description when-mixedcontent="active-loaded">&identity.description.activeLoaded;</description>
-        <description when-mixedcontent="active-loaded">&identity.description.activeLoaded2; <label observes="identity-popup-mcb-learn-more"/></description>
+        <description when-mixedcontent="active-loaded"
+                     and-when-loginforms="secure">&identity.description.activeLoaded;</description>
+        <description when-mixedcontent="active-loaded"
+                     and-when-loginforms="secure">&identity.description.activeLoaded2; <label observes="identity-popup-mcb-learn-more"/></description>
+        <!-- Show only the first message when there are insecure login forms,
+             and make sure the Learn More link is included. -->
+        <description when-mixedcontent="active-loaded"
+                     and-when-loginforms="insecure">&identity.description.activeLoaded; <label observes="identity-popup-mcb-learn-more"/></description>
 
         <!-- Buttons to enable/disable mixed content blocking. -->
         <button when-mixedcontent="active-blocked"
                 label="&identity.disableMixedContentBlocking.label;"
                 accesskey="&identity.disableMixedContentBlocking.accesskey;"
                 oncommand="gIdentityHandler.disableMixedContentProtection()"/>
         <button when-mixedcontent="active-loaded"
                 label="&identity.enableMixedContentBlocking.label;"
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -1,329 +1,204 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 
 Cu.import("resource://gre/modules/devtools/shared/event-emitter.js");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
   DefaultWeakMap,
   ignoreEvent,
   runSafe,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> BrowserAction]
 var browserActionMap = new WeakMap();
 
-// WeakMap[Extension -> docshell]
-// This map is a cache of the windowless browser that's used to render ImageData
-// for the browser_action icon.
-var imageRendererMap = new WeakMap();
-
 function browserActionOf(extension)
 {
   return browserActionMap.get(extension);
 }
 
-function makeWidgetId(id)
-{
-  id = id.toLowerCase();
-  return id.replace(/[^a-z0-9_-]/g, "_");
-}
-
 var nextActionId = 0;
 
 // Responsible for the browser_action section of the manifest as well
 // as the associated popup.
 function BrowserAction(options, extension)
 {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-browser-action";
   this.widget = null;
 
-  this.title = new DefaultWeakMap(extension.localize(options.default_title));
-  this.badgeText = new DefaultWeakMap();
-  this.badgeBackgroundColor = new DefaultWeakMap();
-  this.icon = new DefaultWeakMap(options.default_icon);
-  this.popup = new DefaultWeakMap(options.default_popup);
+  let title = extension.localize(options.default_title || "");
+  let popup = extension.localize(options.default_popup || "");
+  if (popup) {
+    popup = extension.baseURI.resolve(popup);
+  }
 
-  this.context = null;
+  this.defaults = {
+    title: title,
+    badgeText: "",
+    badgeBackgroundColor: null,
+    icon: IconDetails.normalize({ path: options.default_icon }, extension,
+                                null, true),
+    popup: popup,
+  };
+
+  this.tabContext = new TabContext(tab => Object.create(this.defaults),
+                                   extension);
+
+  EventEmitter.decorate(this);
 }
 
 BrowserAction.prototype = {
   build() {
     let widget = CustomizableUI.createWidget({
       id: this.id,
       type: "custom",
       removable: true,
       defaultArea: CustomizableUI.AREA_NAVBAR,
       onBuild: document => {
         let node = document.createElement("toolbarbutton");
         node.id = this.id;
         node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional badged-button");
         node.setAttribute("constrain-size", "true");
 
-        this.updateTab(null, node);
+        this.updateButton(node, this.defaults);
 
         let tabbrowser = document.defaultView.gBrowser;
-        tabbrowser.tabContainer.addEventListener("TabSelect", this);
 
         node.addEventListener("command", event => {
           let tab = tabbrowser.selectedTab;
           let popup = this.getProperty(tab, "popup");
           if (popup) {
             this.togglePopup(node, popup);
           } else {
             this.emit("click");
           }
         });
 
         return node;
       },
     });
-    this.widget = widget;
-  },
 
-  handleEvent(event) {
-    if (event.type == "TabSelect") {
-      let window = event.target.ownerDocument.defaultView;
-      let tabbrowser = window.gBrowser;
-      let instance = CustomizableUI.getWidget(this.id).forWindow(window);
-      if (instance) {
-        this.updateTab(tabbrowser.selectedTab, instance.node);
-      }
-    }
+    this.tabContext.on("tab-select",
+                       (evt, tab) => { this.updateWindow(tab.ownerDocument.defaultView); })
+
+    this.widget = widget;
   },
 
   togglePopup(node, popupResource) {
-    let popupURL = this.extension.baseURI.resolve(popupResource);
-
-    let document = node.ownerDocument;
-    let panel = document.createElement("panel");
-    panel.setAttribute("class", "browser-action-panel");
-    panel.setAttribute("type", "arrow");
-    panel.setAttribute("flip", "slide");
-    node.appendChild(panel);
-
-    panel.addEventListener("popuphidden", () => {
-      this.context.unload();
-      this.context = null;
-      panel.remove();
-    });
-
-    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    let browser = document.createElementNS(XUL_NS, "browser");
-    browser.setAttribute("type", "content");
-    browser.setAttribute("disableglobalhistory", "true");
-    panel.appendChild(browser);
-
-    let loadListener = () => {
-      panel.removeEventListener("load", loadListener);
-
-      this.context = new ExtensionPage(this.extension, {
-        type: "popup",
-        contentWindow: browser.contentWindow,
-        uri: Services.io.newURI(popupURL, null, null),
-        docShell: browser.docShell,
-      });
-      GlobalManager.injectInDocShell(browser.docShell, this.extension, this.context);
-      browser.setAttribute("src", popupURL);
-
-      let contentLoadListener = () => {
-        browser.removeEventListener("load", contentLoadListener);
-
-        let contentViewer = browser.docShell.contentViewer;
-        let width = {}, height = {};
-        try {
-          contentViewer.getContentSize(width, height);
-          [width, height] = [width.value, height.value];
-        } catch (e) {
-          // getContentSize can throw
-          [width, height] = [400, 400];
-        }
-
-        let window = document.defaultView;
-        width /= window.devicePixelRatio;
-        height /= window.devicePixelRatio;
-        width = Math.min(width, 800);
-        height = Math.min(height, 800);
-
-        browser.setAttribute("width", width);
-        browser.setAttribute("height", height);
-
-        let anchor = document.getAnonymousElementByAttribute(node, "class", "toolbarbutton-icon");
-        panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
-      };
-      browser.addEventListener("load", contentLoadListener, true);
-    };
-    panel.addEventListener("load", loadListener);
+    openPanel(node, popupResource, this.extension);
   },
 
-  // Initialize the toolbar icon and popup given that |tab| is the
-  // current tab and |node| is the CustomizableUI node. Note: |tab|
-  // will be null if we don't know the current tab yet (during
-  // initialization).
-  updateTab(tab, node) {
-    let window = node.ownerDocument.defaultView;
-
-    let title = this.getProperty(tab, "title");
-    if (title) {
-      node.setAttribute("tooltiptext", title);
-      node.setAttribute("label", title);
+  // Update the toolbar button |node| with the tab context data
+  // in |tabData|.
+  updateButton(node, tabData) {
+    if (tabData.title) {
+      node.setAttribute("tooltiptext", tabData.title);
+      node.setAttribute("label", tabData.title);
+      node.setAttribute("aria-label", tabData.title);
     } else {
       node.removeAttribute("tooltiptext");
       node.removeAttribute("label");
+      node.removeAttribute("aria-label");
     }
 
-    let badgeText = this.badgeText.get(tab);
-    if (badgeText) {
-      node.setAttribute("badge", badgeText);
+    if (tabData.badgeText) {
+      node.setAttribute("badge", tabData.badgeText);
     } else {
       node.removeAttribute("badge");
     }
 
-    function toHex(n) {
-      return Math.floor(n / 16).toString(16) + (n % 16).toString(16);
-    }
-
     let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
                                         'class', 'toolbarbutton-badge');
     if (badgeNode) {
-      let color = this.badgeBackgroundColor.get(tab);
+      let color = tabData.badgeBackgroundColor;
       if (Array.isArray(color)) {
         color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
       }
-      badgeNode.style.backgroundColor = color;
+      badgeNode.style.backgroundColor = color || "";
     }
 
-    let iconURL = this.getIcon(tab, node);
+    let iconURL = IconDetails.getURL(
+      tabData.icon, node.ownerDocument.defaultView, this.extension);
     node.setAttribute("image", iconURL);
   },
 
-  // Note: tab is allowed to be null here.
-  getIcon(tab, node) {
-    let icon = this.icon.get(tab);
-
-    let url;
-    if (typeof(icon) != "object") {
-      url = icon;
-    } else {
-      let window = node.ownerDocument.defaultView;
-      let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                        .getInterface(Components.interfaces.nsIDOMWindowUtils);
-      let res = {value: 1}
-      utils.getResolution(res);
-
-      let size = res.value == 1 ? 19 : 38;
-      url = icon[size];
-    }
-
-    if (url) {
-      return this.extension.baseURI.resolve(url);
-    } else {
-      return "chrome://browser/content/extension.svg";
-    }
-  },
-
   // Update the toolbar button for a given window.
   updateWindow(window) {
-    let tab = window.gBrowser ? window.gBrowser.selectedTab : null;
-    let node = CustomizableUI.getWidget(this.id).forWindow(window).node;
-    this.updateTab(tab, node);
+    let widget = this.widget.forWindow(window);
+    if (widget) {
+      let tab = window.gBrowser.selectedTab;
+      this.updateButton(widget.node, this.tabContext.get(tab));
+    }
   },
 
   // Update the toolbar button when the extension changes the icon,
   // title, badge, etc. If it only changes a parameter for a single
   // tab, |tab| will be that tab. Otherwise it will be null.
   updateOnChange(tab) {
     if (tab) {
       if (tab.selected) {
         this.updateWindow(tab.ownerDocument.defaultView);
       }
     } else {
-      let e = Services.wm.getEnumerator("navigator:browser");
-      while (e.hasMoreElements()) {
-        let window = e.getNext();
-        if (window.gBrowser) {
-          this.updateWindow(window);
-        }
+      for (let window of WindowListManager.browserWindows()) {
+        this.updateWindow(window);
       }
     }
   },
 
   // tab is allowed to be null.
   // prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
   setProperty(tab, prop, value) {
-    this[prop].set(tab, value);
+    if (tab == null) {
+      this.defaults[prop] = value;
+    } else {
+      this.tabContext.get(tab)[prop] = value;
+    }
+
     this.updateOnChange(tab);
   },
 
   // tab is allowed to be null.
   // prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
   getProperty(tab, prop) {
-    return this[prop].get(tab);
+    if (tab == null) {
+      return this.defaults[prop];
+    } else {
+      return this.tabContext.get(tab)[prop];
+    }
   },
 
   shutdown() {
-    let widget = CustomizableUI.getWidget(this.id);
-    for (let instance of widget.instances) {
-      let window = instance.node.ownerDocument.defaultView;
-      let tabbrowser = window.gBrowser;
-      tabbrowser.tabContainer.removeEventListener("TabSelect", this);
-    }
-
+    this.tabContext.shutdown();
     CustomizableUI.destroyWidget(this.id);
   },
 };
 
-EventEmitter.decorate(BrowserAction.prototype);
-
 extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
   let browserAction = new BrowserAction(manifest.browser_action, extension);
   browserAction.build();
   browserActionMap.set(extension, browserAction);
 });
 
 extensions.on("shutdown", (type, extension) => {
   if (browserActionMap.has(extension)) {
     browserActionMap.get(extension).shutdown();
     browserActionMap.delete(extension);
   }
-  imageRendererMap.delete(extension);
 });
 
-function convertImageDataToPNG(extension, imageData)
-{
-  let webNav = imageRendererMap.get(extension);
-  if (!webNav) {
-    webNav = Services.appShell.createWindowlessBrowser(false);
-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(extension.baseURI,
-                                                                           {addonId: extension.id});
-    let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
-    let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
-
-    GlobalManager.injectInDocShell(docShell, extension, null);
-
-    docShell.createAboutBlankContentViewer(principal);
-  }
-
-  let document = webNav.document;
-  let canvas = document.createElement("canvas");
-  canvas.width = imageData.width;
-  canvas.height = imageData.height;
-  canvas.getContext("2d").putImageData(imageData, 0, 0);
-
-  let url = canvas.toDataURL("image/png");
-
-  canvas.remove();
-
-  return url;
-}
-
 extensions.registerAPI((extension, context) => {
   return {
     browserAction: {
       onClicked: new EventManager(context, "browserAction.onClicked", fire => {
         let listener = () => {
           let tab = TabManager.activeTab;
           fire(TabManager.convert(extension, tab));
         };
@@ -341,38 +216,40 @@ extensions.registerAPI((extension, conte
       getTitle: function(details, callback) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
         let title = browserActionOf(extension).getProperty(tab, "title");
         runSafe(context, callback, title);
       },
 
       setIcon: function(details, callback) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
-        if (details.imageData) {
-          let url = convertImageDataToPNG(extension, details.imageData);
-          browserActionOf(extension).setProperty(tab, "icon", url);
-        } else {
-          browserActionOf(extension).setProperty(tab, "icon", details.path);
-        }
+        let icon = IconDetails.normalize(details, extension, context);
+        browserActionOf(extension).setProperty(tab, "icon", icon);
       },
 
       setBadgeText: function(details) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
         browserActionOf(extension).setProperty(tab, "badgeText", details.text);
       },
 
       getBadgeText: function(details, callback) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
         let text = browserActionOf(extension).getProperty(tab, "badgeText");
         runSafe(context, callback, text);
       },
 
       setPopup: function(details) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
-        browserActionOf(extension).setProperty(tab, "popup", details.popup);
+        // Note: Chrome resolves arguments to setIcon relative to the calling
+        // context, but resolves arguments to setPopup relative to the extension
+        // root.
+        // For internal consistency, we currently resolve both relative to the
+        // calling context.
+        let url = details.popup && context.uri.resolve(details.popup);
+        browserActionOf(extension).setProperty(tab, "popup", url);
       },
 
       getPopup: function(details, callback) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
         let popup = browserActionOf(extension).getProperty(tab, "popup");
         runSafe(context, callback, popup);
       },
 
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-pageAction.js
@@ -0,0 +1,246 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+  EventManager,
+  DefaultWeakMap,
+  ignoreEvent,
+  runSafe,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> PageAction]
+var pageActionMap = new WeakMap();
+
+
+// Handles URL bar icons, including the |page_action| manifest entry
+// and associated API.
+function PageAction(options, extension)
+{
+  this.extension = extension;
+  this.id = makeWidgetId(extension.id) + "-page-action";
+
+  let title = extension.localize(options.default_title || "");
+  let popup = extension.localize(options.default_popup || "");
+  if (popup) {
+    popup = extension.baseURI.resolve(popup);
+  }
+
+  this.defaults = {
+    show: false,
+    title: title,
+    icon: IconDetails.normalize({ path: options.default_icon }, extension,
+                                null, true),
+    popup: popup && extension.baseURI.resolve(popup),
+  };
+
+  this.tabContext = new TabContext(tab => Object.create(this.defaults),
+                                   extension);
+
+  this.tabContext.on("location-change", this.handleLocationChange.bind(this));
+
+  // WeakMap[ChromeWindow -> <xul:image>]
+  this.buttons = new WeakMap();
+
+  EventEmitter.decorate(this);
+}
+
+PageAction.prototype = {
+  // Returns the value of the property |prop| for the given tab, where
+  // |prop| is one of "show", "title", "icon", "popup".
+  getProperty(tab, prop) {
+    return this.tabContext.get(tab)[prop];
+  },
+
+  // Sets the value of the property |prop| for the given tab to the
+  // given value, symmetrically to |getProperty|.
+  //
+  // If |tab| is currently selected, updates the page action button to
+  // reflect the new value.
+  setProperty(tab, prop, value) {
+    this.tabContext.get(tab)[prop] = value;
+    if (tab.selected) {
+      this.updateButton(tab.ownerDocument.defaultView);
+    }
+  },
+
+  // Updates the page action button in the given window to reflect the
+  // properties of the currently selected tab:
+  //
+  // Updates "tooltiptext" and "aria-label" to match "title" property.
+  // Updates "image" to match the "icon" property.
+  // Shows or hides the icon, based on the "show" property.
+  updateButton(window) {
+    let tabData = this.tabContext.get(window.gBrowser.selectedTab);
+
+    if (!(tabData.show || this.buttons.has(window))) {
+      // Don't bother creating a button for a window until it actually
+      // needs to be shown.
+      return;
+    }
+
+    let button = this.getButton(window);
+
+    if (tabData.show) {
+      // Update the title and icon only if the button is visible.
+
+      if (tabData.title) {
+        button.setAttribute("tooltiptext", tabData.title);
+        button.setAttribute("aria-label", tabData.title);
+      } else {
+        button.removeAttribute("tooltiptext");
+        button.removeAttribute("aria-label");
+      }
+
+      let icon = IconDetails.getURL(tabData.icon, window, this.extension);
+      button.setAttribute("src", icon);
+    }
+
+    button.hidden = !tabData.show;
+  },
+
+  // Create an |image| node and add it to the |urlbar-icons|
+  // container in the given window.
+  addButton(window) {
+    let document = window.document;
+
+    let button = document.createElement("image");
+    button.id = this.id;
+    button.setAttribute("class", "urlbar-icon");
+
+    button.addEventListener("click", event => {
+      if (event.button == 0) {
+        this.handleClick(window);
+      }
+    });
+
+    document.getElementById("urlbar-icons").appendChild(button);
+
+    return button;
+  },
+
+  // Returns the page action button for the given window, creating it if
+  // it doesn't already exist.
+  getButton(window) {
+    if (!this.buttons.has(window)) {
+      let button = this.addButton(window);
+      this.buttons.set(window, button);
+    }
+
+    return this.buttons.get(window);
+  },
+
+  // Handles a click event on the page action button for the given
+  // window.
+  // If the page action has a |popup| property, a panel is opened to
+  // that URL. Otherwise, a "click" event is emitted, and dispatched to
+  // the any click listeners in the add-on.
+  handleClick(window) {
+    let tab = window.gBrowser.selectedTab;
+    let popup = this.tabContext.get(tab).popup;
+
+    if (popup) {
+      openPanel(this.getButton(window), popup, this.extension);
+    } else {
+      this.emit("click", tab);
+    }
+  },
+
+  handleLocationChange(eventType, tab, fromBrowse) {
+    if (fromBrowse) {
+      this.tabContext.clear(tab);
+    }
+    this.updateButton(tab.ownerDocument.defaultView);
+  },
+
+  shutdown() {
+    this.tabContext.shutdown();
+
+    for (let window of WindowListManager.browserWindows()) {
+      if (this.buttons.has(window)) {
+        this.buttons.get(window).remove();
+      }
+    }
+  },
+};
+
+PageAction.for = extension => {
+  return pageActionMap.get(extension);
+};
+
+
+extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
+  let pageAction = new PageAction(manifest.page_action, extension);
+  pageActionMap.set(extension, pageAction);
+});
+
+extensions.on("shutdown", (type, extension) => {
+  if (pageActionMap.has(extension)) {
+    pageActionMap.get(extension).shutdown();
+    pageActionMap.delete(extension);
+  }
+});
+
+
+extensions.registerAPI((extension, context) => {
+  return {
+    pageAction: {
+      onClicked: new EventManager(context, "pageAction.onClicked", fire => {
+        let listener = (evt, tab) => {
+          fire(TabManager.convert(extension, tab));
+        };
+        let pageAction = PageAction.for(extension);
+
+        pageAction.on("click", listener);
+        return () => {
+          pageAction.off("click", listener);
+        };
+      }).api(),
+
+      show(tabId) {
+        let tab = TabManager.getTab(tabId);
+        PageAction.for(extension).setProperty(tab, "show", true);
+      },
+
+      hide(tabId) {
+        let tab = TabManager.getTab(tabId);
+        PageAction.for(extension).setProperty(tab, "show", false);
+      },
+
+      setTitle(details) {
+        let tab = TabManager.getTab(details.tabId);
+        PageAction.for(extension).setProperty(tab, "title", details.title);
+      },
+
+      getTitle(details, callback) {
+        let tab = TabManager.getTab(details.tabId);
+        let title = PageAction.for(extension).getProperty(tab, "title");
+        runSafe(context, callback, title);
+      },
+
+      setIcon(details, callback) {
+        let tab = TabManager.getTab(details.tabId);
+        let icon = IconDetails.normalize(details, extension, context);
+        PageAction.for(extension).setProperty(tab, "icon", icon);
+      },
+
+      setPopup(details) {
+        let tab = TabManager.getTab(details.tabId);
+        // Note: Chrome resolves arguments to setIcon relative to the calling
+        // context, but resolves arguments to setPopup relative to the extension
+        // root.
+        // For internal consistency, we currently resolve both relative to the
+        // calling context.
+        let url = details.popup && context.uri.resolve(details.popup);
+        PageAction.for(extension).setProperty(tab, "popup", url);
+      },
+
+      getPopup(details, callback) {
+        let tab = TabManager.getTab(details.tabId);
+        let popup = PageAction.for(extension).getProperty(tab, "popup");
+        runSafe(context, callback, popup);
+      },
+    }
+  };
+});
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -1,20 +1,257 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
 } = ExtensionUtils;
 
 // This file provides some useful code for the |tabs| and |windows|
 // modules. All of the code is installed on |global|, which is a scope
 // shared among the different ext-*.js scripts.
 
+
+// Manages icon details for toolbar buttons in the |pageAction| and
+// |browserAction| APIs.
+global.IconDetails = {
+  // Accepted icon sizes.
+  SIZES: ["19", "38"],
+
+  // Normalizes the various acceptable input formats into an object
+  // with two properties, "19" and "38", containing icon URLs.
+  normalize(details, extension, context=null, localize=false) {
+    let result = {};
+
+    if (details.imageData) {
+      let imageData = details.imageData;
+
+      if (imageData instanceof Cu.getGlobalForObject(imageData).ImageData) {
+        imageData = {"19": imageData};
+      }
+
+      for (let size of this.SIZES) {
+        if (size in imageData) {
+          result[size] = this.convertImageDataToPNG(imageData[size], context);
+        }
+      }
+    }
+
+    if (details.path) {
+      let path = details.path;
+      if (typeof path != "object") {
+        path = {"19": path};
+      }
+
+      let baseURI = context ? context.uri : extension.baseURI;
+
+      for (let size of this.SIZES) {
+        if (size in path) {
+          let url = path[size];
+          if (localize) {
+            url = extension.localize(url);
+          }
+
+          url = baseURI.resolve(path[size]);
+
+          // The Chrome documentation specifies these parameters as
+          // relative paths. We currently accept absolute URLs as well,
+          // which means we need to check that the extension is allowed
+          // to load them.
+          try {
+            Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
+              extension.principal, url,
+              Services.scriptSecurityManager.DISALLOW_SCRIPT);
+          } catch (e if !context) {
+            // If there's no context, it's because we're handling this
+            // as a manifest directive. Log a warning rather than
+            // raising an error, but don't accept the URL in any case.
+            extension.manifestError(`Access to URL '${url}' denied`);
+            continue;
+          }
+
+          result[size] = url;
+        }
+      }
+    }
+
+    return result;
+  },
+
+  // Returns the appropriate icon URL for the given icons object and the
+  // screen resolution of the given window.
+  getURL(icons, window, extension) {
+    const DEFAULT = "chrome://browser/content/extension.svg";
+
+    // Use the higher resolution image if we're doing any up-scaling
+    // for high resolution monitors.
+    let res = window.devicePixelRatio;
+    let size = res > 1 ? "38" : "19";
+
+    return icons[size] || icons["19"] || icons["38"] || DEFAULT;
+  },
+
+  convertImageDataToPNG(imageData, context) {
+    let document = context.contentWindow.document;
+    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    canvas.width = imageData.width;
+    canvas.height = imageData.height;
+    canvas.getContext("2d").putImageData(imageData, 0, 0);
+
+    return canvas.toDataURL("image/png");
+  }
+};
+
+global.makeWidgetId = id => {
+  id = id.toLowerCase();
+  // FIXME: This allows for collisions.
+  return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+// Open a panel anchored to the given node, containing a browser opened
+// to the given URL, owned by the given extension. If |popupURL| is not
+// an absolute URL, it is resolved relative to the given extension's
+// base URL.
+global.openPanel = (node, popupURL, extension) => {
+  let document = node.ownerDocument;
+
+  let popupURI = Services.io.newURI(popupURL, null, extension.baseURI);
+
+  Services.scriptSecurityManager.checkLoadURIWithPrincipal(
+    extension.principal, popupURI,
+    Services.scriptSecurityManager.DISALLOW_SCRIPT);
+
+  let panel = document.createElement("panel");
+  panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
+  panel.setAttribute("class", "browser-extension-panel");
+  panel.setAttribute("type", "arrow");
+  panel.setAttribute("flip", "slide");
+
+  let anchor;
+  if (node.localName == "toolbarbutton") {
+    // Toolbar buttons are a special case. The panel becomes a child of
+    // the button, and is anchored to the button's icon.
+    node.appendChild(panel);
+    anchor = document.getAnonymousElementByAttribute(node, "class", "toolbarbutton-icon");
+  } else {
+    // In all other cases, the panel is anchored to the target node
+    // itself, and is a child of a popupset node.
+    document.getElementById("mainPopupSet").appendChild(panel);
+    anchor = node;
+  }
+
+  let context;
+  panel.addEventListener("popuphidden", () => {
+    context.unload();
+    panel.remove();
+  });
+
+  const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+  let browser = document.createElementNS(XUL_NS, "browser");
+  browser.setAttribute("type", "content");
+  browser.setAttribute("disableglobalhistory", "true");
+  panel.appendChild(browser);
+
+  let loadListener = () => {
+    panel.removeEventListener("load", loadListener);
+
+    context = new ExtensionPage(extension, {
+      type: "popup",
+      contentWindow: browser.contentWindow,
+      uri: popupURI,
+      docShell: browser.docShell,
+    });
+    GlobalManager.injectInDocShell(browser.docShell, extension, context);
+    browser.setAttribute("src", context.uri.spec);
+
+    let contentLoadListener = () => {
+      browser.removeEventListener("load", contentLoadListener, true);
+
+      let contentViewer = browser.docShell.contentViewer;
+      let width = {}, height = {};
+      try {
+        contentViewer.getContentSize(width, height);
+        [width, height] = [width.value, height.value];
+      } catch (e) {
+        // getContentSize can throw
+        [width, height] = [400, 400];
+      }
+
+      let window = document.defaultView;
+      width /= window.devicePixelRatio;
+      height /= window.devicePixelRatio;
+      width = Math.min(width, 800);
+      height = Math.min(height, 800);
+
+      browser.setAttribute("width", width);
+      browser.setAttribute("height", height);
+
+      panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
+    };
+    browser.addEventListener("load", contentLoadListener, true);
+  };
+  panel.addEventListener("load", loadListener);
+
+  return panel;
+}
+
+// Manages tab-specific context data, and dispatching tab select events
+// across all windows.
+global.TabContext = function TabContext(getDefaults, extension) {
+  this.extension = extension;
+  this.getDefaults = getDefaults;
+
+  this.tabData = new WeakMap();
+
+  AllWindowEvents.addListener("progress", this);
+  AllWindowEvents.addListener("TabSelect", this);
+
+  EventEmitter.decorate(this);
+}
+
+TabContext.prototype = {
+  get(tab) {
+    if (!this.tabData.has(tab)) {
+      this.tabData.set(tab, this.getDefaults(tab));
+    }
+
+    return this.tabData.get(tab);
+  },
+
+  clear(tab) {
+    this.tabData.delete(tab);
+  },
+
+  handleEvent(event) {
+    if (event.type == "TabSelect") {
+      let tab = event.target;
+      this.emit("tab-select", tab);
+      this.emit("location-change", tab);
+    }
+  },
+
+  onLocationChange(browser, webProgress, request, locationURI, flags) {
+    let gBrowser = browser.ownerDocument.defaultView.gBrowser;
+    if (browser === gBrowser.selectedBrowser) {
+      let tab = gBrowser.getTabForBrowser(browser);
+      this.emit("location-change", tab, true);
+    }
+  },
+
+  shutdown() {
+    AllWindowEvents.removeListener("progress", this);
+    AllWindowEvents.removeListener("TabSelect", this);
+  },
+};
+
 // Manages mapping between XUL tabs and extension tab IDs.
 global.TabManager = {
   _tabs: new WeakMap(),
   _nextId: 1,
 
   getId(tab) {
     if (this._tabs.has(tab)) {
       return this._tabs.get(tab);
@@ -34,19 +271,17 @@ global.TabManager = {
         return this.getId(tab);
       }
     }
     return -1;
   },
 
   getTab(tabId) {
     // FIXME: Speed this up without leaking memory somehow.
-    let e = Services.wm.getEnumerator("navigator:browser");
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
+    for (let window of WindowListManager.browserWindows()) {
       if (!window.gBrowser) {
         continue;
       }
       for (let tab of window.gBrowser.tabs) {
         if (this.getId(tab) == tabId) {
           return tab;
         }
       }
@@ -127,19 +362,17 @@ global.WindowManager = {
       return this._windows.get(window);
     }
     let id = this._nextId++;
     this._windows.set(window, id);
     return id;
   },
 
   getWindow(id) {
-    let e = Services.wm.getEnumerator("navigator:browser");
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
+    for (let window of WindowListManager.browserWindows(true)) {
       if (this.getId(window) == id) {
         return window;
       }
     }
     return null;
   },
 
   convert(extension, window, getInfo) {
@@ -167,25 +400,35 @@ global.WindowManager = {
 
 // Manages listeners for window opening and closing. A window is
 // considered open when the "load" event fires on it. A window is
 // closed when a "domwindowclosed" notification fires for it.
 global.WindowListManager = {
   _openListeners: new Set(),
   _closeListeners: new Set(),
 
+  // Returns an iterator for all browser windows. Unless |includeIncomplete| is
+  // true, only fully-loaded windows are returned.
+  *browserWindows(includeIncomplete = false) {
+    let e = Services.wm.getEnumerator("navigator:browser");
+    while (e.hasMoreElements()) {
+      let window = e.getNext();
+      if (includeIncomplete || window.document.readyState == "complete") {
+        yield window;
+      }
+    }
+  },
+
   addOpenListener(listener, fireOnExisting = true) {
     if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
       Services.ww.registerNotification(this);
     }
     this._openListeners.add(listener);
 
-    let e = Services.wm.getEnumerator("navigator:browser");
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
+    for (let window of this.browserWindows(true)) {
       if (window.document.readyState != "complete") {
         window.addEventListener("load", this);
       } else if (fireOnExisting) {
         listener(window);
       }
     }
   },
 
@@ -258,17 +501,21 @@ global.AllWindowEvents = {
 
     if (!this._listeners.has(type)) {
       this._listeners.set(type, new Set());
     }
     let list = this._listeners.get(type);
     list.add(listener);
 
     if (needOpenListener) {
-      WindowListManager.addOpenListener(this.openListener);
+      WindowListManager.addOpenListener(this.openListener, false);
+    }
+
+    for (let window of WindowListManager.browserWindows()) {
+      this.addWindowListener(window, type, listener);
     }
   },
 
   removeListener(type, listener) {
     if (type == "domwindowopened") {
       return WindowListManager.removeOpenListener(listener);
     } else if (type == "domwindowclosed") {
       return WindowListManager.removeCloseListener(listener);
@@ -278,36 +525,38 @@ global.AllWindowEvents = {
     listeners.delete(listener);
     if (listeners.length == 0) {
       this._listeners.delete(type);
       if (this._listeners.size == 0) {
         WindowListManager.removeOpenListener(this.openListener);
       }
     }
 
-    let e = Services.wm.getEnumerator("navigator:browser");
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
+    for (let window of WindowListManager.browserWindows()) {
       if (type == "progress") {
         window.gBrowser.removeTabsProgressListener(listener);
       } else {
         window.removeEventListener(type, listener);
       }
     }
   },
 
+  addWindowListener(window, eventType, listener) {
+    if (eventType == "progress") {
+      window.gBrowser.addTabsProgressListener(listener);
+    } else {
+      window.addEventListener(eventType, listener);
+    }
+  },
+
   // Runs whenever the "load" event fires for a new window.
   openListener(window) {
     for (let [eventType, listeners] of AllWindowEvents._listeners) {
       for (let listener of listeners) {
-        if (eventType == "progress") {
-          window.gBrowser.addTabsProgressListener(listener);
-        } else {
-          window.addEventListener(eventType, listener);
-        }
+        this.addWindowListener(window, eventType, listener);
       }
     }
   },
 };
 
 // Subclass of EventManager where we just need to call
 // add/removeEventListener on each XUL window.
 global.WindowEventManager = function(context, name, event, listener)
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -2,11 +2,12 @@
 # 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/.
 
 browser.jar:
     content/browser/extension.svg
     content/browser/ext-utils.js
     content/browser/ext-contextMenus.js
     content/browser/ext-browserAction.js
+    content/browser/ext-pageAction.js
     content/browser/ext-tabs.js
     content/browser/ext-windows.js
     content/browser/ext-bookmarks.js
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -2,17 +2,20 @@
 support-files =
   head.js
   context.html
   ctxmenu-image.png
 
 [browser_ext_simple.js]
 [browser_ext_currentWindow.js]
 [browser_ext_browserAction_simple.js]
-[browser_ext_browserAction_icon.js]
+[browser_ext_browserAction_pageAction_icon.js]
+[browser_ext_browserAction_context.js]
+[browser_ext_pageAction_context.js]
+[browser_ext_pageAction_popup.js]
 [browser_ext_contextMenus.js]
 [browser_ext_getViews.js]
 [browser_ext_tabs_executeScript.js]
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_update.js]
 [browser_ext_tabs_sendMessage.js]
 [browser_ext_windows_update.js]
 [browser_ext_contentscript_connect.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -0,0 +1,244 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* testTabSwitchContext() {
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {
+        "default_icon": "default.png",
+        "default_popup": "default.html",
+        "default_title": "Default Title",
+      },
+      "permissions": ["tabs"],
+    },
+
+    background: function () {
+      var details = [
+        { "icon": browser.runtime.getURL("default.png"),
+          "popup": browser.runtime.getURL("default.html"),
+          "title": "Default Title",
+          "badge": "",
+          "badgeBackgroundColor": null },
+        { "icon": browser.runtime.getURL("1.png"),
+          "popup": browser.runtime.getURL("default.html"),
+          "title": "Default Title",
+          "badge": "",
+          "badgeBackgroundColor": null },
+        { "icon": browser.runtime.getURL("2.png"),
+          "popup": browser.runtime.getURL("2.html"),
+          "title": "Title 2",
+          "badge": "2",
+          "badgeBackgroundColor": [0xff, 0, 0] },
+        { "icon": browser.runtime.getURL("1.png"),
+          "popup": browser.runtime.getURL("default-2.html"),
+          "title": "Default Title 2",
+          "badge": "d2",
+          "badgeBackgroundColor": [0, 0xff, 0] },
+        { "icon": browser.runtime.getURL("default-2.png"),
+          "popup": browser.runtime.getURL("default-2.html"),
+          "title": "Default Title 2",
+          "badge": "d2",
+          "badgeBackgroundColor": [0, 0xff, 0] },
+      ];
+
+      var tabs = [];
+
+      var tests = [
+        expect => {
+          browser.test.log("Initial state, expect default properties.");
+          expect(details[0]);
+        },
+        expect => {
+          browser.test.log("Change the icon in the current tab. Expect default properties excluding the icon.");
+          browser.browserAction.setIcon({ tabId: tabs[0], path: "1.png" });
+          expect(details[1]);
+        },
+        expect => {
+          browser.test.log("Create a new tab. Expect default properties.");
+          browser.tabs.create({ active: true, url: "about:blank?0" }, tab => {
+            tabs.push(tab.id);
+            expect(details[0]);
+          });
+        },
+        expect => {
+          browser.test.log("Change properties. Expect new properties.");
+          var tabId = tabs[1];
+          browser.browserAction.setIcon({ tabId, path: "2.png" });
+          browser.browserAction.setPopup({ tabId, popup: "2.html" });
+          browser.browserAction.setTitle({ tabId, title: "Title 2" });
+          browser.browserAction.setBadgeText({ tabId, text: "2" });
+          browser.browserAction.setBadgeBackgroundColor({ tabId, color: [0xff, 0, 0] });
+
+          expect(details[2]);
+        },
+        expect => {
+          browser.test.log("Navigate to a new page. Expect no changes.");
+
+          // TODO: This listener should not be necessary, but the |tabs.update|
+          // callback currently fires too early in e10s windows.
+          browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+            if (tabId == tabs[1] && changed.url) {
+              browser.tabs.onUpdated.removeListener(listener);
+              expect(details[2]);
+            }
+          });
+
+          browser.tabs.update(tabs[1], { url: "about:blank?1" });
+        },
+        expect => {
+          browser.test.log("Switch back to the first tab. Expect previously set properties.");
+          browser.tabs.update(tabs[0], { active: true }, () => {
+            expect(details[1]);
+          });
+        },
+        expect => {
+          browser.test.log("Change default values, expect those changes reflected.");
+          browser.browserAction.setIcon({ path: "default-2.png" });
+          browser.browserAction.setPopup({ popup: "default-2.html" });
+          browser.browserAction.setTitle({ title: "Default Title 2" });
+          browser.browserAction.setBadgeText({ text: "d2" });
+          browser.browserAction.setBadgeBackgroundColor({ color: [0, 0xff, 0] });
+          expect(details[3]);
+        },
+        expect => {
+          browser.test.log("Switch back to tab 2. Expect former value, unaffected by changes to defaults in previous step.");
+          browser.tabs.update(tabs[1], { active: true }, () => {
+            expect(details[2]);
+          });
+        },
+        expect => {
+          browser.test.log("Delete tab, switch back to tab 1. Expect previous results again.");
+          browser.tabs.remove(tabs[1], () => {
+            expect(details[3]);
+          });
+        },
+        expect => {
+          browser.test.log("Create a new tab. Expect new default properties.");
+          browser.tabs.create({ active: true, url: "about:blank?2" }, tab => {
+            tabs.push(tab.id);
+            expect(details[4]);
+          });
+        },
+        expect => {
+          browser.test.log("Delete tab.");
+          browser.tabs.remove(tabs[2], () => {
+            expect(details[3]);
+          });
+        },
+      ];
+
+      // Gets the current details of the browser action, and returns a
+      // promise that resolves to an object containing them.
+      function getDetails() {
+        return new Promise(resolve => {
+          return browser.tabs.query({ active: true, currentWindow: true }, resolve);
+        }).then(tabs => {
+          var tabId = tabs[0].id;
+
+          return Promise.all([
+            new Promise(resolve => browser.browserAction.getTitle({tabId}, resolve)),
+            new Promise(resolve => browser.browserAction.getPopup({tabId}, resolve)),
+            new Promise(resolve => browser.browserAction.getBadgeText({tabId}, resolve)),
+            new Promise(resolve => browser.browserAction.getBadgeBackgroundColor({tabId}, resolve))])
+        }).then(details => {
+          return Promise.resolve({ title: details[0],
+                                   popup: details[1],
+                                   badge: details[2],
+                                   badgeBackgroundColor: details[3] });
+        });
+      }
+
+
+      // Runs the next test in the `tests` array, checks the results,
+      // and passes control back to the outer test scope.
+      function nextTest() {
+        var test = tests.shift();
+
+        test(expecting => {
+          // Check that the API returns the expected values, and then
+          // run the next test.
+          getDetails().then(details => {
+            browser.test.assertEq(expecting.title, details.title,
+                                  "expected value from getTitle");
+
+            browser.test.assertEq(expecting.popup, details.popup,
+                                  "expected value from getPopup");
+
+            browser.test.assertEq(expecting.badge, details.badge,
+                                  "expected value from getBadge");
+
+            browser.test.assertEq(String(expecting.badgeBackgroundColor),
+                                  String(details.badgeBackgroundColor),
+                                  "expected value from getBadgeBackgroundColor");
+
+            // Check that the actual icon has the expected values, then
+            // run the next test.
+            browser.test.sendMessage("nextTest", expecting, tests.length);
+          });
+        });
+      }
+
+      browser.test.onMessage.addListener((msg) => {
+        if (msg != "runNextTest") {
+          browser.test.fail("Expecting 'runNextTest' message");
+        }
+
+        nextTest();
+      });
+
+      browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
+        tabs[0] = resultTabs[0].id;
+
+        nextTest();
+      });
+    },
+  });
+
+  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
+
+  function checkDetails(details) {
+    let button = document.getElementById(browserActionId);
+
+    ok(button, "button exists");
+
+    is(button.getAttribute("image"), details.icon, "icon URL is correct");
+    is(button.getAttribute("tooltiptext"), details.title, "image title is correct");
+    is(button.getAttribute("label"), details.title, "image label is correct");
+    is(button.getAttribute("aria-label"), details.title, "image aria-label is correct");
+    is(button.getAttribute("badge"), details.badge, "badge text is correct");
+
+    if (details.badge && details.badgeBackgroundColor) {
+      let badge = button.ownerDocument.getAnonymousElementByAttribute(
+        button, 'class', 'toolbarbutton-badge');
+
+      let badgeColor = window.getComputedStyle(badge).backgroundColor;
+      let color = details.badgeBackgroundColor;
+      let expectedColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`
+
+      is(badgeColor, expectedColor, "badge color is correct");
+    }
+
+
+    // TODO: Popup URL.
+  }
+
+  let awaitFinish = new Promise(resolve => {
+    extension.onMessage("nextTest", (expecting, testsRemaining) => {
+      checkDetails(expecting);
+
+      if (testsRemaining) {
+        extension.sendMessage("runNextTest")
+      } else {
+        resolve();
+      }
+    });
+  });
+
+  yield extension.startup();
+
+  yield awaitFinish;
+
+  yield extension.unload();
+});
rename from browser/components/extensions/test/browser/browser_ext_browserAction_icon.js
rename to browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -1,40 +1,345 @@
-add_task(function* () {
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Test that various combinations of icon details specs, for both paths
+// and ImageData objects, result in the correct image being displayed in
+// all display resolutions.
+add_task(function* testDetailsObjects() {
+  function background() {
+    function getImageData(color) {
+      var canvas = document.createElement("canvas");
+      canvas.width = 2;
+      canvas.height = 2;
+      var canvasContext = canvas.getContext("2d");
+
+      canvasContext.clearRect(0, 0, canvas.width, canvas.height);
+      canvasContext.fillStyle = color;
+      canvasContext.fillRect(0, 0, 1, 1);
+
+      return {
+        url: canvas.toDataURL("image/png"),
+        imageData: canvasContext.getImageData(0, 0, canvas.width, canvas.height),
+      };
+    }
+
+    var imageData = {
+      red: getImageData("red"),
+      green: getImageData("green"),
+    };
+
+    var iconDetails = [
+      // Only paths.
+      { details: { "path": "a.png" },
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": browser.runtime.getURL("data/a.png"), } },
+      { details: { "path": "/a.png" },
+        resolutions: {
+          "1": browser.runtime.getURL("a.png"),
+          "2": browser.runtime.getURL("a.png"), } },
+      { details: { "path": { "19": "a.png" } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": browser.runtime.getURL("data/a.png"), } },
+      { details: { "path": { "38": "a.png" } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": browser.runtime.getURL("data/a.png"), } },
+      { details: { "path": { "19": "a.png", "38": "a-x2.png" } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": browser.runtime.getURL("data/a-x2.png"), } },
+
+      // Only ImageData objects.
+      { details: { "imageData": imageData.red.imageData },
+        resolutions: {
+          "1": imageData.red.url,
+          "2": imageData.red.url, } },
+      { details: { "imageData": { "19": imageData.red.imageData } },
+        resolutions: {
+          "1": imageData.red.url,
+          "2": imageData.red.url, } },
+      { details: { "imageData": { "38": imageData.red.imageData } },
+        resolutions: {
+          "1": imageData.red.url,
+          "2": imageData.red.url, } },
+      { details: { "imageData": {
+          "19": imageData.red.imageData,
+          "38": imageData.green.imageData } },
+        resolutions: {
+          "1": imageData.red.url,
+          "2": imageData.green.url, } },
+
+      // Mixed path and imageData objects.
+      //
+      // The behavior is currently undefined if both |path| and
+      // |imageData| specify icons of the same size.
+      { details: {
+          "path": { "19": "a.png" },
+          "imageData": { "38": imageData.red.imageData } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": imageData.red.url, } },
+      { details: {
+          "path": { "38": "a.png" },
+          "imageData": { "19": imageData.red.imageData } },
+        resolutions: {
+          "1": imageData.red.url,
+          "2": browser.runtime.getURL("data/a.png"), } },
+
+      // A path or ImageData object by itself is treated as a 19px icon.
+      { details: {
+          "path": "a.png",
+          "imageData": { "38": imageData.red.imageData } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": imageData.red.url, } },
+      { details: {
+          "path": { "38": "a.png" },
+          "imageData": imageData.red.imageData, },
+        resolutions: {
+          "1": imageData.red.url,
+          "2": browser.runtime.getURL("data/a.png"), } },
+    ];
+
+    // Allow serializing ImageData objects for logging.
+    ImageData.prototype.toJSON = () => "<ImageData>";
+
+    var tabId;
+
+    browser.test.onMessage.addListener((msg, test) => {
+      if (msg != "setIcon") {
+        browser.test.fail("expecting 'setIcon' message");
+      }
+
+      var details = iconDetails[test.index];
+      var expectedURL = details.resolutions[test.resolution];
+
+      var detailString = JSON.stringify(details);
+      browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URL ${expectedURL}`)
+
+      browser.browserAction.setIcon(Object.assign({tabId}, details.details));
+      browser.pageAction.setIcon(Object.assign({tabId}, details.details));
+
+      browser.test.sendMessage("imageURL", expectedURL);
+    });
+
+    // Generate a list of tests and resolutions to send back to the test
+    // context.
+    //
+    // This process is a bit convoluted, because the outer test context needs
+    // to handle checking the button nodes and changing the screen resolution,
+    // but it can't pass us icon definitions with ImageData objects. This
+    // shouldn't be a problem, since structured clones should handle ImageData
+    // objects without issue. Unfortunately, |cloneInto| implements a slightly
+    // different algorithm than we use in web APIs, and does not handle them
+    // correctly.
+    var tests = [];
+    for (var [idx, icon] of iconDetails.entries()) {
+      for (var res of Object.keys(icon.resolutions)) {
+        tests.push({ index: idx, resolution: Number(res) });
+      }
+    }
+
+    // Sort by resolution, so we don't needlessly switch back and forth
+    // between each test.
+    tests.sort(test => test.resolution);
+
+    browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+      tabId = tabs[0].id;
+      browser.pageAction.show(tabId);
+
+      browser.test.sendMessage("ready", tests);
+    });
+  }
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "browser_action": {},
+      "page_action": {},
       "background": {
-        "page": "background.html",
+        "page": "data/background.html",
       }
     },
 
     files: {
-      "background.html": `<canvas id="canvas" width="2" height="2">
-        <script src="background.js"></script>`,
-
-      "background.js": function() {
-        var canvas = document.getElementById("canvas");
-        var canvasContext = canvas.getContext("2d");
-
-        canvasContext.clearRect(0, 0, canvas.width, canvas.height);
-        canvasContext.fillStyle = "green";
-        canvasContext.fillRect(0, 0, 1, 1);
-
-        var url = canvas.toDataURL("image/png");
-        var imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);
-        browser.browserAction.setIcon({imageData});
-
-        browser.test.sendMessage("imageURL", url);
-      }
+      "data/background.html": `<script src="background.js"></script>`,
+      "data/background.js": background,
     },
   });
 
-  let [_, url] = yield Promise.all([extension.startup(), extension.awaitMessage("imageURL")]);
+  const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
+  registerCleanupFunction(() => {
+    SpecialPowers.clearUserPref(RESOLUTION_PREF);
+  });
+
+  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+
+  let [, tests] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
-  let widgetId = makeWidgetId(extension.id) + "-browser-action";
-  let node = CustomizableUI.getWidget(widgetId).forWindow(window).node;
+  for (let test of tests) {
+    SpecialPowers.setCharPref(RESOLUTION_PREF, String(test.resolution));
+    is(window.devicePixelRatio, test.resolution, "window has the required resolution");
+
+    extension.sendMessage("setIcon", test);
 
-  let image = node.getAttribute("image");
-  is(image, url, "image is correct");
+    let imageURL = yield extension.awaitMessage("imageURL");
+
+    let browserActionButton = document.getElementById(browserActionId);
+    is(browserActionButton.getAttribute("image"), imageURL, "browser action has the correct image");
+
+    let pageActionImage = document.getElementById(pageActionId);
+    is(pageActionImage.src, imageURL, "page action has the correct image");
+  }
 
   yield extension.unload();
 });
+
+// Test that default icon details in the manifest.json file are handled
+// correctly.
+add_task(function *testDefaultDetails() {
+  // TODO: Test localized variants.
+  let icons = [
+    "foo/bar.png",
+    "/foo/bar.png",
+    { "19": "foo/bar.png" },
+    { "38": "foo/bar.png" },
+    { "19": "foo/bar.png", "38": "baz/quux.png" },
+  ];
+
+  let expectedURL = new RegExp(String.raw`^moz-extension://[^/]+/foo/bar\.png$`);
+
+  for (let icon of icons) {
+    let extension = ExtensionTestUtils.loadExtension({
+      manifest: {
+        "browser_action": { "default_icon": icon },
+        "page_action": { "default_icon": icon },
+      },
+
+      background: function () {
+        browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+          var tabId = tabs[0].id;
+
+          browser.pageAction.show(tabId);
+          browser.test.sendMessage("ready");
+        });
+      }
+    });
+
+    yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+    let browserActionId = makeWidgetId(extension.id) + "-browser-action";
+    let pageActionId = makeWidgetId(extension.id) + "-page-action";
+
+    let browserActionButton = document.getElementById(browserActionId);
+    let image = browserActionButton.getAttribute("image");
+
+    ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`);
+
+    let pageActionImage = document.getElementById(pageActionId);
+    image = pageActionImage.src;
+
+    ok(expectedURL.test(image), `page action image ${image} matches ${expectedURL}`);
+
+    yield extension.unload();
+
+    let node = document.getElementById(pageActionId);
+    is(node, undefined, "pageAction image removed from document");
+  }
+});
+
+
+// Check that attempts to load a privileged URL as an icon image fail.
+add_task(function* testSecureURLsDenied() {
+
+  // Test URLs passed to setIcon.
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {},
+      "page_action": {},
+    },
+
+    background: function () {
+      browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+        var tabId = tabs[0].id;
+
+        var urls = ["chrome://browser/content/browser.xul",
+                    "javascript:true"];
+
+        for (var url of urls) {
+          for (var api of ["pageAction", "browserAction"]) {
+            try {
+              browser[api].setIcon({tabId, path: url});
+
+              browser.test.fail(`Load of '${url}' succeeded. Expected failure.`);
+              browser.test.notifyFail("setIcon security tests");
+              return;
+            } catch (e) {
+              // We can't actually inspect the error here, since the
+              // error object belongs to the privileged scope of the API,
+              // rather than to the extension scope that calls into it.
+              // Just assume it's the expected security error, for now.
+              browser.test.succeed(`Load of '${url}' failed. Expected failure.`);
+            }
+          }
+        }
+
+        browser.test.notifyPass("setIcon security tests");
+      });
+    },
+  });
+
+  yield extension.startup();
+
+  yield extension.awaitFinish();
+  yield extension.unload();
+
+
+  // Test URLs included in the manifest.
+
+  let urls = ["chrome://browser/content/browser.xul",
+              "javascript:true"];
+
+  let matchURLForbidden = url => ({
+    message: new RegExp(`Loading extension.*Access to.*'${url}' denied`),
+  });
+
+  let messages = [matchURLForbidden(urls[0]),
+                  matchURLForbidden(urls[1]),
+                  matchURLForbidden(urls[0]),
+                  matchURLForbidden(urls[1])];
+
+  let waitForConsole = new Promise(resolve => {
+    // Not necessary in browser-chrome tests, but monitorConsole gripes
+    // if we don't call it.
+    SimpleTest.waitForExplicitFinish();
+
+    SimpleTest.monitorConsole(resolve, messages);
+  });
+
+  extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {
+        "default_icon": {
+          "19": urls[0],
+          "38": urls[1],
+        },
+      },
+      "page_action": {
+        "default_icon": {
+          "19": urls[0],
+          "38": urls[1],
+        },
+      },
+    },
+  });
+
+  yield extension.startup();
+  yield extension.unload();
+
+  SimpleTest.endMonitorConsole();
+  yield waitForConsole;
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -0,0 +1,209 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* testTabSwitchContext() {
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "page_action": {
+        "default_icon": "default.png",
+        "default_popup": "default.html",
+        "default_title": "Default Title",
+      },
+      "permissions": ["tabs"],
+    },
+
+    background: function () {
+      var details = [
+        { "icon": browser.runtime.getURL("default.png"),
+          "popup": browser.runtime.getURL("default.html"),
+          "title": "Default Title" },
+        { "icon": browser.runtime.getURL("1.png"),
+          "popup": browser.runtime.getURL("default.html"),
+          "title": "Default Title" },
+        { "icon": browser.runtime.getURL("2.png"),
+          "popup": browser.runtime.getURL("2.html"),
+          "title": "Title 2" },
+      ];
+
+      var tabs = [];
+
+      var tests = [
+        expect => {
+          browser.test.log("Initial state. No icon visible.");
+          expect(null);
+        },
+        expect => {
+          browser.test.log("Show the icon on the first tab, expect default properties.");
+          browser.pageAction.show(tabs[0]);
+          expect(details[0]);
+        },
+        expect => {
+          browser.test.log("Change the icon. Expect default properties excluding the icon.");
+          browser.pageAction.setIcon({ tabId: tabs[0], path: "1.png" });
+          expect(details[1]);
+        },
+        expect => {
+          browser.test.log("Create a new tab. No icon visible.");
+          browser.tabs.create({ active: true, url: "about:blank?0" }, tab => {
+            tabs.push(tab.id);
+            expect(null);
+          });
+        },
+        expect => {
+          browser.test.log("Change properties. Expect new properties.");
+          var tabId = tabs[1];
+          browser.pageAction.show(tabId);
+          browser.pageAction.setIcon({ tabId, path: "2.png" });
+          browser.pageAction.setPopup({ tabId, popup: "2.html" });
+          browser.pageAction.setTitle({ tabId, title: "Title 2" });
+
+          expect(details[2]);
+        },
+        expect => {
+          browser.test.log("Navigate to a new page. Expect icon hidden.");
+
+          // TODO: This listener should not be necessary, but the |tabs.update|
+          // callback currently fires too early in e10s windows.
+          browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+            if (tabId == tabs[1] && changed.url) {
+              browser.tabs.onUpdated.removeListener(listener);
+              expect(null);
+            }
+          });
+
+          browser.tabs.update(tabs[1], { url: "about:blank?1" });
+        },
+        expect => {
+          browser.test.log("Show the icon. Expect default properties again.");
+          browser.pageAction.show(tabs[1]);
+          expect(details[0]);
+        },
+        expect => {
+          browser.test.log("Switch back to the first tab. Expect previously set properties.");
+          browser.tabs.update(tabs[0], { active: true }, () => {
+            expect(details[1]);
+          });
+        },
+        expect => {
+          browser.test.log("Hide the icon on tab 2. Switch back, expect hidden.");
+          browser.pageAction.hide(tabs[1]);
+          browser.tabs.update(tabs[1], { active: true }, () => {
+            expect(null);
+          });
+        },
+        expect => {
+          browser.test.log("Switch back to tab 1. Expect previous results again.");
+          browser.tabs.remove(tabs[1], () => {
+            expect(details[1]);
+          });
+        },
+        expect => {
+          browser.test.log("Hide the icon. Expect hidden.");
+          browser.pageAction.hide(tabs[0]);
+          expect(null);
+        },
+      ];
+
+      // Gets the current details of the page action, and returns a
+      // promise that resolves to an object containing them.
+      function getDetails() {
+        return new Promise(resolve => {
+          return browser.tabs.query({ active: true, currentWindow: true }, resolve);
+        }).then(tabs => {
+          var tabId = tabs[0].id;
+
+          return Promise.all([
+            new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)),
+            new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))])
+        }).then(details => {
+          return Promise.resolve({ title: details[0],
+                                   popup: details[1] });
+        });
+      }
+
+
+      // Runs the next test in the `tests` array, checks the results,
+      // and passes control back to the outer test scope.
+      function nextTest() {
+        var test = tests.shift();
+
+        test(expecting => {
+          function finish() {
+            // Check that the actual icon has the expected values, then
+            // run the next test.
+            browser.test.sendMessage("nextTest", expecting, tests.length);
+          }
+
+          if (expecting) {
+            // Check that the API returns the expected values, and then
+            // run the next test.
+            getDetails().then(details => {
+              browser.test.assertEq(expecting.title, details.title,
+                                    "expected value from getTitle");
+
+              browser.test.assertEq(expecting.popup, details.popup,
+                                    "expected value from getPopup");
+
+              finish();
+            });
+          } else {
+            finish();
+          }
+        });
+      }
+
+      browser.test.onMessage.addListener((msg) => {
+        if (msg != "runNextTest") {
+          browser.test.fail("Expecting 'runNextTest' message");
+        }
+
+        nextTest();
+      });
+
+      browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
+        tabs[0] = resultTabs[0].id;
+
+        nextTest();
+      });
+    },
+  });
+
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+
+  function checkDetails(details) {
+    let image = document.getElementById(pageActionId);
+    if (details == null) {
+      ok(image == null || image.hidden, "image is hidden");
+    } else {
+      ok(image, "image exists");
+
+      is(image.src, details.icon, "icon URL is correct");
+      is(image.getAttribute("tooltiptext"), details.title, "image title is correct");
+      is(image.getAttribute("aria-label"), details.title, "image aria-label is correct");
+      // TODO: Popup URL.
+    }
+  }
+
+  let awaitFinish = new Promise(resolve => {
+    extension.onMessage("nextTest", (expecting, testsRemaining) => {
+      checkDetails(expecting);
+
+      if (testsRemaining) {
+        extension.sendMessage("runNextTest")
+      } else {
+        resolve();
+      }
+    });
+  });
+
+  yield extension.startup();
+
+  yield awaitFinish;
+
+  yield extension.unload();
+
+  let node = document.getElementById(pageActionId);
+  is(node, undefined, "pageAction image removed from document");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -0,0 +1,215 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function promisePopupShown(popup) {
+  return new Promise(resolve => {
+    if (popup.popupOpen) {
+      resolve();
+    } else {
+      let onPopupShown = event => {
+        popup.removeEventListener("popupshown", onPopupShown);
+        resolve();
+      };
+      popup.addEventListener("popupshown", onPopupShown);
+    }
+  });
+}
+
+add_task(function* testPageActionPopup() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "background": {
+        "page": "data/background.html"
+      },
+      "page_action": {
+        "default_popup": "popup-a.html"
+      }
+    },
+
+    files: {
+      "popup-a.html": `<script src="popup-a.js"></script>`,
+      "popup-a.js": function() {
+        browser.runtime.sendMessage("from-popup-a");
+      },
+
+      "data/popup-b.html": `<script src="popup-b.js"></script>`,
+      "data/popup-b.js": function() {
+        browser.runtime.sendMessage("from-popup-b");
+      },
+
+      "data/background.html": `<script src="background.js"></script>`,
+
+      "data/background.js": function() {
+        var tabId;
+
+        var tests = [
+          () => {
+            sendClick({ expectEvent: false, expectPopup: "a" });
+          },
+          () => {
+            sendClick({ expectEvent: false, expectPopup: "a" });
+          },
+          () => {
+            browser.pageAction.setPopup({ tabId, popup: "popup-b.html" });
+            sendClick({ expectEvent: false, expectPopup: "b" });
+          },
+          () => {
+            sendClick({ expectEvent: false, expectPopup: "b" });
+          },
+          () => {
+            browser.pageAction.setPopup({ tabId, popup: "" });
+            sendClick({ expectEvent: true, expectPopup: null });
+          },
+          () => {
+            sendClick({ expectEvent: true, expectPopup: null });
+          },
+          () => {
+            browser.pageAction.setPopup({ tabId, popup: "/popup-a.html" });
+            sendClick({ expectEvent: false, expectPopup: "a" });
+          },
+        ];
+
+        var expect = {};
+        function sendClick({ expectEvent, expectPopup }) {
+          expect = { event: expectEvent, popup: expectPopup };
+          browser.test.sendMessage("send-click");
+        }
+
+        browser.runtime.onMessage.addListener(msg => {
+          if (expect.popup) {
+            browser.test.assertEq(msg, `from-popup-${expect.popup}`,
+                                  "expected popup opened");
+          } else {
+            browser.test.fail("unexpected popup");
+          }
+
+          expect.popup = null;
+          browser.test.sendMessage("next-test");
+        });
+
+        browser.pageAction.onClicked.addListener(() => {
+          if (expect.event) {
+            browser.test.succeed("expected click event received");
+          } else {
+            browser.test.fail("unexpected click event");
+          }
+
+          expect.event = false;
+          browser.test.sendMessage("next-test");
+        });
+
+        browser.test.onMessage.addListener((msg) => {
+          if (msg != "next-test") {
+            browser.test.fail("Expecting 'next-test' message");
+          }
+
+          if (tests.length) {
+            var test = tests.shift();
+            test();
+          } else {
+            browser.test.notifyPass("pageaction-tests-done");
+          }
+        });
+
+        browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+          tabId = tabs[0].id;
+
+          browser.pageAction.show(tabId);
+          browser.test.sendMessage("next-test");
+        });
+      },
+    },
+  });
+
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  let panelId = makeWidgetId(extension.id) + "-panel";
+
+  extension.onMessage("send-click", () => {
+    let image = document.getElementById(pageActionId);
+
+    let evt = new MouseEvent("click", {});
+    image.dispatchEvent(evt);
+  });
+
+  extension.onMessage("next-test", Task.async(function* () {
+    let panel = document.getElementById(panelId);
+    if (panel) {
+      yield promisePopupShown(panel);
+      panel.hidePopup();
+
+      panel = document.getElementById(panelId);
+      is(panel, undefined, "panel successfully removed from document after hiding");
+    }
+
+    extension.sendMessage("next-test");
+  }));
+
+
+  yield Promise.all([extension.startup(), extension.awaitFinish("pageaction-tests-done")]);
+
+  yield extension.unload();
+
+  let node = document.getElementById(pageActionId);
+  is(node, undefined, "pageAction image removed from document");
+
+  let panel = document.getElementById(panelId);
+  is(panel, undefined, "pageAction panel removed from document");
+});
+
+
+add_task(function* testPageActionSecurity() {
+  const URL = "chrome://browser/content/browser.xul";
+
+  let matchURLForbidden = url => ({
+    message: new RegExp(`Loading extension.*Access to.*'${URL}' denied`),
+  });
+
+  let messages = [/Access to restricted URI denied/,
+                  /Access to restricted URI denied/];
+
+  let waitForConsole = new Promise(resolve => {
+    // Not necessary in browser-chrome tests, but monitorConsole gripes
+    // if we don't call it.
+    SimpleTest.waitForExplicitFinish();
+
+    SimpleTest.monitorConsole(resolve, messages);
+  });
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": { "default_popup": URL },
+      "page_action": { "default_popup": URL },
+    },
+
+    background: function () {
+      browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+        var tabId = tabs[0].id;
+
+        browser.pageAction.show(tabId);
+        browser.test.sendMessage("ready");
+      });
+    },
+  });
+
+  yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+
+  let browserAction = document.getElementById(browserActionId);
+  let evt = new CustomEvent("command", {});
+  browserAction.dispatchEvent(evt);
+
+  let pageAction = document.getElementById(pageActionId);
+  evt = new MouseEvent("click", {});
+  pageAction.dispatchEvent(evt);
+
+  yield extension.unload();
+
+  let node = document.getElementById(pageActionId);
+  is(node, undefined, "pageAction image removed from document");
+
+  SimpleTest.endMonitorConsole();
+  yield waitForConsole;
+});
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -637,16 +637,17 @@ BrowserGlue.prototype = {
     os.addObserver(this, "restart-in-safe-mode", false);
     os.addObserver(this, "flash-plugin-hang", false);
     os.addObserver(this, "xpi-signature-changed", false);
     os.addObserver(this, "autocomplete-did-enter-text", false);
     os.addObserver(this, "tablet-mode-change", false);
 
     ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
+    ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-bookmarks.js");
 
     this._flashHangCount = 0;
   },
 
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -90,30 +90,40 @@ this.ContentWebRTC = {
     }
   }
 };
 
 function handlePCRequest(aSubject, aTopic, aData) {
   let { windowID, innerWindowID, callID, isSecure } = aSubject;
   let contentWindow = Services.wm.getOuterWindowWithId(windowID);
 
+  let mm = getMessageManagerForWindow(contentWindow);
+  if (!mm) {
+    // Workaround for Bug 1207784. To use WebRTC, add-ons right now use
+    // hiddenWindow.mozRTCPeerConnection which is only privileged on OSX. Other
+    // platforms end up here without a message manager.
+    // TODO: Remove once there's a better way (1215591).
+
+    // Skip permission check in the absence of a message manager.
+    Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
+    return;
+  }
+
   if (!contentWindow.pendingPeerConnectionRequests) {
     setupPendingListsInitially(contentWindow);
   }
   contentWindow.pendingPeerConnectionRequests.add(callID);
 
   let request = {
     windowID: windowID,
     innerWindowID: innerWindowID,
     callID: callID,
     documentURI: contentWindow.document.documentURI,
     secure: isSecure,
   };
-
-  let mm = getMessageManagerForWindow(contentWindow);
   mm.sendAsyncMessage("rtcpeer:Request", request);
 }
 
 function handleGUMRequest(aSubject, aTopic, aData) {
   let constraints = aSubject.getConstraints();
   let secure = aSubject.isSecure;
   let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
 
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -932,16 +932,19 @@ toolbar .toolbarbutton-1:-moz-any(@prima
 }
 
 #urlbar-icons {
   -moz-box-align: center;
 }
 
 .urlbar-icon {
   padding: 0 3px;
+  /* 16x16 icon with border-box sizing */
+  width: 22px;
+  height: 16px;
 }
 
 #urlbar-search-footer {
   border-top: 1px solid hsla(210,4%,10%,.14);
   background-color: hsla(210,4%,10%,.07);
 }
 
 #urlbar-search-settings {
@@ -1937,13 +1940,13 @@ toolbarbutton.chevron > .toolbarbutton-i
 
 #context-navigation > .menuitem-iconic > .menu-iconic-left {
   visibility: visible;
   /* override toolkit/themes/linux/global/menu.css */
   -moz-padding-end: 0 !important;
   -moz-margin-end: 0 !important;
 }
 
-.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
+.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
 %include ../shared/usercontext/usercontext.inc.css
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -161,13 +161,13 @@ browser.jar:
   skin/classic/browser/syncQuota.css
   skin/classic/browser/syncProgress-horizontalbar.png
   skin/classic/browser/syncProgress-horizontalbar@2x.png
 #endif
 #ifdef E10S_TESTING_ONLY
   skin/classic/browser/e10s-64@2x.png (../shared/e10s-64@2x.png)
 #endif
 
-../extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/chrome.jar:
+[extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar:
 % override chrome://browser/skin/feeds/audioFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/audioFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/videoFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/videoFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1723,16 +1723,19 @@ toolbar .toolbarbutton-1 > .toolbarbutto
 }
 
 #urlbar-icons {
   -moz-box-align: center;
 }
 
 .urlbar-icon {
   padding: 0 3px;
+  /* 16x16 icon with border-box sizing */
+  width: 22px;
+  height: 16px;
 }
 
 #urlbar-search-footer {
   border-top: 1px solid hsla(210,4%,10%,.14);
   background-color: hsla(210,4%,10%,.07);
 }
 
 #urlbar-search-settings {
@@ -2010,17 +2013,16 @@ richlistitem[type~="action"][actiontype=
 #page-report-button[open="true"] {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
 @media (min-resolution: 2dppx) {
   #page-report-button {
     list-style-image: url("chrome://browser/skin/urlbar-popup-blocked@2x.png");
     -moz-image-region: rect(0, 32px, 32px, 0);
-    width: 22px;
   }
 
   #page-report-button:hover:active,
   #page-report-button[open="true"] {
     -moz-image-region: rect(0, 64px, 32px, 32px);
   }
 }
 
@@ -3612,13 +3614,13 @@ notification[value="loop-sharing-notific
 
 %include ../shared/contextmenu.inc.css
 
 #context-navigation > .menuitem-iconic {
   padding-left: 0;
   padding-right: 0;
 }
 
-.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
+.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
 %include ../shared/usercontext/usercontext.inc.css
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -270,17 +270,17 @@ browser.jar:
   skin/classic/browser/yosemite/sync-horizontalbar.png      (sync-horizontalbar-yosemite.png)
   skin/classic/browser/yosemite/sync-horizontalbar@2x.png   (sync-horizontalbar-yosemite@2x.png)
   skin/classic/browser/yosemite/thumburger.png              (customizableui/thumburger-yosemite.png)
   skin/classic/browser/yosemite/thumburger@2x.png           (customizableui/thumburger-yosemite@2x.png)
 #ifdef E10S_TESTING_ONLY
   skin/classic/browser/e10s-64@2x.png (../shared/e10s-64@2x.png)
 #endif
 
-../extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/chrome.jar:
+[extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar:
 % override chrome://browser/skin/feeds/audioFeedIcon.png                   chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/audioFeedIcon16.png                 chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/videoFeedIcon.png                   chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/videoFeedIcon16.png                 chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/toolbarbutton-dropmarker.png              chrome://browser/skin/lion/toolbarbutton-dropmarker.png                 os=Darwin osversion>=10.7
 % override chrome://browser/skin/tabbrowser/alltabs-box-bkgnd-icon.png     chrome://browser/skin/lion/tabbrowser/alltabs-box-bkgnd-icon.png        os=Darwin osversion>=10.7
 % override chrome://browser/skin/tabview/tabview.png                       chrome://browser/skin/lion/tabview/tabview.png                          os=Darwin osversion>=10.7
 % override chrome://browser/skin/places/toolbar.png                        chrome://browser/skin/lion/places/toolbar.png                           os=Darwin osversion>=10.7
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -1,38 +1,48 @@
 %if 0
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 %endif
 
 /* Hide all conditional elements by default. */
-:-moz-any([when-connection],[when-mixedcontent],[when-ciphers]) {
+:-moz-any([when-connection],[when-mixedcontent],[when-ciphers],[when-loginforms]) {
   display: none;
 }
 
 /* Show the right elements for the right connection states. */
 #identity-popup[connection=not-secure] [when-connection~=not-secure],
 #identity-popup[connection=secure-ev] [when-connection~=secure-ev],
 #identity-popup[connection=secure] [when-connection~=secure],
 #identity-popup[connection=chrome] [when-connection~=chrome],
 #identity-popup[connection=file] [when-connection~=file],
+/* Show insecure login forms messages when needed. */
+#identity-popup[loginforms=insecure] [when-loginforms=insecure],
 /* Show weak cipher messages when needed. */
 #identity-popup[ciphers=weak] [when-ciphers~=weak],
 /* Show mixed content warnings when needed */
 #identity-popup[mixedcontent~=active-loaded] [when-mixedcontent=active-loaded],
 #identity-popup[mixedcontent~=passive-loaded]:not([mixedcontent~=active-loaded]) [when-mixedcontent=passive-loaded],
 #identity-popup[mixedcontent~=active-blocked]:not([mixedcontent~=passive-loaded]) [when-mixedcontent=active-blocked],
 /* Show the right elements when there is mixed passive content loaded and active blocked. */
 #identity-popup[mixedcontent~=active-blocked][mixedcontent~=passive-loaded] [when-mixedcontent~=active-blocked][when-mixedcontent~=passive-loaded],
 /* Show 'disable MCB' button always when there is mixed active content blocked. */
 #identity-popup-securityView-body[mixedcontent~=active-blocked] > button[when-mixedcontent=active-blocked] {
   display: inherit;
 }
 
+/* Hide redundant messages based on insecure login forms presence. */
+#identity-popup[loginforms=secure] [and-when-loginforms=insecure] {
+  display: none;
+}
+#identity-popup[loginforms=insecure] [and-when-loginforms=secure] {
+  display: none;
+}
+
 /* Hide 'not secure' message in subview when weak cipher or mixed content messages are shown. */
 #identity-popup-securityView-body:-moz-any([mixedcontent],[ciphers]) > description[when-connection=not-secure],
 /* Hide 'passive-loaded (only)' message when there is mixed passive content loaded and active blocked. */
 #identity-popup-securityView-body[mixedcontent~=passive-loaded][mixedcontent~=active-blocked] > description[when-mixedcontent=passive-loaded] {
   display: none;
 }
 
 /* Make sure hidden elements don't accidentally become visible from one of the
@@ -219,16 +229,18 @@
 /* Use [isbroken] to make sure we don't show a lock on an http page. See Bug 1192162. */
 #identity-popup[ciphers=weak] #identity-popup-securityView,
 #identity-popup[ciphers=weak] #identity-popup-security-content,
 #identity-popup[mixedcontent~=passive-loaded][isbroken] #identity-popup-securityView,
 #identity-popup[mixedcontent~=passive-loaded][isbroken] #identity-popup-security-content {
   background-image: url(chrome://browser/skin/controlcenter/conn-degraded.svg);
 }
 
+#identity-popup[loginforms=insecure] #identity-popup-securityView,
+#identity-popup[loginforms=insecure] #identity-popup-security-content,
 #identity-popup[mixedcontent~=active-loaded][isbroken] #identity-popup-securityView,
 #identity-popup[mixedcontent~=active-loaded][isbroken] #identity-popup-security-content {
   background-image: url(chrome://browser/skin/controlcenter/mcb-disabled.svg);
 }
 
 #identity-popup-security-descriptions > description {
   margin-top: 6px;
   color: Graytext;
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -118,16 +118,17 @@
   list-style-image: url(chrome://branding/content/identity-icons-brand.svg);
 }
 
 .verifiedDomain > #identity-icons > #page-proxy-favicon[pageproxystate="valid"],
 .verifiedIdentity > #identity-icons > #page-proxy-favicon[pageproxystate="valid"] {
   list-style-image: url(chrome://browser/skin/identity-secure.svg);
 }
 
+.insecureLoginForms > #identity-icons > #page-proxy-favicon[pageproxystate="valid"],
 .mixedActiveContent > #identity-icons > #page-proxy-favicon[pageproxystate="valid"] {
   list-style-image: url(chrome://browser/skin/identity-mixed-active-loaded.svg);
 }
 
 .weakCipher > #identity-icons > #page-proxy-favicon[pageproxystate="valid"],
 .mixedDisplayContent > #identity-icons > #page-proxy-favicon[pageproxystate="valid"],
 .mixedDisplayContentLoadedActiveBlocked > #identity-icons > #page-proxy-favicon[pageproxystate="valid"] {
   list-style-image: url(chrome://browser/skin/identity-mixed-passive-loaded.svg);
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1320,16 +1320,19 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 }
 
 #urlbar-icons {
   -moz-box-align: center;
 }
 
 .urlbar-icon {
   padding: 0 3px;
+  /* 16x16 icon with border-box sizing */
+  width: 22px;
+  height: 16px;
 }
 
 .search-go-container {
   padding: 2px 2px;
 }
 
 #urlbar-search-footer {
   border-top: 1px solid hsla(210,4%,10%,.14);
@@ -2797,13 +2800,13 @@ notification[value="loop-sharing-notific
   margin-top: -4px;
 }
 
 
 @media not all and (-moz-os-version: windows-xp) {
 %include browser-aero.css
 }
 
-.browser-action-panel > .panel-arrowcontainer > .panel-arrowcontent {
+.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
 %include ../shared/usercontext/usercontext.inc.css
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -280,17 +280,17 @@ browser.jar:
   skin/classic/browser/syncProgress-toolbar-inverted@2x.png
   skin/classic/browser/syncProgress-toolbar-XPVista7.png
   skin/classic/browser/syncProgress-toolbar-XPVista7@2x.png
 #endif
 #ifdef E10S_TESTING_ONLY
   skin/classic/browser/e10s-64@2x.png (../shared/e10s-64@2x.png)
 #endif
 
-../extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/chrome.jar:
+[extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar:
 % override chrome://browser/skin/page-livemarks.png                   chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/audioFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/audioFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/videoFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/videoFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
 
 % override chrome://browser/skin/aboutSessionRestore-window-icon.png  chrome://browser/skin/preferences/application.png                 os!=WINNT
 % override chrome://browser/skin/aboutSessionRestore-window-icon.png  chrome://browser/skin/preferences/application.png                 os=WINNT osversion<6
--- a/build/docs/jar-manifests.rst
+++ b/build/docs/jar-manifests.rst
@@ -26,16 +26,23 @@ more complex ``jar.mn`` is at ``toolkit/
 Shipping Chrome Files
 =====================
 
 To ship chrome files in a JAR, an indented line indicates a file to be packaged::
 
    <jarfile>.jar:
      path/in/jar/file_name.xul     (source/tree/location/file_name.xul)
 
+The JAR location may be preceded with a base path between square brackets::
+   [base/path] <jarfile>.jar:
+     path/in/jar/file_name.xul     (source/tree/location/file_name.xul)
+
+In this case, the jar will be directly located under the given ``base/bath``,
+while without a base path, it will be under a ``chrome`` directory.
+
 If the JAR manifest and packaged file live in the same directory, the path and
 parenthesis can be omitted. In other words, the following two lines are
 equivalent::
 
    path/in/jar/same_place.xhtml     (same_place.xhtml)
    path/in/jar/same_place.xhtml
 
 The source tree location may also be an *absolute* path (taken from the
--- a/build/stlport/moz.build
+++ b/build/stlport/moz.build
@@ -65,9 +65,10 @@ if CONFIG['GNU_CXX']:
         '-Wno-type-limits',
         '-Wno-unused-local-typedefs',
     ]
 
 # Force to build a static library, instead of a fake library, without
 # installing it in dist/lib.
 NO_EXPAND_LIBS = True
 
+# We allow warnings for third-party code that can be updated from upstream.
 ALLOW_COMPILER_WARNINGS = True
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -1251,17 +1251,17 @@ ifneq (,$(DIST_SUBDIR))
 ifndef XPI_ROOT_APPID
 $(error XPI_ROOT_APPID is not defined - langpacks will break.)
 endif
 endif
 endif
 
 libs realchrome:: $(FINAL_TARGET)/chrome
 	$(call py_action,jar_maker,\
-	  $(QUIET) -j $(FINAL_TARGET)/chrome \
+	  $(QUIET) -d $(FINAL_TARGET) \
 	  $(MAKE_JARS_FLAGS) $(DEFINES) $(ACDEFINES) $(MOZ_DEBUG_DEFINES) \
 	  $(JAR_MANIFEST))
 
 endif
 
 # This is a temporary check to ensure patches relying on the old behavior
 # of silently picking up jar.mn files continue to work.
 else # No JAR_MANIFEST
--- a/db/sqlite3/src/moz.build
+++ b/db/sqlite3/src/moz.build
@@ -4,16 +4,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 NO_VISIBILITY_FLAGS = True
 
 EXPORTS += [
     'sqlite3.h',
 ]
 
+# We allow warnings for third-party code that can be updated from upstream.
 ALLOW_COMPILER_WARNINGS = True
 
 if CONFIG['MOZ_FOLD_LIBS']:
     # When folding libraries, sqlite is actually in the nss library.
     FINAL_LIBRARY = 'nss'
 else:
     # The final library is in config/external/sqlite
     FINAL_LIBRARY = 'sqlite'
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -475,20 +475,20 @@ Workers.prototype = {
       }
     });
   },
 
   _onWorkerListChanged: function () {
     this._updateWorkerList();
   },
 
-  _onWorkerSelect: function (type, workerActor) {
+  _onWorkerSelect: function (workerActor) {
     DebuggerController.client.attachWorker(workerActor, (response, workerClient) => {
-      gDevTools.showToolbox(devtools.TargetFactory.forWorker(workerClient),
-                            "jsdebugger", devtools.Toolbox.HostType.WINDOW);
+      gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+                            "jsdebugger", Toolbox.HostType.WINDOW);
     });
   }
 };
 
 /**
  * ThreadState keeps the UI up to date with the state of the
  * thread (paused/attached/etc.).
  */
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/breakdown.js
@@ -0,0 +1,43 @@
+/* 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";
+
+// @TODO 1215606
+// Use this assert instead of utils when fixed.
+// const { assert } = require("devtools/shared/DevToolsUtils");
+const { breakdownEquals, createSnapshot, assert } = require("../utils");
+const { actions, snapshotState: states } = require("../constants");
+const { takeCensus } = require("./snapshot");
+
+const setBreakdownAndRefresh = exports.setBreakdownAndRefresh = function (heapWorker, breakdown) {
+  return function *(dispatch, getState) {
+    // Clears out all stored census data and sets
+    // the breakdown
+    dispatch(setBreakdown(breakdown));
+    let snapshot = getState().snapshots.find(s => s.selected);
+
+    // If selected snapshot does not have updated census if the breakdown
+    // changed, retake the census with new breakdown
+    if (snapshot && !breakdownEquals(snapshot.breakdown, breakdown)) {
+      yield dispatch(takeCensus(heapWorker, snapshot));
+    }
+  };
+};
+
+/**
+ * Clears out all census data in the snapshots and sets
+ * a new breakdown.
+ *
+ * @param {Breakdown} breakdown
+ */
+const setBreakdown = exports.setBreakdown = function (breakdown) {
+  // @TODO 1215606
+  assert(typeof breakdown === "object" && breakdown.by,
+    `Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(breakdown)}`);
+
+  return {
+    type: actions.SET_BREAKDOWN,
+    breakdown,
+  }
+};
--- a/devtools/client/memory/actions/moz.build
+++ b/devtools/client/memory/actions/moz.build
@@ -1,8 +1,9 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'breakdown.js',
     'snapshot.js',
 )
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -1,40 +1,57 @@
 /* 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";
 
 // @TODO 1215606
 // Use this assert instead of utils when fixed.
 // const { assert } = require("devtools/shared/DevToolsUtils");
-const { createSnapshot, assert } = require("../utils");
+const { getSnapshot, breakdownEquals, createSnapshot, assert } = require("../utils");
 const { actions, snapshotState: states } = require("../constants");
 
 /**
  * A series of actions are fired from this task to save, read and generate the initial
  * census from a snapshot.
  *
  * @param {MemoryFront}
  * @param {HeapAnalysesClient}
  * @param {Object}
  */
-const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function takeSnapshotAndCensus (front, heapWorker) {
-  return function *(dispatch, getStore) {
+const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, heapWorker) {
+  return function *(dispatch, getState) {
     let snapshot = yield dispatch(takeSnapshot(front));
     yield dispatch(readSnapshot(heapWorker, snapshot));
     yield dispatch(takeCensus(heapWorker, snapshot));
   };
 };
 
 /**
+ * Selects a snapshot and if the snapshot's census is using a different
+ * breakdown, take a new census.
+ *
+ * @param {HeapAnalysesClient}
+ * @param {Snapshot}
+ */
+const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, snapshot) {
+  return function *(dispatch, getState) {
+    dispatch(selectSnapshot(snapshot));
+
+    // Attempt to take another census; if the snapshot already is using
+    // the correct breakdown, this will noop.
+    yield dispatch(takeCensus(heapWorker, snapshot));
+  };
+};
+
+/**
  * @param {MemoryFront}
  */
-const takeSnapshot = exports.takeSnapshot = function takeSnapshot (front) {
-  return function *(dispatch, getStore) {
+const takeSnapshot = exports.takeSnapshot = function (front) {
+  return function *(dispatch, getState) {
     let snapshot = createSnapshot();
     dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
     dispatch(selectSnapshot(snapshot));
 
     let path = yield front.saveHeapSnapshot();
     dispatch({ type: actions.TAKE_SNAPSHOT_END, snapshot, path });
 
     return snapshot;
@@ -44,52 +61,65 @@ const takeSnapshot = exports.takeSnapsho
 /**
  * Reads a snapshot into memory; necessary to do before taking
  * a census on the snapshot. May only be called once per snapshot.
  *
  * @param {HeapAnalysesClient}
  * @param {Snapshot} snapshot,
  */
 const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, snapshot) {
-  return function *(dispatch, getStore) {
+  return function *(dispatch, getState) {
     // @TODO 1215606
     assert(snapshot.state === states.SAVED,
       "Should only read a snapshot once");
 
     dispatch({ type: actions.READ_SNAPSHOT_START, snapshot });
     yield heapWorker.readHeapSnapshot(snapshot.path);
     dispatch({ type: actions.READ_SNAPSHOT_END, snapshot });
   };
 };
 
 /**
  * @param {HeapAnalysesClient} heapWorker
  * @param {Snapshot} snapshot,
  *
- * @see {Snapshot} model defined in devtools/client/memory/app.js
+ * @see {Snapshot} model defined in devtools/client/memory/models.js
  * @see `devtools/shared/heapsnapshot/HeapAnalysesClient.js`
  * @see `js/src/doc/Debugger/Debugger.Memory.md` for breakdown details
  */
-const takeCensus = exports.takeCensus = function takeCensus (heapWorker, snapshot) {
-  return function *(dispatch, getStore) {
+const takeCensus = exports.takeCensus = function (heapWorker, snapshot) {
+  return function *(dispatch, getState) {
     // @TODO 1215606
     assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
       "Can only take census of snapshots in READ or SAVED_CENSUS state");
 
-    let breakdown = getStore().breakdown;
-    dispatch({ type: actions.TAKE_CENSUS_START, snapshot, breakdown });
+    let census;
+    let breakdown = getState().breakdown;
+
+    // If breakdown hasn't changed, don't do anything
+    if (breakdownEquals(breakdown, snapshot.breakdown)) {
+      return;
+    }
 
-    let census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, { asTreeNode: true });
-    dispatch({ type: actions.TAKE_CENSUS_END, snapshot, census });
+    // Keep taking a census if the breakdown changes during. Recheck
+    // that the breakdown used for the census is the same as
+    // the state's breakdown.
+    do {
+      breakdown = getState().breakdown;
+      dispatch({ type: actions.TAKE_CENSUS_START, snapshot, breakdown });
+      census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, { asTreeNode: true });
+    } while (!breakdownEquals(breakdown, getState().breakdown));
+
+    dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, census });
   };
 };
 
 /**
  * @param {Snapshot}
- * @see {Snapshot} model defined in devtools/client/memory/app.js
+ * @see {Snapshot} model defined in devtools/client/memory/models.js
  */
-const selectSnapshot = exports.selectSnapshot = function takeSnapshot (snapshot) {
+const selectSnapshot = exports.selectSnapshot = function (snapshot) {
   return {
     type: actions.SELECT_SNAPSHOT,
     snapshot
   };
 };
 
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.js
@@ -1,64 +1,23 @@
 const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { selectSnapshot, takeSnapshotAndCensus } = require("./actions/snapshot");
-const { snapshotState } = require("./constants");
+const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot");
+const { setBreakdownAndRefresh } = require("./actions/breakdown");
+const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils");
 const Toolbar = createFactory(require("./components/toolbar"));
 const List = createFactory(require("./components/list"));
 const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
 const HeapView = createFactory(require("./components/heap"));
-
-const stateModel = {
-  /**
-   * {MemoryFront}
-   * Used to communicate with the platform.
-   */
-  front: PropTypes.any,
-
-  /**
-   * {HeapAnalysesClient}
-   * Used to communicate with the worker that performs analyses on heaps.
-   */
-  heapWorker: PropTypes.any,
-
-  /**
-   * The breakdown object DSL describing how we want
-   * the census data to be.
-   * @see `js/src/doc/Debugger/Debugger.Memory.md`
-   */
-  breakdown: PropTypes.object.isRequired,
-
-  /**
-   * {Array<Snapshot>}
-   * List of references to all snapshots taken
-   */
-  snapshots: PropTypes.arrayOf(PropTypes.shape({
-    // Unique ID for a snapshot
-    id: PropTypes.number.isRequired,
-    // fs path to where the snapshot is stored; used to
-    // identify the snapshot for HeapAnalysesClient.
-    path: PropTypes.string,
-    // Whether or not this snapshot is currently selected.
-    selected: PropTypes.bool.isRequired,
-    // Whther or not the snapshot has been read into memory.
-    // Only needed to do once.
-    snapshotRead: PropTypes.bool.isRequired,
-    // State the snapshot is in
-    // @see ./constants.js
-    state: PropTypes.oneOf(Object.keys(snapshotState)).isRequired,
-    // Data of a census breakdown
-    census: PropTypes.any,
-  }))
-};
+const { app: appModel } = require("./models");
 
 const App = createClass({
   displayName: "memory-tool",
 
-  propTypes: stateModel,
+  propTypes: appModel,
 
   childContextTypes: {
     front: PropTypes.any,
     heapWorker: PropTypes.any,
   },
 
   getChildContext() {
     return {
@@ -70,27 +29,27 @@ const App = createClass({
   render() {
     let { dispatch, snapshots, front, heapWorker, breakdown } = this.props;
     let selectedSnapshot = snapshots.find(s => s.selected);
 
     return (
       dom.div({ id: "memory-tool" }, [
 
         Toolbar({
-          buttons: [{
-            className: "take-snapshot",
-            onClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker))
-          }]
+          breakdowns: getBreakdownDisplayData(),
+          onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
+          onBreakdownChange: breakdown =>
+            dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))),
         }),
 
         dom.div({ id: "memory-tool-container" }, [
           List({
             itemComponent: SnapshotListItem,
             items: snapshots,
-            onClick: snapshot => dispatch(selectSnapshot(snapshot))
+            onClick: snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot))
           }),
 
           HeapView({
             snapshot: selectedSnapshot,
             onSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker))
           }),
         ])
       ])
--- a/devtools/client/memory/components/heap.js
+++ b/devtools/client/memory/components/heap.js
@@ -1,46 +1,47 @@
 const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
 const { getSnapshotStatusText } = require("../utils");
 const { snapshotState: states } = require("../constants");
+const { snapshot: snapshotModel } = require("../models");
 const TAKE_SNAPSHOT_TEXT = "Take snapshot";
 
 /**
  * Main view for the memory tool -- contains several panels for different states;
  * an initial state of only a button to take a snapshot, loading states, and the
  * heap view tree.
  */
 
 const Heap = module.exports = createClass({
   displayName: "heap-view",
 
   propTypes: {
     onSnapshotClick: PropTypes.func.isRequired,
-    snapshot: PropTypes.any,
+    snapshot: snapshotModel,
   },
 
   render() {
     let { snapshot, onSnapshotClick } = this.props;
     let pane;
     let census = snapshot ? snapshot.census : null;
     let state = snapshot ? snapshot.state : "initial";
-    let statusText = getSnapshotStatusText(snapshot);
 
     switch (state) {
       case "initial":
         pane = dom.div({ className: "heap-view-panel", "data-state": "initial" },
           dom.button({ className: "take-snapshot", onClick: onSnapshotClick }, TAKE_SNAPSHOT_TEXT)
         );
         break;
       case states.SAVING:
       case states.SAVED:
       case states.READING:
       case states.READ:
       case states.SAVING_CENSUS:
-        pane = dom.div({ className: "heap-view-panel", "data-state": state }, statusText);
+        pane = dom.div({ className: "heap-view-panel", "data-state": state },
+          getSnapshotStatusText(snapshot));
         break;
       case states.SAVED_CENSUS:
         pane = dom.div({ className: "heap-view-panel", "data-state": "loaded" }, JSON.stringify(census || {}));
         break;
     }
 
     return (
       dom.div({ id: "heap-view", "data-state": state }, pane)
--- a/devtools/client/memory/components/snapshot-list-item.js
+++ b/devtools/client/memory/components/snapshot-list-item.js
@@ -1,17 +1,18 @@
 const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
 const { getSnapshotStatusText } = require("../utils");
+const { snapshot: snapshotModel } = require("../models");
 
 const SnapshotListItem = module.exports = createClass({
   displayName: "snapshot-list-item",
 
   propTypes: {
     onClick: PropTypes.func,
-    item: PropTypes.any.isRequired,
+    item: snapshotModel.isRequired,
     index: PropTypes.number.isRequired,
   },
 
   render() {
     let { index, item, onClick } = this.props;
     let className = `snapshot-list-item ${item.selected ? " selected" : ""}`;
     let statusText = getSnapshotStatusText(item);
 
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -1,16 +1,26 @@
-const { DOM, createClass } = require("devtools/client/shared/vendor/react");
+const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
 
 const Toolbar = module.exports = createClass({
   displayName: "toolbar",
+  propTypes: {
+    breakdowns: PropTypes.arrayOf(PropTypes.shape({
+      name: PropTypes.string.isRequired,
+      displayName: PropTypes.string.isRequired,
+    })).isRequired,
+    onTakeSnapshotClick: PropTypes.func.isRequired,
+    onBreakdownChange: PropTypes.func.isRequired,
+  },
 
   render() {
-    let buttons = this.props.buttons;
+    let { onTakeSnapshotClick, onBreakdownChange, breakdowns } = this.props;
     return (
-      DOM.div({ className: "devtools-toolbar" }, ...buttons.map(spec => {
-        return DOM.button(Object.assign({}, spec, {
-          className: `${spec.className || "" } devtools-button`
-        }));
-      }))
+      DOM.div({ className: "devtools-toolbar" }, [
+        DOM.button({ className: `take-snapshot devtools-button`, onClick: onTakeSnapshotClick }),
+        DOM.select({
+          className: `select-breakdown`,
+          onChange: e => onBreakdownChange(e.target.value),
+        }, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName)))
+      ])
     );
   }
 });
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -16,16 +16,49 @@ actions.READ_SNAPSHOT_END = "read-snapsh
 
 // When a census is being performed on a heap snapshot
 actions.TAKE_CENSUS_START = "take-census-start";
 actions.TAKE_CENSUS_END = "take-census-end";
 
 // Fired by UI to select a snapshot to view.
 actions.SELECT_SNAPSHOT = "select-snapshot";
 
+const COUNT = { by: "count", count: true, bytes: true };
+const INTERNAL_TYPE = { by: "internalType", then: COUNT };
+const ALLOCATION_STACK = { by: "allocationStack", then: COUNT, noStack: COUNT };
+const OBJECT_CLASS = { by: "objectClass", then: COUNT, other: COUNT };
+
+const breakdowns = exports.breakdowns = {
+  coarseType: {
+    displayName: "Coarse Type",
+    breakdown: {
+      by: "coarseType",
+      objects: ALLOCATION_STACK,
+      strings: ALLOCATION_STACK,
+      scripts: INTERNAL_TYPE,
+      other: INTERNAL_TYPE,
+    }
+  },
+
+  allocationStack: {
+    displayName: "Allocation Site",
+    breakdown: ALLOCATION_STACK,
+  },
+
+  objectClass: {
+    displayName: "Object Class",
+    breakdown: OBJECT_CLASS,
+  },
+
+  internalType: {
+    displayName: "Internal Type",
+    breakdown: INTERNAL_TYPE,
+  },
+};
+
 const snapshotState = exports.snapshotState = {};
 
 /**
  * Various states a snapshot can be in.
  * An FSM describing snapshot states:
  *
  * SAVING -> SAVED -> READING -> READ   <-  <-  <- SAVED_CENSUS
  *                                    ↘             ↗
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/models.js
@@ -0,0 +1,61 @@
+const { MemoryFront } = require("devtools/server/actors/memory");
+const HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+const { PropTypes } = require("devtools/client/shared/vendor/react");
+const { snapshotState: states } = require("./constants");
+
+/**
+ * The breakdown object DSL describing how we want
+ * the census data to be.
+ * @see `js/src/doc/Debugger/Debugger.Memory.md`
+ */
+let breakdownModel = exports.breakdown = PropTypes.shape({
+  by: PropTypes.oneOf(["coarseType", "allocationStack", "objectClass", "internalType"]).isRequired,
+});
+
+/**
+ * Snapshot model.
+ */
+let snapshotModel = exports.snapshot = PropTypes.shape({
+  // Unique ID for a snapshot
+  id: PropTypes.number.isRequired,
+  // Whether or not this snapshot is currently selected.
+  selected: PropTypes.bool.isRequired,
+  // fs path to where the snapshot is stored; used to
+  // identify the snapshot for HeapAnalysesClient.
+  path: PropTypes.string,
+  // Data of a census breakdown
+  census: PropTypes.object,
+  // The breakdown used to generate the current census
+  breakdown: breakdownModel,
+  // State the snapshot is in
+  // @see ./constants.js
+  state: function (props, propName) {
+    let stateNames = Object.keys(states);
+    let current = props.state;
+    let shouldHavePath = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS];
+    let shouldHaveCensus = [states.SAVED_CENSUS];
+
+    if (!stateNames.contains(current)) {
+      throw new Error(`Snapshot state must be one of ${stateNames}.`);
+    }
+    if (shouldHavePath.contains(current) && !path) {
+      throw new Error(`Snapshots in state ${current} must have a snapshot path.`);
+    }
+    if (shouldHaveCensus.contains(current) && (!props.census || !props.breakdown)) {
+      throw new Error(`Snapshots in state ${current} must have a census and breakdown.`);
+    }
+  },
+});
+
+let appModel = exports.app = {
+  // {MemoryFront} Used to communicate with platform
+  front: PropTypes.instanceOf(MemoryFront),
+  // {HeapAnalysesClient} Used to interface with snapshots
+  heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
+  // The breakdown object DSL describing how we want
+  // the census data to be.
+  // @see `js/src/doc/Debugger/Debugger.Memory.md`
+  breakdown: breakdownModel.isRequired,
+  // List of reference to all snapshots taken
+  snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
+};
--- a/devtools/client/memory/moz.build
+++ b/devtools/client/memory/moz.build
@@ -9,16 +9,17 @@ DIRS += [
     'modules',
     'reducers',
 ]
 
 DevToolsModules(
     'app.js',
     'constants.js',
     'initializer.js',
+    'models.js',
     'panel.js',
     'reducers.js',
     'store.js',
     'utils.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
--- a/devtools/client/memory/reducers/breakdown.js
+++ b/devtools/client/memory/reducers/breakdown.js
@@ -1,15 +1,16 @@
-const { actions } = require("../constants");
+const { actions, breakdowns } = require("../constants");
+const DEFAULT_BREAKDOWN = breakdowns.coarseType.breakdown;
 
-// Hardcoded breakdown for now
-const DEFAULT_BREAKDOWN = {
-  by: "internalType",
-  then: { by: "count", count: true, bytes: true }
+let handlers = Object.create(null);
+
+handlers[actions.SET_BREAKDOWN] = function (_, action) {
+  return Object.assign({}, action.breakdown);
 };
 
-/**
- * Not much to do here yet until we can change breakdowns,
- * but this gets it in our store.
- */
 module.exports = function (state=DEFAULT_BREAKDOWN, action) {
-  return Object.assign({}, DEFAULT_BREAKDOWN);
+  let handle = handlers[action.type];
+  if (handle) {
+    return handle(state, action);
+  }
+  return state;
 };
--- a/devtools/client/memory/reducers/snapshots.js
+++ b/devtools/client/memory/reducers/snapshots.js
@@ -28,23 +28,25 @@ handlers[actions.READ_SNAPSHOT_END] = fu
   snapshot.state = states.READ;
   return [...snapshots];
 };
 
 handlers[actions.TAKE_CENSUS_START] = function (snapshots, action) {
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.SAVING_CENSUS;
   snapshot.census = null;
+  snapshot.breakdown = action.breakdown;
   return [...snapshots];
 };
 
 handlers[actions.TAKE_CENSUS_END] = function (snapshots, action) {
   let snapshot = getSnapshot(snapshots, action.snapshot);
   snapshot.state = states.SAVED_CENSUS;
   snapshot.census = action.census;
+  snapshot.breakdown = action.breakdown;
   return [...snapshots];
 };
 
 handlers[actions.SELECT_SNAPSHOT] = function (snapshots, action) {
   return snapshots.map(s => {
     s.selected = s.id === action.snapshot.id;
     return s;
   });
--- a/devtools/client/memory/test/unit/head.js
+++ b/devtools/client/memory/test/unit/head.js
@@ -54,8 +54,37 @@ function waitUntilState (store, predicat
     }
   }
 
   // Fire the check immediately incase the action has already occurred
   check();
 
   return deferred.promise;
 }
+
+function waitUntilSnapshotState (store, expected) {
+  let predicate = () => {
+    let snapshots = store.getState().snapshots;
+    do_print(snapshots.map(x => x.state));
+    return snapshots.length === expected.length &&
+           expected.every((state, i) => state === "*" || snapshots[i].state === state);
+  };
+  do_print(`Waiting for snapshots to be of state: ${expected}`);
+  return waitUntilState(store, predicate);
+}
+
+function isBreakdownType (census, type) {
+  // Little sanity check, all censuses should have atleast a children array
+  if (!census || !Array.isArray(census.children)) {
+    return false;
+  }
+  switch (type) {
+    case "coarseType":
+      return census.children.find(c => c.name === "objects");
+    case "objectClass":
+      return census.children.find(c => c.name === "Function");
+    case "internalType":
+      return census.children.find(c => c.name === "js::BaseShape") &&
+             !census.children.find(c => c.name === "objects");
+    default:
+      throw new Error(`isBreakdownType does not yet support ${type}`);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-set-breakdown-and-refresh-01.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the task creator `setBreakdownAndRefreshAndRefresh()` for breakdown changing.
+ * We test this rather than `setBreakdownAndRefresh` directly, as we use the refresh action
+ * in the app itself composed from `setBreakdownAndRefresh`
+ */
+
+let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
+let { breakdownEquals } = require("devtools/client/memory/utils");
+let { setBreakdownAndRefresh } = require("devtools/client/memory/actions/breakdown");
+let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  let { getState, dispatch } = store;
+
+  // Test default breakdown with no snapshots
+  equal(getState().breakdown.by, "coarseType", "default coarseType breakdown selected at start.");
+  dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.objectClass.breakdown));
+  equal(getState().breakdown.by, "objectClass", "breakdown changed with no snapshots");
+
+  // Test invalid breakdowns
+  ok(getState().errors.length === 0, "No error actions in the queue.");
+  dispatch(setBreakdownAndRefresh(heapWorker, {}));
+  yield waitUntilState(store, () => getState().errors.length === 1);
+  ok(true, "Emits an error action when passing in an invalid breakdown object");
+
+  equal(getState().breakdown.by, "objectClass",
+    "current breakdown unchanged when passing invalid breakdown");
+
+  // Test new snapshots
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  ok(isBreakdownType(getState().snapshots[0].census, "objectClass"),
+    "New snapshots use the current, non-default breakdown");
+
+
+  // Updates when changing breakdown during `SAVING`
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING]);
+  dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.coarseType.breakdown));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]);
+
+  ok(isBreakdownType(getState().snapshots[1].census, "coarseType"),
+    "Breakdown can be changed while saving snapshots, uses updated breakdown in census");
+
+
+  // Updates when changing breakdown during `SAVING_CENSUS`
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVING_CENSUS]);
+  dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.objectClass.breakdown));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+
+  ok(breakdownEquals(getState().snapshots[2].breakdown, breakdowns.objectClass.breakdown),
+    "Breakdown can be changed while saving census, stores updated breakdown in snapshot");
+  ok(isBreakdownType(getState().snapshots[2].census, "objectClass"),
+    "Breakdown can be changed while saving census, uses updated breakdown in census");
+
+  // Updates census on currently selected snapshot when changing breakdown
+  ok(getState().snapshots[2].selected, "Third snapshot currently selected");
+  dispatch(setBreakdownAndRefresh(heapWorker, breakdowns.internalType.breakdown));
+  yield waitUntilState(store, () => isBreakdownType(getState().snapshots[2].census, "internalType"));
+  ok(isBreakdownType(getState().snapshots[2].census, "internalType"),
+    "Snapshot census updated when changing breakdowns after already generating one census");
+
+  // Does not update unselected censuses
+  ok(!getState().snapshots[1].selected, "Second snapshot unselected currently");
+  ok(breakdownEquals(getState().snapshots[1].breakdown, breakdowns.coarseType.breakdown),
+    "Second snapshot using `coarseType` breakdown still and not yet updated to correct breakdown");
+  ok(isBreakdownType(getState().snapshots[1].census, "coarseType"),
+    "Second snapshot using `coarseType` still for census and not yet updated to correct breakdown");
+
+  // Updates to current breakdown when switching to stale snapshot
+  dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1]));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVING_CENSUS, states.SAVED_CENSUS]);
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]);
+
+  ok(getState().snapshots[1].selected, "Second snapshot selected currently");
+  ok(breakdownEquals(getState().snapshots[1].breakdown, breakdowns.internalType.breakdown),
+    "Second snapshot using `internalType` breakdown and updated to correct breakdown");
+  ok(isBreakdownType(getState().snapshots[1].census, "internalType"),
+    "Second snapshot using `internalType` for census and updated to correct breakdown");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-set-breakdown-and-refresh-02.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the task creator `setBreakdownAndRefreshAndRefresh()` for custom
+ * breakdowns.
+ */
+
+let { snapshotState: states } = require("devtools/client/memory/constants");
+let { breakdownEquals } = require("devtools/client/memory/utils");
+let { setBreakdownAndRefresh } = require("devtools/client/memory/actions/breakdown");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+let custom = { by: "internalType", then: { by: "count", bytes: true }};
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  let { getState, dispatch } = store;
+
+  dispatch(setBreakdownAndRefresh(heapWorker, custom));
+  ok(breakdownEquals(getState().breakdown, custom),
+    "Custom breakdown stored in breakdown state.");
+
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+
+  ok(breakdownEquals(getState().snapshots[0].breakdown, custom),
+    "New snapshot stored custom breakdown when done taking census");
+  ok(getState().snapshots[0].census.children.length, "Census has some children");
+  // Ensure we don't have `count` in any results
+  ok(getState().snapshots[0].census.children.every(c => !c.count), "Census used custom breakdown");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_action-set-breakdown.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the action creator `setBreakdown()` for breakdown changing.
+ * Does not test refreshing the census information, check `setBreakdownAndRefresh` action
+ * for that.
+ */
+
+let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
+let { setBreakdown } = require("devtools/client/memory/actions/breakdown");
+let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  let front = new StubbedMemoryFront();
+  let heapWorker = new HeapAnalysesClient();
+  yield front.attach();
+  let store = Store();
+  let { getState, dispatch } = store;
+
+  // Test default breakdown with no snapshots
+  equal(getState().breakdown.by, "coarseType", "default coarseType breakdown selected at start.");
+  dispatch(setBreakdown(breakdowns.objectClass.breakdown));
+  equal(getState().breakdown.by, "objectClass", "breakdown changed with no snapshots");
+
+  // Test invalid breakdowns
+  try {
+    dispatch(setBreakdown({}));
+    ok(false, "Throws when passing in an invalid breakdown object");
+  } catch (e) {
+    ok(true, "Throws when passing in an invalid breakdown object");
+  }
+  equal(getState().breakdown.by, "objectClass",
+    "current breakdown unchanged when passing invalid breakdown");
+
+  // Test new snapshots
+  dispatch(takeSnapshotAndCensus(front, heapWorker));
+  yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
+  ok(isBreakdownType(getState().snapshots[0].census, "objectClass"),
+    "New snapshots use the current, non-default breakdown");
+});
--- a/devtools/client/memory/test/unit/test_action-take-census.js
+++ b/devtools/client/memory/test/unit/test_action-take-census.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)`
  */
 
-var { snapshotState: states } = require("devtools/client/memory/constants");
+var { snapshotState: states, breakdowns } = require("devtools/client/memory/constants");
+var { breakdownEquals } = require("devtools/client/memory/utils");
 var { ERROR_TYPE } = require("devtools/client/shared/redux/middleware/task");
 var actions = require("devtools/client/memory/actions/snapshot");
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function *() {
@@ -38,12 +39,14 @@ add_task(function *() {
   yield waitUntilState(store, () => store.getState().snapshots[0].state === states.READ);
 
   store.dispatch(actions.takeCensus(heapWorker, snapshot));
   yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVING_CENSUS);
   yield waitUntilState(store, () => store.getState().snapshots[0].state === states.SAVED_CENSUS);
 
   snapshot = store.getState().snapshots[0];
   ok(snapshot.census, "Snapshot has census after saved census");
-  ok(snapshot.census.children.length, "Census is in tree node form with the default breakdown");
-  ok(snapshot.census.children.find(t => t.name === "JSObject"),
+  ok(snapshot.census.children.length, "Census is in tree node form");
+  ok(isBreakdownType(snapshot.census, "coarseType"),
     "Census is in tree node form with the default breakdown");
+  ok(breakdownEquals(snapshot.breakdown, breakdowns.coarseType.breakdown),
+    "Snapshot stored correct breakdown used for the census");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/unit/test_utils.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of
+ * taking a snapshot, and its sub-actions.
+ */
+
+let utils = require("devtools/client/memory/utils");
+let { snapshotState: states, breakdowns } = require("devtools/client/memory/constants");
+let { Preferences } = require("resource://gre/modules/Preferences.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function *() {
+  ok(utils.breakdownEquals(breakdowns.allocationStack.breakdown, {
+    by: "allocationStack",
+    then: { by: "count", count: true, bytes: true },
+    noStack: { by: "count", count: true, bytes: true },
+  }), "utils.breakdownEquals() passes with preset"),
+
+  ok(!utils.breakdownEquals(breakdowns.allocationStack.breakdown, {
+    by: "allocationStack",
+    then: { by: "count", count: false, bytes: true },
+    noStack: { by: "count", count: true, bytes: true },
+  }), "utils.breakdownEquals() fails when deep properties do not match");
+
+  ok(!utils.breakdownEquals(breakdowns.allocationStack.breakdown, {
+    by: "allocationStack",
+    then: { by: "count", bytes: true },
+    noStack: { by: "count", count: true, bytes: true },
+  }), "utils.breakdownEquals() fails when deep properties are missing.");
+
+  let s1 = utils.createSnapshot();
+  let s2 = utils.createSnapshot();
+  ok(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state");
+  ok(s1.id !== s2.id, "utils.createSnapshot() creates snapshot with unique ids");
+
+  ok(utils.breakdownEquals(utils.breakdownNameToSpec("coarseType"), breakdowns.coarseType.breakdown),
+    "utils.breakdownNameToSpec() works for presets");
+  ok(utils.breakdownEquals(utils.breakdownNameToSpec("coarseType"), breakdowns.coarseType.breakdown),
+    "utils.breakdownNameToSpec() works for presets");
+
+  let custom = { by: "internalType", then: { by: "count", bytes: true }};
+  Preferences.set("devtools.memory.custom-breakdowns", JSON.stringify({ "My Breakdown": custom }));
+
+  ok(utils.breakdownEquals(utils.getCustomBreakdowns()["My Breakdown"], custom),
+    "utils.getCustomBreakdowns() returns custom breakdowns");
+});
--- a/devtools/client/memory/test/unit/xpcshell.ini
+++ b/devtools/client/memory/test/unit/xpcshell.ini
@@ -1,11 +1,15 @@
 [DEFAULT]
 tags = devtools
 head = head.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_action-select-snapshot.js]
+[test_action-set-breakdown.js]
+[test_action-set-breakdown-and-refresh-01.js]
+[test_action-set-breakdown-and-refresh-02.js]
 [test_action-take-census.js]
 [test_action-take-snapshot.js]
 [test_action-take-snapshot-and-census.js]
+[test_utils.js]
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -1,41 +1,125 @@
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns";
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
-const { snapshotState: states } = require("./constants");
+const { snapshotState: states, breakdowns } = require("./constants");
 const SAVING_SNAPSHOT_TEXT = "Saving snapshot...";
 const READING_SNAPSHOT_TEXT = "Reading snapshot...";
 const SAVING_CENSUS_TEXT = "Taking heap census...";
 
 // @TODO 1215606
 // Use DevToolsUtils.assert when fixed.
 exports.assert = function (condition, message) {
   if (!condition) {
     const err = new Error("Assertion failure: " + message);
     DevToolsUtils.reportException("DevToolsUtils.assert", err);
     throw err;
   }
 };
 
 /**
+ * Returns an array of objects with the unique key `name`
+ * and `displayName` for each breakdown.
+ *
+ * @return {Object{name, displayName}}
+ */
+exports.getBreakdownDisplayData = function () {
+  return exports.getBreakdownNames().map(name => {
+    // If it's a preset use the display name value
+    let preset = breakdowns[name];
+    let displayName = name;
+    if (preset && preset.displayName) {
+      displayName = preset.displayName;
+    }
+    return { name, displayName };
+  });
+};
+
+/**
+ * Returns an array of the unique names for each breakdown in
+ * presets and custom pref.
+ *
+ * @return {Array<Breakdown>}
+ */
+exports.getBreakdownNames = function () {
+  let custom = exports.getCustomBreakdowns();
+  return Object.keys(Object.assign({}, breakdowns, custom));
+};
+
+/**
+ * Returns custom breakdowns defined in `devtools.memory.custom-breakdowns` pref.
+ *
+ * @return {Object}
+ */
+exports.getCustomBreakdowns = function () {
+  let customBreakdowns = Object.create(null);
+  try {
+    customBreakdowns = JSON.parse(Preferences.get(CUSTOM_BREAKDOWN_PREF)) || Object.create(null);
+  } catch (e) {
+    DevToolsUtils.reportException(
+      `String stored in "${CUSTOM_BREAKDOWN_PREF}" pref cannot be parsed by \`JSON.parse()\`.`);
+  }
+  return customBreakdowns;
+}
+
+/**
+ * Converts a breakdown preset name, like "allocationStack", and returns the
+ * spec for the breakdown. Also checks properties of keys in the `devtools.memory.custom-breakdowns`
+ * pref. If not found, returns an empty object.
+ *
+ * @param {String} name
+ * @return {Object}
+ */
+
+exports.breakdownNameToSpec = function (name) {
+  let customBreakdowns = exports.getCustomBreakdowns();
+
+  // If breakdown is already a breakdown, use it
+  if (typeof name === "object") {
+    return name;
+  }
+  // If it's in our custom breakdowns, use it
+  else if (name in customBreakdowns) {
+    return customBreakdowns[name];
+  }
+  // If breakdown name is in our presets, use that
+  else if (name in breakdowns) {
+    return breakdowns[name].breakdown;
+  }
+  return Object.create(null);
+};
+
+/**
  * Returns a string representing a readable form of the snapshot's state.
  *
  * @param {Snapshot} snapshot
  * @return {String}
  */
 exports.getSnapshotStatusText = function (snapshot) {
-  switch (snapshot && snapshot.state) {
+  exports.assert((snapshot || {}).state,
+    `Snapshot must have expected state, found ${(snapshot || {}).state}.`);
+
+  switch (snapshot.state) {
     case states.SAVING:
       return SAVING_SNAPSHOT_TEXT;
     case states.SAVED:
     case states.READING:
       return READING_SNAPSHOT_TEXT;
-    case states.READ:
     case states.SAVING_CENSUS:
       return SAVING_CENSUS_TEXT;
+    // If it's read, it shouldn't have any label, as we could've cleared the
+    // census cache by changing the breakdown, and we should lazily
+    // go to SAVING_CENSUS. If it's SAVED_CENSUS, we have no status to display.
+    case states.READ:
+    case states.SAVED_CENSUS:
+      return "";
   }
+
+  DevToolsUtils.reportException(`Snapshot in unexpected state: ${snapshot.state}`);
   return "";
 }
 
 /**
  * Takes an array of snapshots and a snapshot and returns
  * the snapshot instance in `snapshots` that matches
  * the snapshot passed in.
  *
@@ -61,8 +145,48 @@ exports.createSnapshot = function create
   let id = ++INC_ID;
   return {
     id,
     state: states.SAVING,
     census: null,
     path: null,
   };
 };
+
+/**
+ * Takes two objects and compares them deeply, returning
+ * a boolean indicating if they're equal or not. Used for breakdown
+ * comparison.
+ *
+ * @param {Any} obj1
+ * @param {Any} obj2
+ * @return {Boolean}
+ */
+exports.breakdownEquals = function (obj1, obj2) {
+  let type1 = typeof obj1;
+  let type2 = typeof obj2;
+
+  // Quick checks
+  if (type1 !== type2 || (Array.isArray(obj1) !== Array.isArray(obj2))) {
+    return false;
+  }
+
+  if (obj1 === obj2) {
+    return true;
+  }
+
+  if (Array.isArray(obj1)) {
+    if (obj1.length !== obj2.length) { return false; }
+    return obj1.every((_, i) => exports.breakdownEquals(obj[1], obj2[i]));
+  }
+  else if (type1 === "object") {
+    let k1 = Object.keys(obj1);
+    let k2 = Object.keys(obj2);
+
+    if (k1.length !== k2.length) {
+      return false;
+    }
+
+    return k1.every(k => exports.breakdownEquals(obj1[k], obj2[k]));
+  }
+
+  return false;
+};
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -98,16 +98,18 @@ pref("devtools.debugger.ui.panes-instrum
 pref("devtools.debugger.ui.panes-visible-on-startup", false);
 pref("devtools.debugger.ui.variables-sorting-enabled", true);
 pref("devtools.debugger.ui.variables-only-enum-visible", false);
 pref("devtools.debugger.ui.variables-searchbox-visible", false);
 
 // Enable the Memory tools
 pref("devtools.memory.enabled", false);
 
+pref("devtools.memory.custom-breakdowns", "{}");
+
 // Enable the Performance tools
 pref("devtools.performance.enabled", true);
 
 // The default Performance UI settings
 pref("devtools.performance.memory.sample-probability", "0.05");
 // Can't go higher than this without causing internal allocation overflows while
 // serializing the allocations data over the RDP.
 pref("devtools.performance.memory.max-log-length", 125000);
--- a/devtools/shared/heapsnapshot/census-tree-node.js
+++ b/devtools/shared/heapsnapshot/census-tree-node.js
@@ -58,30 +58,34 @@ CensusTreeNodeBreakdowns.count = functio
   }
 };
 
 CensusTreeNodeBreakdowns.internalType = function (node, breakdown, report) {
   node.children = [];
   for (let key of Object.keys(report)) {
     node.children.push(new CensusTreeNode(breakdown.then, report[key], key));
   }
-}
+};
 
 CensusTreeNodeBreakdowns.objectClass = function (node, breakdown, report) {
   node.children = [];
   for (let key of Object.keys(report)) {
     let bd = key === "other" ? breakdown.other : breakdown.then;
     node.children.push(new CensusTreeNode(bd, report[key], key));
   }
-}
+};
 
 CensusTreeNodeBreakdowns.coarseType = function (node, breakdown, report) {
   node.children = [];
   for (let type of Object.keys(breakdown).filter(type => COARSE_TYPES.has(type))) {
     node.children.push(new CensusTreeNode(breakdown[type], report[type], type));
   }
-}
+};
+
+CensusTreeNodeBreakdowns.allocationStack = function (node, breakdown, report) {
+  node.children = [];
+};
 
 function sortByBytes (a, b) {
   return (b.bytes || 0) - (a.bytes || 0);
 }
 
 exports.CensusTreeNode = CensusTreeNode;
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -782,17 +782,17 @@ NetworkMonitor.prototype = {
         return true;
       }
     }
 
     // The following check is necessary because beacon channels don't come
     // associated with a load group. Bug 1160837 will hopefully introduce a
     // platform fix that will render the following code entirely useless.
     if (aChannel.loadInfo &&
-        aChannel.loadInfo.contentPolicyType == Ci.nsIContentPolicy.TYPE_BEACON) {
+        aChannel.loadInfo.externalContentPolicyType == Ci.nsIContentPolicy.TYPE_BEACON) {
       let nonE10sMatch = this.window &&
                          aChannel.loadInfo.loadingDocument === this.window.document;
       let e10sMatch = this.topFrame &&
                       this.topFrame.contentPrincipal &&
                       this.topFrame.contentPrincipal.equals(aChannel.loadInfo.loadingPrincipal) &&
                       this.topFrame.contentPrincipal.URI.spec == aChannel.referrer.spec;
       let b2gMatch = this.appId &&
                      aChannel.loadInfo.loadingPrincipal.appId === this.appId;
@@ -833,17 +833,17 @@ NetworkMonitor.prototype = {
     event.fromCache = fromCache;
 
     if (extraStringData) {
       event.headersSize = extraStringData.length;
     }
 
     // Determine if this is an XHR request.
     httpActivity.isXHR = event.isXHR =
-        (aChannel.loadInfo.contentPolicyType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST);
+        (aChannel.loadInfo.externalContentPolicyType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST);
 
     // Determine the HTTP version.
     let httpVersionMaj = {};
     let httpVersionMin = {};
     aChannel.QueryInterface(Ci.nsIHttpChannelInternal);
     aChannel.getRequestVersion(httpVersionMaj, httpVersionMin);
 
     event.httpVersion = "HTTP/" + httpVersionMaj.value + "." +
--- a/docshell/test/unit/test_bug442584.js
+++ b/docshell/test/unit/test_bug442584.js
@@ -9,26 +9,22 @@ function run_test() {
   // Fill up the queue
   prefs.setBoolPref("network.prefetch-next", true);
   for (var i = 0; i < 5; i++) {
     var uri = ios.newURI("http://localhost/" + i, null, null);
     prefetch.prefetchURI(uri, uri, null, true);
   }
 
   // Make sure the queue has items in it...
-  var queue = prefetch.enumerateQueue();
-  do_check_true(queue.hasMoreElements());
+  do_check_true(prefetch.hasMoreElements());
 
   // Now disable the pref to force the queue to empty...
   prefs.setBoolPref("network.prefetch-next", false);
-  queue = prefetch.enumerateQueue();
-  do_check_false(queue.hasMoreElements());
+  do_check_false(prefetch.hasMoreElements());
 
   // Now reenable the pref, and add more items to the queue.
   prefs.setBoolPref("network.prefetch-next", true);
   for (var i = 0; i < 5; i++) {
     var uri = ios.newURI("http://localhost/" + i, null, null);
     prefetch.prefetchURI(uri, uri, null, true);
   }
-  queue = prefetch.enumerateQueue();
-  do_check_true(queue.hasMoreElements());
+  do_check_true(prefetch.hasMoreElements());
 }
-
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -1715,30 +1715,30 @@ Element::UnbindFromTree(bool aDeep, bool
                   "kids!");
 
   RemoveFromIdTable();
 
   // Make sure to unbind this node before doing the kids
   nsIDocument* document =
     HasFlag(NODE_FORCE_XBL_BINDINGS) ? OwnerDoc() : GetComposedDoc();
 
+  if (HasPointerLock()) {
+    nsIDocument::UnlockPointer();
+  }
   if (aNullParent) {
     if (IsFullScreenAncestor()) {
       // The element being removed is an ancestor of the full-screen element,
       // exit full-screen state.
       nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
                                       NS_LITERAL_CSTRING("DOM"), OwnerDoc(),
                                       nsContentUtils::eDOM_PROPERTIES,
                                       "RemovedFullScreenElement");
       // Fully exit full-screen.
       nsIDocument::ExitFullscreenInDocTree(OwnerDoc());
     }
-    if (HasPointerLock()) {
-      nsIDocument::UnlockPointer();
-    }
 
     if (GetParent() && GetParent()->IsInUncomposedDoc()) {
       // Update the editable descendant count in the ancestors before we
       // lose the reference to the parent.
       int32_t editableDescendantChange = -1 * EditableInclusiveDescendantCount(this);
       if (editableDescendantChange != 0) {
         nsIContent* parent = GetParent();
         while (parent) {
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -663,21 +663,16 @@ public:
   already_AddRefed<nsIHTMLCollection>
     GetElementsByTagName(const nsAString& aQualifiedName);
   already_AddRefed<nsIHTMLCollection>
     GetElementsByTagNameNS(const nsAString& aNamespaceURI,
                            const nsAString& aLocalName,
                            ErrorResult& aError);
   already_AddRefed<nsIHTMLCollection>
     GetElementsByClassName(const nsAString& aClassNames);
-  bool MozMatchesSelector(const nsAString& aSelector,
-                          ErrorResult& aError)
-  {
-    return Matches(aSelector, aError);
-  }
   void SetPointerCapture(int32_t aPointerId, ErrorResult& aError)
   {
     bool activeState = false;
     if (!nsIPresShell::GetPointerInfo(aPointerId, activeState)) {
       aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR);
       return;
     }
     if (!IsInDoc()) {
@@ -1764,18 +1759,18 @@ NS_IMETHOD GetScrollTopMax(int32_t* aScr
 {                                                                             \
   *aScrollTopMax = Element::ScrollTopMax();                                   \
   return NS_OK;                                                               \
 }                                                                             \
 NS_IMETHOD MozMatchesSelector(const nsAString& selector,                      \
                               bool* _retval) final override                   \
 {                                                                             \
   mozilla::ErrorResult rv;                                                    \
-  *_retval = Element::MozMatchesSelector(selector, rv);                       \
-  return rv.StealNSResult();                                                      \
+  *_retval = Element::Matches(selector, rv);                                  \
+  return rv.StealNSResult();                                                  \
 }                                                                             \
 NS_IMETHOD SetCapture(bool retargetToElement) final override                  \
 {                                                                             \
   Element::SetCapture(retargetToElement);                                     \
   return NS_OK;                                                               \
 }                                                                             \
 NS_IMETHOD ReleaseCapture(void) final override                                \
 {                                                                             \
--- a/dom/base/URLSearchParams.cpp
+++ b/dom/base/URLSearchParams.cpp
@@ -413,10 +413,28 @@ URLSearchParams::Serialize(nsAString& aV
 void
 URLSearchParams::NotifyObserver()
 {
   if (mObserver) {
     mObserver->URLSearchParamsUpdated(this);
   }
 }
 
+uint32_t
+URLSearchParams::GetIterableLength() const
+{
+  return mParams->Length();
+}
+
+const nsAString&
+URLSearchParams::GetKeyAtIndex(uint32_t aIndex) const
+{
+  return mParams->GetKeyAtIndex(aIndex);
+}
+
+const nsAString&
+URLSearchParams::GetValueAtIndex(uint32_t aIndex) const
+{
+  return mParams->GetValueAtIndex(aIndex);
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/base/URLSearchParams.h
+++ b/dom/base/URLSearchParams.h
@@ -86,16 +86,33 @@ public:
   // Returns true if aName was found and deleted, false otherwise.
   bool Delete(const nsAString& aName);
 
   void DeleteAll()
   {
     mParams.Clear();
   }
 
+  uint32_t Length() const
+  {
+    return mParams.Length();
+  }
+
+  const nsAString& GetKeyAtIndex(uint32_t aIndex) const
+  {
+    MOZ_ASSERT(aIndex < mParams.Length());
+    return mParams[aIndex].mKey;
+  }
+
+  const nsAString& GetValueAtIndex(uint32_t aIndex) const
+  {
+    MOZ_ASSERT(aIndex < mParams.Length());
+    return mParams[aIndex].mValue;
+  }
+
 private:
   void DecodeString(const nsACString& aInput, nsAString& aOutput);
   void ConvertString(const nsACString& aInput, nsAString& aOutput);
 
   struct Param
   {
     nsString mKey;
     nsString mValue;
@@ -148,16 +165,20 @@ public:
   void Set(const nsAString& aName, const nsAString& aValue);
 
   void Append(const nsAString& aName, const nsAString& aValue);
 
   bool Has(const nsAString& aName);
 
   void Delete(const nsAString& aName);
 
+  uint32_t GetIterableLength() const;
+  const nsAString& GetKeyAtIndex(uint32_t aIndex) const;
+  const nsAString& GetValueAtIndex(uint32_t aIndex) const;
+
   void Stringify(nsString& aRetval) const
   {
     Serialize(aRetval);
   }
 
   typedef URLParams::ForEachIterator ForEachIterator;
 
   bool
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -3191,17 +3191,17 @@ convertSheetType(uint32_t aSheetType)
       return nsIDocument::eAgentSheet;
     case nsDOMWindowUtils::USER_SHEET:
       return nsIDocument::eUserSheet;
     case nsDOMWindowUtils::AUTHOR_SHEET:
       return nsIDocument::eAuthorSheet;
     default:
       NS_ASSERTION(false, "wrong type");
       // we must return something although this should never happen
-      return nsIDocument::SheetTypeCount;
+      return nsIDocument::AdditionalSheetTypeCount;
   }
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::LoadSheet(nsIURI *aSheetURI, uint32_t aSheetType)
 {
   NS_ENSURE_ARG_POINTER(aSheetURI);
   NS_ENSURE_ARG(aSheetType == AGENT_SHEET ||
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -2408,17 +2408,17 @@ nsDocument::RemoveDocStyleSheetsFromStyl
         shell->StyleSet()->RemoveDocStyleSheet(sheet);
       }
     }
     // XXX Tell observers?
   }
 }
 
 void
-nsDocument::RemoveStyleSheetsFromStyleSets(nsCOMArray<nsIStyleSheet>& aSheets, nsStyleSet::sheetType aType)
+nsDocument::RemoveStyleSheetsFromStyleSets(nsCOMArray<nsIStyleSheet>& aSheets, SheetType aType)
 {
   // The stylesheets should forget us
   int32_t indx = aSheets.Count();
   while (--indx >= 0) {
     nsIStyleSheet* sheet = aSheets[indx];
     sheet->SetOwningDocument(nullptr);
 
     if (sheet->IsApplicable()) {
@@ -2435,26 +2435,27 @@ nsDocument::RemoveStyleSheetsFromStyleSe
 
 void
 nsDocument::ResetStylesheetsToURI(nsIURI* aURI)
 {
   MOZ_ASSERT(aURI);
 
   mozAutoDocUpdate upd(this, UPDATE_STYLE, true);
   RemoveDocStyleSheetsFromStyleSets();
-  RemoveStyleSheetsFromStyleSets(mOnDemandBuiltInUASheets, nsStyleSet::eAgentSheet);
-  RemoveStyleSheetsFromStyleSets(mAdditionalSheets[eAgentSheet], nsStyleSet::eAgentSheet);
-  RemoveStyleSheetsFromStyleSets(mAdditionalSheets[eUserSheet], nsStyleSet::eUserSheet);
-  RemoveStyleSheetsFromStyleSets(mAdditionalSheets[eAuthorSheet], nsStyleSet::eDocSheet);
+  RemoveStyleSheetsFromStyleSets(mOnDemandBuiltInUASheets, SheetType::Agent);
+  RemoveStyleSheetsFromStyleSets(mAdditionalSheets[eAgentSheet], SheetType::Agent);
+  RemoveStyleSheetsFromStyleSets(mAdditionalSheets[eUserSheet], SheetType::User);
+  RemoveStyleSheetsFromStyleSets(mAdditionalSheets[eAuthorSheet], SheetType::Doc);
 
   // Release all the sheets
   mStyleSheets.Clear();
   mOnDemandBuiltInUASheets.Clear();
-  for (uint32_t i = 0; i < SheetTypeCount; ++i)
-    mAdditionalSheets[i].Clear();
+  for (auto& sheets : mAdditionalSheets) {
+    sheets.Clear();
+  }
 
   // NOTE:  We don't release the catalog sheets.  It doesn't really matter
   // now, but it could in the future -- in which case not releasing them
   // is probably the right thing to do.
 
   // Now reset our inline style and attribute sheets.
   if (mAttrStyleSheet) {
     mAttrStyleSheet->Reset();
@@ -2478,42 +2479,42 @@ nsDocument::ResetStylesheetsToURI(nsIURI
     FillStyleSet(shell->StyleSet());
   }
 }
 
 static bool
 AppendAuthorSheet(nsIStyleSheet *aSheet, void *aData)
 {
   nsStyleSet *styleSet = static_cast<nsStyleSet*>(aData);
-  styleSet->AppendStyleSheet(nsStyleSet::eDocSheet, aSheet);
+  styleSet->AppendStyleSheet(SheetType::Doc, aSheet);
   return true;
 }
 
 static void
 AppendSheetsToStyleSet(nsStyleSet* aStyleSet,
                        const nsCOMArray<nsIStyleSheet>& aSheets,
-                       nsStyleSet::sheetType aType)
+                       SheetType aType)
 {
   for (int32_t i = aSheets.Count() - 1; i >= 0; --i) {
     aStyleSet->AppendStyleSheet(aType, aSheets[i]);
   }
 }
 
 
 void
 nsDocument::FillStyleSet(nsStyleSet* aStyleSet)
 {
   NS_PRECONDITION(aStyleSet, "Must have a style set");
-  NS_PRECONDITION(aStyleSet->SheetCount(nsStyleSet::eDocSheet) == 0,
+  NS_PRECONDITION(aStyleSet->SheetCount(SheetType::Doc) == 0,
                   "Style set already has document sheets?");
 
   // We could consider moving this to nsStyleSet::Init, to match its
   // handling of the eAnimationSheet and eTransitionSheet levels.
-  aStyleSet->DirtyRuleProcessors(nsStyleSet::ePresHintSheet);
-  aStyleSet->DirtyRuleProcessors(nsStyleSet::eStyleAttrSheet);
+  aStyleSet->DirtyRuleProcessors(SheetType::PresHint);
+  aStyleSet->DirtyRuleProcessors(SheetType::StyleAttr);
 
   int32_t i;
   for (i = mStyleSheets.Count() - 1; i >= 0; --i) {
     nsIStyleSheet* sheet = mStyleSheets[i];
     if (sheet->IsApplicable()) {
       aStyleSet->AddDocStyleSheet(sheet, this);
     }
   }
@@ -2523,26 +2524,26 @@ nsDocument::FillStyleSet(nsStyleSet* aSt
     sheetService->AuthorStyleSheets()->EnumerateForwards(AppendAuthorSheet,
                                                          aStyleSet);
   }
 
   // Iterate backwards to maintain order
   for (i = mOnDemandBuiltInUASheets.Count() - 1; i >= 0; --i) {
     nsIStyleSheet* sheet = mOnDemandBuiltInUASheets[i];
     if (sheet->IsApplicable()) {
-      aStyleSet->PrependStyleSheet(nsStyleSet::eAgentSheet, sheet);
+      aStyleSet->PrependStyleSheet(SheetType::Agent, sheet);
     }
   }
 
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eAgentSheet],
-                         nsStyleSet::eAgentSheet);
+                         SheetType::Agent);
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eUserSheet],
-                         nsStyleSet::eUserSheet);
+                         SheetType::User);
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eAuthorSheet],
-                         nsStyleSet::eDocSheet);
+                         SheetType::Doc);
 }
 
 static void
 WarnIfSandboxIneffective(nsIDocShell* aDocShell,
                          uint32_t aSandboxFlags,
                          nsIChannel* aChannel)
 {
   // If the document is sandboxed (via the HTML5 iframe sandbox
@@ -4136,17 +4137,17 @@ nsDocument::AddOnDemandBuiltInUASheet(CS
   if (aSheet->IsApplicable()) {
     // This is like |AddStyleSheetToStyleSets|, but for an agent sheet.
     nsCOMPtr<nsIPresShell> shell = GetShell();
     if (shell) {
       // Note that prepending here is necessary to make sure that html.css etc.
       // do not override Firefox OS/Mobile's content.css sheet. Maybe we should
       // have an insertion point to match the order of
       // nsDocumentViewer::CreateStyleSet though?
-      shell->StyleSet()->PrependStyleSheet(nsStyleSet::eAgentSheet, aSheet);
+      shell->StyleSet()->PrependStyleSheet(SheetType::Agent, aSheet);
     }
   }
 
   NotifyStyleSheetAdded(aSheet, false);
 }
 
 int32_t
 nsDocument::GetNumberOfStyleSheets() const
@@ -4368,30 +4369,30 @@ nsDocument::NotifyStyleSheetApplicableSt
     mozilla::services::GetObserverService();
   if (observerService) {
     observerService->NotifyObservers(static_cast<nsIDocument*>(this),
                                      "style-sheet-applicable-state-changed",
                                      nullptr);
   }
 }
 
-static nsStyleSet::sheetType
+static SheetType
 ConvertAdditionalSheetType(nsIDocument::additionalSheetType aType)
 {
   switch(aType) {
     case nsIDocument::eAgentSheet:
-      return nsStyleSet::eAgentSheet;
+      return SheetType::Agent;
     case nsIDocument::eUserSheet:
-      return nsStyleSet::eUserSheet;
+      return SheetType::User;
     case nsIDocument::eAuthorSheet:
-      return nsStyleSet::eDocSheet;
+      return SheetType::Doc;
     default:
-      NS_ASSERTION(false, "wrong type");
+      MOZ_ASSERT(false, "wrong type");
       // we must return something although this should never happen
-      return nsStyleSet::eSheetTypeCount;
+      return SheetType::Count;
   }
 }
 
 static int32_t
 FindSheet(const nsCOMArray<nsIStyleSheet>& aSheets, nsIURI* aSheetURI)
 {
   for (int32_t i = aSheets.Count() - 1; i >= 0; i-- ) {
     bool bEqual;
@@ -4455,17 +4456,17 @@ nsDocument::AddAdditionalStyleSheet(addi
   if (!aSheet->IsApplicable())
     return NS_ERROR_INVALID_ARG;
 
   mAdditionalSheets[aType].AppendObject(aSheet);
 
   BeginUpdate(UPDATE_STYLE);
   nsCOMPtr<nsIPresShell> shell = GetShell();
   if (shell) {
-    nsStyleSet::sheetType type = ConvertAdditionalSheetType(aType);
+    SheetType type = ConvertAdditionalSheetType(aType);
     shell->StyleSet()->AppendStyleSheet(type, aSheet);
   }
 
   // Passing false, so documet.styleSheets.length will not be affected by
   // these additional sheets.
   NotifyStyleSheetAdded(aSheet, false);
   EndUpdate(UPDATE_STYLE);
   return NS_OK;
@@ -4483,17 +4484,17 @@ nsDocument::RemoveAdditionalStyleSheet(a
     nsCOMPtr<nsIStyleSheet> sheetRef = sheets[i];
     sheets.RemoveObjectAt(i);
 
     BeginUpdate(UPDATE_STYLE);
     if (!mIsGoingAway) {
       MOZ_ASSERT(sheetRef->IsApplicable());
       nsCOMPtr<nsIPresShell> shell = GetShell();
       if (shell) {
-        nsStyleSet::sheetType type = ConvertAdditionalSheetType(aType);
+        SheetType type = ConvertAdditionalSheetType(aType);
         shell->StyleSet()->RemoveStyleSheet(type, sheetRef);
       }
     }
 
     // Passing false, so documet.styleSheets.length will not be affected by
     // these additional sheets.
     NotifyStyleSheetRemoved(sheetRef, false);
     EndUpdate(UPDATE_STYLE);
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -1511,17 +1511,17 @@ public:
 protected:
   already_AddRefed<nsIPresShell> doCreateShell(nsPresContext* aContext,
                                                nsViewManager* aViewManager,
                                                nsStyleSet* aStyleSet,
                                                nsCompatibility aCompatMode);
 
   void RemoveDocStyleSheetsFromStyleSets();
   void RemoveStyleSheetsFromStyleSets(nsCOMArray<nsIStyleSheet>& aSheets, 
-                                      nsStyleSet::sheetType aType);
+                                      mozilla::SheetType aType);
   void ResetStylesheetsToURI(nsIURI* aURI);
   void FillStyleSet(nsStyleSet* aStyleSet);
 
   // Return whether all the presshells for this document are safe to flush
   bool IsSafeToFlush() const;
 
   void DispatchPageTransition(mozilla::dom::EventTarget* aDispatchTarget,
                               const nsAString& aType,
@@ -1566,17 +1566,17 @@ protected:
 
   // Weak reference to our sink for in case we no longer have a parser.  This
   // will allow us to flush out any pending stuff from the sink even if
   // EndLoad() has already happened.
   nsWeakPtr mWeakSink;
 
   nsCOMArray<nsIStyleSheet> mStyleSheets;
   nsCOMArray<nsIStyleSheet> mOnDemandBuiltInUASheets;
-  nsCOMArray<nsIStyleSheet> mAdditionalSheets[SheetTypeCount];
+  nsCOMArray<nsIStyleSheet> mAdditionalSheets[AdditionalSheetTypeCount];
 
   // Array of observers
   nsTObserverArray<nsIDocumentObserver*> mObservers;
 
   // Tracker for animations that are waiting to start.
   // nullptr until GetOrCreatePendingAnimationTracker is called.
   RefPtr<mozilla::PendingAnimationTracker> mPendingAnimationTracker;
 
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -4956,26 +4956,26 @@ nsGlobalWindow::GetInnerWidth(int32_t* a
 
   ErrorResult rv;
   *aInnerWidth = GetInnerWidth(rv);
 
   return rv.StealNSResult();
 }
 
 void
-nsGlobalWindow::SetInnerWidthOuter(int32_t aInnerWidth, ErrorResult& aError)
+nsGlobalWindow::SetInnerWidthOuter(int32_t aInnerWidth, ErrorResult& aError, bool aCallerIsChrome)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
 
   if (!mDocShell) {
     aError.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
 
-  CheckSecurityWidthAndHeight(&aInnerWidth, nullptr);
+  CheckSecurityWidthAndHeight(&aInnerWidth, nullptr, aCallerIsChrome);
 
   RefPtr<nsIPresShell> presShell = mDocShell->GetPresShell();
 
   if (presShell && presShell->GetIsViewportOverridden())
   {
     nscoord height = 0;
 
     RefPtr<nsPresContext> presContext;
@@ -4994,38 +4994,38 @@ nsGlobalWindow::SetInnerWidthOuter(int32
   nsCOMPtr<nsIBaseWindow> docShellAsWin(do_QueryInterface(mDocShell));
   docShellAsWin->GetSize(&unused, &height);
   aError = SetDocShellWidthAndHeight(CSSToDevIntPixels(aInnerWidth), height);
 }
 
 void
 nsGlobalWindow::SetInnerWidth(int32_t aInnerWidth, ErrorResult& aError)
 {
-  FORWARD_TO_OUTER_OR_THROW(SetInnerWidthOuter, (aInnerWidth, aError), aError, );
+  FORWARD_TO_OUTER_OR_THROW(SetInnerWidthOuter, (aInnerWidth, aError, nsContentUtils::IsCallerChrome()), aError, );
 }
 
 void
 nsGlobalWindow::SetInnerWidth(JSContext* aCx, JS::Handle<JS::Value> aValue,
                               ErrorResult& aError)
 {
   SetReplaceableWindowCoord(aCx, &nsGlobalWindow::SetInnerWidth,
                             aValue, "innerWidth", aError);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::SetInnerWidth(int32_t aInnerWidth)
 {
-  FORWARD_TO_INNER(SetInnerWidth, (aInnerWidth), NS_ERROR_UNEXPECTED);
+  FORWARD_TO_OUTER(SetInnerWidth, (aInnerWidth), NS_ERROR_UNEXPECTED);
 
   if (IsFrame()) {
     return NS_OK;
   }
 
   ErrorResult rv;
-  SetInnerWidth(aInnerWidth, rv);
+  SetInnerWidthOuter(aInnerWidth, rv, /* aCallerIsChrome = */ true);
 
   return rv.StealNSResult();
 }
 
 int32_t
 nsGlobalWindow::GetInnerHeightOuter(ErrorResult& aError)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
@@ -5057,17 +5057,17 @@ nsGlobalWindow::GetInnerHeight(int32_t* 
 
   ErrorResult rv;
   *aInnerHeight = GetInnerHeight(rv);
 
   return rv.StealNSResult();
 }
 
 void
-nsGlobalWindow::SetInnerHeightOuter(int32_t aInnerHeight, ErrorResult& aError)
+nsGlobalWindow::SetInnerHeightOuter(int32_t aInnerHeight, ErrorResult& aError, bool aCallerIsChrome)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
 
   if (!mDocShell) {
     aError.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
 
@@ -5076,56 +5076,56 @@ nsGlobalWindow::SetInnerHeightOuter(int3
   if (presShell && presShell->GetIsViewportOverridden())
   {
     RefPtr<nsPresContext> presContext;
     presContext = presShell->GetPresContext();
 
     nsRect shellArea = presContext->GetVisibleArea();
     nscoord height = aInnerHeight;
     nscoord width = shellArea.width;
-    CheckSecurityWidthAndHeight(nullptr, &height);
+    CheckSecurityWidthAndHeight(nullptr, &height, aCallerIsChrome);
     SetCSSViewportWidthAndHeight(width,
                                  nsPresContext::CSSPixelsToAppUnits(height));
     return;
   }
 
   int32_t height = 0;
   int32_t width  = 0;
 
   nsCOMPtr<nsIBaseWindow> docShellAsWin(do_QueryInterface(mDocShell));
   docShellAsWin->GetSize(&width, &height);
-  CheckSecurityWidthAndHeight(nullptr, &aInnerHeight);
+  CheckSecurityWidthAndHeight(nullptr, &aInnerHeight, aCallerIsChrome);
   aError = SetDocShellWidthAndHeight(width, CSSToDevIntPixels(aInnerHeight));
 }
 
 void
 nsGlobalWindow::SetInnerHeight(int32_t aInnerHeight, ErrorResult& aError)
 {
-  FORWARD_TO_OUTER_OR_THROW(SetInnerHeightOuter, (aInnerHeight, aError), aError, );
+  FORWARD_TO_OUTER_OR_THROW(SetInnerHeightOuter, (aInnerHeight, aError, nsContentUtils::IsCallerChrome()), aError, );
 }
 
 void
 nsGlobalWindow::SetInnerHeight(JSContext* aCx, JS::Handle<JS::Value> aValue,
                                ErrorResult& aError)
 {
   SetReplaceableWindowCoord(aCx, &nsGlobalWindow::SetInnerHeight,
                             aValue, "innerHeight", aError);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::SetInnerHeight(int32_t aInnerHeight)
 {
-  FORWARD_TO_INNER(SetInnerHeight, (aInnerHeight), NS_ERROR_UNEXPECTED);
+  FORWARD_TO_OUTER(SetInnerHeight, (aInnerHeight), NS_ERROR_UNEXPECTED);
 
   if (IsFrame()) {
     return NS_OK;
   }
 
   ErrorResult rv;
-  SetInnerHeight(aInnerHeight, rv);
+  SetInnerHeightOuter(aInnerHeight, rv, /* aCallerIsChrome = */ true);
 
   return rv.StealNSResult();
 }
 
 nsIntSize
 nsGlobalWindow::GetOuterSize(ErrorResult& aError)
 {
   MOZ_ASSERT(IsOuterWindow());
@@ -5220,28 +5220,29 @@ nsGlobalWindow::GetOuterHeight(int32_t* 
   ErrorResult rv;
   *aOuterHeight = GetOuterHeight(rv);
 
   return rv.StealNSResult();
 }
 
 void
 nsGlobalWindow::SetOuterSize(int32_t aLengthCSSPixels, bool aIsWidth,
-                             ErrorResult& aError)
+                             ErrorResult& aError, bool aCallerIsChrome)
 {
   MOZ_ASSERT(IsOuterWindow());
 
   nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
   if (!treeOwnerAsWin) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
 
   CheckSecurityWidthAndHeight(aIsWidth ? &aLengthCSSPixels : nullptr,
-                              aIsWidth ? nullptr : &aLengthCSSPixels);
+                              aIsWidth ? nullptr : &aLengthCSSPixels,
+                              aCallerIsChrome);
 
   int32_t width, height;
   aError = treeOwnerAsWin->GetSize(&width, &height);
   if (aError.Failed()) {
     return;
   }
 
   int32_t lengthDevPixels = CSSToDevIntPixels(aLengthCSSPixels);
@@ -5249,85 +5250,85 @@ nsGlobalWindow::SetOuterSize(int32_t aLe
     width = lengthDevPixels;
   } else {
     height = lengthDevPixels;
   }
   aError = treeOwnerAsWin->SetSize(width, height, true);    
 }
 
 void
-nsGlobalWindow::SetOuterWidthOuter(int32_t aOuterWidth, ErrorResult& aError)
+nsGlobalWindow::SetOuterWidthOuter(int32_t aOuterWidth, ErrorResult& aError, bool aCallerIsChrome)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
 
-  SetOuterSize(aOuterWidth, true, aError);
+  SetOuterSize(aOuterWidth, true, aError, aCallerIsChrome);
 }
 
 void
 nsGlobalWindow::SetOuterWidth(int32_t aOuterWidth, ErrorResult& aError)
 {
-  FORWARD_TO_OUTER_OR_THROW(SetOuterWidthOuter, (aOuterWidth, aError), aError, );
+  FORWARD_TO_OUTER_OR_THROW(SetOuterWidthOuter, (aOuterWidth, aError, nsContentUtils::IsCallerChrome()), aError, );
 }
 
 void
 nsGlobalWindow::SetOuterWidth(JSContext* aCx, JS::Handle<JS::Value> aValue,
                               ErrorResult& aError)
 {
   SetReplaceableWindowCoord(aCx, &nsGlobalWindow::SetOuterWidth,
                             aValue, "outerWidth", aError);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::SetOuterWidth(int32_t aOuterWidth)
 {
-  FORWARD_TO_INNER(SetOuterWidth, (aOuterWidth), NS_ERROR_UNEXPECTED);
+  FORWARD_TO_OUTER(SetOuterWidth, (aOuterWidth), NS_ERROR_UNEXPECTED);
 
   if (IsFrame()) {
     return NS_OK;
   }
 
   ErrorResult rv;
-  SetOuterWidth(aOuterWidth, rv);
-
-  return rv.StealNSResult();
-}
-
-void
-nsGlobalWindow::SetOuterHeightOuter(int32_t aOuterHeight, ErrorResult& aError)
+  SetOuterWidthOuter(aOuterWidth, rv, /* aCallerIsChrome = */ true);
+
+  return rv.StealNSResult();
+}
+
+void
+nsGlobalWindow::SetOuterHeightOuter(int32_t aOuterHeight, ErrorResult& aError, bool aCallerIsChrome)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
 
-  SetOuterSize(aOuterHeight, false, aError);
+  SetOuterSize(aOuterHeight, false, aError, aCallerIsChrome);
 }
 
 void
 nsGlobalWindow::SetOuterHeight(int32_t aOuterHeight, ErrorResult& aError)
 {
-  FORWARD_TO_OUTER_OR_THROW(SetOuterHeightOuter, (aOuterHeight, aError), aError, );
+  FORWARD_TO_OUTER_OR_THROW(SetOuterHeightOuter, (aOuterHeight, aError, nsContentUtils::IsCallerChrome()), aError, );
 }
 
 void
 nsGlobalWindow::SetOuterHeight(JSContext* aCx, JS::Handle<JS::Value> aValue,
                                ErrorResult& aError)
 {
   SetReplaceableWindowCoord(aCx, &nsGlobalWindow::SetOuterHeight,
                             aValue, "outerHeight", aError);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::SetOuterHeight(int32_t aOuterHeight)
 {
-  FORWARD_TO_INNER(SetOuterHeight, (aOuterHeight), NS_ERROR_UNEXPECTED);
+  FORWARD_TO_OUTER(SetOuterHeight, (aOuterHeight), NS_ERROR_UNEXPECTED);
 
   if (IsFrame()) {
     return NS_OK;
   }
 
   ErrorResult rv;
-  SetOuterHeight(aOuterHeight, rv);
+  SetOuterHeightOuter(aOuterHeight, rv, /* aCallerIsChrome = */ true);
 
   return rv.StealNSResult();
 }
 
 nsIntPoint
 nsGlobalWindow::GetScreenXY(ErrorResult& aError)
 {
   MOZ_ASSERT(IsOuterWindow());
@@ -5639,63 +5640,63 @@ nsGlobalWindow::MatchMedia(const nsAStri
   ErrorResult rv;
   RefPtr<MediaQueryList> mediaQueryList = MatchMedia(aMediaQueryList, rv);
   mediaQueryList.forget(aResult);
 
   return rv.StealNSResult();
 }
 
 void
-nsGlobalWindow::SetScreenXOuter(int32_t aScreenX, ErrorResult& aError)
+nsGlobalWindow::SetScreenXOuter(int32_t aScreenX, ErrorResult& aError, bool aCallerIsChrome)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
 
   nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
   if (!treeOwnerAsWin) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
 
   int32_t x, y;
   aError = treeOwnerAsWin->GetPosition(&x, &y);
   if (aError.Failed()) {
     return;
   }
 
-  CheckSecurityLeftAndTop(&aScreenX, nullptr);
+  CheckSecurityLeftAndTop(&aScreenX, nullptr, aCallerIsChrome);
   x = CSSToDevIntPixels(aScreenX);
 
   aError = treeOwnerAsWin->SetPosition(x, y);
 }
 
 void
 nsGlobalWindow::SetScreenX(int32_t aScreenX, ErrorResult& aError)
 {
-  FORWARD_TO_OUTER_OR_THROW(SetScreenXOuter, (aScreenX, aError), aError, );
+  FORWARD_TO_OUTER_OR_THROW(SetScreenXOuter, (aScreenX, aError, nsContentUtils::IsCallerChrome()), aError, );
 }
 
 void
 nsGlobalWindow::SetScreenX(JSContext* aCx, JS::Handle<JS::Value> aValue,
                            ErrorResult& aError)
 {
   SetReplaceableWindowCoord(aCx, &nsGlobalWindow::SetScreenX,
                             aValue, "screenX", aError);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::SetScreenX(int32_t aScreenX)
 {
-  FORWARD_TO_INNER(SetScreenX, (aScreenX), NS_ERROR_UNEXPECTED);
+  FORWARD_TO_OUTER(SetScreenX, (aScreenX), NS_ERROR_UNEXPECTED);
 
   if (IsFrame()) {
     return NS_OK;
   }
 
   ErrorResult rv;
-  SetScreenX(aScreenX, rv);
+  SetScreenXOuter(aScreenX, rv, /* aCallerIsChrome = */ true);
 
   return rv.StealNSResult();
 }
 
 int32_t
 nsGlobalWindow::GetScreenYOuter(ErrorResult& aError)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
@@ -5725,76 +5726,76 @@ nsGlobalWindow::GetScreenY(JSContext* aC
                            JS::MutableHandle<JS::Value> aValue,
                            ErrorResult& aError)
 {
   GetReplaceableWindowCoord(aCx, &nsGlobalWindow::GetScreenY, aValue,
                             aError);
 }
 
 void
-nsGlobalWindow::SetScreenYOuter(int32_t aScreenY, ErrorResult& aError)
+nsGlobalWindow::SetScreenYOuter(int32_t aScreenY, ErrorResult& aError, bool aCallerIsChrome)
 {
   MOZ_RELEASE_ASSERT(IsOuterWindow());
 
   nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
   if (!treeOwnerAsWin) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
 
   int32_t x, y;
   aError = treeOwnerAsWin->GetPosition(&x, &y);
   if (aError.Failed()) {
     return;
   }
 
-  CheckSecurityLeftAndTop(nullptr, &aScreenY);
+  CheckSecurityLeftAndTop(nullptr, &aScreenY, aCallerIsChrome);
   y = CSSToDevIntPixels(aScreenY);
 
   aError = treeOwnerAsWin->SetPosition(x, y);
 }
 
 void
 nsGlobalWindow::SetScreenY(int32_t aScreenY, ErrorResult& aError)
 {
-  FORWARD_TO_OUTER_OR_THROW(SetScreenYOuter, (aScreenY, aError), aError, );
+  FORWARD_TO_OUTER_OR_THROW(SetScreenYOuter, (aScreenY, aError, nsContentUtils::IsCallerChrome()), aError, );
 }
 
 void
 nsGlobalWindow::SetScreenY(JSContext* aCx, JS::Handle<JS::Value> aValue,
                            ErrorResult& aError)
 {
   SetReplaceableWindowCoord(aCx, &nsGlobalWindow::SetScreenY,
                             aValue, "screenY", aError);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::SetScreenY(int32_t aScreenY)
 {
-  FORWARD_TO_INNER(SetScreenY, (aScreenY), NS_ERROR_UNEXPECTED);
+  FORWARD_TO_OUTER(SetScreenY, (aScreenY), NS_ERROR_UNEXPECTED);
 
   if (IsFrame()) {
     return NS_OK;
   }
 
   ErrorResult rv;
-  SetScreenY(aScreenY, rv);
+  SetScreenYOuter(aScreenY, rv, /* aCallerIsChrome = */ true);
 
   return rv.StealNSResult();
 }
 
 // NOTE: Arguments to this function should have values scaled to
 // CSS pixels, not device pixels.
 void
-nsGlobalWindow::CheckSecurityWidthAndHeight(int32_t* aWidth, int32_t* aHeight)
+nsGlobalWindow::CheckSecurityWidthAndHeight(int32_t* aWidth, int32_t* aHeight, bool aCallerIsChrome)
 {
   MOZ_ASSERT(IsOuterWindow());
 
 #ifdef MOZ_XUL
-  if (!nsContentUtils::IsCallerChrome()) {
+  if (!aCallerIsChrome) {
     // if attempting to resize the window, hide any open popups
     nsContentUtils::HidePopupsInDocument(mDoc);
   }
 #endif
 
   // This one is easy. Just ensure the variable is greater than 100;
   if ((aWidth && *aWidth < 100) || (aHeight && *aHeight < 100)) {
     // Check security state for use in determing window dimensions
@@ -5843,25 +5844,25 @@ nsGlobalWindow::SetCSSViewportWidthAndHe
   shellArea.width = aInnerWidth;
 
   presContext->SetVisibleArea(shellArea);
 }
 
 // NOTE: Arguments to this function should have values scaled to
 // CSS pixels, not device pixels.
 void
-nsGlobalWindow::CheckSecurityLeftAndTop(int32_t* aLeft, int32_t* aTop)
+nsGlobalWindow::CheckSecurityLeftAndTop(int32_t* aLeft, int32_t* aTop, bool aCallerIsChrome)
 {
   MOZ_ASSERT(IsOuterWindow());
 
   // This one is harder. We have to get the screen size and window dimensions.
 
   // Check security state for use in determing window dimensions
 
-  if (!nsContentUtils::IsCallerChrome()) {
+  if (!aCallerIsChrome) {
 #ifdef MOZ_XUL
     // if attempting to move the window, hide any open popups
     nsContentUtils::HidePopupsInDocument(mDoc);
 #endif
 
     nsGlobalWindow* rootWindow =
       static_cast<nsGlobalWindow*>(GetPrivateRoot());
     if (rootWindow) {
@@ -7618,17 +7619,17 @@ nsGlobalWindow::MoveToOuter(int32_t aXPo
   nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
   if (!treeOwnerAsWin) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
 
   // Mild abuse of a "size" object so we don't need more helper functions.
   nsIntSize cssPos(aXPos, aYPos);
-  CheckSecurityLeftAndTop(&cssPos.width, &cssPos.height);
+  CheckSecurityLeftAndTop(&cssPos.width, &cssPos.height, aCallerIsChrome);
 
   nsIntSize devPos = CSSToDevIntPixels(cssPos);
 
   aError = treeOwnerAsWin->SetPosition(devPos.width, devPos.height);
 }
 
 void
 nsGlobalWindow::MoveTo(int32_t aXPos, int32_t aYPos, ErrorResult& aError)
@@ -7678,17 +7679,17 @@ nsGlobalWindow::MoveByOuter(int32_t aXDi
   }
 
   // mild abuse of a "size" object so we don't need more helper functions
   nsIntSize cssPos(DevToCSSIntPixels(nsIntSize(x, y)));
 
   cssPos.width += aXDif;
   cssPos.height += aYDif;
   
-  CheckSecurityLeftAndTop(&cssPos.width, &cssPos.height);
+  CheckSecurityLeftAndTop(&cssPos.width, &cssPos.height, aCallerIsChrome);
 
   nsIntSize newDevPos(CSSToDevIntPixels(cssPos));
 
   aError = treeOwnerAsWin->SetPosition(newDevPos.width, newDevPos.height);
 }
 
 void
 nsGlobalWindow::MoveBy(int32_t aXDif, int32_t aYDif, ErrorResult& aError)
@@ -7736,17 +7737,17 @@ nsGlobalWindow::ResizeToOuter(int32_t aW
 
   nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
   if (!treeOwnerAsWin) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
   
   nsIntSize cssSize(aWidth, aHeight);
-  CheckSecurityWidthAndHeight(&cssSize.width, &cssSize.height);
+  CheckSecurityWidthAndHeight(&cssSize.width, &cssSize.height, aCallerIsChrome);
 
   nsIntSize devSz(CSSToDevIntPixels(cssSize));
 
   aError = treeOwnerAsWin->SetSize(devSz.width, devSz.height, true);
 }
 
 void
 nsGlobalWindow::ResizeTo(int32_t aWidth, int32_t aHeight, ErrorResult& aError)
@@ -7816,17 +7817,17 @@ nsGlobalWindow::ResizeByOuter(int32_t aW
   // into CSS pixels, add the arguments, do the security check, and
   // then convert back to device pixels for the call to SetSize.
 
   nsIntSize cssSize(DevToCSSIntPixels(nsIntSize(width, height)));
 
   cssSize.width += aWidthDif;
   cssSize.height += aHeightDif;
 
-  CheckSecurityWidthAndHeight(&cssSize.width, &cssSize.height);
+  CheckSecurityWidthAndHeight(&cssSize.width, &cssSize.height, aCallerIsChrome);
 
   nsIntSize newDevSize(CSSToDevIntPixels(cssSize));
 
   aError = treeOwnerAsWin->SetSize(newDevSize.width, newDevSize.height, true);
 }
 
 void
 nsGlobalWindow::ResizeBy(int32_t aWidthDif, int32_t aHeightDif,
@@ -7883,17 +7884,17 @@ nsGlobalWindow::SizeToContentOuter(Error
   // rules.
   nsCOMPtr<nsIDocShellTreeOwner> treeOwner = GetTreeOwner();
   if (!treeOwner) {
     aError.Throw(NS_ERROR_FAILURE);
     return;
   }
 
   nsIntSize cssSize(DevToCSSIntPixels(nsIntSize(width, height)));
-  CheckSecurityWidthAndHeight(&cssSize.width, &cssSize.height);
+  CheckSecurityWidthAndHeight(&cssSize.width, &cssSize.height, aCallerIsChrome);
 
   nsIntSize newDevSize(CSSToDevIntPixels(cssSize));
 
   aError = treeOwner->SizeShellTo(mDocShell, newDevSize.width,
                                   newDevSize.height);
 }
 
 void
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -1202,37 +1202,37 @@ protected:
                                  mozilla::ErrorResult& aError);
   void SetReplaceableWindowCoord(JSContext* aCx, WindowCoordSetter aSetter,
                                  JS::Handle<JS::Value> aValue,
                                  const char* aPropName,
                                  mozilla::ErrorResult& aError);
   // And the implementations of WindowCoordGetter/WindowCoordSetter.
   int32_t GetInnerWidthOuter(mozilla::ErrorResult& aError);
   int32_t GetInnerWidth(mozilla::ErrorResult& aError);
-  void SetInnerWidthOuter(int32_t aInnerWidth, mozilla::ErrorResult& aError);
+  void SetInnerWidthOuter(int32_t aInnerWidth, mozilla::ErrorResult& aError, bool aCallerIsChrome);
   void SetInnerWidth(int32_t aInnerWidth, mozilla::ErrorResult& aError);
   int32_t GetInnerHeightOuter(mozilla::ErrorResult& aError);
   int32_t GetInnerHeight(mozilla::ErrorResult& aError);
-  void SetInnerHeightOuter(int32_t aInnerHeight, mozilla::ErrorResult& aError);
+  void SetInnerHeightOuter(int32_t aInnerHeight, mozilla::ErrorResult& aError, bool aCallerIsChrome);
   void SetInnerHeight(int32_t aInnerHeight, mozilla::ErrorResult& aError);
   int32_t GetScreenXOuter(mozilla::ErrorResult& aError);
   int32_t GetScreenX(mozilla::ErrorResult& aError);
-  void SetScreenXOuter(int32_t aScreenX, mozilla::ErrorResult& aError);
+  void SetScreenXOuter(int32_t aScreenX, mozilla::ErrorResult& aError, bool aCallerIsChrome);
   void SetScreenX(int32_t aScreenX, mozilla::ErrorResult& aError);
   int32_t GetScreenYOuter(mozilla::ErrorResult& aError);
   int32_t GetScreenY(mozilla::ErrorResult& aError);
-  void SetScreenYOuter(int32_t aScreenY, mozilla::ErrorResult& aError);
+  void SetScreenYOuter(int32_t aScreenY, mozilla::ErrorResult& aError, bool aCallerIsChrome);
   void SetScreenY(int32_t aScreenY, mozilla::ErrorResult& aError);
   int32_t GetOuterWidthOuter(mozilla::ErrorResult& aError);
   int32_t GetOuterWidth(mozilla::ErrorResult& aError);
-  void SetOuterWidthOuter(int32_t aOuterWidth, mozilla::ErrorResult& aError);
+  void SetOuterWidthOuter(int32_t aOuterWidth, mozilla::ErrorResult& aError, bool aCallerIsChrome);
   void SetOuterWidth(int32_t aOuterWidth, mozilla::ErrorResult& aError);
   int32_t GetOuterHeightOuter(mozilla::ErrorResult& aError);
   int32_t GetOuterHeight(mozilla::ErrorResult& aError);
-  void SetOuterHeightOuter(int32_t aOuterHeight, mozilla::ErrorResult& aError);
+  void SetOuterHeightOuter(int32_t aOuterHeight, mozilla::ErrorResult& aError, bool aCallerIsChrome);
   void SetOuterHeight(int32_t aOuterHeight, mozilla::ErrorResult& aError);
 
   // Array of idle observers that are notified of idle events.
   nsTObserverArray<IdleObserverHolder> mIdleObservers;
 
   // Idle timer used for function callbacks to notify idle observers.
   nsCOMPtr<nsITimer> mIdleTimer;
 
@@ -1452,18 +1452,18 @@ public:
 
   // Inner windows only.
   nsresult FireHashchange(const nsAString &aOldURL, const nsAString &aNewURL);
 
   void FlushPendingNotifications(mozFlushType aType);
 
   // Outer windows only.
   void EnsureReflowFlushAndPaint();
-  void CheckSecurityWidthAndHeight(int32_t* width, int32_t* height);
-  void CheckSecurityLeftAndTop(int32_t* left, int32_t* top);
+  void CheckSecurityWidthAndHeight(int32_t* width, int32_t* height, bool aCallerIsChrome);
+  void CheckSecurityLeftAndTop(int32_t* left, int32_t* top, bool aCallerIsChrome);
 
   // Outer windows only.
   // Arguments to this function should have values in app units
   void SetCSSViewportWidthAndHeight(nscoord width, nscoord height);
   // Arguments to this function should have values in device pixels
   nsresult SetDocShellWidthAndHeight(int32_t width, int32_t height);
 
   static bool CanSetProperty(const char *aPrefName);
@@ -1481,17 +1481,17 @@ public:
   void GetScrollMaxXYOuter(int32_t* aScrollMaxX, int32_t* aScrollMaxY);
   void GetScrollMaxXY(int32_t* aScrollMaxX, int32_t* aScrollMaxY,
                       mozilla::ErrorResult& aError);
 
   // Outer windows only.
   nsresult GetInnerSize(mozilla::CSSIntSize& aSize);
   nsIntSize GetOuterSize(mozilla::ErrorResult& aError);
   void SetOuterSize(int32_t aLengthCSSPixels, bool aIsWidth,
-                    mozilla::ErrorResult& aError);
+                    mozilla::ErrorResult& aError, bool aCallerIsChrome);
   nsRect GetInnerScreenRect();
 
   void ScrollTo(const mozilla::CSSIntPoint& aScroll,
                 const mozilla::dom::ScrollOptions& aOptions);
 
   bool IsFrame()
   {
     return GetParentInternal() != nullptr;
--- a/dom/base/nsHostObjectProtocolHandler.cpp
+++ b/dom/base/nsHostObjectProtocolHandler.cpp
@@ -566,21 +566,24 @@ nsHostObjectProtocolHandler::NewChannel2
 
   ErrorResult rv;
   nsCOMPtr<nsIInputStream> stream;
   blob->GetInternalStream(getter_AddRefs(stream), rv);
   if (NS_WARN_IF(rv.Failed())) {
     return rv.StealNSResult();
   }
 
+  nsAutoString contentType;
+  blob->GetType(contentType);
+
   nsCOMPtr<nsIChannel> channel;
   rv = NS_NewInputStreamChannelInternal(getter_AddRefs(channel),
                                         uri,
                                         stream,
-                                        EmptyCString(), // aContentType
+                                        NS_ConvertUTF16toUTF8(contentType),
                                         EmptyCString(), // aContentCharset
                                         aLoadInfo);
   if (NS_WARN_IF(rv.Failed())) {
     return rv.StealNSResult();
   }
 
   nsString type;
   blob->GetType(type);
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -939,17 +939,17 @@ public:
    */
   virtual void SetStyleSheetApplicableState(nsIStyleSheet* aSheet,
                                             bool aApplicable) = 0;
 
   enum additionalSheetType {
     eAgentSheet,
     eUserSheet,
     eAuthorSheet,
-    SheetTypeCount
+    AdditionalSheetTypeCount
   };
 
   virtual nsresult LoadAdditionalStyleSheet(additionalSheetType aType, nsIURI* aSheetURI) = 0;
   virtual nsresult AddAdditionalStyleSheet(additionalSheetType aType, nsIStyleSheet* aSheet) = 0;
   virtual void RemoveAdditionalStyleSheet(additionalSheetType aType, nsIURI* sheetURI) = 0;
   virtual nsIStyleSheet* FirstAdditionalAuthorSheet() = 0;
 
   /**
--- a/dom/base/nsScriptLoader.cpp
+++ b/dom/base/nsScriptLoader.cpp
@@ -270,59 +270,92 @@ nsScriptLoader::ShouldLoadScript(nsIDocu
   rv = CheckContentPolicy(aDocument, aContext, aURI, aType, aIsPreLoad);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   return NS_OK;
 }
 
+class ContextMediator : public nsIStreamLoaderObserver
+{
+public:
+  explicit ContextMediator(nsScriptLoader *aScriptLoader, nsISupports *aContext)
+  : mScriptLoader(aScriptLoader)
+  , mContext(aContext) {}
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISTREAMLOADEROBSERVER
+
+private:
+  virtual ~ContextMediator() {}
+  RefPtr<nsScriptLoader> mScriptLoader;
+  nsCOMPtr<nsISupports>  mContext;
+};
+
+NS_IMPL_ISUPPORTS(ContextMediator, nsIStreamLoaderObserver)
+
+NS_IMETHODIMP
+ContextMediator::OnStreamComplete(nsIStreamLoader* aLoader,
+                                  nsISupports* aContext,
+                                  nsresult aStatus,
+                                  uint32_t aStringLen,
+                                  const uint8_t* aString)
+{
+  // pass arguments through except for the aContext,
+  // we have to mediate and use mContext instead.
+  return mScriptLoader->OnStreamComplete(aLoader, mContext, aStatus,
+                                         aStringLen, aString);
+}
+
 nsresult
 nsScriptLoader::StartLoad(nsScriptLoadRequest *aRequest, const nsAString &aType,
                           bool aScriptFromHead)
 {
-  nsISupports *context = aRequest->mElement.get()
-                         ? static_cast<nsISupports *>(aRequest->mElement.get())
-                         : static_cast<nsISupports *>(mDocument);
-  nsresult rv = ShouldLoadScript(mDocument, context, aRequest->mURI, aType, aRequest->IsPreload());
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-
-  nsCOMPtr<nsILoadGroup> loadGroup = mDocument->GetDocumentLoadGroup();
-
-  nsCOMPtr<nsPIDOMWindow> window(do_QueryInterface(mDocument->MasterDocument()->GetWindow()));
-
-  if (!window) {
-    return NS_ERROR_NULL_POINTER;
-  }
-
-  nsIDocShell *docshell = window->GetDocShell();
-
-  nsCOMPtr<nsIInterfaceRequestor> prompter(do_QueryInterface(docshell));
-
   // If this document is sandboxed without 'allow-scripts', abort.
   if (mDocument->GetSandboxFlags() & SANDBOXED_SCRIPTS) {
     return NS_OK;
   }
 
   nsContentPolicyType contentPolicyType = aRequest->IsPreload()
                                           ? nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD
                                           : nsIContentPolicy::TYPE_INTERNAL_SCRIPT;
+  nsCOMPtr<nsINode> context;
+  if (aRequest->mElement) {
+    context = do_QueryInterface(aRequest->mElement);
+  }
+  else {
+    context = mDocument;
+  }
+
+  nsCOMPtr<nsILoadGroup> loadGroup = mDocument->GetDocumentLoadGroup();
+  nsCOMPtr<nsPIDOMWindow> window(do_QueryInterface(mDocument->MasterDocument()->GetWindow()));
+  NS_ENSURE_TRUE(window, NS_ERROR_NULL_POINTER);
+  nsIDocShell *docshell = window->GetDocShell();
+  nsCOMPtr<nsIInterfaceRequestor> prompter(do_QueryInterface(docshell));
+
+  nsSecurityFlags securityFlags =
+    aRequest->mCORSMode == CORS_NONE
+    ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL
+    : nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS;
+  if (aRequest->mCORSMode == CORS_USE_CREDENTIALS) {
+    securityFlags |= nsILoadInfo::SEC_REQUIRE_CORS_WITH_CREDENTIALS;
+  }
+  securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME;
 
   nsCOMPtr<nsIChannel> channel;
-  rv = NS_NewChannel(getter_AddRefs(channel),
-                     aRequest->mURI,
-                     mDocument,
-                     nsILoadInfo::SEC_NORMAL,
-                     contentPolicyType,
-                     loadGroup,
-                     prompter,
-                     nsIRequest::LOAD_NORMAL |
-                     nsIChannel::LOAD_CLASSIFY_URI);
+  nsresult rv = NS_NewChannel(getter_AddRefs(channel),
+                              aRequest->mURI,
+                              context,
+                              securityFlags,
+                              contentPolicyType,
+                              loadGroup,
+                              prompter,
+                              nsIRequest::LOAD_NORMAL |
+                              nsIChannel::LOAD_CLASSIFY_URI);
 
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsIScriptElement *script = aRequest->mElement;
   nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel));
 
   if (cos) {
     if (aScriptFromHead &&
@@ -351,36 +384,23 @@ nsScriptLoader::StartLoad(nsScriptLoadRe
       nsINetworkPredictor::LEARN_LOAD_SUBRESOURCE, loadContext);
 
   // Set the initiator type
   nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChannel));
   if (timedChannel) {
     timedChannel->SetInitiatorType(NS_LITERAL_STRING("script"));
   }
 
+  RefPtr<ContextMediator> mediator = new ContextMediator(this, aRequest);
+
   nsCOMPtr<nsIStreamLoader> loader;
-  rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
+  rv = NS_NewStreamLoader(getter_AddRefs(loader), mediator);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  nsCOMPtr<nsIStreamListener> listener = loader.get();
-
-  if (aRequest->mCORSMode != CORS_NONE) {
-    bool withCredentials = (aRequest->mCORSMode == CORS_USE_CREDENTIALS);
-    RefPtr<nsCORSListenerProxy> corsListener =
-      new nsCORSListenerProxy(listener, mDocument->NodePrincipal(),
-                              withCredentials);
-    rv = corsListener->Init(channel, DataURIHandling::Allow);
-    NS_ENSURE_SUCCESS(rv, rv);
-    listener = corsListener;
-  }
-
-  rv = channel->AsyncOpen(listener, aRequest);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  return NS_OK;
+  return channel->AsyncOpen2(loader);
 }
 
 bool
 nsScriptLoader::PreloadURIComparator::Equals(const PreloadInfo &aPi,
                                              nsIURI * const &aURI) const
 {
   bool same;
   return NS_SUCCEEDED(aPi.mRequest->mURI->Equals(aURI, &same)) &&
--- a/dom/base/nsXMLHttpRequest.cpp
+++ b/dom/base/nsXMLHttpRequest.cpp
@@ -46,20 +46,17 @@
 #include "nsIDOMWindow.h"
 #include "nsIVariant.h"
 #include "nsVariant.h"
 #include "nsIScriptError.h"
 #include "nsIStreamConverterService.h"
 #include "nsICachingChannel.h"
 #include "nsContentUtils.h"
 #include "nsCycleCollectionParticipant.h"
-#include "nsIContentPolicy.h"
-#include "nsContentPolicyUtils.h"
 #include "nsError.h"
-#include "nsCORSListenerProxy.h"
 #include "nsIHTMLDocument.h"
 #include "nsIStorageStream.h"
 #include "nsIPromptFactory.h"
 #include "nsIWindowWatcher.h"
 #include "nsIConsoleService.h"
 #include "nsIContentSecurityPolicy.h"
 #include "nsAsyncRedirectVerifyHelper.h"
 #include "nsStringBuffer.h"
@@ -121,17 +118,17 @@ using namespace mozilla::dom;
 // The above states are mutually exclusive, change with ChangeState() only.
 // The states below can be combined.
 #define XML_HTTP_REQUEST_ABORTED        (1 << 7)  // Internal
 #define XML_HTTP_REQUEST_ASYNC          (1 << 8)  // Internal
 #define XML_HTTP_REQUEST_PARSEBODY      (1 << 9)  // Internal
 #define XML_HTTP_REQUEST_SYNCLOOPING    (1 << 10) // Internal
 #define XML_HTTP_REQUEST_BACKGROUND     (1 << 13) // Internal
 #define XML_HTTP_REQUEST_USE_XSITE_AC   (1 << 14) // Internal
-#define XML_HTTP_REQUEST_NEED_AC_PREFLIGHT (1 << 15) // Internal
+#define XML_HTTP_REQUEST_NEED_AC_PREFLIGHT_IF_XSITE (1 << 15) // Internal
 #define XML_HTTP_REQUEST_AC_WITH_CREDENTIALS (1 << 16) // Internal
 #define XML_HTTP_REQUEST_TIMED_OUT (1 << 17) // Internal
 #define XML_HTTP_REQUEST_DELETED (1 << 18) // Internal
 
 #define XML_HTTP_REQUEST_LOADSTATES         \
   (XML_HTTP_REQUEST_UNSENT |                \
    XML_HTTP_REQUEST_OPENED |                \
    XML_HTTP_REQUEST_HEADERS_RECEIVED |      \
@@ -1528,57 +1525,25 @@ nsXMLHttpRequest::GetCurrentJARChannel()
 }
 
 bool
 nsXMLHttpRequest::IsSystemXHR()
 {
   return mIsSystem || nsContentUtils::IsSystemPrincipal(mPrincipal);
 }
 
-nsresult
+void
 nsXMLHttpRequest::CheckChannelForCrossSiteRequest(nsIChannel* aChannel)
 {
   // A system XHR (chrome code or a web app with the right permission) can
-  // always perform cross-site requests. In the web app case, however, we
-  // must still check for protected URIs like file:///.
-  if (IsSystemXHR()) {
-    if (!nsContentUtils::IsSystemPrincipal(mPrincipal)) {
-      nsIScriptSecurityManager *secMan = nsContentUtils::GetSecurityManager();
-      nsCOMPtr<nsIURI> uri;
-      aChannel->GetOriginalURI(getter_AddRefs(uri));
-      return secMan->CheckLoadURIWithPrincipal(
-        mPrincipal, uri, nsIScriptSecurityManager::STANDARD);
-    }
-    return NS_OK;
-  }
-
-  // If this is a same-origin request or the channel's URI inherits
-  // its principal, it's allowed.
-  if (nsContentUtils::CheckMayLoad(mPrincipal, aChannel, true)) {
-    return NS_OK;
+  // load anything, and same-origin loads are always allowed.
+  if (!IsSystemXHR() &&
+      !nsContentUtils::CheckMayLoad(mPrincipal, aChannel, true)) {
+    mState |= XML_HTTP_REQUEST_USE_XSITE_AC;
   }
-
-  // This is a cross-site request
-  mState |= XML_HTTP_REQUEST_USE_XSITE_AC;
-
-  // Check if we need to do a preflight request.
-  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
-  NS_ENSURE_TRUE(httpChannel, NS_ERROR_DOM_BAD_URI);
-
-  nsAutoCString method;
-  httpChannel->GetRequestMethod(method);
-  if (!mCORSUnsafeHeaders.IsEmpty() ||
-      (mUpload && mUpload->HasListeners()) ||
-      (!method.LowerCaseEqualsLiteral("get") &&
-       !method.LowerCaseEqualsLiteral("post") &&
-       !method.LowerCaseEqualsLiteral("head"))) {
-    mState |= XML_HTTP_REQUEST_NEED_AC_PREFLIGHT;
-  }
-
-  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsXMLHttpRequest::Open(const nsACString& method, const nsACString& url,
                        bool async, const nsAString& user,
                        const nsAString& password, uint8_t optional_argc)
 {
   if (!optional_argc) {
@@ -1675,31 +1640,16 @@ nsXMLHttpRequest::Open(const nsACString&
     baseURI = doc->GetBaseURI();
   }
 
   rv = NS_NewURI(getter_AddRefs(uri), url, nullptr, baseURI);
   if (NS_FAILED(rv)) return rv;
 
   rv = CheckInnerWindowCorrectness();
   NS_ENSURE_SUCCESS(rv, rv);
-  int16_t shouldLoad = nsIContentPolicy::ACCEPT;
-  rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST,
-                                 uri,
-                                 mPrincipal,
-                                 doc,
-                                 EmptyCString(), //mime guess
-                                 nullptr,         //extra
-                                 &shouldLoad,
-                                 nsContentUtils::GetContentPolicy(),
-                                 nsContentUtils::GetSecurityManager());
-  if (NS_FAILED(rv)) return rv;
-  if (NS_CP_REJECTED(shouldLoad)) {
-    // Disallowed by content policy
-    return NS_ERROR_CONTENT_BLOCKED;
-  }
 
   // XXXbz this is wrong: we should only be looking at whether
   // user/password were passed, not at the values!  See bug 759624.
   if (user.WasPassed() && !user.Value().IsEmpty()) {
     nsAutoCString userpass;
     CopyUTF16toUTF8(user.Value(), userpass);
     if (password.WasPassed() && !password.Value().IsEmpty()) {
       userpass.Append(':');
@@ -1712,30 +1662,37 @@ nsXMLHttpRequest::Open(const nsACString&
   // operations will merge/override correctly.
   mAlreadySetHeaders.Clear();
 
   // When we are called from JS we can find the load group for the page,
   // and add ourselves to it. This way any pending requests
   // will be automatically aborted if the user leaves the page.
   nsCOMPtr<nsILoadGroup> loadGroup = GetLoadGroup();
 
-  nsSecurityFlags secFlags = nsILoadInfo::SEC_NORMAL;
+  nsSecurityFlags secFlags;
   nsLoadFlags loadFlags = nsIRequest::LOAD_BACKGROUND;
-  if (IsSystemXHR()) {
-    // Don't give this document the system principal.  We need to keep track of
-    // mPrincipal being system because we use it for various security checks
-    // that should be passing, but the document data shouldn't get a system
-    // principal.  Hence we set the sandbox flag in loadinfo, so that 
-    // GetChannelResultPrincipal will give us the nullprincipal.
-    secFlags |= nsILoadInfo::SEC_SANDBOXED;
-
-    //For a XHR, disable interception
+  if (nsContentUtils::IsSystemPrincipal(mPrincipal)) {
+    // When chrome is loading we want to make sure to sandbox any potential
+    // result document. We also want to allow cross-origin loads.
+    secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL |
+               nsILoadInfo::SEC_SANDBOXED;
+  }
+  else if (IsSystemXHR()) {
+    // For pages that have appropriate permissions, we want to still allow
+    // cross-origin loads, but make sure that the any potential result
+    // documents get the same principal as the loader.
+    secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
+               nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL;
     loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
-  } else {
-    secFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL;
+  }
+  else {
+    // Otherwise use CORS. Again, make sure that potential result documents
+    // use the same principal as the loader.
+    secFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS |
+               nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL;
   }
 
   // If we have the document, use it
   if (doc) {
     rv = NS_NewChannel(getter_AddRefs(mChannel),
                        uri,
                        doc,
                        secFlags,
@@ -1750,36 +1707,36 @@ nsXMLHttpRequest::Open(const nsACString&
                        mPrincipal,
                        secFlags,
                        nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST,
                        loadGroup,
                        nullptr,   // aCallbacks
                        loadFlags);
   }
 
-  if (NS_FAILED(rv)) return rv;
+  NS_ENSURE_SUCCESS(rv, rv);
 
   mState &= ~(XML_HTTP_REQUEST_USE_XSITE_AC |
-              XML_HTTP_REQUEST_NEED_AC_PREFLIGHT);
+              XML_HTTP_REQUEST_NEED_AC_PREFLIGHT_IF_XSITE);
 
   nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel));
   if (httpChannel) {
     rv = httpChannel->SetRequestMethod(method);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Set the initiator type
     nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChannel));
     if (timedChannel) {
       timedChannel->SetInitiatorType(NS_LITERAL_STRING("xmlhttprequest"));
     }
   }
 
   ChangeState(XML_HTTP_REQUEST_OPENED);
 
-  return rv;
+  return NS_OK;
 }
 
 void
 nsXMLHttpRequest::PopulateNetworkInterfaceId()
 {
   if (mNetworkInterfaceId.IsEmpty()) {
     return;
   }
@@ -2813,24 +2770,31 @@ nsXMLHttpRequest::Send(nsIVariant* aVari
       if (!nsContentUtils::IsAllowedNonCorsContentType(contentTypeHeader)) {
         mCORSUnsafeHeaders.AppendElement(NS_LITERAL_CSTRING("Content-Type"));
       }
     }
   }
 
   ResetResponse();
 
-  rv = CheckChannelForCrossSiteRequest(mChannel);
-  NS_ENSURE_SUCCESS(rv, rv);
+  CheckChannelForCrossSiteRequest(mChannel);
 
   bool withCredentials = !!(mState & XML_HTTP_REQUEST_AC_WITH_CREDENTIALS);
 
-  // Hook us up to listen to redirects and the like
-  mChannel->GetNotificationCallbacks(getter_AddRefs(mNotificationCallbacks));
-  mChannel->SetNotificationCallbacks(this);
+  if (!IsSystemXHR() && withCredentials) {
+    // This is quite sad. We have to create the channel in .open(), since the
+    // chrome-only xhr.channel API depends on that. However .withCredentials
+    // can be modified after, so we don't know what to set the
+    // SEC_REQUIRE_CORS_WITH_CREDENTIALS flag to when the channel is
+    // created. So set it here using a hacky internal API.
+
+    // Not doing this for system XHR uses since those don't use CORS.
+    nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo();
+    static_cast<LoadInfo*>(loadInfo.get())->SetWithCredentialsSecFlag();
+  }
 
   // Blocking gets are common enough out of XHR that we should mark
   // the channel slow by default for pipeline purposes
   AddLoadFlags(mChannel, nsIRequest::INHIBIT_PIPELINE);
 
   nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(mChannel));
   if (cos) {
     // we never let XHR be blocked by head CSS/JS loads to avoid
@@ -2841,44 +2805,23 @@ nsXMLHttpRequest::Send(nsIVariant* aVari
 
   nsCOMPtr<nsIHttpChannelInternal>
     internalHttpChannel(do_QueryInterface(mChannel));
   if (internalHttpChannel) {
     // Disable Necko-internal response timeouts.
     internalHttpChannel->SetResponseTimeoutEnabled(false);
   }
 
-  nsCOMPtr<nsIStreamListener> listener = this;
-  if (!IsSystemXHR()) {
-    // Always create a nsCORSListenerProxy here even if it's
-    // a same-origin request right now, since it could be redirected.
-    RefPtr<nsCORSListenerProxy> corsListener =
-      new nsCORSListenerProxy(listener, mPrincipal, withCredentials);
-    rv = corsListener->Init(mChannel, DataURIHandling::Allow);
-    NS_ENSURE_SUCCESS(rv, rv);
-    listener = corsListener;
-  }
-  else {
-    // Because of bug 682305, we can't let listener be the XHR object itself
-    // because JS wouldn't be able to use it. So if we haven't otherwise
-    // created a listener around 'this', do so now.
-
-    listener = new nsStreamListenerWrapper(listener);
-  }
-
   if (mIsAnon) {
     AddLoadFlags(mChannel, nsIRequest::LOAD_ANONYMOUS);
   }
   else {
     AddLoadFlags(mChannel, nsIChannel::LOAD_EXPLICIT_CREDENTIALS);
   }
 
-  NS_ASSERTION(listener != this,
-               "Using an object as a listener that can't be exposed to JS");
-
   // When we are sync loading, we need to bypass the local cache when it would
   // otherwise block us waiting for exclusive access to the cache.  If we don't
   // do this, then we could dead lock in some cases (see bug 309424).
   //
   // Also don't block on the cache entry on async if it is busy - favoring parallelism
   // over cache hit rate for xhr. This does not disable the cache everywhere -
   // only in cases where more than one channel for the same URI is accessed
   // simultanously.
@@ -2901,20 +2844,29 @@ nsXMLHttpRequest::Send(nsIVariant* aVari
       contentType.Equals(UNKNOWN_CONTENT_TYPE)) {
     mChannel->SetContentType(NS_LITERAL_CSTRING("application/xml"));
   }
 
   // We're about to send the request.  Start our timeout.
   mRequestSentTime = PR_Now();
   StartTimeoutTimer();
 
+  // Check if we need to do a preflight request.
+  if (!mCORSUnsafeHeaders.IsEmpty() ||
+      (mUpload && mUpload->HasListeners()) ||
+      (!method.LowerCaseEqualsLiteral("get") &&
+       !method.LowerCaseEqualsLiteral("post") &&
+       !method.LowerCaseEqualsLiteral("head"))) {
+    mState |= XML_HTTP_REQUEST_NEED_AC_PREFLIGHT_IF_XSITE;
+  }
+
   // Set up the preflight if needed
-  if (mState & XML_HTTP_REQUEST_NEED_AC_PREFLIGHT) {
-    // Check to see if this initial OPTIONS request has already been cached
-    // in our special Access Control Cache.
+  if ((mState & XML_HTTP_REQUEST_USE_XSITE_AC) &&
+      (mState & XML_HTTP_REQUEST_NEED_AC_PREFLIGHT_IF_XSITE)) {
+    NS_ENSURE_TRUE(internalHttpChannel, NS_ERROR_DOM_BAD_URI);
 
     rv = internalHttpChannel->SetCorsPreflightParameters(mCORSUnsafeHeaders,
                                                          withCredentials, mPrincipal);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   mIsMappedArrayBuffer = false;
   if (mResponseType == XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER &&
@@ -2927,26 +2879,40 @@ nsXMLHttpRequest::Send(nsIVariant* aVari
       uri->GetScheme(scheme);
       if (scheme.LowerCaseEqualsLiteral("app") ||
           scheme.LowerCaseEqualsLiteral("jar")) {
         mIsMappedArrayBuffer = true;
       }
     }
   }
 
+  // Hook us up to listen to redirects and the like
+  // Only do this very late since this creates a cycle between
+  // the channel and us. This cycle has to be manually broken if anything
+  // below fails.
+  mChannel->GetNotificationCallbacks(getter_AddRefs(mNotificationCallbacks));
+  mChannel->SetNotificationCallbacks(this);
+
   // Start reading from the channel
-  rv = mChannel->AsyncOpen(listener, nullptr);
+  // Because of bug 682305, we can't let listener be the XHR object itself
+  // because JS wouldn't be able to use it. So create a listener around 'this'.
+  // Make sure to hold a strong reference so that we don't leak the wrapper.
+  nsCOMPtr<nsIStreamListener> listener = new nsStreamListenerWrapper(this);
+  rv = mChannel->AsyncOpen2(listener);
+  listener = nullptr;
 
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    // Drop our ref to the channel to avoid cycles
+    // Drop our ref to the channel to avoid cycles. Also drop channel's
+    // ref to us to be extra safe.
+    mChannel->SetNotificationCallbacks(mNotificationCallbacks);
     mChannel = nullptr;
+
     return rv;
   }
 
-  // Either AsyncOpen was called, or CORS will open the channel later.
   mWaitingForOnStopRequest = true;
 
   // If we're synchronous, spin an event loop here and wait
   if (!(mState & XML_HTTP_REQUEST_ASYNC)) {
     mState |= XML_HTTP_REQUEST_SYNCLOOPING;
 
     nsCOMPtr<nsIDocument> suspendedDoc;
     nsCOMPtr<nsIRunnable> resumeTimeoutRunnable;
@@ -3379,27 +3345,22 @@ nsXMLHttpRequest::AsyncOnChannelRedirect
                                          uint32_t    aFlags,
                                          nsIAsyncVerifyRedirectCallback *callback)
 {
   NS_PRECONDITION(aNewChannel, "Redirect without a channel?");
 
   nsresult rv;
 
   if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags)) {
-    rv = CheckChannelForCrossSiteRequest(aNewChannel);
-    if (NS_FAILED(rv)) {
-      NS_WARNING("nsXMLHttpRequest::OnChannelRedirect: "
-                 "CheckChannelForCrossSiteRequest returned failure");
-      return rv;
-    }
+    CheckChannelForCrossSiteRequest(aNewChannel);
 
     // Disable redirects for preflighted cross-site requests entirely for now
-    // Note, do this after the call to CheckChannelForCrossSiteRequest
-    // to make sure that XML_HTTP_REQUEST_USE_XSITE_AC is up-to-date
-    if ((mState & XML_HTTP_REQUEST_NEED_AC_PREFLIGHT)) {
+    if ((mState & XML_HTTP_REQUEST_USE_XSITE_AC) &&
+        (mState & XML_HTTP_REQUEST_NEED_AC_PREFLIGHT_IF_XSITE)) {
+       aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI);
        return NS_ERROR_DOM_BAD_URI;
     }
   }
 
   // Prepare to receive callback
   mRedirectCallback = callback;
   mNewRedirectChannel = aNewChannel;
 
@@ -3550,17 +3511,17 @@ nsXMLHttpRequest::OnStatus(nsIRequest *a
 
   return NS_OK;
 }
 
 bool
 nsXMLHttpRequest::AllowUploadProgress()
 {
   return !(mState & XML_HTTP_REQUEST_USE_XSITE_AC) ||
-    (mState & XML_HTTP_REQUEST_NEED_AC_PREFLIGHT);
+    (mState & XML_HTTP_REQUEST_NEED_AC_PREFLIGHT_IF_XSITE);
 }
 
 /////////////////////////////////////////////////////
 // nsIInterfaceRequestor methods:
 //
 NS_IMETHODIMP
 nsXMLHttpRequest::GetInterface(const nsIID & aIID, void **aResult)
 {
--- a/dom/base/nsXMLHttpRequest.h
+++ b/dom/base/nsXMLHttpRequest.h
@@ -621,17 +621,17 @@ protected:
   void ChangeStateToDone();
 
   /**
    * Check if aChannel is ok for a cross-site request by making sure no
    * inappropriate headers are set, and no username/password is set.
    *
    * Also updates the XML_HTTP_REQUEST_USE_XSITE_AC bit.
    */
-  nsresult CheckChannelForCrossSiteRequest(nsIChannel* aChannel);
+  void CheckChannelForCrossSiteRequest(nsIChannel* aChannel);
 
   void StartProgressEventTimer();
 
   friend class AsyncVerifyRedirectCallbackForwarder;
   void OnRedirectVerifyCallback(nsresult result);
 
   nsresult Open(const nsACString& method, const nsACString& url, bool async,
                 const mozilla::dom::Optional<nsAString>& user,
--- a/dom/base/test/file_XHRResponseURL.js
+++ b/dom/base/test/file_XHRResponseURL.js
@@ -45,38 +45,16 @@ function info(aMessage) {
   var obj = {
     type: "info",
     message: aMessage
   };
   ++message.ping;
   message(obj);
 }
 
-function todo(aBool, aMessage) {
-  var obj = {
-    type: "todo",
-    bool: aBool,
-    message: aMessage
-  };
-  ++message.ping;
-  message(obj);
-}
-
-function todo_is(aActual, aExpected, aMessage) {
-  var obj = {
-    type: "todo_is",
-    actual: aActual,
-    expected: aExpected,
-    message: aMessage
-  };
-  ++message.ping;
-  message(obj);
-}
-
-
 function request(aURL) {
   return new Promise(function (aResolve, aReject) {
     var xhr = new XMLHttpRequest();
     xhr.open("GET", aURL);
     xhr.addEventListener("load", function () {
       xhr.succeeded = true;
       aResolve(xhr);
     });
@@ -120,25 +98,21 @@ function testSuccessResponse() {
       message: "request to same-origin redirects several times and finally go to same-origin URL",
       requestURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text"),
       responseURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text"
     },
     {
       message: "request to same-origin redirect to cross-origin URL",
       requestURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/base/test/file_XHRResponseURL.text",
       responseURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
     {
       message: "request to same-origin redirects several times and finally go to cross-origin URL",
       requestURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/base/test/file_XHRResponseURL.text"),
       responseURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
 
     // tests that start with cross-origin request
     {
       message: "request to cross-origin without redirect",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.text",
       responseURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.text"
     },
@@ -146,53 +120,41 @@ function testSuccessResponse() {
       message: "request to cross-origin redirect back to same-origin URL",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text",
       responseURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text"
     },
     {
       message: "request to cross-origin redirect to the same cross-origin URL",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/base/test/file_XHRResponseURL.text",
       responseURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
     {
       message: "request to cross-origin redirect to another cross-origin URL",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://example.org/tests/dom/base/test/file_XHRResponseURL.text",
       responseURL: "http://example.org/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
     {
       message: "request to cross-origin redirects several times and finally go to same-origin URL",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text"),
       responseURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
     {
       message: "request to cross-origin redirects several times and finally go to the same cross-origin URL",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://example.com/tests/dom/base/test/file_XHRResponseURL.text"),
       responseURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
     {
       message: "request to cross-origin redirects several times and finally go to another cross-origin URL",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://example.org/tests/dom/base/test/file_XHRResponseURL.text"),
       responseURL: "http://example.org/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
     {
       message: "request to cross-origin redirects to another cross-origin and finally go to the other cross-origin URL",
       requestURL: "http://example.com/tests/dom/base/test/file_XHRResponseURL.sjs?url=" + encodeURIComponent("http://example.org/tests/dom/base/test/file_XHRResponseURL.sjs?url=http://test1.example.com/tests/dom/base/test/file_XHRResponseURL.text"),
       responseURL: "http://test1.example.com/tests/dom/base/test/file_XHRResponseURL.text",
-      skip: isInWorker(),
-      reason: "cross-origin redirect request not works on Workers, see bug 882458"
     },
     {
       message: "request URL has fragment",
       requestURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text#fragment",
       responseURL: "http://mochi.test:8888/tests/dom/base/test/file_XHRResponseURL.text"
     },
 
     // tests for non-http(s) URL
@@ -204,23 +166,18 @@ function testSuccessResponse() {
     {
       message: "request to blob: URL",
       requestURL: blobURL,
       responseURL: blobURL
     }
   ];
 
   var sequence = createSequentialRequest(parameters, function (aXHR, aParam) {
-    if (aParam.skip) {
-      todo(aXHR.succeeded, aParam.reason);
-      todo_is(aXHR.responseURL, aParam.responseURL, aParam.reason);
-    } else {
-      ok(aXHR.succeeded, "assert request succeeded");
-      is(aXHR.responseURL, aParam.responseURL, aParam.message);
-    }
+    ok(aXHR.succeeded, "assert request succeeded");
+    is(aXHR.responseURL, aParam.responseURL, aParam.message);
   });
 
   sequence.then(function () {
     URL.revokeObjectURL(blobURL);
   });
 
   return sequence;
 }
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -505,17 +505,16 @@ skip-if = (buildapp == 'b2g' && toolkit 
 [test_bug428847.html]
 [test_bug429157.html]
 [test_bug431082.html]
 [test_bug431701.html]
 [test_bug431833.html]
 [test_bug433533.html]
 [test_bug433662.html]
 [test_bug435425.html]
-[test_bug438519.html]
 [test_bug444030.xhtml]
 [test_bug444322.html]
 [test_bug444722.html]
 [test_bug448993.html]
 [test_bug450160.html]
 [test_bug451376.html]
 [test_bug453521.html]
 [test_bug453736.html]
deleted file mode 100644
--- a/dom/base/test/test_bug438519.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=438519
--->
-<head>
-  <title>Test for Bug 438519</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>        
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-<body onload="doTest()">
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=438519">Mozilla Bug 438519</a>
-<p id="display"></p>
-<div id="content" style="display:none">
-
-<iframe id="empty" src="data:text/xml,<!DOCTYPE HTML []><html></html>"></iframe>
-<iframe id="missing" src="data:text/xml,<!DOCTYPE HTML><html></html>"></iframe>
-<iframe id="entity" src="data:text/xml,<!DOCTYPE HTML [ <!ENTITY foo 'foo'> ]><html></html>"></iframe>
-
-</div>
-<pre id="test">
-<script class="testbody" type="text/javascript">
-
-/** Test for Bug 218236 **/
-
-SimpleTest.waitForExplicitFinish();
-
-function doTest() {
-  function checkInternalSubset(id, expected) {
-    var e = document.getElementById(id);
-    is(e.contentDocument.doctype.internalSubset, expected, "checking '" + id + "'");
-  }
-
-  checkInternalSubset("empty", "");
-  checkInternalSubset("missing", null);
-  checkInternalSubset("entity", " <!ENTITY foo 'foo'> ");
-  SimpleTest.finish();
-}
-
-</script>
-</pre>
-</body>
-</html>
-
--- a/dom/base/test/test_createHTMLDocument.html
+++ b/dom/base/test/test_createHTMLDocument.html
@@ -19,17 +19,16 @@ function checkDoc(title, expectedtitle, 
   var doc = document.implementation.createHTMLDocument(title);
   is(doc.readyState, "complete");
   is(doc.compatMode, "CSS1Compat");
   // Opera doesn't have a doctype: DSK-311092
   ok(doc.doctype, "Need a doctype");
   is(doc.doctype.name, "html");
   is(doc.doctype.publicId, "");
   is(doc.doctype.systemId, "");
-  is(doc.doctype.internalSubset, null, "internalSubset should be null!");
   isElement(doc.documentElement, "html");
   isElement(doc.documentElement.firstChild, "head");
   if (title !== undefined) {
     is(doc.documentElement.firstChild.childNodes.length, 1);
     isElement(doc.documentElement.firstChild.firstChild, "title");
     // Doesn't always work out in WebKit.
     ok(doc.documentElement.firstChild.firstChild.firstChild, "Need a text node.");
     is(doc.documentElement.firstChild.firstChild.firstChild.data, expectedtitle);
--- a/dom/base/test/test_urlSearchParams.html
+++ b/dom/base/test/test_urlSearchParams.html
@@ -232,28 +232,81 @@ https://bugzilla.mozilla.org/show_bug.cg
     is(u.getAll('name1').length, 3, "URLSearchParams.getAll('name1').length should be 3");
     u.set('name1','firstPair');
     is(u.get('name1'), 'firstPair', "URL.searchParams.get('name1') should return firstPair");
     is(u.getAll('name1').length, 1, "URLSearchParams.getAll('name1').length should be 1");
 
     runTest();
   }
 
+  function testIterable() {
+    var u = new URLSearchParams();
+    u.set('1','2');
+    u.set('2','4');
+    u.set('3','6');
+    u.set('4','8');
+    u.set('5','10');
+
+    var key_iter = u.keys();
+    var value_iter = u.values();
+    var entries_iter = u.entries();
+    for (var i = 0; i < 5; ++i) {
+      var v = i + 1;
+      var key = key_iter.next();
+      var value = value_iter.next();
+      var entry = entries_iter.next();
+      is(key.value, v.toString(), "Correct Key iterator: " + v.toString());
+      ok(!key.done, "Key.done is false");
+      is(value.value, (v * 2).toString(), "Correct Value iterator: " + (v * 2).toString());
+      ok(!value.done, "Value.done is false");
+      is(entry.value[0], v.toString(), "Correct Entry 0 iterator: " + v.toString());
+      is(entry.value[1], (v * 2).toString(), "Correct Entry 1 iterator: " + (v * 2).toString());
+      ok(!entry.done, "Entry.done is false");
+    }
+
+    var last = key_iter.next();
+    ok(last.done, "Nothing more to read.");
+    is(last.value, undefined, "Undefined is the last key");
+
+    last = value_iter.next();
+    ok(last.done, "Nothing more to read.");
+    is(last.value, undefined, "Undefined is the last value");
+
+    last = entries_iter.next();
+    ok(last.done, "Nothing more to read.");
+
+    key_iter = u.keys();
+    key_iter.next();
+    key_iter.next();
+    u.delete('1');
+    u.delete('2');
+    u.delete('3');
+    u.delete('4');
+    u.delete('5');
+
+    last = key_iter.next();
+    ok(last.done, "Nothing more to read.");
+    is(last.value, undefined, "Undefined is the last key");
+
+    runTest();
+  }
+
   var tests = [
     testSimpleURLSearchParams,
     testCopyURLSearchParams,
     testParserURLSearchParams,
     testURL,
     function() { testElement(document.getElementById('anchor')) },
     function() { testElement(document.getElementById('area')) },
     testEncoding,
     testOrdering,
     testDelete,
     testGetNULL,
-    testSet
+    testSet,
+    testIterable
   ];
 
   function runTest() {
     if (!tests.length) {
       SimpleTest.finish();
       return;
     }
 
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -695,20 +695,16 @@ DOMInterfaces = {
     'wrapperCache': False,
 },
 
 'InputStream': {
     'nativeType': 'nsIInputStream',
     'notflattened': True
 },
 
-'IterableIterator': {
-    'skipGen': True
-},
-
 'KeyEvent': {
     'concrete': False
 },
 
 'LegacyMozTCPSocket': {
     'headerFile': 'TCPSocket.h',
     'wrapperCache': False,
 },
--- a/dom/bindings/parser/WebIDL.py
+++ b/dom/bindings/parser/WebIDL.py
@@ -735,17 +735,17 @@ class IDLInterface(IDLObjectWithScope, I
                 # Check that we only have one interface declaration (currently
                 # there can only be one maplike/setlike declaration per
                 # interface)
                 if self.maplikeOrSetlikeOrIterable:
                     raise WebIDLError("%s declaration used on "
                                       "interface that already has %s "
                                       "declaration" %
                                       (member.maplikeOrSetlikeOrIterableType,
-                                       self.maplikeOrSetlike.maplikeOrSetlikeOrIterableType),
+                                       self.maplikeOrSetlikeOrIterable.maplikeOrSetlikeOrIterableType),
                                       [self.maplikeOrSetlikeOrIterable.location,
                                        member.location])
                 self.maplikeOrSetlikeOrIterable = member
                 # If we've got a maplike or setlike declaration, we'll be building all of
                 # our required methods in Codegen. Generate members now.
                 self.maplikeOrSetlikeOrIterable.expand(self.members, self.isJSImplemented())
 
         # Now that we've merged in our partial interfaces, set the
@@ -6562,54 +6562,52 @@ class Parser(Tokenizer):
         self._filename = None
 
     def finish(self):
         # If we have interfaces that are iterable, create their
         # iterator interfaces and add them to the productions array.
         interfaceStatements = [p for p in self._productions if
                                isinstance(p, IDLInterface)]
 
+        iterableIteratorIface = None
         for iface in interfaceStatements:
             iterable = None
             # We haven't run finish() on the interface yet, so we don't know
             # whether our interface is maplike/setlike/iterable or not. This
             # means we have to loop through the members to see if we have an
             # iterable member.
             for m in iface.members:
                 if isinstance(m, IDLIterable):
                     iterable = m
                     break
             if iterable:
+                def simpleExtendedAttr(str):
+                    return IDLExtendedAttribute(iface.location, (str, ))
+                nextMethod = IDLMethod(
+                    iface.location,
+                    IDLUnresolvedIdentifier(iface.location, "next"),
+                    BuiltinTypes[IDLBuiltinType.Types.object], [])
+                nextMethod.addExtendedAttributes([simpleExtendedAttr("Throws")])
                 itr_ident = IDLUnresolvedIdentifier(iface.location,
                                                     iface.identifier.name + "Iterator")
                 itr_iface = IDLInterface(iface.location, self.globalScope(),
-                                         itr_ident, None, [],
+                                         itr_ident, None, [nextMethod],
                                          isKnownNonPartial=True)
-                itr_iface.addExtendedAttributes([IDLExtendedAttribute(iface.location,
-                                                                      ("NoInterfaceObject", ))])
+                itr_iface.addExtendedAttributes([simpleExtendedAttr("NoInterfaceObject")])
                 # Make sure the exposure set for the iterator interface is the
                 # same as the exposure set for the iterable interface, because
                 # we're going to generate methods on the iterable that return
                 # instances of the iterator.
                 itr_iface._exposureGlobalNames = set(iface._exposureGlobalNames)
-                # Always append generated iterable interfaces and their
-                # matching implements statements after the interface they're a
-                # member of, otherwise nativeType generation won't work
-                # correctly.
+                # Always append generated iterable interfaces after the
+                # interface they're a member of, otherwise nativeType generation
+                # won't work correctly.
                 itr_iface.iterableInterface = iface
                 self._productions.append(itr_iface)
                 iterable.iteratorType = IDLWrapperType(iface.location, itr_iface)
-                itrPlaceholder = IDLIdentifierPlaceholder(iface.location,
-                                                          IDLUnresolvedIdentifier(iface.location,
-                                                                                  "IterableIterator"))
-                implements = IDLImplementsStatement(iface.location,
-                                                    IDLIdentifierPlaceholder(iface.location,
-                                                                             itr_ident),
-                                                    itrPlaceholder)
-                self._productions.append(implements)
 
         # Then, finish all the IDLImplementsStatements.  In particular, we
         # have to make sure we do those before we do the IDLInterfaces.
         # XXX khuey hates this bit and wants to nuke it from orbit.
         implementsStatements = [p for p in self._productions if
                                 isinstance(p, IDLImplementsStatement)]
         otherStatements = [p for p in self._productions if
                            not isinstance(p, IDLImplementsStatement)]
--- a/dom/bindings/parser/tests/test_interface_maplikesetlikeiterable.py
+++ b/dom/bindings/parser/tests/test_interface_maplikesetlikeiterable.py
@@ -5,100 +5,112 @@ def WebIDLTest(parser, harness):
     def shouldPass(prefix, iface, expectedMembers, numProductions=1):
         p = parser.reset()
         p.parse(iface)
         results = p.finish()
         harness.check(len(results), numProductions,
                       "%s - Should have production count %d" % (prefix, numProductions))
         harness.ok(isinstance(results[0], WebIDL.IDLInterface),
                    "%s - Should be an IDLInterface" % (prefix))
-        harness.check(len(results[0].members), len(expectedMembers),
-                      "%s - Should be %d members" % (prefix,
-                                                     len(expectedMembers)))
+        # Make a copy, since we plan to modify it
+        expectedMembers = list(expectedMembers)
         for m in results[0].members:
             name = m.identifier.name
             if (name, type(m)) in expectedMembers:
                 harness.ok(True, "%s - %s - Should be a %s" % (prefix, name,
                                                                type(m)))
-            elif isinstance(m, WebIDL.IDLMaplikeOrSetlike):
-                harness.ok(True, "%s - %s - Should be a MaplikeOrSetlike" %
-                           (prefix, name))
+                expectedMembers.remove((name, type(m)))
             else:
                 harness.ok(False, "%s - %s - Unknown symbol of type %s" %
                            (prefix, name, type(m)))
+        # A bit of a hoop because we can't generate the error string if we pass
+        if len(expectedMembers) == 0:
+            harness.ok(True, "Found all the members")
+        else:
+            harness.ok(False,
+                       "Expected member not found: %s of type %s" %
+                       (expectedMembers[0][0], expectedMembers[0][1]))
         return results
 
     def shouldFail(prefix, iface):
         try:
             p = parser.reset()
             p.parse(iface)
             p.finish()
             harness.ok(False,
                        prefix + " - Interface passed when should've failed")
         except WebIDL.WebIDLError, e:
             harness.ok(True,
                        prefix + " - Interface failed as expected")
         except Exception, e:
             harness.ok(False,
-                       prefix + " - Interface failed but not as a WebIDLError exception")
+                       prefix + " - Interface failed but not as a WebIDLError exception: %s" % e)
 
     iterableMembers = [(x, WebIDL.IDLMethod) for x in ["entries", "keys",
                                                        "values"]]
-    setROMembers = ([(x, WebIDL.IDLMethod) for x in ["has", "foreach"]] +
+    setROMembers = ([(x, WebIDL.IDLMethod) for x in ["has", "forEach"]] +
                     [("__setlike", WebIDL.IDLMaplikeOrSetlike)] +
                     iterableMembers)
     setROMembers.extend([("size", WebIDL.IDLAttribute)])
     setRWMembers = ([(x, WebIDL.IDLMethod) for x in ["add",
                                                      "clear",
                                                      "delete"]] +
                     setROMembers)
     setROChromeMembers = ([(x, WebIDL.IDLMethod) for x in ["__add",
                                                            "__clear",
                                                            "__delete"]] +
                           setROMembers)
     setRWChromeMembers = ([(x, WebIDL.IDLMethod) for x in ["__add",
                                                            "__clear",
                                                            "__delete"]] +
                           setRWMembers)
-    mapROMembers = ([(x, WebIDL.IDLMethod) for x in ["get", "has", "foreach"]] +
+    mapROMembers = ([(x, WebIDL.IDLMethod) for x in ["get", "has", "forEach"]] +
                     [("__maplike", WebIDL.IDLMaplikeOrSetlike)] +
                     iterableMembers)
     mapROMembers.extend([("size", WebIDL.IDLAttribute)])
     mapRWMembers = ([(x, WebIDL.IDLMethod) for x in ["set",
                                                      "clear",
                                                      "delete"]] + mapROMembers)
     mapRWChromeMembers = ([(x, WebIDL.IDLMethod) for x in ["__set",
                                                            "__clear",
                                                            "__delete"]] +
                           mapRWMembers)
 
+    # OK, now that we've used iterableMembers to set up the above, append
+    # __iterable to it for the iterable<> case.
+    iterableMembers.append(("__iterable", WebIDL.IDLIterable))
+
     disallowedIterableNames = ["keys", "entries", "values"]
     disallowedMemberNames = ["forEach", "has", "size"] + disallowedIterableNames
     mapDisallowedMemberNames = ["get"] + disallowedMemberNames
     disallowedNonMethodNames = ["clear", "delete"]
     mapDisallowedNonMethodNames = ["set"] + disallowedNonMethodNames
     setDisallowedNonMethodNames = ["add"] + disallowedNonMethodNames
 
     #
     # Simple Usage Tests
     #
 
     shouldPass("Iterable (key only)",
                """
                interface Foo1 {
                iterable<long>;
                };
-               """, iterableMembers)
+               """, iterableMembers,
+               # numProductions == 2 because of the generated iterator iface,
+               numProductions=2)
 
     shouldPass("Iterable (key and value)",
                """
                interface Foo1 {
                iterable<long, long>;
                };
-               """, iterableMembers)
+               """, iterableMembers,
+               # numProductions == 2 because of the generated iterator iface,
+               numProductions=2)
 
     shouldPass("Maplike (readwrite)",
                """
                interface Foo1 {
                maplike<long, long>;
                };
                """, mapRWMembers)
 
@@ -569,10 +581,10 @@ def WebIDLTest(parser, harness):
                    long delete(long a, long b, double c, double d);
                    };
                    """, mapRWMembers)
 
     for m in r[0].members:
         if m.identifier.name in ["clear", "set", "delete"]:
             harness.ok(m.isMethod(), "%s should be a method" % m.identifier.name)
             harness.check(m.maxArgCount, 4, "%s should have 4 arguments" % m.identifier.name)
-            harness.ok(not m.isMaplikeOrSetlikeMethod(),
+            harness.ok(not m.isMaplikeOrSetlikeOrIterableMethod(),
                        "%s should not be a maplike/setlike function" % m.identifier.name)
--- a/dom/cache/DBSchema.cpp
+++ b/dom/cache/DBSchema.cpp
@@ -187,18 +187,17 @@ static_assert(int(HeadersGuardEnum::None
               int(HeadersGuardEnum::Request_no_cors) == 2 &&
               int(HeadersGuardEnum::Response) == 3 &&
               int(HeadersGuardEnum::Immutable) == 4 &&
               int(HeadersGuardEnum::EndGuard_) == 5,
               "HeadersGuardEnum values are as expected");
 static_assert(int(RequestMode::Same_origin) == 0 &&
               int(RequestMode::No_cors) == 1 &&
               int(RequestMode::Cors) == 2 &&
-              int(RequestMode::Cors_with_forced_preflight) == 3 &&
-              int(RequestMode::EndGuard_) == 4,
+              int(RequestMode::EndGuard_) == 3,
               "RequestMode values are as expected");
 static_assert(int(RequestCredentials::Omit) == 0 &&
               int(RequestCredentials::Same_origin) == 1 &&
               int(RequestCredentials::Include) == 2 &&
               int(RequestCredentials::EndGuard_) == 3,
               "RequestCredentials values are as expected");
 static_assert(int(RequestCache::Default) == 0 &&
               int(RequestCache::No_store) == 1 &&
--- a/dom/canvas/WebGL2Context.h
+++ b/dom/canvas/WebGL2Context.h
@@ -66,16 +66,21 @@ public:
 
     // -------------------------------------------------------------------------
     // Framebuffer objects - WebGL2ContextFramebuffers.cpp
 
     void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
                          GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
                          GLbitfield mask, GLenum filter);
     void FramebufferTextureLayer(GLenum target, GLenum attachment, WebGLTexture* texture, GLint level, GLint layer);
+
+    virtual JS::Value GetFramebufferAttachmentParameter(JSContext* cx, GLenum target,
+                                                        GLenum attachment, GLenum pname,
+                                                        ErrorResult& rv) override;
+
     void InvalidateFramebuffer(GLenum target, const dom::Sequence<GLenum>& attachments,
                                ErrorResult& rv);
     void InvalidateSubFramebuffer (GLenum target, const dom::Sequence<GLenum>& attachments, GLint x, GLint y,
                                    GLsizei width, GLsizei height, ErrorResult& rv);
     void ReadBuffer(GLenum mode);
 
 
     // -------------------------------------------------------------------------
--- a/dom/canvas/WebGL2ContextFramebuffers.cpp
+++ b/dom/canvas/WebGL2ContextFramebuffers.cpp
@@ -3,20 +3,27 @@
  * 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 "WebGL2Context.h"
 
 #include "GLContext.h"
 #include "GLScreenBuffer.h"
 #include "WebGLContextUtils.h"
+#include "WebGLFormats.h"
 #include "WebGLFramebuffer.h"
 
 namespace mozilla {
 
+using gl::GLContext;
+using gl::GLFormats;
+using webgl::EffectiveFormat;
+using webgl::FormatInfo;
+using webgl::ComponentType;
+
 // Returns one of FLOAT, INT, UNSIGNED_INT.
 // Fixed-points (normalized ints) are considered FLOAT.
 static GLenum
 ValueTypeForFormat(GLenum internalFormat)
 {
     switch (internalFormat) {
     // Fixed-point
     case LOCAL_GL_R8:
@@ -413,16 +420,113 @@ WebGL2Context::FramebufferTextureLayer(G
     if (!fb) {
         return ErrorInvalidOperation("framebufferTextureLayer: cannot modify"
                                      " framebuffer 0.");
     }
 
     fb->FramebufferTextureLayer(attachment, texture, level, layer);
 }
 
+JS::Value
+WebGL2Context::GetFramebufferAttachmentParameter(JSContext* cx,
+                                                 GLenum target,
+                                                 GLenum attachment,
+                                                 GLenum pname,
+                                                 ErrorResult& rv)
+{
+    if (IsContextLost())
+        return JS::NullValue();
+
+    // OpenGL ES 3.0.4 (August 27, 2014) 6.1. QUERYING GL STATE 240
+    // "getFramebufferAttachmentParamter returns information about attachments of a bound
+    // framebuffer object. target must be DRAW_FRAMEBUFFER, READ_FRAMEBUFFER, or
+    // FRAMEBUFFER."
+
+    if (!ValidateFramebufferTarget(target, "getFramebufferAttachmentParameter"))
+        return JS::NullValue();
+
+    // FRAMEBUFFER is equivalent to DRAW_FRAMEBUFFER.
+    if (target == LOCAL_GL_FRAMEBUFFER)
+        target = LOCAL_GL_DRAW_FRAMEBUFFER;
+
+    WebGLFramebuffer* boundFB = nullptr;
+    switch (target) {
+    case LOCAL_GL_DRAW_FRAMEBUFFER: boundFB = mBoundDrawFramebuffer; break;
+    case LOCAL_GL_READ_FRAMEBUFFER: boundFB = mBoundReadFramebuffer; break;
+    }
+
+    if (boundFB) {
+        return boundFB->GetAttachmentParameter(cx, attachment, pname, rv);
+    }
+
+    // Handle default FB
+    const gl::GLFormats& formats = gl->GetGLFormats();
+    GLenum internalFormat = LOCAL_GL_NONE;
+
+    /* If the default framebuffer is bound to target, then attachment must be BACK,
+       identifying the color buffer; DEPTH, identifying the depth buffer; or STENCIL,
+       identifying the stencil buffer. */
+    switch (attachment) {
+    case LOCAL_GL_BACK:
+        internalFormat = formats.color_texInternalFormat;
+        break;
+
+    case LOCAL_GL_DEPTH:
+        internalFormat = formats.depth;
+        break;
+
+    case LOCAL_GL_STENCIL:
+        internalFormat = formats.stencil;
+        break;
+
+    default:
+        ErrorInvalidEnum("getFramebufferAttachmentParameter: Can only query "
+                         "attachment BACK, DEPTH, or STENCIL from default "
+                         "framebuffer");
+        return JS::NullValue();
+    }
+
+    const FormatInfo* info = webgl::GetInfoBySizedFormat(internalFormat);
+    MOZ_RELEASE_ASSERT(info);
+    EffectiveFormat effectiveFormat = info->effectiveFormat;
+
+    switch (pname) {
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
+        return JS::Int32Value(LOCAL_GL_FRAMEBUFFER_DEFAULT);
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
+        return JS::Int32Value(webgl::GetComponentSize(effectiveFormat, pname));
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
+        if (attachment == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT &&
+            pname == LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE)
+        {
+            ErrorInvalidOperation("getFramebufferAttachmentParameter: Querying "
+                                  "FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE against "
+                                  "DEPTH_STENCIL_ATTACHMENT is an error.");
+            return JS::NullValue();
+        }
+
+        return JS::Int32Value(webgl::GetComponentType(effectiveFormat));
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
+        return JS::Int32Value(webgl::GetColorEncoding(effectiveFormat));
+    }
+
+    /* Any combinations of framebuffer type and pname not described above will generate an
+       INVALID_ENUM error. */
+    ErrorInvalidEnum("getFramebufferAttachmentParameter: Invalid combination of ");
+    return JS::NullValue();
+}
+
 // Map attachments intended for the default buffer, to attachments for a non-
 // default buffer.
 static bool
 TranslateDefaultAttachments(const dom::Sequence<GLenum>& in, dom::Sequence<GLenum>* out)
 {
     for (size_t i = 0; i < in.Length(); i++) {
         switch (in[i]) {
             case LOCAL_GL_COLOR:
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -453,19 +453,19 @@ public:
 
     void GetBufferParameter(JSContext*, GLenum target, GLenum pname,
                             JS::MutableHandle<JS::Value> retval)
     {
         retval.set(GetBufferParameter(target, pname));
     }
 
     GLenum GetError();
-    JS::Value GetFramebufferAttachmentParameter(JSContext* cx, GLenum target,
-                                                GLenum attachment, GLenum pname,
-                                                ErrorResult& rv);
+    virtual JS::Value GetFramebufferAttachmentParameter(JSContext* cx, GLenum target,
+                                                        GLenum attachment, GLenum pname,
+                                                        ErrorResult& rv);
 
     void GetFramebufferAttachmentParameter(JSContext* cx, GLenum target,
                                            GLenum attachment, GLenum pname,
                                            JS::MutableHandle<JS::Value> retval,
                                            ErrorResult& rv)
     {
         retval.set(GetFramebufferAttachmentParameter(cx, target, attachment,
                                                      pname, rv));
--- a/dom/canvas/WebGLFormats.cpp
+++ b/dom/canvas/WebGLFormats.cpp
@@ -804,10 +804,324 @@ FormatUsageAuthority::AddUnpackOption(GL
 
     MOZ_RELEASE_ASSERT(usage->asTexture);
 
     auto res = usage->validUnpacks.insert(unpack);
     bool didInsert = res.second;
     MOZ_ALWAYS_TRUE(didInsert);
 }
 
+////////////////////////////////////////////////////////////////////////////////
+
+struct ComponentSizes
+{
+    GLubyte redSize;
+    GLubyte greenSize;
+    GLubyte blueSize;
+    GLubyte alphaSize;
+    GLubyte depthSize;
+    GLubyte stencilSize;
+};
+
+static ComponentSizes kComponentSizes[] = {
+    // GLES 3.0.4, p128-129, "Required Texture Formats"
+    // "Texture and renderbuffer color formats"
+    { 32, 32, 32, 32,  0,  0 }, // RGBA32I,
+    { 32, 32, 32, 32,  0,  0 }, // RGBA32UI,
+    { 16, 16, 16, 16,  0,  0 }, // RGBA16I,
+    { 16, 16, 16, 16,  0,  0 }, // RGBA16UI,
+    {  8,  8,  8,  8,  0,  0 }, // RGBA8,
+    {  8,  8,  8,  8,  0,  0 }, // RGBA8I,
+    {  8,  8,  8,  8,  0,  0 }, // RGBA8UI,
+    {  8,  8,  8,  8,  0,  0 }, // SRGB8_ALPHA8,
+    { 10, 10, 10,  2,  0,  0 }, // RGB10_A2,
+    { 10, 10, 10,  2,  0,  0 }, // RGB10_A2UI,
+    {  4,  4,  4,  4,  0,  0 }, // RGBA4,
+    {  5,  5,  5,  1,  0,  0 }, // RGB5_A1,
+
+    {  8,  8,  8,  0,  0,  0 }, // RGB8,
+    {  8,  8,  8,  0,  0,  0 }, // RGB565,
+
+    { 32, 32,  0,  0,  0,  0 }, // RG32I,
+    { 32, 32,  0,  0,  0,  0 }, // RG32UI,
+    { 16, 16,  0,  0,  0,  0 }, // RG16I,
+    { 16, 16,  0,  0,  0,  0 }, // RG16UI,
+    {  8,  8,  0,  0,  0,  0 }, // RG8,
+    {  8,  8,  0,  0,  0,  0 }, // RG8I,
+    {  8,  8,  0,  0,  0,  0 }, // RG8UI,
+
+    { 32,  0,  0,  0,  0,  0 }, // R32I,
+    { 32,  0,  0,  0,  0,  0 }, // R32UI,
+    { 16,  0,  0,  0,  0,  0 }, // R16I,
+    { 16,  0,  0,  0,  0,  0 }, // R16UI,
+    {  8,  0,  0,  0,  0,  0 }, // R8,
+    {  8,  0,  0,  0,  0,  0 }, // R8I,
+    {  8,  0,  0,  0,  0,  0 }, // R8UI,
+
+    // "Texture-only color formats"
+    { 32, 32, 32, 32,  0,  0 }, // RGBA32F,
+    { 16, 16, 16, 16,  0,  0 }, // RGBA16F,
+    {  8,  8,  8,  8,  0,  0 }, // RGBA8_SNORM,
+
+    { 32, 32, 32,  0,  0,  0 }, // RGB32F,
+    { 32, 32, 32,  0,  0,  0 }, // RGB32I,
+    { 32, 32, 32,  0,  0,  0 }, // RGB32UI,
+
+    { 16, 16, 16,  0,  0,  0 }, // RGB16F,
+    { 16, 16, 16,  0,  0,  0 }, // RGB16I,
+    { 16, 16, 16,  0,  0,  0 }, // RGB16UI,
+
+    {  8,  8,  8,  0,  0,  0 }, // RGB8_SNORM,
+    {  8,  8,  8,  0,  0,  0 }, // RGB8I,
+    {  8,  8,  8,  0,  0,  0 }, // RGB8UI,
+    {  8,  8,  8,  0,  0,  0 }, // SRGB8,
+
+    { 11, 11, 11,  0,  0,  0 }, // R11F_G11F_B10F,
+    {  9,  9,  9,  0,  0,  0 }, // RGB9_E5,
+
+    { 32, 32,  0,  0,  0,  0 }, // RG32F,
+    { 16, 16,  0,  0,  0,  0 }, // RG16F,
+    {  8,  8,  0,  0,  0,  0 }, // RG8_SNORM,
+
+    { 32,  0,  0,  0,  0,  0 }, // R32F,
+    { 16,  0,  0,  0,  0,  0 }, // R16F,
+    {  8,  0,  0,  0,  0,  0 }, // R8_SNORM,
+
+    // "Depth formats"
+    {  0,  0,  0,  0, 32,  0 }, // DEPTH_COMPONENT32F,
+    {  0,  0,  0,  0, 24,  0 }, // DEPTH_COMPONENT24,
+    {  0,  0,  0,  0, 16,  0 }, // DEPTH_COMPONENT16,
+
+    // "Combined depth+stencil formats"
+    {  0,  0,  0,  0, 32,  8 }, // DEPTH32F_STENCIL0,
+    {  0,  0,  0,  0, 24,  8 }, // DEPTH24_STENCIL8,
+
+    // GLES 3.0.4, p205-206, "Required Renderbuffer Formats"
+    {  0,  0,  0,  0,  0,  8 }, // STENCIL_INDEX8,
+
+    // GLES 3.0.4, p128, table 3.12.
+    {  8,  8,  8,  8,  0,  0 }, // Luminance8Alpha8,
+    {  8,  8,  8,  0,  0,  0 }, // Luminance8,
+    {  0,  0,  0,  8,  0,  0 }, // Alpha8,
+
+    // GLES 3.0.4, p147, table 3.19
+    // GLES 3.0.4, p286+, $C.1 "ETC Compressed Texture Image Formats"
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_R11_EAC,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_SIGNED_R11_EAC,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RG11_EAC,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_SIGNED_RG11_EAC,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGB8_ETC2,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_SRGB8_ETC2,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGBA8_ETC2_EAC,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_SRGB8_ALPHA8_ETC2_EAC,
+
+    // AMD_compressed_ATC_texture
+    {  8,  8,  8,  0,  0,  0 }, // ATC_RGB_AMD,
+    {  8,  8,  8,  8,  0,  0 }, // ATC_RGBA_EXPLICIT_ALPHA_AMD,
+    {  8,  8,  8,  8,  0,  0 }, // ATC_RGBA_INTERPOLATED_ALPHA_AMD,
+
+    // EXT_texture_compression_s3tc
+    {  8,  8,  8,  0,  0,  0 }, // COMPRESSED_RGB_S3TC_DXT1,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGBA_S3TC_DXT1,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGBA_S3TC_DXT3,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGBA_S3TC_DXT5,
+
+    // IMG_texture_compression_pvrtc
+    {  8,  8,  8,  0,  0,  0 }, // COMPRESSED_RGB_PVRTC_4BPPV1,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGBA_PVRTC_4BPPV1,
+    {  8,  8,  8,  0,  0,  0 }, // COMPRESSED_RGB_PVRTC_2BPPV1,
+    {  8,  8,  8,  8,  0,  0 }, // COMPRESSED_RGBA_PVRTC_2BPPV1,
+
+    // OES_compressed_ETC1_RGB8_texture
+    {  8,  8,  8,  0,  0,  0 }, // ETC1_RGB8,
+
+    // OES_texture_float
+    { 32, 32, 32, 32,  0,  0 }, // Luminance32FAlpha32F,
+    { 32, 32, 32,  0,  0,  0 }, // Luminance32F,
+    {  0,  0,  0, 32,  0,  0 }, // Alpha32F,
+
+    // OES_texture_half_float
+    { 16, 16, 16, 16, 0, 0 }, // Luminance16FAlpha16F,
+    { 16, 16, 16,  0, 0, 0 }, // Luminance16F,
+    {  0,  0,  0, 16, 0, 0 }, // Alpha16F,
+
+    {  0, } // MAX
+};
+
+GLint
+GetComponentSize(EffectiveFormat format, GLenum component)
+{
+    ComponentSizes compSize = kComponentSizes[(int) format];
+    switch (component) {
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
+    case LOCAL_GL_RENDERBUFFER_RED_SIZE:
+    case LOCAL_GL_RED_BITS:
+        return compSize.redSize;
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
+    case LOCAL_GL_RENDERBUFFER_GREEN_SIZE:
+    case LOCAL_GL_GREEN_BITS:
+        return compSize.greenSize;
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
+    case LOCAL_GL_RENDERBUFFER_BLUE_SIZE:
+    case LOCAL_GL_BLUE_BITS:
+        return compSize.blueSize;
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
+    case LOCAL_GL_RENDERBUFFER_ALPHA_SIZE:
+    case LOCAL_GL_ALPHA_BITS:
+        return compSize.alphaSize;
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
+    case LOCAL_GL_RENDERBUFFER_DEPTH_SIZE:
+    case LOCAL_GL_DEPTH_BITS:
+        return compSize.depthSize;
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
+    case LOCAL_GL_RENDERBUFFER_STENCIL_SIZE:
+    case LOCAL_GL_STENCIL_BITS:
+        return compSize.stencilSize;
+    }
+
+    return 0;
+}
+
+static GLenum kComponentTypes[] = {
+    // GLES 3.0.4, p128-129, "Required Texture Formats"
+    // "Texture and renderbuffer color formats"
+    LOCAL_GL_INT,                       // RGBA32I,
+    LOCAL_GL_UNSIGNED_INT,              // RGBA32UI,
+    LOCAL_GL_INT,                       // RGBA16I,
+    LOCAL_GL_UNSIGNED_INT,              // RGBA16UI,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // RGBA8,
+    LOCAL_GL_INT,                       // RGBA8I,
+    LOCAL_GL_UNSIGNED_INT,              // RGBA8UI,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // SRGB8_ALPHA8,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // RGB10_A2,
+    LOCAL_GL_UNSIGNED_INT,              // RGB10_A2UI,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // RGBA4,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // RGB5_A1,
+
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // RGB8,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // RGB565,
+
+    LOCAL_GL_INT,                       // RG32I,
+    LOCAL_GL_UNSIGNED_INT,              // RG32UI,
+    LOCAL_GL_INT,                       // RG16I,
+    LOCAL_GL_UNSIGNED_INT,              // RG16UI,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // RG8,
+    LOCAL_GL_INT,                       // RG8I,
+    LOCAL_GL_UNSIGNED_INT,              // RG8UI,
+
+    LOCAL_GL_INT,                       // R32I,
+    LOCAL_GL_UNSIGNED_INT,              // R32UI,
+    LOCAL_GL_INT,                       // R16I,
+    LOCAL_GL_UNSIGNED_INT,              // R16UI,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // R8,
+    LOCAL_GL_INT,                       // R8I,
+    LOCAL_GL_UNSIGNED_INT,              // R8UI,
+
+    // "Texture-only color formats"
+    LOCAL_GL_FLOAT,                     // RGBA32F,
+    LOCAL_GL_FLOAT,                     // RGBA16F,
+    LOCAL_GL_SIGNED_NORMALIZED,         // RGBA8_SNORM,
+
+    LOCAL_GL_FLOAT,                     // RGB32F,
+    LOCAL_GL_INT,                       // RGB32I,
+    LOCAL_GL_UNSIGNED_INT,              // RGB32UI,
+
+    LOCAL_GL_FLOAT,                     // RGB16F,
+    LOCAL_GL_INT,                       // RGB16I,
+    LOCAL_GL_UNSIGNED_INT,              // RGB16UI,
+
+    LOCAL_GL_SIGNED_NORMALIZED,         // RGB8_SNORM,
+    LOCAL_GL_INT,                       // RGB8I,
+    LOCAL_GL_UNSIGNED_INT,              // RGB8UI,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // SRGB8,
+
+    LOCAL_GL_FLOAT,                     // R11F_G11F_B10F,
+    LOCAL_GL_FLOAT,                     // RGB9_E5,
+
+    LOCAL_GL_FLOAT,                     // RG32F,
+    LOCAL_GL_FLOAT,                     // RG16F,
+    LOCAL_GL_SIGNED_NORMALIZED,         // RG8_SNORM,
+
+    LOCAL_GL_FLOAT,                     // R32F,
+    LOCAL_GL_FLOAT,                     // R16F,
+    LOCAL_GL_SIGNED_NORMALIZED,         // R8_SNORM,
+
+    // "Depth formats"
+    LOCAL_GL_FLOAT,                     // DEPTH_COMPONENT32F,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // DEPTH_COMPONENT24,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // DEPTH_COMPONENT16,
+
+    // "Combined depth+stencil formats"
+    LOCAL_GL_FLOAT,                     // DEPTH32F_STENCIL8,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // DEPTH24_STENCIL8,
+
+    // GLES 3.0.4, p205-206, "Required Renderbuffer Formats"
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // STENCIL_INDEX8,
+
+    // GLES 3.0.4, p128, table 3.12.
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // Luminance8Alpha8,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // Luminance8,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // Alpha8,
+
+    // GLES 3.0.4, p147, table 3.19
+    // GLES 3.0.4, p286+, $C.1 "ETC Compressed Texture Image Formats"
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_R11_EAC,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_SIGNED_R11_EAC,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RG11_EAC,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_SIGNED_RG11_EAC,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGB8_ETC2,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_SRGB8_ETC2,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGBA8_ETC2_EAC,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_SRGB8_ALPHA8_ETC2_EAC,
+
+    // AMD_compressed_ATC_texture
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // ATC_RGB_AMD,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // ATC_RGBA_EXPLICIT_ALPHA_AMD,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // ATC_RGBA_INTERPOLATED_ALPHA_AMD,
+
+    // EXT_texture_compression_s3tc
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGB_S3TC_DXT1,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGBA_S3TC_DXT1,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGBA_S3TC_DXT3,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGBA_S3TC_DXT5,
+
+    // IMG_texture_compression_pvrtc
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGB_PVRTC_4BPPV1,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGBA_PVRTC_4BPPV1,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGB_PVRTC_2BPPV1,
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // COMPRESSED_RGBA_PVRTC_2BPPV1,
+
+    // OES_compressed_ETC1_RGB8_texture
+    LOCAL_GL_UNSIGNED_NORMALIZED,       // ETC1_RGB8,
+
+    // OES_texture_float
+    LOCAL_GL_FLOAT,                     // Luminance32FAlpha32F,
+    LOCAL_GL_FLOAT,                     // Luminance32F,
+    LOCAL_GL_FLOAT,                     // Alpha32F,
+
+    // OES_texture_half_float
+    LOCAL_GL_FLOAT,                     // Luminance16FAlpha16F,
+    LOCAL_GL_FLOAT,                     // Luminance16F,
+    LOCAL_GL_FLOAT,                     // Alpha16F,
+
+    LOCAL_GL_NONE // MAX
+};
+
+GLenum
+GetComponentType(EffectiveFormat format)
+{
+    return kComponentTypes[(int) format];
+}
+
+GLenum
+GetColorEncoding(EffectiveFormat format)
+{
+    const bool isSRGB = (GetFormatInfo(format)->colorComponentType ==
+                         ComponentType::NormUIntSRGB);
+    return (isSRGB) ? LOCAL_GL_SRGB : LOCAL_GL_LINEAR;
+}
+
 } // namespace webgl
 } // namespace mozilla
--- a/dom/canvas/WebGLFormats.h
+++ b/dom/canvas/WebGLFormats.h
@@ -247,19 +247,27 @@ private:
 public:
     void AddFormat(EffectiveFormat format, bool asRenderbuffer, bool isRenderable,
                    bool asTexture, bool isFilterable);
 
     void AddUnpackOption(GLenum unpackFormat, GLenum unpackType,
                          EffectiveFormat effectiveFormat);
 
     FormatUsageInfo* GetUsage(EffectiveFormat format);
-
     FormatUsageInfo* GetUsage(const FormatInfo* format)
     {
+        if (!format)
+            return nullptr;
+
         return GetUsage(format->effectiveFormat);
     }
 };
 
+////////////////////////////////////////////////////////////////////////////////
+
+GLint GetComponentSize(EffectiveFormat format, GLenum component);
+GLenum GetComponentType(EffectiveFormat format);
+GLenum GetColorEncoding(EffectiveFormat format);
+
 } // namespace webgl
 } // namespace mozilla
 
 #endif // WEBGL_FORMATS_H_
--- a/dom/canvas/WebGLFramebuffer.cpp
+++ b/dom/canvas/WebGLFramebuffer.cpp
@@ -428,16 +428,76 @@ WebGLFBAttachPoint::FinalizeAttachment(g
     if (Renderbuffer()) {
         Renderbuffer()->FramebufferRenderbuffer(attachmentLoc);
         return;
     }
 
     MOZ_CRASH();
 }
 
+JS::Value
+WebGLFBAttachPoint::GetParameter(WebGLContext* context, GLenum pname)
+{
+    // TODO: WebGLTexture and WebGLRenderbuffer should store FormatInfo instead of doing
+    // this dance every time.
+    const GLenum internalFormat = EffectiveInternalFormat().get();
+    const webgl::FormatInfo* info = webgl::GetInfoBySizedFormat(internalFormat);
+    MOZ_ASSERT(info);
+
+    WebGLTexture* tex = Texture();
+
+    switch (pname) {
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
+        return JS::Int32Value(webgl::GetComponentSize(info->effectiveFormat, pname));
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
+        return JS::Int32Value(webgl::GetComponentType(info->effectiveFormat));
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
+        return JS::Int32Value(webgl::GetColorEncoding(info->effectiveFormat));
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL:
+        if (tex) {
+            return JS::Int32Value(MipLevel());
+        }
+        break;
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE:
+        if (tex) {
+            int32_t face = 0;
+            if (tex->Target() == LOCAL_GL_TEXTURE_CUBE_MAP) {
+                face = ImageTarget().get();
+            }
+            return JS::Int32Value(face);
+        }
+        break;
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER:
+        if (tex) {
+            int32_t layer = 0;
+            if (tex->Target() == LOCAL_GL_TEXTURE_2D_ARRAY ||
+                tex->Target() == LOCAL_GL_TEXTURE_3D)
+            {
+                layer = Layer();
+            }
+            return JS::Int32Value(layer);
+        }
+        break;
+    }
+
+    context->ErrorInvalidEnum("getFramebufferParameter: Invalid combination of "
+                              "attachment and pname.");
+    return JS::NullValue();
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // WebGLFramebuffer
 
 WebGLFramebuffer::WebGLFramebuffer(WebGLContext* webgl, GLuint fbo)
     : WebGLContextBoundObject(webgl)
     , mGLName(fbo)
     , mStatus(0)
     , mReadBufferMode(LOCAL_GL_COLOR_ATTACHMENT0)
@@ -982,16 +1042,120 @@ WebGLFramebuffer::ValidateForRead(const 
         mContext->ErrorInvalidOperation("readPixels: ");
         return false;
     }
 
     *out_format = attachPoint.EffectiveInternalFormat();
     return true;
 }
 
+static bool
+AttachmentsDontMatch(const WebGLFBAttachPoint& a, const WebGLFBAttachPoint& b)
+{
+    if (a.Texture()) {
+        return (a.Texture() != b.Texture());
+    }
+
+    if (a.Renderbuffer()) {
+        return (a.Renderbuffer() != b.Renderbuffer());
+    }
+
+    return false;
+}
+
+JS::Value
+WebGLFramebuffer::GetAttachmentParameter(JSContext* cx,
+                                         GLenum attachment,
+                                         GLenum pname,
+                                         ErrorResult& rv)
+{
+    // "If a framebuffer object is bound to target, then attachment must be one of the
+    // attachment points of the framebuffer listed in table 4.6."
+    switch (attachment) {
+    case LOCAL_GL_DEPTH_ATTACHMENT:
+    case LOCAL_GL_DEPTH_STENCIL_ATTACHMENT:
+        break;
+
+    case LOCAL_GL_STENCIL_ATTACHMENT:
+        // "If attachment is DEPTH_STENCIL_ATTACHMENT, and different objects are bound to
+        //  the depth and stencil attachment points of target, the query will fail and
+        //  generate an INVALID_OPERATION error. If the same object is bound to both
+        //  attachment points, information about that object will be returned."
+
+        // Does this mean it has to be the same level or layer? Because the queries are
+        // independent of level or layer.
+        if (AttachmentsDontMatch(DepthAttachment(), StencilAttachment())) {
+            mContext->ErrorInvalidOperation("getFramebufferAttachmentParameter: "
+                                            "DEPTH_ATTACHMENT and STENCIL_ATTACHMENT "
+                                            "have different objects bound.");
+            return JS::NullValue();
+        }
+        break;
+
+    default:
+        if (attachment < LOCAL_GL_COLOR_ATTACHMENT0 ||
+            attachment > mContext->LastColorAttachment())
+        {
+            mContext->ErrorInvalidEnum("getFramebufferAttachmentParameter: Can only "
+                                       "query COLOR_ATTACHMENTi, DEPTH_ATTACHMENT, "
+                                       "DEPTH_STENCIL_ATTACHMENT, or STENCIL_ATTACHMENT "
+                                       "on framebuffer.");
+            return JS::NullValue();
+        }
+    }
+
+    if (attachment == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT &&
+        pname == LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE)
+    {
+        mContext->ErrorInvalidOperation("getFramebufferAttachmentParameter: Querying "
+                                        "FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE against "
+                                        "DEPTH_STENCIL_ATTACHMENT is an error.");
+        return JS::NullValue();
+    }
+
+    GLenum objectType = LOCAL_GL_NONE;
+    auto& fba = GetAttachPoint(attachment);
+    if (fba.Texture()) {
+        objectType = LOCAL_GL_TEXTURE;
+    } else if (fba.Renderbuffer()) {
+        objectType = LOCAL_GL_RENDERBUFFER;
+    }
+
+    switch (pname) {
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
+        return JS::Int32Value(objectType);
+
+    case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME:
+        if (objectType == LOCAL_GL_NONE) {
+            return JS::NullValue();
+        }
+
+        if (objectType == LOCAL_GL_RENDERBUFFER) {
+            const WebGLRenderbuffer* rb = fba.Renderbuffer();
+            return mContext->WebGLObjectAsJSValue(cx, rb, rv);
+        }
+
+        /* If the value of FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is TEXTURE, then */
+        if (objectType == LOCAL_GL_TEXTURE) {
+            const WebGLTexture* tex = fba.Texture();
+            return mContext->WebGLObjectAsJSValue(cx, tex, rv);
+        }
+        break;
+    }
+
+    if (objectType == LOCAL_GL_NONE) {
+        mContext->ErrorInvalidOperation("getFramebufferAttachmentParameter: No "
+                                        "attachment at %s",
+                                        mContext->EnumName(attachment));
+        return JS::NullValue();
+    }
+
+    return fba.GetParameter(mContext, pname);
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Goop.
 
 JSObject*
 WebGLFramebuffer::WrapObject(JSContext* cx, JS::Handle<JSObject*> givenProto)
 {
     return dom::WebGLFramebufferBinding::Wrap(cx, this, givenProto);
 }
--- a/dom/canvas/WebGLFramebuffer.h
+++ b/dom/canvas/WebGLFramebuffer.h
@@ -57,17 +57,17 @@ public:
     void Clear() {
         SetRenderbuffer(nullptr);
     }
 
     void SetTexImage(WebGLTexture* tex, TexImageTarget target, GLint level);
     void SetTexImageLayer(WebGLTexture* tex, TexImageTarget target, GLint level,
                           GLint layer);
     void SetRenderbuffer(WebGLRenderbuffer* rb);
-    
+
     const WebGLTexture* Texture() const {
         return mTexturePtr;
     }
     WebGLTexture* Texture() {
         return mTexturePtr;
     }
     const WebGLRenderbuffer* Renderbuffer() const {
         return mRenderbufferPtr;
@@ -90,16 +90,18 @@ public:
 
     const WebGLRectangleObject& RectangleObject() const;
 
     bool HasImage() const;
     bool IsComplete() const;
 
     void FinalizeAttachment(gl::GLContext* gl,
                             FBAttachment attachmentLoc) const;
+
+    JS::Value GetParameter(WebGLContext* context, GLenum pname);
 };
 
 class WebGLFramebuffer final
     : public nsWrapperCache
     , public WebGLRefCountedObject<WebGLFramebuffer>
     , public LinkedListElement<WebGLFramebuffer>
     , public WebGLContextBoundObject
     , public SupportsWeakPtr<WebGLFramebuffer>
@@ -220,13 +222,16 @@ public:
 
     void EnsureColorAttachPoints(size_t colorAttachmentId);
 
     void InvalidateFramebufferStatus() const {
         mStatus = 0;
     }
 
     bool ValidateForRead(const char* info, TexInternalFormat* const out_format);
+
+    JS::Value GetAttachmentParameter(JSContext* cx, GLenum attachment, GLenum pname,
+                                     ErrorResult& rv);
 };
 
 } // namespace mozilla
 
 #endif // WEBGL_FRAMEBUFFER_H_
--- a/dom/canvas/WebGLProgram.h
+++ b/dom/canvas/WebGLProgram.h
@@ -14,18 +14,16 @@
 #include "mozilla/RefPtr.h"
 #include "mozilla/WeakPtr.h"
 #include "nsString.h"
 #include "nsWrapperCache.h"
 
 #include "WebGLObjectModel.h"
 
 
-template<class> class nsRefPtr;
-
 namespace mozilla {
 class ErrorResult;
 class WebGLActiveInfo;
 class WebGLProgram;
 class WebGLShader;
 class WebGLUniformLocation;
 
 namespace dom {
--- a/dom/events/DataTransfer.cpp
+++ b/dom/events/DataTransfer.cpp
@@ -26,16 +26,17 @@
 #include "nsVariant.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/DataTransferBinding.h"
 #include "mozilla/dom/Directory.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/FileList.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/OSFileSystem.h"
+#include "mozilla/dom/Promise.h"
 
 namespace mozilla {
 namespace dom {
 
 inline void
 ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
                             TransferItem& aField,
                             const char* aName,
--- a/dom/events/DataTransfer.h
+++ b/dom/events/DataTransfer.h
@@ -15,32 +15,32 @@
 #include "nsIDOMElement.h"
 #include "nsIDragService.h"
 #include "nsCycleCollectionParticipant.h"
 
 #include "nsAutoPtr.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/dom/File.h"
-#include "mozilla/dom/Promise.h"
 
 class nsINode;
 class nsITransferable;
 class nsISupportsArray;
 class nsILoadContext;
 
 namespace mozilla {
 
 class EventStateManager;
 
 namespace dom {
 
 class DOMStringList;
 class Element;
 class FileList;
+class Promise;
 template<typename T> class Optional;
 
 /**
  * TransferItem is used to hold data for a particular format. Each piece of
  * data has a principal set from the caller which added it. This allows a
  * caller that wishes to retrieve the data to only be able to access the data
  * it is allowed to, yet still allow a chrome caller to retrieve any of the
  * data.
--- a/dom/events/Touch.cpp
+++ b/dom/events/Touch.cpp
@@ -62,16 +62,33 @@ Touch::Touch(int32_t aIdentifier,
   mRotationAngle = aRotationAngle;
   mForce = aForce;
 
   mChanged = false;
   mMessage = 0;
   nsJSContext::LikelyShortLivingObjectCreated();
 }
 
+Touch::Touch(const Touch& aOther)
+  : mTarget(aOther.mTarget)
+  , mRefPoint(aOther.mRefPoint)
+  , mChanged(aOther.mChanged)
+  , mMessage(aOther.mMessage)
+  , mIdentifier(aOther.mIdentifier)
+  , mPagePoint(aOther.mPagePoint)
+  , mClientPoint(aOther.mClientPoint)
+  , mScreenPoint(aOther.mScreenPoint)
+  , mRadius(aOther.mRadius)
+  , mRotationAngle(aOther.mRotationAngle)
+  , mForce(aOther.mForce)
+  , mPointsInitialized(aOther.mPointsInitialized)
+{
+  nsJSContext::LikelyShortLivingObjectCreated();
+}
+
 Touch::~Touch()
 {
 }
 
 // static
 bool
 Touch::PrefEnabled(JSContext* aCx, JSObject* aGlobal)
 {
--- a/dom/events/Touch.h
+++ b/dom/events/Touch.h
@@ -40,16 +40,17 @@ public:
         int32_t aRadiusY,
         float aRotationAngle,
         float aForce);
   Touch(int32_t aIdentifier,
         LayoutDeviceIntPoint aPoint,
         nsIntPoint aRadius,
         float aRotationAngle,
         float aForce);
+  Touch(const Touch& aOther);
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Touch)
 
   void InitializePoints(nsPresContext* aPresContext, WidgetEvent* aEvent);
 
   void SetTarget(EventTarget* aTarget);
 
--- a/dom/events/TouchEvent.cpp
+++ b/dom/events/TouchEvent.cpp
@@ -181,20 +181,20 @@ TouchEvent::ChangedTouches()
 // static
 bool
 TouchEvent::PrefEnabled(JSContext* aCx, JSObject* aGlobal)
 {
   bool prefValue = false;
   int32_t flag = 0;
   if (NS_SUCCEEDED(Preferences::GetInt("dom.w3c_touch_events.enabled", &flag))) {
     if (flag == 2) {
-#ifdef XP_WIN
+#if defined(XP_WIN) || MOZ_WIDGET_GTK == 3
       static bool sDidCheckTouchDeviceSupport = false;
       static bool sIsTouchDeviceSupportPresent = false;
-      // On Windows we auto-detect based on device support.
+      // On Windows and GTK3 we auto-detect based on device support.
       if (!sDidCheckTouchDeviceSupport) {
         sDidCheckTouchDeviceSupport = true;
         sIsTouchDeviceSupportPresent = WidgetUtils::IsTouchDeviceSupportPresent();
       }
       prefValue = sIsTouchDeviceSupportPresent;
 #else
       NS_WARNING("dom.w3c_touch_events.enabled=2 not implemented!");
       prefValue = false;
--- a/dom/events/moz.build
+++ b/dom/events/moz.build
@@ -73,16 +73,17 @@ UNIFIED_SOURCES += [
     'AnimationEvent.cpp',
     'AsyncEventDispatcher.cpp',
     'BeforeAfterKeyboardEvent.cpp',
     'BeforeUnloadEvent.cpp',
     'ClipboardEvent.cpp',
     'CommandEvent.cpp',
     'CompositionEvent.cpp',
     'ContentEventHandler.cpp',
+    'CustomEvent.cpp',
     'DataContainerEvent.cpp',
     'DataTransfer.cpp',
     'DeviceMotionEvent.cpp',
     'DOMEventTargetHelper.cpp',
     'DragEvent.cpp',
     'Event.cpp',
     'EventDispatcher.cpp',
     'EventListenerManager.cpp',
@@ -112,17 +113,16 @@ UNIFIED_SOURCES += [
     'UIEvent.cpp',
     'WheelEvent.cpp',
     'WheelHandlingHelper.cpp',
     'XULCommandEvent.cpp',
 ]
 
 # nsEventStateManager.cpp should be built separately because of Mac OS X headers.
 SOURCES += [
-    'CustomEvent.cpp',
     'EventStateManager.cpp',
 ]
 
 if CONFIG['MOZ_WEBSPEECH']:
     UNIFIED_SOURCES += ['SpeechRecognitionError.cpp']
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -45,363 +45,144 @@ NS_IMPL_ISUPPORTS(FetchDriver,
                   nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor,
                   nsIThreadRetargetableStreamListener)
 
 FetchDriver::FetchDriver(InternalRequest* aRequest, nsIPrincipal* aPrincipal,
                          nsILoadGroup* aLoadGroup)
   : mPrincipal(aPrincipal)
   , mLoadGroup(aLoadGroup)
   , mRequest(aRequest)
-  , mFetchRecursionCount(0)
-  , mCORSFlagEverSet(false)
+  , mHasBeenCrossSite(false)
   , mResponseAvailableCalled(false)
+  , mFetchCalled(false)
 {
 }
 
 FetchDriver::~FetchDriver()
 {
   // We assert this since even on failures, we should call
   // FailWithNetworkError().
   MOZ_ASSERT(mResponseAvailableCalled);
 }
 
 nsresult
 FetchDriver::Fetch(FetchDriverObserver* aObserver)
 {
   workers::AssertIsOnMainThread();
+  MOZ_ASSERT(!mFetchCalled);
+  mFetchCalled = true;
+
   mObserver = aObserver;
 
   Telemetry::Accumulate(Telemetry::SERVICE_WORKER_REQUEST_PASSTHROUGH,
                         mRequest->WasCreatedByFetchEvent());
 
-  return Fetch(false /* CORS flag */);
+  // FIXME(nsm): Deal with HSTS.
+
+  MOZ_RELEASE_ASSERT(!mRequest->IsSynchronous(),
+                     "Synchronous fetch not supported");
+
+  nsCOMPtr<nsIRunnable> r =
+    NS_NewRunnableMethod(this, &FetchDriver::ContinueFetch);
+  return NS_DispatchToCurrentThread(r);
 }
 
 nsresult
-FetchDriver::Fetch(bool aCORSFlag)
-{
-  // We do not currently implement parts of the spec that lead to recursion.
-  MOZ_ASSERT(mFetchRecursionCount == 0);
-  mFetchRecursionCount++;
-
-  // FIXME(nsm): Deal with HSTS.
-
-  if (!mRequest->IsSynchronous() && mFetchRecursionCount <= 1) {
-    nsCOMPtr<nsIRunnable> r =
-      NS_NewRunnableMethodWithArg<bool>(this, &FetchDriver::ContinueFetch, aCORSFlag);
-    nsresult rv = NS_DispatchToCurrentThread(r);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      FailWithNetworkError();
-    }
-    return rv;
-  }
-
-  MOZ_CRASH("Synchronous fetch not supported");
-}
-
-FetchDriver::MainFetchOp
-FetchDriver::SetTaintingAndGetNextOp(bool aCORSFlag)
+FetchDriver::SetTainting()
 {
   workers::AssertIsOnMainThread();
 
+  // If we've already been cross-site then we should be fully updated
+  if (mHasBeenCrossSite) {
+    return NS_OK;
+  }
+
   nsAutoCString url;
   mRequest->GetURL(url);
   nsCOMPtr<nsIURI> requestURI;
   nsresult rv = NS_NewURI(getter_AddRefs(requestURI), url,
                           nullptr, nullptr);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return MainFetchOp(NETWORK_ERROR);
-  }
-
-  // CSP/mixed content checks.
-  int16_t shouldLoad;
-  rv = NS_CheckContentLoadPolicy(mRequest->ContentPolicyType(),
-                                 requestURI,
-                                 mPrincipal,
-                                 mDocument,
-                                 // FIXME(nsm): Should MIME be extracted from
-                                 // Content-Type header?
-                                 EmptyCString(), /* mime guess */
-                                 nullptr, /* extra */
-                                 &shouldLoad,
-                                 nsContentUtils::GetContentPolicy(),
-                                 nsContentUtils::GetSecurityManager());
-  if (NS_WARN_IF(NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad))) {
-    // Disallowed by content policy.
-    return MainFetchOp(NETWORK_ERROR);
-  }
+  NS_ENSURE_SUCCESS(rv, rv);
 
   // Begin Step 8 of the Main Fetch algorithm
   // https://fetch.spec.whatwg.org/#fetching
 
-  nsAutoCString scheme;
-  rv = requestURI->GetScheme(scheme);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return MainFetchOp(NETWORK_ERROR);
-  }
-
   // request's current url's origin is request's origin and the CORS flag is unset
   // request's current url's scheme is "data" and request's same-origin data-URL flag is set
   // request's current url's scheme is "about"
-  rv = mPrincipal->CheckMayLoad(requestURI, false /* report */,
-                                false /* allowIfInheritsPrincipal */);
-  if ((!aCORSFlag && NS_SUCCEEDED(rv)) ||
-      (scheme.EqualsLiteral("data") && mRequest->SameOriginDataURL()) ||
-      scheme.EqualsLiteral("about")) {
-    return MainFetchOp(BASIC_FETCH);
+
+  // We have to manually check about:blank here since it's not treated as
+  // an inheriting URL by CheckMayLoad.
+  if (NS_IsAboutBlank(requestURI) ||
+       NS_SUCCEEDED(mPrincipal->CheckMayLoad(requestURI, false /* report */,
+                                             true /*allowIfInheritsPrincipal*/))) {
+    // What the spec calls "basic fetch" is handled within our necko channel
+    // code.  Therefore everything goes through HTTP Fetch
+    return NS_OK;
   }
 
+  mHasBeenCrossSite = true;
+
   // request's mode is "same-origin"
   if (mRequest->Mode() == RequestMode::Same_origin) {
-    return MainFetchOp(NETWORK_ERROR);
+    return NS_ERROR_DOM_BAD_URI;
   }
 
   // request's mode is "no-cors"
   if (mRequest->Mode() == RequestMode::No_cors) {
     mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_OPAQUE);
-    return MainFetchOp(BASIC_FETCH);
-  }
-
-  // request's current url's scheme is not one of "http" and "https"
-  if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("https")) {
-    return MainFetchOp(NETWORK_ERROR);
-  }
-
-  // request's mode is "cors-with-forced-preflight"
-  // request's unsafe-request flag is set and either request's method is not
-  // a simple method or a header in request's header list is not a simple header
-  if (mRequest->Mode() == RequestMode::Cors_with_forced_preflight ||
-      (mRequest->UnsafeRequest() && (!mRequest->HasSimpleMethod() ||
-                                     !mRequest->Headers()->HasOnlySimpleHeaders()))) {
-    mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_CORS);
-    mRequest->SetRedirectMode(RequestRedirect::Error);
-
-    // Note, the following text from Main Fetch step 8 is handled in
-    // nsCORSListenerProxy when CheckRequestApproved() fails:
-    //
-    //  The result of performing an HTTP fetch using request with the CORS
-    //  flag and CORS-preflight flag set. If the result is a network error,
-    //  clear cache entries using request.
-
-    return MainFetchOp(HTTP_FETCH, true /* cors */, true /* preflight */);
+    // What the spec calls "basic fetch" is handled within our necko channel
+    // code.  Therefore everything goes through HTTP Fetch
+    return NS_OK;
   }
 
   // Otherwise
   mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_CORS);
-  return MainFetchOp(HTTP_FETCH, true /* cors */, false /* preflight */);
+
+  return NS_OK;
 }
 
 nsresult
-FetchDriver::ContinueFetch(bool aCORSFlag)
+FetchDriver::ContinueFetch()
 {
   workers::AssertIsOnMainThread();
 
-  MainFetchOp nextOp = SetTaintingAndGetNextOp(aCORSFlag);
-
-  if (nextOp.mType == NETWORK_ERROR) {
-    return FailWithNetworkError();
-  }
-
-  if (nextOp.mType == BASIC_FETCH) {
-    return BasicFetch();
-  }
-
-  if (nextOp.mType == HTTP_FETCH) {
-    return HttpFetch(nextOp.mCORSFlag, nextOp.mCORSPreflightFlag);
-  }
-
-  MOZ_ASSERT_UNREACHABLE("Unexpected main fetch operation!");
-  return FailWithNetworkError();
- }
-
-nsresult
-FetchDriver::BasicFetch()
-{
-  nsAutoCString url;
-  mRequest->GetURL(url);
-  nsCOMPtr<nsIURI> uri;
-  nsresult rv = NS_NewURI(getter_AddRefs(uri),
-                 url,
-                 nullptr,
-                 nullptr);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
+  nsresult rv = HttpFetch();
+  if (NS_FAILED(rv)) {
     FailWithNetworkError();
-    return rv;
-  }
-
-  nsAutoCString scheme;
-  rv = uri->GetScheme(scheme);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    FailWithNetworkError();
-    return rv;
-  }
-
-  if (scheme.LowerCaseEqualsLiteral("about")) {
-    if (url.EqualsLiteral("about:blank")) {
-      RefPtr<InternalResponse> response =
-        new InternalResponse(200, NS_LITERAL_CSTRING("OK"));
-      ErrorResult result;
-      response->Headers()->Append(NS_LITERAL_CSTRING("content-type"),
-                                  NS_LITERAL_CSTRING("text/html;charset=utf-8"),
-                                  result);
-      MOZ_ASSERT(!result.Failed());
-      nsCOMPtr<nsIInputStream> body;
-      rv = NS_NewCStringInputStream(getter_AddRefs(body), EmptyCString());
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        FailWithNetworkError();
-        return rv;
-      }
-
-      response->SetBody(body);
-      BeginResponse(response);
-      return SucceedWithResponse();
-    }
-    return FailWithNetworkError();
   }
-
-  if (scheme.LowerCaseEqualsLiteral("blob")) {
-    RefPtr<BlobImpl> blobImpl;
-    rv = NS_GetBlobForBlobURI(uri, getter_AddRefs(blobImpl));
-    BlobImpl* blob = static_cast<BlobImpl*>(blobImpl.get());
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      FailWithNetworkError();
-      return rv;
-    }
-
-    RefPtr<InternalResponse> response = new InternalResponse(200, NS_LITERAL_CSTRING("OK"));
-    ErrorResult result;
-    uint64_t size = blob->GetSize(result);
-    if (NS_WARN_IF(result.Failed())) {
-      FailWithNetworkError();
-      return result.StealNSResult();
-    }
-
-    nsAutoString sizeStr;
-    sizeStr.AppendInt(size);
-    response->Headers()->Append(NS_LITERAL_CSTRING("Content-Length"), NS_ConvertUTF16toUTF8(sizeStr), result);
-    if (NS_WARN_IF(result.Failed())) {
-      FailWithNetworkError();
-      return result.StealNSResult();
-    }
-
-    nsAutoString type;
-    blob->GetType(type);
-    response->Headers()->Append(NS_LITERAL_CSTRING("Content-Type"), NS_ConvertUTF16toUTF8(type), result);
-    if (NS_WARN_IF(result.Failed())) {
-      FailWithNetworkError();
-      return result.StealNSResult();
-    }
-
-    nsCOMPtr<nsIInputStream> stream;
-    blob->GetInternalStream(getter_AddRefs(stream), result);
-    if (NS_WARN_IF(result.Failed())) {
-      FailWithNetworkError();
-      return result.StealNSResult();
-    }
-
-    response->SetBody(stream);
-    BeginResponse(response);
-    return SucceedWithResponse();
-  }
-
-  if (scheme.LowerCaseEqualsLiteral("data")) {
-    nsAutoCString method;
-    mRequest->GetMethod(method);
-    if (method.LowerCaseEqualsASCII("get")) {
-      nsresult rv;
-      nsCOMPtr<nsIProtocolHandler> dataHandler =
-        do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "data", &rv);
-
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
-
-      nsCOMPtr<nsIChannel> channel;
-      rv = dataHandler->NewChannel(uri, getter_AddRefs(channel));
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
-
-      nsCOMPtr<nsIInputStream> stream;
-      rv = channel->Open(getter_AddRefs(stream));
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
-
-      // nsDataChannel will parse the data URI when it is Open()ed and set the
-      // correct content type and charset.
-      nsAutoCString contentType;
-      if (NS_SUCCEEDED(channel->GetContentType(contentType))) {
-        nsAutoCString charset;
-        if (NS_SUCCEEDED(channel->GetContentCharset(charset)) && !charset.IsEmpty()) {
-          contentType.AppendLiteral(";charset=");
-          contentType.Append(charset);
-        }
-      } else {
-        NS_WARNING("Could not get content type from data channel");
-      }
-
-      RefPtr<InternalResponse> response = new InternalResponse(200, NS_LITERAL_CSTRING("OK"));
-      ErrorResult result;
-      response->Headers()->Append(NS_LITERAL_CSTRING("Content-Type"), contentType, result);
-      if (NS_WARN_IF(result.Failed())) {
-        FailWithNetworkError();
-        return result.StealNSResult();
-      }
-
-      response->SetBody(stream);
-      BeginResponse(response);
-      return SucceedWithResponse();
-    }
-
-    return FailWithNetworkError();
-  }
-
-  if (scheme.LowerCaseEqualsLiteral("http") ||
-      scheme.LowerCaseEqualsLiteral("https") ||
-      scheme.LowerCaseEqualsLiteral("app")) {
-    return HttpFetch();
-  }
-
-  return FailWithNetworkError();
+ 
+  return rv;
 }
 
 // This function implements the "HTTP Fetch" algorithm from the Fetch spec.
 // Functionality is often split between here, the CORS listener proxy and the
 // Necko HTTP implementation.
 nsresult
-FetchDriver::HttpFetch(bool aCORSFlag, bool aCORSPreflightFlag, bool aAuthenticationFlag)
+FetchDriver::HttpFetch()
 {
   // Step 1. "Let response be null."
   mResponse = nullptr;
   nsresult rv;
 
-  // We need to track the CORS flag through redirects.  Since there is no way
-  // for us to go from CORS mode to non-CORS mode, we just need to remember
-  // if it has ever been set.
-  mCORSFlagEverSet = mCORSFlagEverSet || aCORSFlag;
-
   nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    FailWithNetworkError();
-    return rv;
-  }
+  NS_ENSURE_SUCCESS(rv, rv);
 
   nsAutoCString url;
   mRequest->GetURL(url);
   nsCOMPtr<nsIURI> uri;
   rv = NS_NewURI(getter_AddRefs(uri),
                           url,
                           nullptr,
                           nullptr,
                           ios);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    FailWithNetworkError();
-    return rv;
-  }
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = SetTainting();
+  NS_ENSURE_SUCCESS(rv, rv);
 
   // Step 2 deals with letting ServiceWorkers intercept requests. This is
   // handled by Necko after the channel is opened.
   // FIXME(nsm): Bug 1119026: The channel's skip service worker flag should be
   // set based on the Request's flag.
 
   // Step 3.1 "If the CORS preflight flag is set and one of these conditions is
   // true..." is handled by the CORS proxy.
@@ -414,54 +195,85 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
   // if it succeeds, so we need to have everything setup for the original
   // request too.
 
   // Step 3.3 "Let credentials flag be set if one of
   //  - request's credentials mode is "include"
   //  - request's credentials mode is "same-origin" and either the CORS flag
   //    is unset or response tainting is "opaque"
   // is true, and unset otherwise."
-  bool useCredentials = false;
-  if (mRequest->GetCredentialsMode() == RequestCredentials::Include ||
-      (mRequest->GetCredentialsMode() == RequestCredentials::Same_origin && !aCORSFlag &&
-       mRequest->GetResponseTainting() != InternalRequest::RESPONSETAINT_OPAQUE)) {
-    useCredentials = true;
-  }
 
   // This is effectivetly the opposite of the use credentials flag in "HTTP
   // network or cache fetch" in the spec and decides whether to transmit
   // cookies and other identifying information. LOAD_ANONYMOUS also prevents
   // new cookies sent by the server from being stored.  This value will
   // propagate across redirects, which is what we want.
-  const nsLoadFlags credentialsFlag = useCredentials ? 0 : nsIRequest::LOAD_ANONYMOUS;
+  const nsLoadFlags credentialsFlag =
+    (mRequest->GetCredentialsMode() == RequestCredentials::Omit ||
+    (mHasBeenCrossSite &&
+     mRequest->GetCredentialsMode() == RequestCredentials::Same_origin &&
+     mRequest->Mode() == RequestMode::No_cors)) ?
+    nsIRequest::LOAD_ANONYMOUS : 0;
 
   // Set skip serviceworker flag.
   // While the spec also gates on the client being a ServiceWorker, we can't
   // infer that here. Instead we rely on callers to set the flag correctly.
   const nsLoadFlags bypassFlag = mRequest->SkipServiceWorker() ?
                                  nsIChannel::LOAD_BYPASS_SERVICE_WORKER : 0;
 
+  nsSecurityFlags secFlags;
+  if (mRequest->Mode() == RequestMode::Cors &&
+      mRequest->GetCredentialsMode() == RequestCredentials::Include) {
+    secFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS |
+               nsILoadInfo::SEC_REQUIRE_CORS_WITH_CREDENTIALS;
+  } else if (mRequest->Mode() == RequestMode::Cors) {
+    secFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS;
+  } else if (mRequest->Mode() == RequestMode::Same_origin) {
+    secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS;
+  } else if (mRequest->Mode() == RequestMode::No_cors) {
+    secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS;
+  } else {
+    MOZ_ASSERT_UNREACHABLE("Unexpected request mode!");
+    return NS_ERROR_UNEXPECTED;
+  }
+
   // From here on we create a channel and set its properties with the
   // information from the InternalRequest. This is an implementation detail.
   MOZ_ASSERT(mLoadGroup);
   nsCOMPtr<nsIChannel> chan;
-  rv = NS_NewChannel(getter_AddRefs(chan),
-                     uri,
-                     mPrincipal,
-                     nsILoadInfo::SEC_NORMAL,
-                     mRequest->ContentPolicyType(),
-                     mLoadGroup,
-                     nullptr, /* aCallbacks */
-                     nsIRequest::LOAD_NORMAL | credentialsFlag | bypassFlag,
-                     ios);
+
+  // For dedicated workers mDocument refers to the parent document of the
+  // worker (why do we do that?). In that case we don't want to use the
+  // document here since that is not the correct principal.
+  if (mDocument && mDocument->NodePrincipal() == mPrincipal) {
+    rv = NS_NewChannel(getter_AddRefs(chan),
+                       uri,
+                       mDocument,
+                       secFlags |
+                         nsILoadInfo::SEC_ABOUT_BLANK_INHERITS,
+                       mRequest->ContentPolicyType(),
+                       mLoadGroup,
+                       nullptr, /* aCallbacks */
+                       nsIRequest::LOAD_NORMAL | credentialsFlag | bypassFlag,
+                       ios);
+  } else {
+    rv = NS_NewChannel(getter_AddRefs(chan),
+                       uri,
+                       mPrincipal,
+                       secFlags |
+                         nsILoadInfo::SEC_ABOUT_BLANK_INHERITS,
+                       mRequest->ContentPolicyType(),
+                       mLoadGroup,
+                       nullptr, /* aCallbacks */
+                       nsIRequest::LOAD_NORMAL | credentialsFlag | bypassFlag,
+                       ios);
+  }
+  NS_ENSURE_SUCCESS(rv, rv);
+
   mLoadGroup = nullptr;
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    FailWithNetworkError();
-    return rv;
-  }
 
   // Insert ourselves into the notification callbacks chain so we can handle
   // cross-origin redirects.
 #ifdef DEBUG
   {
     nsCOMPtr<nsIInterfaceRequestor> notificationCallbacks;
     chan->GetNotificationCallbacks(getter_AddRefs(notificationCallbacks));
     MOZ_ASSERT(!notificationCallbacks);
@@ -480,20 +292,17 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
   // ---------------------------
   // Step 1 "Let HTTPRequest..." The channel is the HTTPRequest.
   nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan);
   if (httpChan) {
     // Copy the method.
     nsAutoCString method;
     mRequest->GetMethod(method);
     rv = httpChan->SetRequestMethod(method);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      FailWithNetworkError();
-      return rv;
-    }
+    NS_ENSURE_SUCCESS(rv, rv);
 
     // Set the same headers.
     nsAutoTArray<InternalHeaders::Entry, 5> headers;
     mRequest->Headers()->GetEntries(headers);
     for (uint32_t i = 0; i < headers.Length(); ++i) {
       if (headers[i].mValue.IsEmpty()) {
         httpChan->SetEmptyRequestHeader(headers[i].mName);
       } else {
@@ -503,55 +312,46 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
 
     // Step 2. Set the referrer.
     nsAutoString referrer;
     mRequest->GetReferrer(referrer);
     if (referrer.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) {
       rv = nsContentUtils::SetFetchReferrerURIWithPolicy(mPrincipal,
                                                          mDocument,
                                                          httpChan);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
+      NS_ENSURE_SUCCESS(rv, rv);
     } else if (referrer.IsEmpty()) {
       rv = httpChan->SetReferrerWithPolicy(nullptr, net::RP_No_Referrer);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
+      NS_ENSURE_SUCCESS(rv, rv);
     } else {
       // From "Determine request's Referrer" step 3
       // "If request's referrer is a URL, let referrerSource be request's
       // referrer."
       //
       // XXXnsm - We never actually hit this from a fetch() call since both
       // fetch and Request() create a new internal request whose referrer is
       // always set to about:client. Should we just crash here instead until
       // someone tries to use FetchDriver for non-fetch() APIs?
       nsCOMPtr<nsIURI> referrerURI;
       rv = NS_NewURI(getter_AddRefs(referrerURI), referrer, nullptr, nullptr);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
+      NS_ENSURE_SUCCESS(rv, rv);
 
       rv =
         httpChan->SetReferrerWithPolicy(referrerURI,
                                         mDocument ? mDocument->GetReferrerPolicy() :
                                                     net::RP_Default);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
+      NS_ENSURE_SUCCESS(rv, rv);
     }
 
     // Step 3 "If HTTPRequest's force Origin header flag is set..."
     if (mRequest->ForceOriginHeader()) {
       nsAutoString origin;
       rv = nsContentUtils::GetUTFOrigin(mPrincipal, origin);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
+      NS_ENSURE_SUCCESS(rv, rv);
+
       httpChan->SetRequestHeader(NS_LITERAL_CSTRING("origin"),
                                  NS_ConvertUTF16toUTF8(origin),
                                  false /* merge */);
     }
     // Bug 1120722 - Authorization will be handled later.
     // Auth may require prompting, we don't support it yet.
     // The next patch in this same bug prevents this from aborting the request.
     // Credentials checks for CORS are handled by nsCORSListenerProxy,
@@ -572,96 +372,68 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
   nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(chan);
   if (uploadChan) {
     nsAutoCString contentType;
     ErrorResult result;
     mRequest->Headers()->Get(NS_LITERAL_CSTRING("content-type"), contentType, result);
     // This is an error because the Request constructor explicitly extracts and
     // sets a content-type per spec.
     if (result.Failed()) {
-      return FailWithNetworkError();
+      return result.StealNSResult();
     }
 
     nsCOMPtr<nsIInputStream> bodyStream;
     mRequest->GetBody(getter_AddRefs(bodyStream));
     if (bodyStream) {
       nsAutoCString method;
       mRequest->GetMethod(method);
       rv = uploadChan->ExplicitSetUploadStream(bodyStream, contentType, -1, method, false /* aStreamHasHeaders */);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return FailWithNetworkError();
-      }
+      NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
-  nsCOMPtr<nsIStreamListener> listener = this;
-
-  MOZ_ASSERT_IF(aCORSFlag, mRequest->Mode() == RequestMode::Cors);
-
-  // Only use nsCORSListenerProxy if we are in CORS mode.  Otherwise it
-  // will overwrite the CorsMode flag unconditionally to "cors" or
-  // "cors-with-forced-preflight".
-  if (mRequest->Mode() == RequestMode::Cors) {
-    // Passing false for the credentials flag to nsCORSListenerProxy is semantically
-    // the same as the "same-origin" RequestCredentials value.  We implement further
-    // blocking of credentials for "omit" by setting LOAD_ANONYMOUS manually above.
-    bool corsCredentials =
-      mRequest->GetCredentialsMode() == RequestCredentials::Include;
-
-    // Set up a CORS proxy that will handle the various requirements of the CORS
-    // protocol. It handles the preflight cache and CORS response headers.
-    // If the request is allowed, it will start our original request
-    // and our observer will be notified. On failure, our observer is notified
-    // directly.
-    RefPtr<nsCORSListenerProxy> corsListener =
-      new nsCORSListenerProxy(this, mPrincipal, corsCredentials);
-    rv = corsListener->Init(chan, DataURIHandling::Allow);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return FailWithNetworkError();
-    }
-    listener = corsListener.forget();
-  }
-
   // If preflight is required, start a "CORS preflight fetch"
   // https://fetch.spec.whatwg.org/#cors-preflight-fetch-0. All the
-  // implementation is handled by NS_StartCORSPreflight, we just set up the
-  // unsafeHeaders so they can be verified against the response's
-  // "Access-Control-Allow-Headers" header.
-  if (aCORSPreflightFlag) {
-    MOZ_ASSERT(mRequest->Mode() != RequestMode::No_cors,
-               "FetchDriver::ContinueFetch() should ensure that the request is not no-cors");
-    MOZ_ASSERT(httpChan, "CORS preflight can only be used with HTTP channels");
+  // implementation is handled by the http channel calling into
+  // nsCORSListenerProxy. We just inform it which unsafe headers are included
+  // in the request.
+  if (IsUnsafeRequest()) {
+    if (mRequest->Mode() == RequestMode::No_cors) {
+      return NS_ERROR_DOM_BAD_URI;
+    }
+
+    mRequest->SetRedirectMode(RequestRedirect::Error);
+
     nsAutoTArray<nsCString, 5> unsafeHeaders;
     mRequest->Headers()->GetUnsafeHeaders(unsafeHeaders);
 
     nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(httpChan);
-    rv = internalChan->SetCorsPreflightParameters(unsafeHeaders, useCredentials, mPrincipal);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return FailWithNetworkError();
-    }
+    NS_ENSURE_TRUE(internalChan, NS_ERROR_DOM_BAD_URI);
+
+    rv = internalChan->SetCorsPreflightParameters(
+      unsafeHeaders,
+      mRequest->GetCredentialsMode() == RequestCredentials::Include,
+      mPrincipal);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  rv = chan->AsyncOpen(listener, nullptr);
-
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return FailWithNetworkError();
-  }
+  rv = chan->AsyncOpen2(this);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   // Step 4 onwards of "HTTP Fetch" is handled internally by Necko.
   return NS_OK;
 }
 
-nsresult
-FetchDriver::ContinueHttpFetchAfterNetworkFetch()
+bool
+FetchDriver::IsUnsafeRequest()
 {
-  workers::AssertIsOnMainThread();
-  MOZ_ASSERT(mResponse);
-  MOZ_ASSERT(!mResponse->IsError());
-
-  return SucceedWithResponse();
+  return mHasBeenCrossSite &&
+         (mRequest->UnsafeRequest() &&
+          (!mRequest->HasSimpleMethod() ||
+           !mRequest->Headers()->HasOnlySimpleHeaders()));
 }
 
 already_AddRefed<InternalResponse>
 FetchDriver::BeginAndGetFilteredResponse(InternalResponse* aResponse, nsIURI* aFinalURI)
 {
   MOZ_ASSERT(aResponse);
   nsAutoCString reqURL;
   if (aFinalURI) {
@@ -694,34 +466,16 @@ FetchDriver::BeginAndGetFilteredResponse
 
   MOZ_ASSERT(filteredResponse);
   MOZ_ASSERT(mObserver);
   mObserver->OnResponseAvailable(filteredResponse);
   mResponseAvailableCalled = true;
   return filteredResponse.forget();
 }
 
-void
-FetchDriver::BeginResponse(InternalResponse* aResponse)
-{
-  RefPtr<InternalResponse> r = BeginAndGetFilteredResponse(aResponse, nullptr);
-  // Release the ref.
-}
-
-nsresult
-FetchDriver::SucceedWithResponse()
-{
-  workers::AssertIsOnMainThread();
-  if (mObserver) {
-    mObserver->OnResponseEnd();
-    mObserver = nullptr;
-  }
-  return NS_OK;
-}
-
 nsresult
 FetchDriver::FailWithNetworkError()
 {
   workers::AssertIsOnMainThread();
   RefPtr<InternalResponse> error = InternalResponse::NetworkError();
   if (mObserver) {
     mObserver->OnResponseAvailable(error);
     mResponseAvailableCalled = true;
@@ -780,47 +534,75 @@ FetchDriver::OnStartRequest(nsIRequest* 
     return rv;
   }
 
   // We should only get to the following code once.
   MOZ_ASSERT(!mPipeOutputStream);
   MOZ_ASSERT(mObserver);
 
   RefPtr<InternalResponse> response;
+  nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
   nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
+  nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(aRequest);
+
   if (httpChannel) {
     uint32_t responseStatus;
     httpChannel->GetResponseStatus(&responseStatus);
 
     nsAutoCString statusText;
     httpChannel->GetResponseStatusText(statusText);
 
     response = new InternalResponse(responseStatus, statusText);
 
     RefPtr<FillResponseHeaders> visitor = new FillResponseHeaders(response);
     rv = httpChannel->VisitResponseHeaders(visitor);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       NS_WARNING("Failed to visit all headers.");
     }
-  } else {
-    nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(aRequest);
-    // If it is not an http channel, it has to be a jar one.
-    MOZ_ASSERT(jarChannel);
-
+  } else if (jarChannel) {
     // We simulate the http protocol for jar/app requests
     uint32_t responseStatus = 200;
     nsAutoCString statusText;
     response = new InternalResponse(responseStatus, NS_LITERAL_CSTRING("OK"));
     ErrorResult result;
     nsAutoCString contentType;
     jarChannel->GetContentType(contentType);
     response->Headers()->Append(NS_LITERAL_CSTRING("content-type"),
                                 contentType,
                                 result);
     MOZ_ASSERT(!result.Failed());
+  } else {
+    response = new InternalResponse(200, NS_LITERAL_CSTRING("OK"));
+
+    ErrorResult result;
+    nsAutoCString contentType;
+    rv = channel->GetContentType(contentType);
+    if (NS_SUCCEEDED(rv) && !contentType.IsEmpty()) {
+      nsAutoCString contentCharset;
+      channel->GetContentCharset(contentCharset);
+      if (NS_SUCCEEDED(rv) && !contentCharset.IsEmpty()) {
+        contentType += NS_LITERAL_CSTRING(";charset=") + contentCharset;
+      }
+
+      response->Headers()->Append(NS_LITERAL_CSTRING("Content-Type"),
+                                  contentType,
+                                  result);
+      MOZ_ASSERT(!result.Failed());
+    }
+
+    int64_t contentLength;
+    rv = channel->GetContentLength(&contentLength);
+    if (NS_SUCCEEDED(rv) && contentLength) {
+      nsAutoCString contentLenStr;
+      contentLenStr.AppendInt(contentLength);
+      response->Headers()->Append(NS_LITERAL_CSTRING("Content-Length"),
+                                  contentLenStr,
+                                  result);
+      MOZ_ASSERT(!result.Failed());
+    }
   }
 
   // We open a pipe so that we can immediately set the pipe's read end as the
   // response's body. Setting the segment size to UINT32_MAX means that the
   // pipe has infinite space. The nsIChannel will continue to buffer data in
   // xpcom events even if we block on a fixed size pipe.  It might be possible
   // to suspend the channel and then resume when there is space available, but
   // for now use an infinite pipe to avoid blocking.
@@ -833,17 +615,16 @@ FetchDriver::OnStartRequest(nsIRequest* 
                   false /* blocking output, since the pipe is 'in'finite */ );
   if (NS_WARN_IF(NS_FAILED(rv))) {
     FailWithNetworkError();
     // Cancel request.
     return rv;
   }
   response->SetBody(pipeInputStream);
 
-  nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
   response->InitChannelInfo(channel);
 
   nsCOMPtr<nsIURI> channelURI;
   rv = channel->GetURI(getter_AddRefs(channelURI));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     FailWithNetworkError();
     // Cancel request.
     return rv;
@@ -894,57 +675,67 @@ FetchDriver::OnStopRequest(nsIRequest* a
                            nsresult aStatusCode)
 {
   workers::AssertIsOnMainThread();
   if (NS_FAILED(aStatusCode)) {
     nsCOMPtr<nsIAsyncOutputStream> outputStream = do_QueryInterface(mPipeOutputStream);
     if (outputStream) {
       outputStream->CloseWithStatus(NS_BINDING_FAILED);
     }
+
     // We proceed as usual here, since we've already created a successful response
     // from OnStartRequest.
-    SucceedWithResponse();
-    return aStatusCode;
+  } else {
+    MOZ_ASSERT(mResponse);
+    MOZ_ASSERT(!mResponse->IsError());
+
+    if (mPipeOutputStream) {
+      mPipeOutputStream->Close();
+    }
   }
 
-  if (mPipeOutputStream) {
-    mPipeOutputStream->Close();
+  if (mObserver) {
+    mObserver->OnResponseEnd();
+    mObserver = nullptr;
   }
 
-  ContinueHttpFetchAfterNetworkFetch();
   return NS_OK;
 }
 
 // This is called when the channel is redirected.
 NS_IMETHODIMP
 FetchDriver::AsyncOnChannelRedirect(nsIChannel* aOldChannel,
                                     nsIChannel* aNewChannel,
                                     uint32_t aFlags,
                                     nsIAsyncVerifyRedirectCallback *aCallback)
 {
   NS_PRECONDITION(aNewChannel, "Redirect without a channel?");
 
+  if (NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags) ||
+      NS_IsHSTSUpgradeRedirect(aOldChannel, aNewChannel, aFlags)) {
+    aCallback->OnRedirectVerifyCallback(NS_OK);
+    return NS_OK;
+  }
+
   // HTTP Fetch step 5, "redirect status", step 1
   if (NS_WARN_IF(mRequest->GetRedirectMode() == RequestRedirect::Error)) {
     aOldChannel->Cancel(NS_BINDING_FAILED);
     return NS_BINDING_FAILED;
   }
 
   // HTTP Fetch step 5, "redirect status", steps 2 through 6 are automatically
   // handled by necko before calling AsyncOnChannelRedirect() with the new
   // nsIChannel.
 
   // HTTP Fetch step 5, "redirect status", steps 7 and 8 enforcing a redirect
   // count are done by Necko.  The pref used is "network.http.redirection-limit"
   // which is set to 20 by default.
 
-  // HTTP Fetch Step 9, "redirect status". We only unset this for spec
-  // compatibility. Any actions we take on mRequest here do not affect what the
-  //channel does.
-  mRequest->UnsetSameOriginDataURL();
+  // HTTP Fetch Step 9, "redirect status". This is enforced by the
+  // nsCORSListenerProxy. It forbids redirecting to data:
 
   // HTTP Fetch step 5, "redirect status", step 10 requires us to halt the
   // redirect, but successfully return an opaqueredirect Response to the
   // initiating Fetch.
   if (mRequest->GetRedirectMode() == RequestRedirect::Manual) {
     // Ideally we would simply not cancel the old channel and allow it to
     // be processed as normal.  Unfortunately this is quite fragile and
     // other redirect handlers can easily break it for certain use cases.
@@ -989,112 +780,90 @@ FetchDriver::AsyncOnChannelRedirect(nsIC
   }
 
   // We need to update our request's URL.
   nsAutoCString newUrl;
   newURI->GetSpec(newUrl);
   mRequest->SetURL(newUrl);
 
   // Implement Main Fetch step 8 again on redirect.
-  MainFetchOp nextOp = SetTaintingAndGetNextOp(mCORSFlagEverSet);
-
-  if (nextOp.mType == NETWORK_ERROR) {
-    // Cancel the channel if Main Fetch blocks the redirect from continuing.
-    aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI);
-    return NS_ERROR_DOM_BAD_URI;
-  }
-
-  // Otherwise, we rely on necko and the CORS proxy to do the right thing
-  // as the redirect is followed.  In general this means basic or http
-  // fetch.  If we've ever been CORS, we need to stay CORS.
-  MOZ_ASSERT(nextOp.mType == BASIC_FETCH || nextOp.mType == HTTP_FETCH);
-  MOZ_ASSERT_IF(mCORSFlagEverSet, nextOp.mType == HTTP_FETCH);
-  MOZ_ASSERT_IF(mCORSFlagEverSet, nextOp.mCORSFlag);
-
-  // Examine and possibly set the LOAD_ANONYMOUS flag on the channel.
-  nsLoadFlags flags;
-  rv = aNewChannel->GetLoadFlags(&flags);
+  rv = SetTainting();
   if (NS_FAILED(rv)) {
     aOldChannel->Cancel(rv);
     return rv;
   }
 
-  if (mRequest->GetCredentialsMode() == RequestCredentials::Same_origin &&
-      mRequest->GetResponseTainting() == InternalRequest::RESPONSETAINT_OPAQUE) {
+  // Requests that require preflight are not permitted to redirect.
+  // Fetch spec section 4.2 "HTTP Fetch", step 4.9 just uses the manual
+  // redirect flag to decide whether to execute step 4.10 or not. We do not
+  // represent it in our implementation.
+  // The only thing we do is to check if the request requires a preflight (part
+  // of step 4.9), in which case we abort. This part cannot be done by
+  // nsCORSListenerProxy since it does not have access to mRequest.
+  // which case. Step 4.10.3 is handled by OnRedirectVerifyCallback(), and all
+  // the other steps are handled by nsCORSListenerProxy.
+
+  if (IsUnsafeRequest()) {
+    // We can't handle redirects that require preflight yet.
+    // This is especially true for no-cors requests, which much always be
+    // blocked if they require preflight.
+
+    // Simply fire an error here.
+    aOldChannel->Cancel(NS_BINDING_FAILED);
+    return NS_BINDING_FAILED;
+  }
+
+  // Otherwise, we rely on necko and the CORS proxy to do the right thing
+  // as the redirect is followed.  In general this means http
+  // fetch.  If we've ever been CORS, we need to stay CORS.
+
+  // Possibly set the LOAD_ANONYMOUS flag on the channel.
+  if (mHasBeenCrossSite &&
+      mRequest->GetCredentialsMode() == RequestCredentials::Same_origin &&
+      mRequest->Mode() == RequestMode::No_cors) {
     // In the case of a "no-cors" mode request with "same-origin" credentials,
     // we have to set LOAD_ANONYMOUS manually here in order to avoid sending
     // credentials on a cross-origin redirect.
-    flags |= nsIRequest::LOAD_ANONYMOUS;
-    rv = aNewChannel->SetLoadFlags(flags);
+    nsLoadFlags flags;
+    rv = aNewChannel->GetLoadFlags(&flags);
+    if (NS_SUCCEEDED(rv)) {
+      flags |= nsIRequest::LOAD_ANONYMOUS;
+      rv = aNewChannel->SetLoadFlags(flags);
+    }
     if (NS_FAILED(rv)) {
       aOldChannel->Cancel(rv);
       return rv;
     }
-
-  } else if (mRequest->GetCredentialsMode() == RequestCredentials::Omit) {
+  }
+#ifdef DEBUG
+  {
     // Make sure nothing in the redirect chain screws up our credentials
-    // settings.  LOAD_ANONYMOUS must be set if we RequestCredentials is "omit".
-    MOZ_ASSERT(flags & nsIRequest::LOAD_ANONYMOUS);
-
-  } else if (mRequest->GetCredentialsMode() == RequestCredentials::Same_origin &&
-             nextOp.mCORSFlag) {
-    // We also want to verify the LOAD_ANONYMOUS flag is set when we are in
-    // "same-origin" credentials mode and the CORS flag is set.  We can't
-    // unconditionally assert here, however, because the nsCORSListenerProxy
-    // will set the flag later in the redirect callback chain.  Instead,
-    // perform a weaker assertion here by checking if CORS flag was set
-    // before this redirect.  In that case LOAD_ANONYMOUS must still be set.
-    MOZ_ASSERT_IF(mCORSFlagEverSet, flags & nsIRequest::LOAD_ANONYMOUS);
-
-  } else {
-    // Otherwise, we should be sending credentials
-    MOZ_ASSERT(!(flags & nsIRequest::LOAD_ANONYMOUS));
+    // settings. LOAD_ANONYMOUS must be set if we RequestCredentials is "omit"
+    // or "same-origin".
+    nsLoadFlags flags;
+    aNewChannel->GetLoadFlags(&flags);
+    bool shouldBeAnon =
+      mRequest->GetCredentialsMode() == RequestCredentials::Omit ||
+      (mHasBeenCrossSite &&
+       mRequest->GetCredentialsMode() == RequestCredentials::Same_origin);
+    MOZ_ASSERT(!!(flags & nsIRequest::LOAD_ANONYMOUS) == shouldBeAnon);
   }
-
-  // Track the CORSFlag through redirects.
-  mCORSFlagEverSet = mCORSFlagEverSet || nextOp.mCORSFlag;
+#endif
 
   aCallback->OnRedirectVerifyCallback(NS_OK);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 FetchDriver::CheckListenerChain()
 {
   return NS_OK;
 }
 
-// Returns NS_OK if no preflight is required, error otherwise.
-nsresult
-FetchDriver::DoesNotRequirePreflight(nsIChannel* aChannel)
-{
-  // If this is a same-origin request or the channel's URI inherits
-  // its principal, it's allowed.
-  if (nsContentUtils::CheckMayLoad(mPrincipal, aChannel, true)) {
-    return NS_OK;
-  }
-
-  // Check if we need to do a preflight request.
-  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
-  NS_ENSURE_TRUE(httpChannel, NS_ERROR_DOM_BAD_URI);
-
-  nsAutoCString method;
-  httpChannel->GetRequestMethod(method);
-  if (mRequest->Mode() == RequestMode::Cors_with_forced_preflight ||
-      !mRequest->Headers()->HasOnlySimpleHeaders() ||
-      (!method.LowerCaseEqualsLiteral("get") &&
-       !method.LowerCaseEqualsLiteral("post") &&
-       !method.LowerCaseEqualsLiteral("head"))) {
-    return NS_ERROR_DOM_BAD_URI;
-  }
-
-  return NS_OK;
-}
-
 NS_IMETHODIMP
 FetchDriver::GetInterface(const nsIID& aIID, void **aResult)
 {
   if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
     *aResult = static_cast<nsIChannelEventSink*>(this);
     NS_ADDREF_THIS();
     return NS_OK;
   }
@@ -1112,14 +881,14 @@ FetchDriver::GetInterface(const nsIID& a
 
   return QueryInterface(aIID, aResult);
 }
 
 void
 FetchDriver::SetDocument(nsIDocument* aDocument)
 {
   // Cannot set document after Fetch() has been called.
-  MOZ_ASSERT(mFetchRecursionCount == 0);
+  MOZ_ASSERT(!mFetchCalled);
   mDocument = aDocument;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/fetch/FetchDriver.h
+++ b/dom/fetch/FetchDriver.h
@@ -77,61 +77,35 @@ public:
 private:
   nsCOMPtr<nsIPrincipal> mPrincipal;
   nsCOMPtr<nsILoadGroup> mLoadGroup;
   RefPtr<InternalRequest> mRequest;
   RefPtr<InternalResponse> mResponse;
   nsCOMPtr<nsIOutputStream> mPipeOutputStream;
   RefPtr<FetchDriverObserver> mObserver;
   nsCOMPtr<nsIDocument> mDocument;
-  uint32_t mFetchRecursionCount;
-  bool mCORSFlagEverSet;
+  bool mHasBeenCrossSite;
 
   DebugOnly<bool> mResponseAvailableCalled;
+  DebugOnly<bool> mFetchCalled;
 
   FetchDriver() = delete;
   FetchDriver(const FetchDriver&) = delete;
   FetchDriver& operator=(const FetchDriver&) = delete;
   ~FetchDriver();
 
-  enum MainFetchOpType
-  {
-    NETWORK_ERROR,
-    BASIC_FETCH,
-    HTTP_FETCH,
-    NUM_MAIN_FETCH_OPS
-  };
-
-  struct MainFetchOp
-  {
-    explicit MainFetchOp(MainFetchOpType aType, bool aCORSFlag = false,
-                         bool aCORSPreflightFlag = false)
-      : mType(aType), mCORSFlag(aCORSFlag),
-        mCORSPreflightFlag(aCORSPreflightFlag)
-    { }
-
-    MainFetchOpType mType;
-    bool mCORSFlag;
-    bool mCORSPreflightFlag;
-  };
-
-  nsresult Fetch(bool aCORSFlag);
-  MainFetchOp SetTaintingAndGetNextOp(bool aCORSFlag);
-  nsresult ContinueFetch(bool aCORSFlag);
-  nsresult BasicFetch();
-  nsresult HttpFetch(bool aCORSFlag = false, bool aCORSPreflightFlag = false, bool aAuthenticationFlag = false);
-  nsresult ContinueHttpFetchAfterNetworkFetch();
+  nsresult SetTainting();
+  nsresult ContinueFetch();
+  nsresult HttpFetch();
+  bool IsUnsafeRequest();
   // Returns the filtered response sent to the observer.
   // Callers who don't have access to a channel can pass null for aFinalURI.
   already_AddRefed<InternalResponse>
   BeginAndGetFilteredResponse(InternalResponse* aResponse, nsIURI* aFinalURI);
   // Utility since not all cases need to do any post processing of the filtered
   // response.
-  void BeginResponse(InternalResponse* aResponse);
   nsresult FailWithNetworkError();
-  nsresult SucceedWithResponse();
-  nsresult DoesNotRequirePreflight(nsIChannel* aChannel);
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_FetchDriver_h
--- a/dom/fetch/Request.cpp
+++ b/dom/fetch/Request.cpp
@@ -255,26 +255,16 @@ Request::Constructor(const GlobalObject&
     }
 
     request->SetURL(NS_ConvertUTF16toUTF8(requestURL));
     fallbackMode = RequestMode::Cors;
     fallbackCredentials = RequestCredentials::Omit;
     fallbackCache = RequestCache::Default;
   }
 
-  // CORS-with-forced-preflight is not publicly exposed and should not be
-  // considered a valid value.
-  if (aInit.mMode.WasPassed() &&
-      aInit.mMode.Value() == RequestMode::Cors_with_forced_preflight) {
-    NS_NAMED_LITERAL_STRING(sourceDescription, "'mode' member of RequestInit");
-    NS_NAMED_LITERAL_STRING(value, "cors-with-forced-preflight");
-    NS_NAMED_LITERAL_STRING(type, "RequestMode");
-    aRv.ThrowTypeError<MSG_INVALID_ENUM_VALUE>(&sourceDescription, &value, &type);
-    return nullptr;
-  }
   RequestMode mode = aInit.mMode.WasPassed() ? aInit.mMode.Value() : fallbackMode;
   RequestCredentials credentials =
     aInit.mCredentials.WasPassed() ? aInit.mCredentials.Value()
                                    : fallbackCredentials;
 
   if (mode != RequestMode::EndGuard_) {
     request->ClearCreatedByFetchEvent();
     request->SetMode(mode);
--- a/dom/fetch/Request.h
+++ b/dom/fetch/Request.h
@@ -53,19 +53,16 @@ public:
   GetMethod(nsCString& aMethod) const
   {
     aMethod = mRequest->mMethod;
   }
 
   RequestMode
   Mode() const
   {
-    if (mRequest->mMode == RequestMode::Cors_with_forced_preflight) {
-      return RequestMode::Cors;
-    }
     return mRequest->mMode;
   }
 
   RequestCredentials
   Credentials() const
   {
     return mRequest->mCredentialsMode;
   }
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -2217,31 +2217,40 @@ HTMLMediaElement::ResetConnectionState()
   ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY);
   ChangeDelayLoadStatus(false);
   ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING);
 }
 
 void
 HTMLMediaElement::Play(ErrorResult& aRv)
 {
+  nsresult rv = PlayInternal(nsContentUtils::IsCallerChrome());
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+  }
+}
+
+nsresult
+HTMLMediaElement::PlayInternal(bool aCallerIsChrome)
+{
   // Prevent media element from being auto-started by a script when
   // media.autoplay.enabled=false
   if (!mHasUserInteraction
       && !IsAutoplayEnabled()
       && !EventStateManager::IsHandlingUserInput()
-      && !nsContentUtils::IsCallerChrome()) {
+      && !aCallerIsChrome) {
     LOG(LogLevel::Debug, ("%p Blocked attempt to autoplay media.", this));
 #if defined(MOZ_WIDGET_ANDROID)
     nsContentUtils::DispatchTrustedEvent(OwnerDoc(),
                                          static_cast<nsIContent*>(this),
                                          NS_LITERAL_STRING("MozAutoplayMediaBlocked"),
                                          false,
                                          false);
 #endif
-    return;
+    return NS_OK;
   }
 
   // Play was not blocked so assume user interacted with the element.
   mHasUserInteraction = true;
 
   StopSuspendingAfterFirstFrame();
   SetPlayedOrSeeked(true);
 
@@ -2252,29 +2261,29 @@ HTMLMediaElement::Play(ErrorResult& aRv)
     ResumeLoad(PRELOAD_ENOUGH);
   }
 
   if (Preferences::GetBool("media.block-play-until-visible", false) &&
       !nsContentUtils::IsCallerChrome() &&
       OwnerDoc()->Hidden()) {
     LOG(LogLevel::Debug, ("%p Blocked playback because owner hidden.", this));
     mPlayBlockedBecauseHidden = true;
-    return;
+    return NS_OK;
   }
 
   // Even if we just did Load() or ResumeLoad(), we could already have a decoder
   // here if we managed to clone an existing decoder.
   if (mDecoder) {
     if (mDecoder->IsEndedOrShutdown()) {
       SetCurrentTime(0);
     }
     if (!mPausedForInactiveDocumentOrChannel) {
-      aRv = mDecoder->Play();
-      if (aRv.Failed()) {
-        return;
+      nsresult rv = mDecoder->Play();
+      if (NS_FAILED(rv)) {
+        return rv;
       }
     }
   }
 
   if (mCurrentPlayRangeStart == -1.0) {
     mCurrentPlayRangeStart = CurrentTime();
   }
 
@@ -2300,23 +2309,23 @@ HTMLMediaElement::Play(ErrorResult& aRv)
 
   mPaused = false;
   mAutoplaying = false;
   // We changed mPaused and mAutoplaying which can affect AddRemoveSelfReference
   // and our preload status.
   AddRemoveSelfReference();
   UpdatePreloadAction();
   UpdateSrcMediaStreamPlaying();
+
+  return NS_OK;
 }
 
 NS_IMETHODIMP HTMLMediaElement::Play()
 {
-  ErrorResult rv;
-  Play(rv);
-  return rv.StealNSResult();
+  return PlayInternal(/* aCallerIsChrome = */ true);
 }
 
 HTMLMediaElement::WakeLockBoolWrapper&
 HTMLMediaElement::WakeLockBoolWrapper::operator=(bool val)
 {
   if (mValue == val) {
     return *this;
   }
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -706,16 +706,18 @@ protected:
     void UpdateWakeLock();
 
     bool mValue;
     bool mCanPlay;
     HTMLMediaElement* mOuter;
     nsCOMPtr<nsITimer> mTimer;
   };
 
+  nsresult PlayInternal(bool aCallerIsChrome);
+
   /** Use this method to change the mReadyState member, so required
    * events can be fired.
    */
   void ChangeReadyState(nsMediaReadyState aState);
 
   /**
    * Use this method to change the mNetworkState member, so required
    * actions will be taken during the transition.
--- a/dom/imptests/failures/html/dom/test_historical.html.json
+++ b/dom/imptests/failures/html/dom/test_historical.html.json
@@ -1,11 +1,10 @@
 {
   "Historical DOM features must be removed: CDATASection": true,
   "Historical DOM features must be removed: createCDATASection": true,
   "Historical DOM features must be removed: createAttribute": true,
   "Historical DOM features must be removed: createAttributeNS": true,
   "Historical DOM features must be removed: getAttributeNode": true,
   "Historical DOM features must be removed: getAttributeNodeNS": true,
   "Historical DOM features must be removed: setAttributeNode": true,
-  "Historical DOM features must be removed: removeAttributeNode": true,
-  "DocumentType member must be nuked: internalSubset": true
+  "Historical DOM features must be removed: removeAttributeNode": true
 }
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -481,28 +481,28 @@ TabParent::ShouldSwitchProcess(nsIChanne
   nsCOMPtr<nsIPrincipal> resultPrincipal;
   nsContentUtils::GetSecurityManager()->
     GetChannelResultPrincipal(aChannel, getter_AddRefs(resultPrincipal));
 
   // Log the debug info which is used to decide the need of proces switch.
   nsCOMPtr<nsIURI> uri;
   aChannel->GetURI(getter_AddRefs(uri));
   LogChannelRelevantInfo(uri, loadingPrincipal, resultPrincipal,
-                         loadInfo->GetContentPolicyType());
+                         loadInfo->InternalContentPolicyType());
 
   // Check if the signed package is loaded from the same origin.
   bool sameOrigin = false;
   loadingPrincipal->Equals(resultPrincipal, &sameOrigin);
   if (sameOrigin) {
     LOG("Loading singed package from the same origin. Don't switch process.\n");
     return false;
   }
 
   // If this is not a top level document, there's no need to switch process.
-  if (nsIContentPolicy::TYPE_DOCUMENT != loadInfo->GetContentPolicyType()) {
+  if (nsIContentPolicy::TYPE_DOCUMENT != loadInfo->InternalContentPolicyType()) {
     LOG("Subresource of a document. No need to switch process.\n");
     return false;
   }
 
   // If this is a brand new process created to load the signed package
   // (triggered by previous OnStartSignedPackageRequest), the loading origin
   // will be "moz-safe-about:blank". In that case, we don't need to switch process
   // again. We compare with "moz-safe-about:blank" without appId/isBrowserElement/etc
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -136,18 +136,17 @@ NS_IMPL_ISUPPORTS(MediaMemoryTracker, ns
 
 NS_IMPL_ISUPPORTS0(MediaDecoder)
 
 void
 MediaDecoder::NotifyOwnerActivityChanged()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (!mOwner) {
-    NS_WARNING("MediaDecoder without a decoder owner, can't update dormant");
+  if (mShuttingDown) {
     return;
   }
 
   UpdateDormantState(false /* aDormantTimeout */, false /* aActivity */);
   // Start dormant timer if necessary
   StartDormantTimer();
 }
 
@@ -165,17 +164,18 @@ MediaDecoder::IsHeuristicDormantSupporte
     mIsHeuristicDormantSupported;
 }
 
 void
 MediaDecoder::UpdateDormantState(bool aDormantTimeout, bool aActivity)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (!mDecoderStateMachine ||
+  if (mShuttingDown ||
+      !mDecoderStateMachine ||
       mPlayState == PLAY_STATE_SHUTDOWN ||
       !mOwner->GetVideoFrameContainer() ||
       (mOwner->GetMediaElement() && mOwner->GetMediaElement()->IsBeingDestroyed()) ||
       !mDormantSupported)
   {
     return;
   }
 
@@ -251,17 +251,16 @@ MediaDecoder::StartDormantTimer()
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (!IsHeuristicDormantSupported()) {
     return;
   }
 
   if (mIsHeuristicDormant ||
       mShuttingDown ||
-      !mOwner ||
       !mOwner->IsHidden() ||
       (mPlayState != PLAY_STATE_PAUSED &&
        !IsEnded()))
   {
     return;
   }
 
   if (!mDormantTimer) {
@@ -473,18 +472,16 @@ MediaDecoder::Shutdown()
   if (mResource) {
     mResource->Close();
   }
 
   CancelDormantTimer();
 
   ChangeState(PLAY_STATE_SHUTDOWN);
 
-  mOwner = nullptr;
-
   MediaShutdownManager::Instance().Unregister(this);
 }
 
 MediaDecoder::~MediaDecoder()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MediaMemoryTracker::RemoveMediaDecoder(this);
   UnpinForSeek();
@@ -594,22 +591,18 @@ MediaDecoder::Seek(double aTime, SeekTar
   mLogicalPosition = aTime;
   mWasEndedWhenEnteredDormant = false;
 
   mLogicallySeeking = true;
   SeekTarget target = SeekTarget(timeUsecs, aSeekType);
   CallSeek(target);
 
   if (mPlayState == PLAY_STATE_ENDED) {
-    bool paused = false;
-    if (mOwner) {
-      paused = mOwner->GetPaused();
-    }
     PinForSeek();
-    ChangeState(paused ? PLAY_STATE_PAUSED : PLAY_STATE_PLAYING);
+    ChangeState(mOwner->GetPaused() ? PLAY_STATE_PAUSED : PLAY_STATE_PLAYING);
   }
   return NS_OK;
 }
 
 void
 MediaDecoder::CallSeek(const SeekTarget& aTarget)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -659,24 +652,22 @@ MediaDecoder::MetadataLoaded(nsAutoPtr<M
 
   DECODER_LOG("MetadataLoaded, channels=%u rate=%u hasAudio=%d hasVideo=%d",
               aInfo->mAudio.mChannels, aInfo->mAudio.mRate,
               aInfo->HasAudio(), aInfo->HasVideo());
 
   mInfo = aInfo.forget();
   ConstructMediaTracks();
 
-  if (mOwner) {
-    // Make sure the element and the frame (if any) are told about
-    // our new size.
-    Invalidate();
-    if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
-      mFiredMetadataLoaded = true;
-      mOwner->MetadataLoaded(mInfo, nsAutoPtr<const MetadataTags>(aTags.forget()));
-    }
+  // Make sure the element and the frame (if any) are told about
+  // our new size.
+  Invalidate();
+  if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+    mFiredMetadataLoaded = true;
+    mOwner->MetadataLoaded(mInfo, nsAutoPtr<const MetadataTags>(aTags.forget()));
   }
 }
 
 const char*
 MediaDecoder::PlayStateStr()
 {
   MOZ_ASSERT(NS_IsMainThread());
   switch (mPlayState) {
@@ -701,21 +692,19 @@ MediaDecoder::FirstFrameLoaded(nsAutoPtr
   }
 
   DECODER_LOG("FirstFrameLoaded, channels=%u rate=%u hasAudio=%d hasVideo=%d mPlayState=%s mIsDormant=%d",
               aInfo->mAudio.mChannels, aInfo->mAudio.mRate,
               aInfo->HasAudio(), aInfo->HasVideo(), PlayStateStr(), mIsDormant);
 
   mInfo = aInfo.forget();
 
-  if (mOwner) {
-    Invalidate();
-    if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
-      mOwner->FirstFrameLoaded();
-    }
+  Invalidate();
+  if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+    mOwner->FirstFrameLoaded();
   }
 
   // This can run cache callbacks.
   mResource->EnsureCacheUpToDate();
 
   // The element can run javascript via events
   // before reaching here, so only change the
   // state if we're still set to the original
@@ -731,50 +720,44 @@ MediaDecoder::FirstFrameLoaded(nsAutoPtr
 
 void
 MediaDecoder::ResetConnectionState()
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (mShuttingDown)
     return;
 
-  if (mOwner) {
-    // Notify the media element that connection gets lost.
-    mOwner->ResetConnectionState();
-  }
+  // Notify the media element that connection gets lost.
+  mOwner->ResetConnectionState();
 
   // Since we have notified the media element the connection
   // lost event, the decoder will be reloaded when user tries
   // to play the Rtsp streaming next time.
   Shutdown();
 }
 
 void
 MediaDecoder::NetworkError()
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (mShuttingDown)
     return;
 
-  if (mOwner)
-    mOwner->NetworkError();
-
+  mOwner->NetworkError();
   Shutdown();
 }
 
 void
 MediaDecoder::DecodeError()
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (mShuttingDown)
     return;
 
-  if (mOwner)
-    mOwner->DecodeError();
-
+  mOwner->DecodeError();
   Shutdown();
 }
 
 void
 MediaDecoder::UpdateSameOriginStatus(bool aSameOrigin)
 {
   MOZ_ASSERT(NS_IsMainThread());
   mSameOriginMedia = aSameOrigin;
@@ -810,20 +793,17 @@ MediaDecoder::PlaybackEnded()
   if (mShuttingDown ||
       mLogicallySeeking ||
       mPlayState == PLAY_STATE_LOADING) {
     return;
   }
 
   ChangeState(PLAY_STATE_ENDED);
   InvalidateWithFlags(VideoFrameContainer::INVALIDATE_FORCE);
-
-  if (mOwner)  {
-    mOwner->PlaybackEnded();
-  }
+  mOwner->PlaybackEnded();
 
   // This must be called after |mOwner->PlaybackEnded()| call above, in order
   // to fire the required durationchange.
   if (IsInfinite()) {
     SetInfinite(false);
   }
 }
 
@@ -882,44 +862,50 @@ MediaDecoder::UpdatePlaybackRate()
 
   mResource->SetPlaybackRate(rate);
 }
 
 void
 MediaDecoder::NotifySuspendedStatusChanged()
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (mResource && mOwner) {
+  if (mShuttingDown) {
+    return;
+  }
+  if (mResource) {
     bool suspended = mResource->IsSuspendedByCache();
     mOwner->NotifySuspendedByCache(suspended);
   }
 }
 
 void
 MediaDecoder::NotifyBytesDownloaded()
 {
   MOZ_ASSERT(NS_IsMainThread());
+  if (mShuttingDown) {
+    return;
+  }
   UpdatePlaybackRate();
-  if (mOwner) {
-    mOwner->DownloadProgressed();
-  }
+  mOwner->DownloadProgressed();
 }
 
 void
 MediaDecoder::NotifyDownloadEnded(nsresult aStatus)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  if (mShuttingDown) {
+    return;
+  }
+
   DECODER_LOG("NotifyDownloadEnded, status=%x", aStatus);
 
   if (aStatus == NS_BINDING_ABORTED) {
     // Download has been cancelled by user.
-    if (mOwner) {
-      mOwner->LoadAborted();
-    }
+    mOwner->LoadAborted();
     return;
   }
 
   UpdatePlaybackRate();
 
   if (NS_SUCCEEDED(aStatus)) {
     // A final progress event will be fired by the MediaResource calling
     // DownloadSuspended on the element.
@@ -929,19 +915,20 @@ MediaDecoder::NotifyDownloadEnded(nsresu
     NetworkError();
   }
 }
 
 void
 MediaDecoder::NotifyPrincipalChanged()
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (mOwner) {
-    mOwner->NotifyDecoderPrincipalChanged();
+  if (mShuttingDown) {
+    return;
   }
+  mOwner->NotifyDecoderPrincipalChanged();
 }
 
 void
 MediaDecoder::NotifyBytesConsumed(int64_t aBytes, int64_t aOffset)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mShuttingDown || mIgnoreProgressData) {
@@ -973,37 +960,33 @@ MediaDecoder::OnSeekResolved(SeekResolve
     if (aVal.mAtEnd) {
       ChangeState(PLAY_STATE_ENDED);
     }
     mLogicallySeeking = false;
   }
 
   UpdateLogicalPosition(aVal.mEventVisibility);
 
-  if (mOwner) {
-    if (aVal.mEventVisibility != MediaDecoderEventVisibility::Suppressed) {
-      mOwner->SeekCompleted();
-      if (fireEnded) {
-        mOwner->PlaybackEnded();
-      }
+  if (aVal.mEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+    mOwner->SeekCompleted();
+    if (fireEnded) {
+      mOwner->PlaybackEnded();
     }
   }
 }
 
 void
 MediaDecoder::SeekingStarted(MediaDecoderEventVisibility aEventVisibility)
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (mShuttingDown)
     return;
 
-  if (mOwner) {
-    if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
-      mOwner->SeekStarted();
-    }
+  if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+    mOwner->SeekStarted();
   }
 }
 
 void
 MediaDecoder::ChangeState(PlayState aState)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -1047,27 +1030,31 @@ MediaDecoder::UpdateLogicalPosition(Medi
   mLogicalPosition = currentPosition;
 
   // Invalidate the frame so any video data is displayed.
   // Do this before the timeupdate event so that if that
   // event runs JavaScript that queries the media size, the
   // frame has reflowed and the size updated beforehand.
   Invalidate();
 
-  if (mOwner && logicalPositionChanged &&
+  if (logicalPositionChanged &&
       aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
     FireTimeUpdate();
   }
 }
 
 void
 MediaDecoder::DurationChanged()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  if (mShuttingDown) {
+    return;
+  }
+
   double oldDuration = mDuration;
   if (IsInfinite()) {
     mDuration = std::numeric_limits<double>::infinity();
   } else if (mExplicitDuration.Ref().isSome()) {
     mDuration = mExplicitDuration.Ref().ref();
   } else if (mStateMachineDuration.Ref().isSome()) {
     mDuration = mStateMachineDuration.Ref().ref().ToSeconds();
   }
@@ -1078,17 +1065,17 @@ MediaDecoder::DurationChanged()
 
   DECODER_LOG("Duration changed to %f", mDuration);
 
   // Duration has changed so we should recompute playback rate
   UpdatePlaybackRate();
 
   // See https://www.w3.org/Bugs/Public/show_bug.cgi?id=28822 for a discussion
   // of whether we should fire durationchange on explicit infinity.
-  if (mOwner && mFiredMetadataLoaded &&
+  if (mFiredMetadataLoaded &&
       (!mozilla::IsInfinite<double>(mDuration) || mExplicitDuration.Ref().isSome())) {
     mOwner->DispatchAsyncEvent(NS_LITERAL_STRING("durationchange"));
   }
 
   if (CurrentPosition() > TimeUnit::FromSeconds(mDuration).ToMicroseconds()) {
     Seek(mDuration, SeekTarget::Accurate);
   }
 }
@@ -1210,17 +1197,17 @@ MediaDecoder::SetPlaybackRate(double aPl
   if (mPlaybackRate == 0.0) {
     mPausedForPlaybackRateNull = true;
     Pause();
   } else if (mPausedForPlaybackRateNull) {
     // Play() uses mPausedForPlaybackRateNull value, so must reset it first
     mPausedForPlaybackRateNull = false;
     // If the playbackRate is no longer null, restart the playback, iff the
     // media was playing.
-    if (mOwner && !mOwner->GetPaused()) {
+    if (!mOwner->GetPaused()) {
       Play();
     }
   }
 }
 
 void
 MediaDecoder::SetPreservesPitch(bool aPreservesPitch)
 {
@@ -1341,18 +1328,19 @@ MediaDecoder::GetMediaOwner() const
 {
   return mOwner;
 }
 
 void
 MediaDecoder::FireTimeUpdate()
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (!mOwner)
+  if (mShuttingDown) {
     return;
+  }
   mOwner->FireTimeUpdate(true);
 }
 
 void
 MediaDecoder::PinForSeek()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MediaResource* resource = GetResource();
@@ -1566,21 +1554,17 @@ MediaDecoder::GetOwner()
   return mOwner;
 }
 
 void
 MediaDecoder::ConstructMediaTracks()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (mMediaTracksConstructed) {
-    return;
-  }
-
-  if (!mOwner || !mInfo) {
+  if (mShuttingDown || mMediaTracksConstructed || !mInfo) {
     return;
   }
 
   HTMLMediaElement* element = mOwner->GetMediaElement();
   if (!element) {
     return;
   }
 
@@ -1606,17 +1590,17 @@ MediaDecoder::ConstructMediaTracks()
   }
 }
 
 void
 MediaDecoder::RemoveMediaTracks()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (!mOwner) {
+  if (mShuttingDown) {
     return;
   }
 
   HTMLMediaElement* element = mOwner->GetMediaElement();
   if (!element) {
     return;
   }
 
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -745,17 +745,18 @@ private:
   virtual void NotifyDecodedFrames(uint32_t aParsed, uint32_t aDecoded,
                                    uint32_t aDropped) override
   {
     GetFrameStatistics().NotifyDecodedFrames(aParsed, aDecoded, aDropped);
   }
 
   void UpdateReadyState()
   {
-    if (mOwner) {
+    MOZ_ASSERT(NS_IsMainThread());
+    if (!mShuttingDown) {
       mOwner->UpdateReadyState();
     }
   }
 
   virtual MediaDecoderOwner::NextFrameStatus NextFrameStatus() { return mNextFrameStatus; }
 
 protected:
   virtual ~MediaDecoder();
@@ -863,17 +864,17 @@ protected:
 
   const char* PlayStateStr();
 
   void OnMetadataUpdate(TimedMetadata&& aMetadata);
 
   // This should only ever be accessed from the main thread.
   // It is set in Init and cleared in Shutdown when the element goes away.
   // The decoder does not add a reference the element.
-  MediaDecoderOwner* mOwner;
+  MediaDecoderOwner* const mOwner;
 
   // Counters related to decode and presentation of frames.
   FrameStatistics mFrameStats;
 
   RefPtr<VideoFrameContainer> mVideoFrameContainer;
 
   // Data needed to estimate playback data rate. The timeline used for
   // this estimate is "decode time" (where the "current time" is the
--- a/dom/media/MediaDecoderReader.cpp
+++ b/dom/media/MediaDecoderReader.cpp
@@ -99,17 +99,16 @@ MediaDecoderReader::InitializationTask()
 
   // Initialize watchers.
   mWatchManager.Watch(mDuration, &MediaDecoderReader::UpdateBuffered);
 }
 
 MediaDecoderReader::~MediaDecoderReader()
 {
   MOZ_ASSERT(mShutdown);
-  ResetDecode();
   MOZ_COUNT_DTOR(MediaDecoderReader);
 }
 
 size_t MediaDecoderReader::SizeOfVideoQueueInBytes() const
 {
   VideoQueueMemoryFunctor functor;
   mVideoQueue.LockedForEach(functor);
   return functor.mSize;
@@ -245,22 +244,22 @@ MediaDecoderReader::GetBuffered()
   if (!mDuration.Ref().isSome()) {
     return TimeIntervals();
   }
 
   return GetEstimatedBufferedTimeRanges(stream, mDuration.Ref().ref().ToMicroseconds());
 }
 
 RefPtr<MediaDecoderReader::MetadataPromise>
-MediaDecoderReader::AsyncReadMetadata()
+MediaDecoderReader::AsyncReadMetadataInternal()
 {
   typedef ReadMetadataFailureReason Reason;
 
   MOZ_ASSERT(OnTaskQueue());
-  DECODER_LOG("MediaDecoderReader::AsyncReadMetadata");
+  DECODER_LOG("MediaDecoderReader::AsyncReadMetadataInternal");
 
   // Attempt to read the metadata.
   RefPtr<MetadataHolder> metadata = new MetadataHolder();
   nsresult rv = ReadMetadata(&metadata->mInfo, getter_Transfers(metadata->mTags));
 
   // We're not waiting for anything. If we didn't get the metadata, that's an
   // error.
   if (NS_FAILED(rv) || !metadata->mInfo.HasValidMedia()) {
--- a/dom/media/MediaDecoderReader.h
+++ b/dom/media/MediaDecoderReader.h
@@ -89,16 +89,24 @@ public:
   // between the two (even though in practice, Init will always run first right
   // now thanks to the tail dispatcher).
   void InitializationTask();
 
   // Initializes the reader, returns NS_OK on success, or NS_ERROR_FAILURE
   // on failure.
   virtual nsresult Init() { return NS_OK; }
 
+  RefPtr<MetadataPromise> AsyncReadMetadata()
+  {
+    return OnTaskQueue() ?
+           AsyncReadMetadataInternal() :
+           InvokeAsync(OwnerThread(), this, __func__,
+                       &MediaDecoderReader::AsyncReadMetadataInternal);
+  }
+
   // Release media resources they should be released in dormant state
   // The reader can be made usable again by calling ReadMetadata().
   void ReleaseMediaResources()
   {
     if (OnTaskQueue()) {
       ReleaseMediaResourcesInternal();
       return;
     }
@@ -172,28 +180,16 @@ public:
   // in buffering mode. Some readers support a promise-based mechanism by which
   // they notify the state machine when the data arrives.
   virtual bool IsWaitForDataSupported() { return false; }
   virtual RefPtr<WaitForDataPromise> WaitForData(MediaData::Type aType) { MOZ_CRASH(); }
 
   virtual bool HasAudio() = 0;
   virtual bool HasVideo() = 0;
 
-  // The default implementation of AsyncReadMetadata is implemented in terms of
-  // synchronous ReadMetadata() calls. Implementations may also
-  // override AsyncReadMetadata to create a more proper async implementation.
-  virtual RefPtr<MetadataPromise> AsyncReadMetadata();
-
-  // Read header data for all bitstreams in the file. Fills aInfo with
-  // the data required to present the media, and optionally fills *aTags
-  // with tag metadata from the file.
-  // Returns NS_OK on success, or NS_ERROR_FAILURE on failure.
-  virtual nsresult ReadMetadata(MediaInfo* aInfo,
-                                MetadataTags** aTags) { MOZ_CRASH(); }
-
   // Fills aInfo with the latest cached data required to present the media,
   // ReadUpdatedMetadata will always be called once ReadMetadata has succeeded.
   virtual void ReadUpdatedMetadata(MediaInfo* aInfo) { };
 
   // Moves the decode head to aTime microseconds. aEndTime denotes the end
   // time of the media in usecs. This is only needed for OggReader, and should
   // probably be removed somehow.
   virtual RefPtr<SeekPromise>
@@ -263,16 +259,28 @@ public:
 
   virtual size_t SizeOfVideoQueueInFrames();
   virtual size_t SizeOfAudioQueueInFrames();
 
 private:
   virtual void ReleaseMediaResourcesInternal() {}
   virtual void DisableHardwareAccelerationInternal() {}
 
+  // Read header data for all bitstreams in the file. Fills aInfo with
+  // the data required to present the media, and optionally fills *aTags
+  // with tag metadata from the file.
+  // Returns NS_OK on success, or NS_ERROR_FAILURE on failure.
+  virtual nsresult ReadMetadata(MediaInfo*, MetadataTags**) { MOZ_CRASH(); }
+
+  // The default implementation of AsyncReadMetadataInternal is implemented in
+  // terms of synchronous ReadMetadata() calls. Implementations may also
+  // override AsyncReadMetadataInternal to create a more proper async
+  // implementation.
+  virtual RefPtr<MetadataPromise> AsyncReadMetadataInternal();
+
 protected:
   friend class TrackBuffer;
   virtual void NotifyDataArrivedInternal(uint32_t aLength, int64_t aOffset) { }
 
   void NotifyDataArrived(const media::Interval<int64_t>& aInfo)
   {
     MOZ_ASSERT(OnTaskQueue());
     NS_ENSURE_TRUE_VOID(!mShutdown);
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -12,16 +12,17 @@
 
 #include <algorithm>
 #include <stdint.h>
 
 #include "gfx2DGlue.h"
 
 #include "mediasink/DecodedAudioDataSink.h"
 #include "mediasink/AudioSinkWrapper.h"
+#include "mediasink/VideoSink.h"
 #include "mediasink/DecodedStream.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Logging.h"
 #include "mozilla/mozalloc.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/SharedThreadPool.h"
 #include "mozilla/TaskQueue.h"
@@ -197,27 +198,25 @@ static void InitVideoQueuePrefs() {
 
 MediaDecoderStateMachine::MediaDecoderStateMachine(MediaDecoder* aDecoder,
                                                    MediaDecoderReader* aReader,
                                                    bool aRealTime) :
   mDecoder(aDecoder),
   mTaskQueue(new TaskQueue(GetMediaThreadPool(MediaThreadType::PLAYBACK),
                            /* aSupportsTailDispatch = */ true)),
   mWatchManager(this, mTaskQueue),
-  mProducerID(ImageContainer::AllocateProducerID()),
   mRealTime(aRealTime),
   mDispatchedStateMachine(false),
   mDelayedScheduler(mTaskQueue),
   mState(DECODER_STATE_DECODING_NONE, "MediaDecoderStateMachine::mState"),
   mCurrentFrameID(0),
   mObservedDuration(TimeUnit(), "MediaDecoderStateMachine::mObservedDuration"),
   mFragmentEndTime(-1),
   mReader(aReader),
   mDecodedAudioEndTime(-1),
-  mVideoFrameEndTime(-1),
   mDecodedVideoEndTime(-1),
   mPlaybackRate(1.0),
   mLowAudioThresholdUsecs(detail::LOW_AUDIO_USECS),
   mAmpleAudioThresholdUsecs(detail::AMPLE_AUDIO_USECS),
   mQuickBufferingLowDataThresholdUsecs(detail::QUICK_BUFFERING_LOW_DATA_USECS),
   mIsAudioPrerolling(false),
   mIsVideoPrerolling(false),
   mAudioCaptured(false, "MediaDecoderStateMachine::mAudioCaptured"),
@@ -300,17 +299,17 @@ MediaDecoderStateMachine::MediaDecoderSt
 
   mAudioQueueListener = AudioQueue().PopEvent().Connect(
     mTaskQueue, this, &MediaDecoderStateMachine::OnAudioPopped);
   mVideoQueueListener = VideoQueue().PopEvent().Connect(
     mTaskQueue, this, &MediaDecoderStateMachine::OnVideoPopped);
 
   mMetadataManager.Connect(mReader->TimedMetadataEvent(), OwnerThread());
 
-  mMediaSink = CreateAudioSink();
+  mMediaSink = CreateMediaSink(mAudioCaptured);
 
 #ifdef MOZ_EME
   mCDMProxyPromise.Begin(mDecoder->RequestCDMProxy()->Then(
     OwnerThread(), __func__, this,
     &MediaDecoderStateMachine::OnCDMProxyReady,
     &MediaDecoderStateMachine::OnCDMProxyNotReady));
 #endif
 }
@@ -376,16 +375,33 @@ MediaDecoderStateMachine::CreateAudioSin
     MOZ_ASSERT(self->OnTaskQueue());
     return new DecodedAudioDataSink(
       self->mAudioQueue, self->GetMediaTime(),
       self->mInfo.mAudio, self->mDecoder->GetAudioChannel());
   };
   return new AudioSinkWrapper(mTaskQueue, audioSinkCreator);
 }
 
+already_AddRefed<media::MediaSink>
+MediaDecoderStateMachine::CreateMediaSink(bool aAudioCaptured)
+{
+  // TODO: We can't really create a new DecodedStream until OutputStreamManager
+  //       is extracted. It is tricky that the implementation of DecodedStream
+  //       happens to allow reuse after shutdown without creating a new one.
+  RefPtr<media::MediaSink> audioSink = aAudioCaptured ?
+    mStreamSink : CreateAudioSink();
+
+  RefPtr<media::MediaSink> mediaSink =
+    new VideoSink(mTaskQueue, audioSink, mVideoQueue,
+                  mDecoder->GetVideoFrameContainer(), mRealTime,
+                  mDecoder->GetFrameStatistics(), AUDIO_DURATION_USECS,
+                  sVideoQueueSendToCompositorSize);
+  return mediaSink.forget();
+}
+
 bool MediaDecoderStateMachine::HasFutureAudio()
 {
   MOZ_ASSERT(OnTaskQueue());
   NS_ASSERTION(HasAudio(), "Should only call HasFutureAudio() when we have audio");
   // We've got audio ready to play if:
   // 1. We've not completed playback of audio, and
   // 2. we either have more than the threshold of decoded audio available, or
   //    we've completely decoded all audio (but not finished playing it yet
@@ -1045,17 +1061,16 @@ nsresult MediaDecoderStateMachine::Init(
 void MediaDecoderStateMachine::StopPlayback()
 {
   MOZ_ASSERT(OnTaskQueue());
   DECODER_LOG("StopPlayback()");
 
   mDecoder->DispatchPlaybackStopped();
 
   if (IsPlaying()) {
-    RenderVideoFrames(1);
     mMediaSink->SetPlaying(false);
     MOZ_ASSERT(!IsPlaying());
   }
 
   DispatchDecodeTasksIfNeeded();
 }
 
 void MediaDecoderStateMachine::MaybeStartPlayback()
@@ -1484,17 +1499,18 @@ MediaDecoderStateMachine::InvokeSeek(See
 }
 
 void MediaDecoderStateMachine::StopMediaSink()
 {
   MOZ_ASSERT(OnTaskQueue());
   if (mMediaSink->IsStarted()) {
     DECODER_LOG("Stop MediaSink");
     mMediaSink->Stop();
-    mMediaSinkPromise.DisconnectIfExists();
+    mMediaSinkAudioPromise.DisconnectIfExists();
+    mMediaSinkVideoPromise.DisconnectIfExists();
   }
 }
 
 void
 MediaDecoderStateMachine::DispatchDecodeTasksIfNeeded()
 {
   MOZ_ASSERT(OnTaskQueue());
 
@@ -1759,22 +1775,30 @@ MediaDecoderStateMachine::RequestVideoDa
 void
 MediaDecoderStateMachine::StartMediaSink()
 {
   MOZ_ASSERT(OnTaskQueue());
   if (!mMediaSink->IsStarted()) {
     mAudioCompleted = false;
     mMediaSink->Start(GetMediaTime(), mInfo);
 
-    auto promise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
-    if (promise) {
-      mMediaSinkPromise.Begin(promise->Then(
+    auto videoPromise = mMediaSink->OnEnded(TrackInfo::kVideoTrack);
+    auto audioPromise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
+
+    if (audioPromise) {
+      mMediaSinkAudioPromise.Begin(audioPromise->Then(
         OwnerThread(), __func__, this,
-        &MediaDecoderStateMachine::OnMediaSinkComplete,
-        &MediaDecoderStateMachine::OnMediaSinkError));
+        &MediaDecoderStateMachine::OnMediaSinkAudioComplete,
+        &MediaDecoderStateMachine::OnMediaSinkAudioError));
+    }
+    if (videoPromise) {
+      mMediaSinkVideoPromise.Begin(videoPromise->Then(
+        OwnerThread(), __func__, this,
+        &MediaDecoderStateMachine::OnMediaSinkVideoComplete,
+        &MediaDecoderStateMachine::OnMediaSinkVideoError));
     }
   }
 }
 
 int64_t MediaDecoderStateMachine::AudioDecodedUsecs()
 {
   MOZ_ASSERT(OnTaskQueue());
   NS_ASSERTION(HasAudio(),
@@ -1827,17 +1851,17 @@ bool MediaDecoderStateMachine::HasLowUnd
   }
 
   if (mBuffered.Ref().IsInvalid()) {
     return false;
   }
 
   int64_t endOfDecodedVideoData = INT64_MAX;
   if (HasVideo() && !VideoQueue().AtEndOfStream()) {
-    endOfDecodedVideoData = VideoQueue().Peek() ? VideoQueue().Peek()->GetEndTime() : mVideoFrameEndTime;
+    endOfDecodedVideoData = VideoQueue().Peek() ? VideoQueue().Peek()->GetEndTime() : VideoEndTime();
   }
   int64_t endOfDecodedAudioData = INT64_MAX;
   if (HasAudio() && !AudioQueue().AtEndOfStream()) {
     // mDecodedAudioEndTime could be -1 when no audio samples are decoded.
     // But that is fine since we consider ourself as low in decoded data when
     // we don't have any decoded audio samples at all.
     endOfDecodedAudioData = mDecodedAudioEndTime;
   }
@@ -2027,17 +2051,17 @@ MediaDecoderStateMachine::AdjustAudioThr
 
 void
 MediaDecoderStateMachine::FinishDecodeFirstFrame()
 {
   MOZ_ASSERT(OnTaskQueue());
   DECODER_LOG("FinishDecodeFirstFrame");
 
   if (!IsRealTime() && !mSentFirstFrameLoadedEvent) {
-    RenderVideoFrames(1);
+    mMediaSink->Redraw();
   }
 
   // If we don't know the duration by this point, we assume infinity, per spec.
   if (mDuration.Ref().isNothing()) {
     mDuration = Some(TimeUnit::FromInfinity());
   }
 
   DECODER_LOG("Media duration %lld, "
@@ -2124,17 +2148,17 @@ MediaDecoderStateMachine::SeekCompleted(
   // seek while quick-buffering, we won't bypass quick buffering mode
   // if we need to buffer after the seek.
   mQuickBuffering = false;
 
   mCurrentSeek.Resolve(mState == DECODER_STATE_COMPLETED, __func__);
   ScheduleStateMachine();
 
   if (video) {
-    RenderVideoFrames(1);
+    mMediaSink->Redraw();
     nsCOMPtr<nsIRunnable> event =
       NS_NewRunnableMethod(mDecoder, &MediaDecoder::Invalidate);
     AbstractThread::MainThread()->Dispatch(event.forget());
   }
 }
 
 class DecoderDisposer
 {
@@ -2251,21 +2275,20 @@ nsresult MediaDecoderStateMachine::RunSt
     case DECODER_STATE_DECODING_NONE: {
       SetState(DECODER_STATE_DECODING_METADATA);
       ScheduleStateMachine();
       return NS_OK;
     }
 
     case DECODER_STATE_DECODING_METADATA: {
       if (!mMetadataRequest.Exists()) {
-        DECODER_LOG("Dispatching AsyncReadMetadata");
+        DECODER_LOG("Calling AsyncReadMetadata");
         // Set mode to METADATA since we are about to read metadata.
         mResource->SetReadMode(MediaCacheStream::MODE_METADATA);
-        mMetadataRequest.Begin(InvokeAsync(DecodeTaskQueue(), mReader.get(), __func__,
-                                           &MediaDecoderReader::AsyncReadMetadata)
+        mMetadataRequest.Begin(mReader->AsyncReadMetadata()
           ->Then(OwnerThread(), __func__, this,
                  &MediaDecoderStateMachine::OnMetadataRead,
                  &MediaDecoderStateMachine::OnMetadataNotRead));
 
       }
       return NS_OK;
     }
 
@@ -2280,17 +2303,17 @@ nsresult MediaDecoderStateMachine::RunSt
         // We're playing, but the element/decoder is in paused state. Stop
         // playing!
         StopPlayback();
       }
 
       // Start playback if necessary so that the clock can be properly queried.
       MaybeStartPlayback();
 
-      UpdateRenderedVideoFrames();
+      UpdatePlaybackPositionPeriodically();
       NS_ASSERTION(!IsPlaying() ||
                    mLogicallySeeking ||
                    IsStateMachineScheduled(),
                    "Must have timer scheduled");
       return NS_OK;
     }
 
     case DECODER_STATE_BUFFERING: {
@@ -2351,17 +2374,17 @@ nsresult MediaDecoderStateMachine::RunSt
       // once to ensure the current playback position is advanced to the
       // end of the media, and so that we update the readyState.
       if (VideoQueue().GetSize() > 1 ||
           (HasAudio() && !mAudioCompleted) ||
           (mAudioCaptured && !mStreamSink->IsFinished()))
       {
         // Start playback if necessary to play the remaining media.
         MaybeStartPlayback();
-        UpdateRenderedVideoFrames();
+        UpdatePlaybackPositionPeriodically();
         NS_ASSERTION(!IsPlaying() ||
                      mLogicallySeeking ||
                      IsStateMachineScheduled(),
                      "Must have timer scheduled");
         return NS_OK;
       }
 
       // StopPlayback in order to reset the IsPlaying() state so audio
@@ -2373,17 +2396,17 @@ nsresult MediaDecoderStateMachine::RunSt
         // our state should have scheduled another state machine run.
         NS_ASSERTION(IsStateMachineScheduled(), "Must have timer scheduled");
         return NS_OK;
       }
 
       if (mPlayState == MediaDecoder::PLAY_STATE_PLAYING &&
           !mSentPlaybackEndedEvent)
       {
-        int64_t clockTime = std::max(AudioEndTime(), mVideoFrameEndTime);
+        int64_t clockTime = std::max(AudioEndTime(), VideoEndTime());
         clockTime = std::max(int64_t(0), std::max(clockTime, Duration().ToMicroseconds()));
         UpdatePlaybackPosition(clockTime);
 
         nsCOMPtr<nsIRunnable> event =
           NS_NewRunnableMethod(mDecoder, &MediaDecoder::PlaybackEnded);
         AbstractThread::MainThread()->Dispatch(event.forget());
 
         mSentPlaybackEndedEvent = true;
@@ -2414,17 +2437,16 @@ MediaDecoderStateMachine::Reset()
              mState == DECODER_STATE_DORMANT ||
              mState == DECODER_STATE_DECODING_NONE);
 
   // Stop the audio thread. Otherwise, MediaSink might be accessing AudioQueue
   // outside of the decoder monitor while we are clearing the queue and causes
   // crash for no samples to be popped.
   StopMediaSink();
 
-  mVideoFrameEndTime = -1;
   mDecodedVideoEndTime = -1;
   mDecodedAudioEndTime = -1;
   mAudioCompleted = false;
   AudioQueue().Reset();
   VideoQueue().Reset();
   mFirstVideoFrameAfterSeek = nullptr;
   mDropAudioUntilNextDiscontinuity = true;
   mDropVideoUntilNextDiscontinuity = true;
@@ -2464,153 +2486,63 @@ MediaDecoderStateMachine::CheckFrameVali
         mCorruptFrames.clear();
       gfxCriticalNote << "Too many dropped/corrupted frames, disabling DXVA";
     }
   } else {
     mCorruptFrames.insert(0);
   }
 }
 
-void MediaDecoderStateMachine::RenderVideoFrames(int32_t aMaxFrames,
-                                                 int64_t aClockTime,
-                                                 const TimeStamp& aClockTimeStamp)
-{
-  MOZ_ASSERT(OnTaskQueue());
-
-  VideoFrameContainer* container = mDecoder->GetVideoFrameContainer();
-  nsAutoTArray<RefPtr<MediaData>,16> frames;
-  VideoQueue().GetFirstElements(aMaxFrames, &frames);
-  if (frames.IsEmpty() || !container) {
-    return;
-  }
-
-  nsAutoTArray<ImageContainer::NonOwningImage,16> images;
-  TimeStamp lastFrameTime;
-  for (uint32_t i = 0; i < frames.Length(); ++i) {
-    VideoData* frame = frames[i]->As<VideoData>();
-
-    bool valid = !frame->mImage || frame->mImage->IsValid();
-    frame->mSentToCompositor = true;
-
-    if (!valid) {
-      continue;
-    }
-
-    int64_t frameTime = frame->mTime;
-    if (frameTime < 0) {
-      // Frame times before the start time are invalid; drop such frames
-      continue;
-    }
-
-
-    TimeStamp t;
-    if (aMaxFrames > 1) {
-      MOZ_ASSERT(!aClockTimeStamp.IsNull());
-      int64_t delta = frame->mTime - aClockTime;
-      t = aClockTimeStamp +
-          TimeDuration::FromMicroseconds(delta / mPlaybackRate);
-      if (!lastFrameTime.IsNull() && t <= lastFrameTime) {
-        // Timestamps out of order; drop the new frame. In theory we should
-        // probably replace the previous frame with the new frame if the
-        // timestamps are equal, but this is a corrupt video file already so
-        // never mind.
-        continue;
-      }
-      lastFrameTime = t;
-    }
-
-    ImageContainer::NonOwningImage* img = images.AppendElement();
-    img->mTimeStamp = t;
-    img->mImage = frame->mImage;
-    img->mFrameID = frame->mFrameID;
-    img->mProducerID = mProducerID;
-
-    VERBOSE_LOG("playing video frame %lld (id=%x) (queued=%i, state-machine=%i, decoder-queued=%i)",
-                frame->mTime, frame->mFrameID,
-                VideoQueue().GetSize() + mReader->SizeOfVideoQueueInFrames(),
-                VideoQueue().GetSize(), mReader->SizeOfVideoQueueInFrames());
-  }
-
-  container->SetCurrentFrames(frames[0]->As<VideoData>()->mDisplay, images);
-}
-
 int64_t
 MediaDecoderStateMachine::GetClock(TimeStamp* aTimeStamp) const
 {
   MOZ_ASSERT(OnTaskQueue());
   int64_t clockTime = mMediaSink->GetPosition(aTimeStamp);
   NS_ASSERTION(GetMediaTime() <= clockTime, "Clock should go forwards.");
   return clockTime;
 }
 
-void MediaDecoderStateMachine::UpdateRenderedVideoFrames()
+void
+MediaDecoderStateMachine::UpdatePlaybackPositionPeriodically()
 {
   MOZ_ASSERT(OnTaskQueue());
 
   if (!IsPlaying() || mLogicallySeeking) {
     return;
   }
 
   if (mAudioCaptured) {
     DiscardStreamData();
   }
 
-  TimeStamp nowTime;
-  const int64_t clockTime = GetClock(&nowTime);
-  // Skip frames up to the frame at the playback position, and figure out
-  // the time remaining until it's time to display the next frame and drop
-  // the current frame.
-  NS_ASSERTION(clockTime >= 0, "Should have positive clock time.");
-  int64_t remainingTime = AUDIO_DURATION_USECS;
-  if (VideoQueue().GetSize() > 0) {
-    RefPtr<MediaData> currentFrame = VideoQueue().PopFront();
-    int32_t framesRemoved = 0;
-    while (VideoQueue().GetSize() > 0) {
-      MediaData* nextFrame = VideoQueue().PeekFront();
-      if (!IsRealTime() && nextFrame->mTime > clockTime) {
-        remainingTime = nextFrame->mTime - clockTime;
-        break;
-      }
-      ++framesRemoved;
-      if (!currentFrame->As<VideoData>()->mSentToCompositor) {
-        mDecoder->NotifyDecodedFrames(0, 0, 1);
-        VERBOSE_LOG("discarding video frame mTime=%lld clock_time=%lld",
-                    currentFrame->mTime, clockTime);
-      }
-      currentFrame = VideoQueue().PopFront();
-
-    }
-    VideoQueue().PushFront(currentFrame);
-    if (framesRemoved > 0) {
-      mVideoFrameEndTime = currentFrame->GetEndTime();
-      FrameStatistics& frameStats = mDecoder->GetFrameStatistics();
-      frameStats.NotifyPresentedFrame();
-    }
-  }
-
-  RenderVideoFrames(sVideoQueueSendToCompositorSize, clockTime, nowTime);
-
   // Cap the current time to the larger of the audio and video end time.
   // This ensures that if we're running off the system clock, we don't
   // advance the clock to after the media end time.
-  if (mVideoFrameEndTime != -1 || AudioEndTime() != -1) {
+  if (VideoEndTime() != -1 || AudioEndTime() != -1) {
+
+    const int64_t clockTime = GetClock();
+    // Skip frames up to the frame at the playback position, and figure out
+    // the time remaining until it's time to display the next frame and drop
+    // the current frame.
+    NS_ASSERTION(clockTime >= 0, "Should have positive clock time.");
+
     // These will be non -1 if we've displayed a video frame, or played an audio frame.
-    int64_t t = std::min(clockTime, std::max(mVideoFrameEndTime, AudioEndTime()));
+    int64_t t = std::min(clockTime, std::max(VideoEndTime(), AudioEndTime()));
     // FIXME: Bug 1091422 - chained ogg files hit this assertion.
     //MOZ_ASSERT(t >= GetMediaTime());
     if (t > GetMediaTime()) {
       UpdatePlaybackPosition(t);
     }
   }
   // Note we have to update playback position before releasing the monitor.
   // Otherwise, MediaDecoder::AddOutputStream could kick in when we are outside
   // the monitor and get a staled value from GetCurrentTimeUs() which hits the
   // assertion in GetClock().
 
-  int64_t delay = std::max<int64_t>(1, remainingTime / mPlaybackRate);
+  int64_t delay = std::max<int64_t>(1, AUDIO_DURATION_USECS / mPlaybackRate);
   ScheduleStateMachineIn(delay);
 }
 
 nsresult
 MediaDecoderStateMachine::DropVideoUpToSeekTarget(MediaData* aSample)
 {
   MOZ_ASSERT(OnTaskQueue());
   RefPtr<VideoData> video(aSample->As<VideoData>());
@@ -2903,32 +2835,67 @@ MediaDecoderStateMachine::AudioEndTime()
   MOZ_ASSERT(OnTaskQueue());
   if (mMediaSink->IsStarted()) {
     return mMediaSink->GetEndTime(TrackInfo::kAudioTrack);
   }
   MOZ_ASSERT(!HasAudio());
   return -1;
 }
 
-void MediaDecoderStateMachine::OnMediaSinkComplete()
+int64_t
+MediaDecoderStateMachine::VideoEndTime() const
+{
+  MOZ_ASSERT(OnTaskQueue());
+  if (mMediaSink->IsStarted()) {
+    return mMediaSink->GetEndTime(TrackInfo::kVideoTrack);
+  }
+  return -1;
+}
+
+void
+MediaDecoderStateMachine::OnMediaSinkVideoComplete()
 {
   MOZ_ASSERT(OnTaskQueue());
-
-  mMediaSinkPromise.Complete();
+  VERBOSE_LOG("[%s]", __func__);
+
+  mMediaSinkVideoPromise.Complete();
+  ScheduleStateMachine();
+}
+
+void
+MediaDecoderStateMachine::OnMediaSinkVideoError()
+{
+  MOZ_ASSERT(OnTaskQueue());
+  VERBOSE_LOG("[%s]", __func__);
+
+  mMediaSinkVideoPromise.Complete();
+  if (HasAudio()) {
+    return;
+  }
+  DecodeError();
+}
+
+void MediaDecoderStateMachine::OnMediaSinkAudioComplete()
+{
+  MOZ_ASSERT(OnTaskQueue());
+  VERBOSE_LOG("[%s]", __func__);
+
+  mMediaSinkAudioPromise.Complete();
   // Set true only when we have audio.
   mAudioCompleted = mInfo.HasAudio();
   // To notify PlaybackEnded as soon as possible.
   ScheduleStateMachine();
 }
 
-void MediaDecoderStateMachine::OnMediaSinkError()
+void MediaDecoderStateMachine::OnMediaSinkAudioError()
 {
   MOZ_ASSERT(OnTaskQueue());
-
-  mMediaSinkPromise.Complete();
+  VERBOSE_LOG("[%s]", __func__);
+
+  mMediaSinkAudioPromise.Complete();
   // Set true only when we have audio.
   mAudioCompleted = mInfo.HasAudio();
 
   // Make the best effort to continue playback when there is video.
   if (HasVideo()) {
     return;
   }
 
@@ -2970,20 +2937,17 @@ MediaDecoderStateMachine::SetAudioCaptur
   // Backup current playback parameters.
   MediaSink::PlaybackParams params = mMediaSink->GetPlaybackParams();
 
   // Stop and shut down the existing sink.
   StopMediaSink();
   mMediaSink->Shutdown();
 
   // Create a new sink according to whether audio is captured.
-  // TODO: We can't really create a new DecodedStream until OutputStreamManager
-  //       is extracted. It is tricky that the implementation of DecodedStream
-  //       happens to allow reuse after shutdown without creating a new one.
-  mMediaSink = aCaptured ? mStreamSink : CreateAudioSink();
+  mMediaSink = CreateMediaSink(aCaptured);
 
   // Restore playback parameters.
   mMediaSink->SetPlaybackParams(params);
 
   // We don't need to call StartMediaSink() here because IsPlaying() is now
   // always in sync with the playing state of MediaSink. It will be started in
   // MaybeStartPlayback() in the next cycle if necessary.
 
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -123,17 +123,16 @@ extern PRLogModuleInfo* gMediaSampleLog;
 */
 class MediaDecoderStateMachine
 {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDecoderStateMachine)
 public:
   typedef MediaDecoderReader::AudioDataPromise AudioDataPromise;
   typedef MediaDecoderReader::VideoDataPromise VideoDataPromise;
   typedef MediaDecoderOwner::NextFrameStatus NextFrameStatus;
-  typedef mozilla::layers::ImageContainer::ProducerID ProducerID;
   typedef mozilla::layers::ImageContainer::FrameID FrameID;
   MediaDecoderStateMachine(MediaDecoder* aDecoder,
                            MediaDecoderReader* aReader,
                            bool aRealTime = false);
 
   nsresult Init();
 
   // Enumeration for the valid decoding states
@@ -460,35 +459,25 @@ protected:
   // media element -- use UpdatePlaybackPosition for that.  Called on the state
   // machine thread, caller must hold the decoder lock.
   void UpdatePlaybackPositionInternal(int64_t aTime);
 
   // Decode monitor must be held. To determine if MDSM needs to turn off HW
   // acceleration.
   void CheckFrameValidity(VideoData* aData);
 
-  // Sets VideoQueue images into the VideoFrameContainer. Called on the shared
-  // state machine thread. Decode monitor must be held. The first aMaxFrames
-  // (at most) are set.
-  // aClockTime and aClockTimeStamp are used as the baseline for deriving
-  // timestamps for the frames; when omitted, aMaxFrames must be 1 and
-  // a null timestamp is passed to the VideoFrameContainer.
-  // If the VideoQueue is empty, this does nothing.
-  void RenderVideoFrames(int32_t aMaxFrames, int64_t aClockTime = 0,
-                         const TimeStamp& aClickTimeStamp = TimeStamp());
-
-  // If we have video, display a video frame if it's time for display has
-  // arrived, otherwise sleep until it's time for the next frame. Update the
-  // current frame time as appropriate, and trigger ready state update.  The
-  // decoder monitor must be held with exactly one lock count. Called on the
-  // state machine thread.
-  void UpdateRenderedVideoFrames();
+  // Update playback position and trigger next update by default time period.
+  // Called on the state machine thread.
+  void UpdatePlaybackPositionPeriodically();
 
   media::MediaSink* CreateAudioSink();
 
+  // Always create mediasink which contains an AudioSink or StreamSink inside.
+  already_AddRefed<media::MediaSink> CreateMediaSink(bool aAudioCaptured);
+
   // Stops the media sink and shut it down.
   // The decoder monitor must be held with exactly one lock count.
   // Called on the state machine thread.
   void StopMediaSink();
 
   // Create and start the media sink.
   // The decoder monitor must be held with exactly one lock count.
   // Called on the state machine thread.
@@ -633,22 +622,24 @@ protected:
   bool IsPausedAndDecoderWaiting();
 
   // These return true if the respecti